feat(ui): display artifact certificates in WorkflowRunDetail

This commit is contained in:
Fu Diwei 2025-02-10 12:13:54 +08:00
parent b8513eb0b6
commit 75c89b3d0b
14 changed files with 170 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import type { WorkflowModel } from "./workflow";
import { type WorkflowModel } from "./workflow";
export interface WorkflowRunModel extends BaseModel {
workflowId: string;

View File

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

View File

@ -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": "名称"
}

View File

@ -207,7 +207,7 @@ const AccessList = () => {
setPageSize(pageSize);
},
}}
rowKey={(record: AccessModel) => record.id}
rowKey={(record) => record.id}
scroll={{ x: "max(100%, 960px)" }}
/>
</div>

View File

@ -276,7 +276,7 @@ const CertificateList = () => {
setPageSize(pageSize);
},
}}
rowKey={(record: CertificateModel) => record.id}
rowKey={(record) => record.id}
scroll={{ x: "max(100%, 960px)" }}
/>
</div>

View File

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

View File

@ -366,7 +366,7 @@ const WorkflowList = () => {
setPageSize(pageSize);
},
}}
rowKey={(record: WorkflowModel) => record.id}
rowKey={(record) => record.id}
scroll={{ x: "max(100%, 960px)" }}
/>
</div>

View File

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