mirror of https://github.com/usual2970/certimate
				
				
				
			feat(ui): display artifact certificates in WorkflowRunDetail
							parent
							
								
									b8513eb0b6
								
							
						
					
					
						commit
						75c89b3d0b
					
				| 
						 | 
				
			
			@ -61,7 +61,22 @@ func (r *WorkflowOutputRepository) SaveWithCertificate(ctx context.Context, work
 | 
			
		|||
		workflowOutput.UpdatedAt = record.GetDateTime("updated").Time()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if certificate != nil {
 | 
			
		||||
	if certificate == nil {
 | 
			
		||||
		panic("certificate is nil")
 | 
			
		||||
	} else {
 | 
			
		||||
		if certificate.WorkflowId != "" && certificate.WorkflowId != workflowOutput.WorkflowId {
 | 
			
		||||
			return workflowOutput, fmt.Errorf("certificate #%s is not belong to workflow #%s", certificate.Id, workflowOutput.WorkflowId)
 | 
			
		||||
		}
 | 
			
		||||
		if certificate.WorkflowRunId != "" && certificate.WorkflowRunId != workflowOutput.RunId {
 | 
			
		||||
			return workflowOutput, fmt.Errorf("certificate #%s is not belong to workflow run #%s", certificate.Id, workflowOutput.RunId)
 | 
			
		||||
		}
 | 
			
		||||
		if certificate.WorkflowNodeId != "" && certificate.WorkflowNodeId != workflowOutput.NodeId {
 | 
			
		||||
			return workflowOutput, fmt.Errorf("certificate #%s is not belong to workflow node #%s", certificate.Id, workflowOutput.NodeId)
 | 
			
		||||
		}
 | 
			
		||||
		if certificate.WorkflowOutputId != "" && certificate.WorkflowOutputId != workflowOutput.Id {
 | 
			
		||||
			return workflowOutput, fmt.Errorf("certificate #%s is not belong to workflow output #%s", certificate.Id, workflowOutput.Id)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		certificate.WorkflowId = workflowOutput.WorkflowId
 | 
			
		||||
		certificate.WorkflowRunId = workflowOutput.RunId
 | 
			
		||||
		certificate.WorkflowNodeId = workflowOutput.NodeId
 | 
			
		||||
| 
						 | 
				
			
			@ -143,5 +158,5 @@ func (r *WorkflowOutputRepository) saveRecord(workflowOutput *domain.WorkflowOut
 | 
			
		|||
		return record, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return record, err
 | 
			
		||||
	return record, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,17 @@
 | 
			
		|||
import { useState } from "react";
 | 
			
		||||
import { useTranslation } from "react-i18next";
 | 
			
		||||
import { Alert, Typography } from "antd";
 | 
			
		||||
import { SelectOutlined as SelectOutlinedIcon } from "@ant-design/icons";
 | 
			
		||||
import { useRequest } from "ahooks";
 | 
			
		||||
import { Alert, Button, Divider, Empty, Table, type TableProps, Tooltip, Typography, notification } from "antd";
 | 
			
		||||
import dayjs from "dayjs";
 | 
			
		||||
import { ClientResponseError } from "pocketbase";
 | 
			
		||||
 | 
			
		||||
import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer";
 | 
			
		||||
import Show from "@/components/Show";
 | 
			
		||||
import { type CertificateModel } from "@/domain/certificate";
 | 
			
		||||
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
 | 
			
		||||
import { listByWorkflowRunId as listCertificateByWorkflowRunId } from "@/repository/certificate";
 | 
			
		||||
import { getErrMsg } from "@/utils/error";
 | 
			
		||||
 | 
			
		||||
export type WorkflowRunDetailProps = {
 | 
			
		||||
  className?: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -45,8 +53,108 @@ const WorkflowRunDetail = ({ data, ...props }: WorkflowRunDetailProps) => {
 | 
			
		|||
          })}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <Show when={data.status === WORKFLOW_RUN_STATUSES.SUCCEEDED}>
 | 
			
		||||
        <Divider />
 | 
			
		||||
 | 
			
		||||
        <WorkflowRunArtifacts runId={data.id} />
 | 
			
		||||
      </Show>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const WorkflowRunArtifacts = ({ runId }: { runId: string }) => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  const [notificationApi, NotificationContextHolder] = notification.useNotification();
 | 
			
		||||
 | 
			
		||||
  const tableColumns: TableProps<CertificateModel>["columns"] = [
 | 
			
		||||
    {
 | 
			
		||||
      key: "$index",
 | 
			
		||||
      align: "center",
 | 
			
		||||
      fixed: "left",
 | 
			
		||||
      width: 50,
 | 
			
		||||
      render: (_, __, index) => index + 1,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      key: "type",
 | 
			
		||||
      title: t("workflow_run_artifact.props.type"),
 | 
			
		||||
      render: () => t("workflow_run_artifact.props.type.certificate"),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      key: "name",
 | 
			
		||||
      title: t("workflow_run_artifact.props.name"),
 | 
			
		||||
      ellipsis: true,
 | 
			
		||||
      render: (_, record) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <Typography.Text delete={!!record.deleted} ellipsis>
 | 
			
		||||
            {record.subjectAltNames}
 | 
			
		||||
          </Typography.Text>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      key: "$action",
 | 
			
		||||
      align: "end",
 | 
			
		||||
      width: 120,
 | 
			
		||||
      render: (_, record) => (
 | 
			
		||||
        <Button.Group>
 | 
			
		||||
          <CertificateDetailDrawer
 | 
			
		||||
            data={record}
 | 
			
		||||
            trigger={
 | 
			
		||||
              <Tooltip title={t("certificate.action.view")}>
 | 
			
		||||
                <Button color="primary" disabled={!!record.deleted} icon={<SelectOutlinedIcon />} variant="text" />
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
        </Button.Group>
 | 
			
		||||
      ),
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
  const [tableData, setTableData] = useState<CertificateModel[]>([]);
 | 
			
		||||
  const { loading: tableLoading } = useRequest(
 | 
			
		||||
    () => {
 | 
			
		||||
      return listCertificateByWorkflowRunId(runId);
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      refreshDeps: [runId],
 | 
			
		||||
      onBefore: () => {
 | 
			
		||||
        setTableData([]);
 | 
			
		||||
      },
 | 
			
		||||
      onSuccess: (res) => {
 | 
			
		||||
        setTableData(res.items);
 | 
			
		||||
      },
 | 
			
		||||
      onError: (err) => {
 | 
			
		||||
        if (err instanceof ClientResponseError && err.isAbort) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        console.error(err);
 | 
			
		||||
        notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
 | 
			
		||||
 | 
			
		||||
        throw err;
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {NotificationContextHolder}
 | 
			
		||||
 | 
			
		||||
      <Typography.Title level={5}>{t("workflow_run.artifacts")}</Typography.Title>
 | 
			
		||||
      <Table<CertificateModel>
 | 
			
		||||
        columns={tableColumns}
 | 
			
		||||
        dataSource={tableData}
 | 
			
		||||
        loading={tableLoading}
 | 
			
		||||
        locale={{
 | 
			
		||||
          emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />,
 | 
			
		||||
        }}
 | 
			
		||||
        pagination={false}
 | 
			
		||||
        rowKey={(record) => record.id}
 | 
			
		||||
        size="small"
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default WorkflowRunDetail;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -301,7 +301,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
 | 
			
		|||
              setPageSize(pageSize);
 | 
			
		||||
            },
 | 
			
		||||
          }}
 | 
			
		||||
          rowKey={(record: WorkflowRunModel) => record.id}
 | 
			
		||||
          rowKey={(record) => record.id}
 | 
			
		||||
          scroll={{ x: "max(100%, 960px)" }}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -86,7 +86,7 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
 | 
			
		|||
        </Form.Item>
 | 
			
		||||
 | 
			
		||||
        <Form.Item name="message" label={t("workflow_node.notify.form.message.label")} rules={[formRule]}>
 | 
			
		||||
          <Input.TextArea autoSize={{ minRows: 3, maxRows: 10 }} placeholder={t("workflow_node.notify.form.message.placeholder")} />
 | 
			
		||||
          <Input.TextArea autoSize={{ minRows: 3, maxRows: 5 }} placeholder={t("workflow_node.notify.form.message.placeholder")} />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
 | 
			
		||||
        <Form.Item className="mb-0">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,8 +3,7 @@ import { useTranslation } from "react-i18next";
 | 
			
		|||
import { Flex, Typography } from "antd";
 | 
			
		||||
import { produce } from "immer";
 | 
			
		||||
 | 
			
		||||
import type { WorkflowNodeConfigForUpload } from "@/domain/workflow";
 | 
			
		||||
import { WorkflowNodeType } from "@/domain/workflow";
 | 
			
		||||
import { type WorkflowNodeConfigForUpload, WorkflowNodeType } from "@/domain/workflow";
 | 
			
		||||
import { useZustandShallowSelector } from "@/hooks";
 | 
			
		||||
import { useWorkflowStore } from "@/stores/workflow";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -141,7 +141,7 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
 | 
			
		|||
        </Form.Item>
 | 
			
		||||
 | 
			
		||||
        <Form.Item name="certificate" label={t("workflow_node.upload.form.certificate.label")} rules={[formRule]}>
 | 
			
		||||
          <Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 10 }} placeholder={t("workflow_node.upload.form.certificate.placeholder")} />
 | 
			
		||||
          <Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 5 }} placeholder={t("workflow_node.upload.form.certificate.placeholder")} />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
 | 
			
		||||
        <Form.Item>
 | 
			
		||||
| 
						 | 
				
			
			@ -151,7 +151,7 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
 | 
			
		|||
        </Form.Item>
 | 
			
		||||
 | 
			
		||||
        <Form.Item name="privateKey" label={t("workflow_node.upload.form.private_key.label")} rules={[formRule]}>
 | 
			
		||||
          <Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 10 }} placeholder={t("workflow_node.upload.form.private_key.placeholder")} />
 | 
			
		||||
          <Input.TextArea readOnly autoSize={{ minRows: 5, maxRows: 5 }} placeholder={t("workflow_node.upload.form.private_key.placeholder")} />
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
 | 
			
		||||
        <Form.Item>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import type { WorkflowModel } from "./workflow";
 | 
			
		||||
import { type WorkflowModel } from "./workflow";
 | 
			
		||||
 | 
			
		||||
export interface WorkflowRunModel extends BaseModel {
 | 
			
		||||
  workflowId: string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,5 +16,11 @@
 | 
			
		|||
  "workflow_run.props.trigger.auto": "Timing",
 | 
			
		||||
  "workflow_run.props.trigger.manual": "Manual",
 | 
			
		||||
  "workflow_run.props.started_at": "Started at",
 | 
			
		||||
  "workflow_run.props.ended_at": "Ended at"
 | 
			
		||||
  "workflow_run.props.ended_at": "Ended at",
 | 
			
		||||
 | 
			
		||||
  "workflow_run.artifacts": "Artifacts",
 | 
			
		||||
 | 
			
		||||
  "workflow_run_artifact.props.type": "Type",
 | 
			
		||||
  "workflow_run_artifact.props.type.certificate": "Certificate",
 | 
			
		||||
  "workflow_run_artifact.props.name": "Name"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,5 +16,11 @@
 | 
			
		|||
  "workflow_run.props.trigger.auto": "定时执行",
 | 
			
		||||
  "workflow_run.props.trigger.manual": "手动执行",
 | 
			
		||||
  "workflow_run.props.started_at": "开始时间",
 | 
			
		||||
  "workflow_run.props.ended_at": "完成时间"
 | 
			
		||||
  "workflow_run.props.ended_at": "完成时间",
 | 
			
		||||
 | 
			
		||||
  "workflow_run.artifacts": "输出产物",
 | 
			
		||||
 | 
			
		||||
  "workflow_run_artifact.props.type": "类型",
 | 
			
		||||
  "workflow_run_artifact.props.type.certificate": "证书",
 | 
			
		||||
  "workflow_run_artifact.props.name": "名称"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -207,7 +207,7 @@ const AccessList = () => {
 | 
			
		|||
            setPageSize(pageSize);
 | 
			
		||||
          },
 | 
			
		||||
        }}
 | 
			
		||||
        rowKey={(record: AccessModel) => record.id}
 | 
			
		||||
        rowKey={(record) => record.id}
 | 
			
		||||
        scroll={{ x: "max(100%, 960px)" }}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -276,7 +276,7 @@ const CertificateList = () => {
 | 
			
		|||
            setPageSize(pageSize);
 | 
			
		||||
          },
 | 
			
		||||
        }}
 | 
			
		||||
        rowKey={(record: CertificateModel) => record.id}
 | 
			
		||||
        rowKey={(record) => record.id}
 | 
			
		||||
        scroll={{ x: "max(100%, 960px)" }}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,8 +15,7 @@ import {
 | 
			
		|||
} from "@ant-design/icons";
 | 
			
		||||
import { PageHeader } from "@ant-design/pro-components";
 | 
			
		||||
import { useRequest } from "ahooks";
 | 
			
		||||
import type { TableProps } from "antd";
 | 
			
		||||
import { Button, Card, Col, Divider, Empty, Flex, Grid, Row, Space, Statistic, Table, Tag, Typography, notification, theme } from "antd";
 | 
			
		||||
import { Button, Card, Col, Divider, Empty, Flex, Grid, Row, Space, Statistic, Table, type TableProps, Tag, Typography, notification, theme } from "antd";
 | 
			
		||||
import dayjs from "dayjs";
 | 
			
		||||
import {
 | 
			
		||||
  CalendarClock as CalendarClockIcon,
 | 
			
		||||
| 
						 | 
				
			
			@ -177,7 +176,7 @@ const Dashboard = () => {
 | 
			
		|||
    () => {
 | 
			
		||||
      return listWorkflowRuns({
 | 
			
		||||
        page: 1,
 | 
			
		||||
        perPage: 5,
 | 
			
		||||
        perPage: 9,
 | 
			
		||||
        expand: true,
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -285,8 +284,9 @@ const Dashboard = () => {
 | 
			
		|||
              emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />,
 | 
			
		||||
            }}
 | 
			
		||||
            pagination={false}
 | 
			
		||||
            rowKey={(record: WorkflowRunModel) => record.id}
 | 
			
		||||
            rowKey={(record) => record.id}
 | 
			
		||||
            scroll={{ x: "max(100%, 960px)" }}
 | 
			
		||||
            size="small"
 | 
			
		||||
          />
 | 
			
		||||
        </Card>
 | 
			
		||||
      </Flex>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -366,7 +366,7 @@ const WorkflowList = () => {
 | 
			
		|||
            setPageSize(pageSize);
 | 
			
		||||
          },
 | 
			
		||||
        }}
 | 
			
		||||
        rowKey={(record: WorkflowModel) => record.id}
 | 
			
		||||
        rowKey={(record) => record.id}
 | 
			
		||||
        scroll={{ x: "max(100%, 960px)" }}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,6 +38,23 @@ export const list = async (request: ListCertificateRequest) => {
 | 
			
		|||
  return pb.collection(COLLECTION_NAME).getList<CertificateModel>(page, perPage, options);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const listByWorkflowRunId = async (workflowRunId: string) => {
 | 
			
		||||
  const pb = getPocketBase();
 | 
			
		||||
 | 
			
		||||
  const options: RecordListOptions = {
 | 
			
		||||
    filter: pb.filter("workflowRunId={:workflowRunId}", {
 | 
			
		||||
      workflowRunId: workflowRunId,
 | 
			
		||||
    }),
 | 
			
		||||
    sort: "-created",
 | 
			
		||||
    requestKey: null,
 | 
			
		||||
  };
 | 
			
		||||
  const items = await pb.collection(COLLECTION_NAME).getFullList<CertificateModel>(options);
 | 
			
		||||
  return {
 | 
			
		||||
    totalItems: items.length,
 | 
			
		||||
    items: items,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const remove = async (record: MaybeModelRecordWithId<CertificateModel>) => {
 | 
			
		||||
  await getPocketBase()
 | 
			
		||||
    .collection(COLLECTION_NAME)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue