feat(plugin-workflow): add execution dropdown for quick navigating (#2404)
This commit is contained in:
		
							parent
							
								
									34ec7388df
								
							
						
					
					
						commit
						f992987187
					
				@ -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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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>
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
				
			|||||||
@ -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`
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user