mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-07 21:19:51 +00:00
feat(ui): display artifact certificates in WorkflowRunDetail
This commit is contained in:
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…
x
Reference in New Issue
Block a user