feat(plugin-workflow): add execution dropdown for quick navigating (#2404)

This commit is contained in:
Junyi 2023-08-06 09:17:48 +07:00 committed by GitHub
parent 34ec7388df
commit f992987187
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 155 additions and 25 deletions

View File

@ -2,21 +2,25 @@ import {
ActionContextProvider, ActionContextProvider,
css, css,
SchemaComponent, SchemaComponent,
useAPIClient,
useCompile, useCompile,
useDocumentTitle, useDocumentTitle,
useResourceActionContext, useResourceActionContext,
} from '@nocobase/client'; } from '@nocobase/client';
import { str2moment } from '@nocobase/utils/client'; import { str2moment } from '@nocobase/utils/client';
import { Breadcrumb, Tag } from 'antd'; import { Breadcrumb, Dropdown, Space, Tag } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { CanvasContent } from './CanvasContent'; import { CanvasContent } from './CanvasContent';
import { ExecutionStatusOptionsMap, JobStatusOptions } from './constants'; import { ExecutionStatusOptionsMap, JobStatusOptions, JobStatusOptionsMap } from './constants';
import { FlowContext, useFlowContext } from './FlowContext'; import { FlowContext, useFlowContext } from './FlowContext';
import { lang, NAMESPACE } from './locale'; import { lang, NAMESPACE } from './locale';
import { instructions } from './nodes'; import { instructions } from './nodes';
import useStyles from './style'; import useStyles from './style';
import { linkNodes } from './utils'; import { linkNodes } from './utils';
import { DownOutlined } from '@ant-design/icons';
import { StatusIcon } from './components/StatusIcon';
import classnames from 'classnames';
function attachJobs(nodes, jobs: any[] = []): void { function attachJobs(nodes, jobs: any[] = []): void {
const nodesMap = new Map(); const nodesMap = new Map();
@ -107,6 +111,96 @@ function JobModal() {
); );
} }
function ExecutionsDropdown(props) {
const { execution } = useFlowContext();
const apiClient = useAPIClient();
const navigate = useNavigate();
const { styles } = useStyles();
const [executionsBefore, setExecutionsBefore] = useState([]);
const [executionsAfter, setExecutionsAfter] = useState([]);
useEffect(() => {
if (!execution) {
return;
}
apiClient
.resource('executions')
.list({
filter: {
key: execution.key,
id: {
$lt: execution.id,
},
},
sort: '-createdAt',
pageSize: 10,
fields: ['id', 'status', 'createdAt'],
})
.then(({ data }) => {
setExecutionsBefore(data.data);
})
.catch(() => {});
}, [execution]);
useEffect(() => {
if (!execution) {
return;
}
apiClient
.resource('executions')
.list({
filter: {
key: execution.key,
id: {
$gt: execution.id,
},
},
sort: 'createdAt',
pageSize: 10,
fields: ['id', 'status', 'createdAt'],
})
.then(({ data }) => {
setExecutionsAfter(data.data.reverse());
})
.catch(() => {});
}, [execution]);
const onClick = useCallback(
({ key }) => {
if (key != execution.id) {
navigate(`/admin/settings/workflow/executions/${key}`);
}
},
[execution],
);
return execution ? (
<Dropdown
menu={{
className: styles.executionsDropdownRowClass,
onClick,
items: [...executionsAfter, execution, ...executionsBefore].map((item) => {
return {
key: item.id,
label: (
<span className={classnames('row', { current: item.id === execution.id })}>
<span className="id">{`#${item.id}`}</span>
<time>{str2moment(item.createdAt).format('YYYY-MM-DD HH:mm:ss')}</time>
</span>
),
icon: <StatusIcon status={item.status} />,
};
}),
}}
>
<Space>
<strong>{`#${execution.id}`}</strong>
<DownOutlined />
</Space>
</Dropdown>
) : null;
}
export function ExecutionCanvas() { export function ExecutionCanvas() {
const compile = useCompile(); const compile = useCompile();
const { data, loading } = useResourceActionContext(); const { data, loading } = useResourceActionContext();
@ -150,7 +244,7 @@ export function ExecutionCanvas() {
items={[ items={[
{ title: <Link to={`/admin/settings/workflow/workflows`}>{lang('Workflow')}</Link> }, { title: <Link to={`/admin/settings/workflow/workflows`}>{lang('Workflow')}</Link> },
{ title: <Link to={`/admin/settings/workflow/workflows/${workflow.id}`}>{workflow.title}</Link> }, { title: <Link to={`/admin/settings/workflow/workflows/${workflow.id}`}>{workflow.title}</Link> },
{ title: <strong>{`#${execution.id}`}</strong> }, { title: <ExecutionsDropdown /> },
]} ]}
/> />
</header> </header>

View File

@ -0,0 +1,30 @@
import React from 'react';
import { Tag } from 'antd';
import classnames from 'classnames';
import { createStyles } from '@nocobase/client';
import { JobStatusOptionsMap } from '../constants';
const useStyles = createStyles(({ css, token }) => ({
statusIconClass: css`
padding: 0;
width: ${token.sizeLG}px;
height: ${token.sizeLG}px;
line-height: ${token.sizeLG}px;
margin-right: 0;
border-radius: 50%;
text-align: center;
`,
}));
export function StatusIcon(props) {
const { icon, color } = JobStatusOptionsMap[props.status];
const { styles } = useStyles();
return (
<Tag color={color} className={classnames(styles.statusIconClass, props.className)}>
{icon}
</Tag>
);
}

View File

@ -36,6 +36,7 @@ import query from './query';
import request from './request'; import request from './request';
import sql from './sql'; import sql from './sql';
import update from './update'; import update from './update';
import { StatusIcon } from '../components/StatusIcon';
export interface Instruction { export interface Instruction {
title: string; title: string;
@ -238,11 +239,10 @@ export function RemoveButton() {
function InnerJobButton({ job, ...props }) { function InnerJobButton({ job, ...props }) {
const { styles } = useStyles(); const { styles } = useStyles();
const { icon, color } = JobStatusOptionsMap[job.status];
return ( return (
<Button {...props} shape="circle" size="small" className={cx(styles.nodeJobButtonClass, props.className)}> <Button {...props} shape="circle" size="small" className={cx(styles.nodeJobButtonClass, props.className)}>
<Tag color={color}>{icon}</Tag> <StatusIcon status={job.status} />
</Button> </Button>
); );
} }
@ -280,7 +280,6 @@ export function JobButton() {
<Dropdown <Dropdown
menu={{ menu={{
items: jobs.map((job) => { items: jobs.map((job) => {
const { icon, color } = JobStatusOptionsMap[job.status];
return { return {
key: job.id, key: job.id,
label: ( label: (
@ -296,9 +295,7 @@ export function JobButton() {
} }
`} `}
> >
<span className={cx(styles.nodeJobButtonClass, 'inner')}> <StatusIcon status={job.status} />
<Tag color={color}>{icon}</Tag>
</span>
<time>{str2moment(job.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time> <time>{str2moment(job.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
</div> </div>
), ),

View File

@ -85,6 +85,29 @@ const useStyles = createStyles(({ css, token }) => {
} }
`, `,
executionsDropdownRowClass: css`
.row {
display: flex;
align-items: baseline;
&.current {
font-weight: bold;
}
.id {
flex-grow: 1;
text-align: right;
}
time {
width: 12em;
margin-left: 0.5rem;
color: ${token.colorText};
font-size: 80%;
}
}
`,
branchBlockClass: css` branchBlockClass: css`
display: flex; display: flex;
position: relative; position: relative;
@ -259,20 +282,6 @@ const useStyles = createStyles(({ css, token }) => {
&[type='button'] { &[type='button'] {
border: none; border: none;
} }
&.inner {
position: static;
}
.ant-tag {
padding: 0;
width: ${token.sizeLG}px;
height: ${token.sizeLG}px;
line-height: ${token.sizeLG}px;
margin-right: 0;
border-radius: 50%;
text-align: center;
}
`, `,
nodeHeaderClass: css` nodeHeaderClass: css`