mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-11 06:59: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()
|
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.WorkflowId = workflowOutput.WorkflowId
|
||||||
certificate.WorkflowRunId = workflowOutput.RunId
|
certificate.WorkflowRunId = workflowOutput.RunId
|
||||||
certificate.WorkflowNodeId = workflowOutput.NodeId
|
certificate.WorkflowNodeId = workflowOutput.NodeId
|
||||||
@ -143,5 +158,5 @@ func (r *WorkflowOutputRepository) saveRecord(workflowOutput *domain.WorkflowOut
|
|||||||
return record, err
|
return record, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return record, err
|
return record, nil
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 dayjs from "dayjs";
|
||||||
|
import { ClientResponseError } from "pocketbase";
|
||||||
|
|
||||||
|
import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer";
|
||||||
import Show from "@/components/Show";
|
import Show from "@/components/Show";
|
||||||
|
import { type CertificateModel } from "@/domain/certificate";
|
||||||
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
|
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
|
||||||
|
import { listByWorkflowRunId as listCertificateByWorkflowRunId } from "@/repository/certificate";
|
||||||
|
import { getErrMsg } from "@/utils/error";
|
||||||
|
|
||||||
export type WorkflowRunDetailProps = {
|
export type WorkflowRunDetailProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -45,8 +53,108 @@ const WorkflowRunDetail = ({ data, ...props }: WorkflowRunDetailProps) => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Show when={data.status === WORKFLOW_RUN_STATUSES.SUCCEEDED}>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<WorkflowRunArtifacts runId={data.id} />
|
||||||
|
</Show>
|
||||||
</div>
|
</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;
|
export default WorkflowRunDetail;
|
||||||
|
@ -301,7 +301,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
|||||||
setPageSize(pageSize);
|
setPageSize(pageSize);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
rowKey={(record: WorkflowRunModel) => record.id}
|
rowKey={(record) => record.id}
|
||||||
scroll={{ x: "max(100%, 960px)" }}
|
scroll={{ x: "max(100%, 960px)" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -86,7 +86,7 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="message" label={t("workflow_node.notify.form.message.label")} rules={[formRule]}>
|
<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>
|
||||||
|
|
||||||
<Form.Item className="mb-0">
|
<Form.Item className="mb-0">
|
||||||
|
@ -3,8 +3,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Flex, Typography } from "antd";
|
import { Flex, Typography } from "antd";
|
||||||
import { produce } from "immer";
|
import { produce } from "immer";
|
||||||
|
|
||||||
import type { WorkflowNodeConfigForUpload } from "@/domain/workflow";
|
import { type WorkflowNodeConfigForUpload, WorkflowNodeType } from "@/domain/workflow";
|
||||||
import { WorkflowNodeType } from "@/domain/workflow";
|
|
||||||
import { useZustandShallowSelector } from "@/hooks";
|
import { useZustandShallowSelector } from "@/hooks";
|
||||||
import { useWorkflowStore } from "@/stores/workflow";
|
import { useWorkflowStore } from "@/stores/workflow";
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="certificate" label={t("workflow_node.upload.form.certificate.label")} rules={[formRule]}>
|
<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>
|
||||||
|
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
@ -151,7 +151,7 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="privateKey" label={t("workflow_node.upload.form.private_key.label")} rules={[formRule]}>
|
<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>
|
||||||
|
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { WorkflowModel } from "./workflow";
|
import { type WorkflowModel } from "./workflow";
|
||||||
|
|
||||||
export interface WorkflowRunModel extends BaseModel {
|
export interface WorkflowRunModel extends BaseModel {
|
||||||
workflowId: string;
|
workflowId: string;
|
||||||
|
@ -16,5 +16,11 @@
|
|||||||
"workflow_run.props.trigger.auto": "Timing",
|
"workflow_run.props.trigger.auto": "Timing",
|
||||||
"workflow_run.props.trigger.manual": "Manual",
|
"workflow_run.props.trigger.manual": "Manual",
|
||||||
"workflow_run.props.started_at": "Started at",
|
"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.auto": "定时执行",
|
||||||
"workflow_run.props.trigger.manual": "手动执行",
|
"workflow_run.props.trigger.manual": "手动执行",
|
||||||
"workflow_run.props.started_at": "开始时间",
|
"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);
|
setPageSize(pageSize);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
rowKey={(record: AccessModel) => record.id}
|
rowKey={(record) => record.id}
|
||||||
scroll={{ x: "max(100%, 960px)" }}
|
scroll={{ x: "max(100%, 960px)" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -276,7 +276,7 @@ const CertificateList = () => {
|
|||||||
setPageSize(pageSize);
|
setPageSize(pageSize);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
rowKey={(record: CertificateModel) => record.id}
|
rowKey={(record) => record.id}
|
||||||
scroll={{ x: "max(100%, 960px)" }}
|
scroll={{ x: "max(100%, 960px)" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,8 +15,7 @@ import {
|
|||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { PageHeader } from "@ant-design/pro-components";
|
import { PageHeader } from "@ant-design/pro-components";
|
||||||
import { useRequest } from "ahooks";
|
import { useRequest } from "ahooks";
|
||||||
import type { TableProps } from "antd";
|
import { Button, Card, Col, Divider, Empty, Flex, Grid, Row, Space, Statistic, Table, type TableProps, Tag, Typography, notification, theme } from "antd";
|
||||||
import { Button, Card, Col, Divider, Empty, Flex, Grid, Row, Space, Statistic, Table, Tag, Typography, notification, theme } from "antd";
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import {
|
import {
|
||||||
CalendarClock as CalendarClockIcon,
|
CalendarClock as CalendarClockIcon,
|
||||||
@ -177,7 +176,7 @@ const Dashboard = () => {
|
|||||||
() => {
|
() => {
|
||||||
return listWorkflowRuns({
|
return listWorkflowRuns({
|
||||||
page: 1,
|
page: 1,
|
||||||
perPage: 5,
|
perPage: 9,
|
||||||
expand: true,
|
expand: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -285,8 +284,9 @@ const Dashboard = () => {
|
|||||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
||||||
}}
|
}}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
rowKey={(record: WorkflowRunModel) => record.id}
|
rowKey={(record) => record.id}
|
||||||
scroll={{ x: "max(100%, 960px)" }}
|
scroll={{ x: "max(100%, 960px)" }}
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -366,7 +366,7 @@ const WorkflowList = () => {
|
|||||||
setPageSize(pageSize);
|
setPageSize(pageSize);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
rowKey={(record: WorkflowModel) => record.id}
|
rowKey={(record) => record.id}
|
||||||
scroll={{ x: "max(100%, 960px)" }}
|
scroll={{ x: "max(100%, 960px)" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,6 +38,23 @@ export const list = async (request: ListCertificateRequest) => {
|
|||||||
return pb.collection(COLLECTION_NAME).getList<CertificateModel>(page, perPage, options);
|
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>) => {
|
export const remove = async (record: MaybeModelRecordWithId<CertificateModel>) => {
|
||||||
await getPocketBase()
|
await getPocketBase()
|
||||||
.collection(COLLECTION_NAME)
|
.collection(COLLECTION_NAME)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user