diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..6f4dc26
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,23 @@
+{
+ // 使用 IntelliSense 了解相关属性。
+ // 悬停以查看现有属性的描述。
+ // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "chrome",
+ "request": "launch",
+ "name": "Launch Chrome against localhost",
+ "url": "http://fennec.localhost/",
+ "webRoot": "${workspaceFolder}/src",
+ "sourceMaps": true,
+ "sourceMapPathOverrides": {
+ "webpack:///./*": "${workspaceFolder}/src/*",
+ "webpack:///src/*": "${workspaceFolder}/src/*",
+ "webpack:///node_modules/*": "${workspaceFolder}/node_modules/*",
+ "webpack:///../node_modules/*": "${workspaceFolder}/node_modules/*"
+ },
+ "showAsyncStacks": true
+ }
+ ]
+}
diff --git a/graphql.schema.json b/graphql.schema.json
index 26fd76e..4c5da1e 100644
--- a/graphql.schema.json
+++ b/graphql.schema.json
@@ -636,7 +636,7 @@
"name": null,
"ofType": {
"kind": "ENUM",
- "name": "PipelineUnits",
+ "name": "TaskStatuses",
"ofType": null
}
},
@@ -689,6 +689,41 @@
"enumValues": null,
"possibleTypes": null
},
+ {
+ "kind": "ENUM",
+ "name": "TaskStatuses",
+ "description": "任务状态",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "success",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "failed",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "working",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pending",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
{
"kind": "SCALAR",
"name": "DateTime",
@@ -862,41 +897,6 @@
"enumValues": null,
"possibleTypes": null
},
- {
- "kind": "ENUM",
- "name": "TaskStatuses",
- "description": "任务状态",
- "fields": null,
- "inputFields": null,
- "interfaces": null,
- "enumValues": [
- {
- "name": "success",
- "description": null,
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "failed",
- "description": null,
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "working",
- "description": null,
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "pending",
- "description": null,
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "possibleTypes": null
- },
{
"kind": "OBJECT",
"name": "PipelineTaskLogMessage",
@@ -906,12 +906,40 @@
"name": "unit",
"description": null,
"args": [],
+ "type": {
+ "kind": "ENUM",
+ "name": "PipelineUnits",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "time",
+ "description": null,
+ "args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
- "kind": "ENUM",
- "name": "PipelineUnits",
+ "kind": "SCALAR",
+ "name": "DateTime",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "message",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
"ofType": null
}
},
@@ -1169,6 +1197,80 @@
},
"isDeprecated": false,
"deprecationReason": null
+ },
+ {
+ "name": "listPipelineTaskByPipelineId",
+ "description": null,
+ "args": [
+ {
+ "name": "pipelineId",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PipelineTask",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "findPipelineTask",
+ "description": null,
+ "args": [
+ {
+ "name": "id",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PipelineTask",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
}
],
"inputFields": null,
diff --git a/package-lock.json b/package-lock.json
index 3bb4fbd..efa5335 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"@fortawesome/react-fontawesome": "^0.1.14",
"@tailwindcss/postcss7-compat": "^2.0.3",
"@types/classnames": "^2.2.11",
+ "@types/ramda": "^0.27.39",
"autoprefixer": "^9.8.6",
"classnames": "^2.2.6",
"formik": "^2.2.6",
@@ -26,6 +27,7 @@
"preact-markup": "^2.1.1",
"preact-render-to-string": "^5.1.4",
"preact-router": "^3.2.1",
+ "ramda": "^0.27.1",
"subscriptions-transport-ws": "^0.9.18",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.3"
},
@@ -3308,6 +3310,14 @@
"integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==",
"dev": true
},
+ "node_modules/@types/ramda": {
+ "version": "0.27.39",
+ "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.27.39.tgz",
+ "integrity": "sha512-od24Hng0uS1NcMSPo6ZzKXN2WJxjbF7IBzQhRCtSDyIShdEJTAWXgQlyDg998aylNrXbVvQxkFJmuaLHQcfczg==",
+ "dependencies": {
+ "ts-toolbelt": "^6.15.1"
+ }
+ },
"node_modules/@types/react": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.2.tgz",
@@ -18018,6 +18028,11 @@
"integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=",
"dev": true
},
+ "node_modules/ramda": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz",
+ "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw=="
+ },
"node_modules/randexp": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
@@ -21460,6 +21475,11 @@
"node": ">=10.0.0"
}
},
+ "node_modules/ts-toolbelt": {
+ "version": "6.15.5",
+ "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz",
+ "integrity": "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A=="
+ },
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
@@ -27487,6 +27507,14 @@
"integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==",
"dev": true
},
+ "@types/ramda": {
+ "version": "0.27.39",
+ "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.27.39.tgz",
+ "integrity": "sha512-od24Hng0uS1NcMSPo6ZzKXN2WJxjbF7IBzQhRCtSDyIShdEJTAWXgQlyDg998aylNrXbVvQxkFJmuaLHQcfczg==",
+ "requires": {
+ "ts-toolbelt": "^6.15.1"
+ }
+ },
"@types/react": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.2.tgz",
@@ -39881,6 +39909,11 @@
"integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=",
"dev": true
},
+ "ramda": {
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz",
+ "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw=="
+ },
"randexp": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz",
@@ -42797,6 +42830,11 @@
"yn": "3.1.1"
}
},
+ "ts-toolbelt": {
+ "version": "6.15.5",
+ "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz",
+ "integrity": "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A=="
+ },
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
diff --git a/package.json b/package.json
index 5a16a38..8c11815 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"@fortawesome/react-fontawesome": "^0.1.14",
"@tailwindcss/postcss7-compat": "^2.0.3",
"@types/classnames": "^2.2.11",
+ "@types/ramda": "^0.27.39",
"autoprefixer": "^9.8.6",
"classnames": "^2.2.6",
"formik": "^2.2.6",
@@ -43,6 +44,7 @@
"preact-markup": "^2.1.1",
"preact-render-to-string": "^5.1.4",
"preact-router": "^3.2.1",
+ "ramda": "^0.27.1",
"subscriptions-transport-ws": "^0.9.18",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.3"
},
diff --git a/src/components/commit-actions/commit-actions.tsx b/src/components/commit-actions/commit-actions.tsx
index 8c149e4..e74a922 100644
--- a/src/components/commit-actions/commit-actions.tsx
+++ b/src/components/commit-actions/commit-actions.tsx
@@ -21,10 +21,11 @@ import {
} from '@fortawesome/free-solid-svg-icons';
import { useLocalObservable } from 'mobx-react';
import { PipelineTask, PipelineUnits } from '../../generated/graphql';
+import { route } from 'preact-router';
const CREATE_PIPELINE_TASK = gql`
mutation CreatePipelineTask($task: CreatePipelineTaskInput!) {
- pipelineTask: createPipelineTask(task: $task) {
+ task: createPipelineTask(task: $task) {
id
units
commit
@@ -50,16 +51,19 @@ class Store {
export const CommitActions = ({ pipeline, commit }: Props) => {
const [createTask] = useMutation<
- PipelineTask,
+ { task: PipelineTask },
{ task: CreatePipelineTaskInput }
>(CREATE_PIPELINE_TASK);
const doWork = async (units: PipelineUnits[]) => {
- await createTask({
+ const { data } = await createTask({
variables: {
task: { pipelineId: pipeline.id, commit, units }
}
});
+ route(
+ `/projects/${pipeline.projectId}/pipelines/${pipeline.id}/tasks/${data?.task.id}`
+ );
};
const commitActionsStore = useLocalObservable(() => new CommitActionsStore());
diff --git a/src/components/pipeline-tasks/pipeline-task-details.scss b/src/components/pipeline-tasks/pipeline-task-details.scss
new file mode 100644
index 0000000..0dc81f9
--- /dev/null
+++ b/src/components/pipeline-tasks/pipeline-task-details.scss
@@ -0,0 +1,25 @@
+.details {
+ @apply pt-2 flex flex-col w-full;
+}
+
+.navContainer {
+ @apply bg-white p-2 text-gray-700;
+ @apply flex;
+}
+.navItem {
+ @apply w-full flex-auto text-center;
+ &:not(:first-child) {
+ @apply border-l border-gray-200;
+ }
+}
+.LogListOfUnit {
+ @apply overflow-scroll;
+}
+.unitLogsContainer {
+ @apply bg-gray-200;
+}
+.unitLogs {
+ @apply whitespace-pre-line font-mono text-sm;
+ @apply p-1 my-px;
+ @apply bg-gray-100;
+}
diff --git a/src/components/pipeline-tasks/pipeline-task-details.scss.d.ts b/src/components/pipeline-tasks/pipeline-task-details.scss.d.ts
new file mode 100644
index 0000000..04b057b
--- /dev/null
+++ b/src/components/pipeline-tasks/pipeline-task-details.scss.d.ts
@@ -0,0 +1,15 @@
+// This file is automatically generated from your CSS. Any edits will be overwritten.
+declare namespace PipelineTaskDetailsScssNamespace {
+ export interface IPipelineTaskDetailsScss {
+ LogListOfUnit: string;
+ details: string;
+ navContainer: string;
+ navItem: string;
+ unitLogs: string;
+ unitLogsContainer: string;
+ }
+}
+
+declare const PipelineTaskDetailsScssModule: PipelineTaskDetailsScssNamespace.IPipelineTaskDetailsScss;
+
+export = PipelineTaskDetailsScssModule;
diff --git a/src/components/pipeline-tasks/pipeline-task-details.tsx b/src/components/pipeline-tasks/pipeline-task-details.tsx
new file mode 100644
index 0000000..7591f8b
--- /dev/null
+++ b/src/components/pipeline-tasks/pipeline-task-details.tsx
@@ -0,0 +1,145 @@
+import { gql, useSubscription, useQuery } from '@apollo/client';
+import { h } from 'preact';
+import { useMemo } from 'preact/hooks';
+import { find, propEq } from 'ramda';
+import { observer, useLocalObservable, useObserver } from 'mobx-react';
+import { makeAutoObservable } from 'mobx';
+import styles from './pipeline-task-details.scss';
+import {
+ PipelineTaskLogs,
+ TaskStatuses,
+ PipelineUnits,
+ PipelineTask,
+ PipelineTaskLogMessage
+} from '../../generated/graphql';
+interface Props {
+ taskId: string;
+}
+
+class Store {
+ logs: PipelineTaskLogs[] = [];
+ task?: PipelineTask;
+ constructor() {
+ makeAutoObservable(this);
+ }
+
+ addLogsFromTask(task: PipelineTask) {
+ for (const log of task.logs) {
+ const taskLog = find(propEq('unit', log.unit), this.logs);
+ if (!taskLog) {
+ this.logs.push(log);
+ } else {
+ taskLog.logs = log.logs + taskLog.logs;
+ }
+ }
+ }
+ addLog(log: PipelineTaskLogMessage) {
+ const taskLog = find(propEq('unit', log.unit), this.logs);
+ if (!taskLog) {
+ this.logs.push({
+ unit: (log.unit as unknown) as PipelineUnits,
+ status: TaskStatuses.Working,
+ startedAt: log.time,
+ logs: log.message
+ });
+ } else {
+ taskLog.logs += log.message;
+ }
+ }
+
+ setTask(task: PipelineTask) {
+ this.task = task;
+ }
+}
+
+const FIND_PIPELINE_TASK = gql`
+ query FindPipelineTask($taskId: String!) {
+ task: findPipelineTask(id: $taskId) {
+ id
+ units
+ commit
+ logs {
+ unit
+ logs
+ }
+ }
+ }
+`;
+
+const PIPELINE_TASK_LOG = gql`
+ subscription PipelineTaskLog($taskId: String!) {
+ log: pipelineTaskLog(taskId: $taskId) {
+ unit
+ time
+ message
+ }
+ }
+`;
+
+export const PipelineTaskDetails = ({ taskId }: Props) => {
+ const { data } = useQuery<{ task: PipelineTask }, { taskId: string }>(
+ FIND_PIPELINE_TASK,
+ { variables: { taskId } }
+ );
+
+ const store = useLocalObservable(() => new Store());
+
+ useMemo(() => {
+ store.task = data?.task;
+ if (!data?.task.logs) {
+ return;
+ }
+ store.addLogsFromTask(data.task);
+ }, [data]);
+
+ useSubscription<
+ {
+ log: PipelineTaskLogMessage;
+ },
+ { taskId: string }
+ >(PIPELINE_TASK_LOG, {
+ variables: { taskId },
+ onSubscriptionData: ({ subscriptionData }) => {
+ const log = subscriptionData.data?.log;
+ if (!log) {
+ return;
+ }
+ store.addLog(log);
+ }
+ });
+ return useObserver(() => (
+