feat(pipelien-tasks): 添加 部署详情页。

This commit is contained in:
Ivan Li 2021-03-20 14:29:49 +08:00
parent 32421599ea
commit 45844817c0
13 changed files with 546 additions and 51 deletions

23
.vscode/launch.json vendored Normal file
View File

@ -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
}
]
}

View File

@ -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,

38
package-lock.json generated
View File

@ -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",

View File

@ -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"
},

View File

@ -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());

View File

@ -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;
}

View File

@ -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;

View File

@ -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(() => (
<section className={styles.details}>
<UnitList store={store} />
<LogListOfUnit store={store} />
</section>
));
};
const UnitList = observer(({ store }: { store: Store }) => {
if (!store.task) {
return <div>Loading...</div>;
}
const tabs = store.task.units.map(unit => (
<li key={unit} className={styles.navItem}>
{unit}
</li>
));
return <ol className={styles.navContainer}>{tabs}</ol>;
});
const LogListOfUnit = observer(({ store }: { store: Store }) => {
const tabs = store.logs.map(unitLogs => (
<li key={unitLogs.unit} className={styles.unitLogs}>
{unitLogs.logs}
</li>
));
return (
<div className={styles.LogListOfUnit}>
<ol className={styles.unitLogsContainer}>{tabs}</ol>
</div>
);
});

View File

@ -0,0 +1,90 @@
import { h } from 'preact';
import styles from './pipeline-tasks.scss';
import { gql, useQuery } from '@apollo/client';
import { PipelineTask, TaskStatuses } from '../../generated/graphql';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faCheckCircle,
faClock,
faExclamationCircle,
faRunning
} from '@fortawesome/free-solid-svg-icons';
const LIST_TASKS = gql`
query ListTasks($pipelineId: String!) {
tasks: listPipelineTaskByPipelineId(pipelineId: $pipelineId) {
id
pipelineId
commit
units
logs {
unit
status
startedAt
endedAt
}
status
startedAt
endedAt
}
}
`;
interface Props {
pipelineId: string;
}
export const PipelineTaskList = ({ pipelineId }: Props) => {
const { data, loading } = useQuery<
{ tasks: PipelineTask[] },
{ pipelineId: string }
>(LIST_TASKS, {
variables: { pipelineId }
});
const items = data?.tasks.map(item => <Item key={item.id} task={item} />);
const list = loading ? (
<span>Loading...</span>
) : (
<ol className={styles.list}>{items}</ol>
);
return <section className={styles.taskList}>{list}</section>;
};
const Item = ({ task }: { task: PipelineTask }) => {
return (
<li key={task.id} className={styles.item}>
<Status status={task.status} />
<span>{task.id}</span>
<span>{task.commit}</span>
<span>{task.startedAt}</span>
<ol className={styles.units}>
{task.units.map((unit, index) => (
<li key={index}>{unit}</li>
))}
</ol>
<section className={styles.details}>{JSON.stringify(task.logs)}</section>
</li>
);
};
const Status = ({ status }: { status: TaskStatuses }) => {
let icon;
switch (status) {
case TaskStatuses.Failed:
icon = faExclamationCircle;
break;
case TaskStatuses.Success:
icon = faCheckCircle;
break;
case TaskStatuses.Pending:
icon = faClock;
break;
case TaskStatuses.Working:
icon = faRunning;
break;
}
return <FontAwesomeIcon icon={icon} />;
};

View File

@ -0,0 +1,17 @@
.taskList {
@apply bg-white w-full;
}
.list {
@apply bg-gray-200 max-h-full overflow-y-auto;
}
.item {
@apply bg-white py-2 px-4 my-px;
@apply grid;
}
.units {
@apply inline text-gray-700;
}
.details {
@apply bg-red-100;
}

View File

@ -0,0 +1,14 @@
// This file is automatically generated from your CSS. Any edits will be overwritten.
declare namespace PipelineTasksScssNamespace {
export interface IPipelineTasksScss {
details: string;
item: string;
list: string;
taskList: string;
units: string;
}
}
declare const PipelineTasksScssModule: PipelineTasksScssNamespace.IPipelineTasksScss;
export = PipelineTasksScssModule;

View File

@ -81,12 +81,20 @@ export type LogList = {
export type PipelineTaskLogs = {
__typename?: 'PipelineTaskLogs';
unit: PipelineUnits;
status: PipelineUnits;
status: TaskStatuses;
startedAt?: Maybe<Scalars['DateTime']>;
endedAt?: Maybe<Scalars['DateTime']>;
logs: Scalars['String'];
};
/** 任务状态 */
export enum TaskStatuses {
Success = 'success',
Failed = 'failed',
Working = 'working',
Pending = 'pending'
}
export type PipelineTask = {
__typename?: 'PipelineTask';
@ -101,17 +109,11 @@ export type PipelineTask = {
endedAt?: Maybe<Scalars['DateTime']>;
};
/** 任务状态 */
export enum TaskStatuses {
Success = 'success',
Failed = 'failed',
Working = 'working',
Pending = 'pending'
}
export type PipelineTaskLogMessage = {
__typename?: 'PipelineTaskLogMessage';
unit: PipelineUnits;
unit?: Maybe<PipelineUnits>;
time: Scalars['DateTime'];
message: Scalars['String'];
};
export type WorkUnitInput = {
@ -131,6 +133,8 @@ export type Query = {
findProject: Project;
listPipelines: Array<Pipeline>;
findPipeline: Pipeline;
listPipelineTaskByPipelineId: Array<PipelineTask>;
findPipelineTask: PipelineTask;
};
@ -148,6 +152,16 @@ export type QueryFindPipelineArgs = {
id: Scalars['String'];
};
export type QueryListPipelineTaskByPipelineIdArgs = {
pipelineId: Scalars['String'];
};
export type QueryFindPipelineTaskArgs = {
id: Scalars['String'];
};
export type Mutation = {
__typename?: 'Mutation';
createProject: Project;

View File

@ -11,6 +11,8 @@ import { Observer, useLocalObservable } from 'mobx-react';
import Router, { RoutableProps, Route } from 'preact-router';
import { gql, useQuery } from '@apollo/client';
import { PipelineList } from '../../components/pipelines/pipeline-list';
import { PipelineTaskList } from '../../components/pipeline-tasks/pipeline-task-list';
import { PipelineTaskDetails } from '../../components/pipeline-tasks/pipeline-task-details';
const FIND_PROJECT = gql`
query FindProject($id: String!) {
@ -86,6 +88,10 @@ export const ProjectDetails = ({ id, path }: Props) => {
<Observer>
{(): any => (
<Router>
<Route
path="/projects/:projectId/pipelines/:pipelineId/tasks/:taskId"
component={PipelineTaskDetails}
/>
<Route
path="/projects/:projectId/pipelines/:pipelineId"
component={CommitLogList}