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,
 | 
			
		||||
  css,
 | 
			
		||||
  SchemaComponent,
 | 
			
		||||
  useAPIClient,
 | 
			
		||||
  useCompile,
 | 
			
		||||
  useDocumentTitle,
 | 
			
		||||
  useResourceActionContext,
 | 
			
		||||
} from '@nocobase/client';
 | 
			
		||||
import { str2moment } from '@nocobase/utils/client';
 | 
			
		||||
import { Breadcrumb, Tag } from 'antd';
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import { Breadcrumb, Dropdown, Space, Tag } from 'antd';
 | 
			
		||||
import React, { useCallback, useEffect, useState } from 'react';
 | 
			
		||||
import { Link, useNavigate } from 'react-router-dom';
 | 
			
		||||
import { CanvasContent } from './CanvasContent';
 | 
			
		||||
import { ExecutionStatusOptionsMap, JobStatusOptions } from './constants';
 | 
			
		||||
import { ExecutionStatusOptionsMap, JobStatusOptions, JobStatusOptionsMap } from './constants';
 | 
			
		||||
import { FlowContext, useFlowContext } from './FlowContext';
 | 
			
		||||
import { lang, NAMESPACE } from './locale';
 | 
			
		||||
import { instructions } from './nodes';
 | 
			
		||||
import useStyles from './style';
 | 
			
		||||
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 {
 | 
			
		||||
  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() {
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const { data, loading } = useResourceActionContext();
 | 
			
		||||
@ -150,7 +244,7 @@ export function ExecutionCanvas() {
 | 
			
		||||
            items={[
 | 
			
		||||
              { title: <Link to={`/admin/settings/workflow/workflows`}>{lang('Workflow')}</Link> },
 | 
			
		||||
              { title: <Link to={`/admin/settings/workflow/workflows/${workflow.id}`}>{workflow.title}</Link> },
 | 
			
		||||
              { title: <strong>{`#${execution.id}`}</strong> },
 | 
			
		||||
              { title: <ExecutionsDropdown /> },
 | 
			
		||||
            ]}
 | 
			
		||||
          />
 | 
			
		||||
        </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 sql from './sql';
 | 
			
		||||
import update from './update';
 | 
			
		||||
import { StatusIcon } from '../components/StatusIcon';
 | 
			
		||||
 | 
			
		||||
export interface Instruction {
 | 
			
		||||
  title: string;
 | 
			
		||||
@ -238,11 +239,10 @@ export function RemoveButton() {
 | 
			
		||||
 | 
			
		||||
function InnerJobButton({ job, ...props }) {
 | 
			
		||||
  const { styles } = useStyles();
 | 
			
		||||
  const { icon, color } = JobStatusOptionsMap[job.status];
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Button {...props} shape="circle" size="small" className={cx(styles.nodeJobButtonClass, props.className)}>
 | 
			
		||||
      <Tag color={color}>{icon}</Tag>
 | 
			
		||||
      <StatusIcon status={job.status} />
 | 
			
		||||
    </Button>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -280,7 +280,6 @@ export function JobButton() {
 | 
			
		||||
    <Dropdown
 | 
			
		||||
      menu={{
 | 
			
		||||
        items: jobs.map((job) => {
 | 
			
		||||
          const { icon, color } = JobStatusOptionsMap[job.status];
 | 
			
		||||
          return {
 | 
			
		||||
            key: job.id,
 | 
			
		||||
            label: (
 | 
			
		||||
@ -296,9 +295,7 @@ export function JobButton() {
 | 
			
		||||
                  }
 | 
			
		||||
                `}
 | 
			
		||||
              >
 | 
			
		||||
                <span className={cx(styles.nodeJobButtonClass, 'inner')}>
 | 
			
		||||
                  <Tag color={color}>{icon}</Tag>
 | 
			
		||||
                </span>
 | 
			
		||||
                <StatusIcon status={job.status} />
 | 
			
		||||
                <time>{str2moment(job.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
 | 
			
		||||
              </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`
 | 
			
		||||
      display: flex;
 | 
			
		||||
      position: relative;
 | 
			
		||||
@ -259,20 +282,6 @@ const useStyles = createStyles(({ css, token }) => {
 | 
			
		||||
      &[type='button'] {
 | 
			
		||||
        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`
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user