feat: support removing workflow runs

This commit is contained in:
Fu Diwei 2025-01-21 23:11:48 +08:00
parent c61b2d2d3f
commit 8dc86209df
6 changed files with 82 additions and 48 deletions

View File

@ -2,12 +2,11 @@
import ( import (
"bytes" "bytes"
"crypto/ecdsa"
"crypto/rsa"
"encoding/pem" "encoding/pem"
"errors" "errors"
"time" "time"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/pavlo-v-chernykh/keystore-go/v4" "github.com/pavlo-v-chernykh/keystore-go/v4"
"software.sslmate.com/src/go-pkcs12" "software.sslmate.com/src/go-pkcs12"
) )
@ -28,23 +27,9 @@ func TransformCertificateFromPEMToPFX(certPem string, privkeyPem string, pfxPass
return nil, err return nil, err
} }
var privkey interface{} privkey, err := certcrypto.ParsePEMPrivateKey([]byte(privkeyPem))
switch cert.PublicKey.(type) { if err != nil {
case *rsa.PublicKey: return nil, err
{
privkey, err = ParsePKCS1PrivateKeyFromPEM(privkeyPem)
if err != nil {
return nil, err
}
}
case *ecdsa.PublicKey:
{
privkey, err = ParseECPrivateKeyFromPEM(privkeyPem)
if err != nil {
return nil, err
}
}
} }
pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, pfxPassword) pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, pfxPassword)

View File

@ -4,17 +4,18 @@ import {
CheckCircleOutlined as CheckCircleOutlinedIcon, CheckCircleOutlined as CheckCircleOutlinedIcon,
ClockCircleOutlined as ClockCircleOutlinedIcon, ClockCircleOutlined as ClockCircleOutlinedIcon,
CloseCircleOutlined as CloseCircleOutlinedIcon, CloseCircleOutlined as CloseCircleOutlinedIcon,
DeleteOutlined as DeleteOutlinedIcon,
SelectOutlined as SelectOutlinedIcon, SelectOutlined as SelectOutlinedIcon,
SyncOutlined as SyncOutlinedIcon, SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useRequest } from "ahooks"; import { useRequest } from "ahooks";
import { Button, Empty, Table, type TableProps, Tag, notification } from "antd"; import { Button, Empty, Modal, Table, type TableProps, Tag, Tooltip, notification } from "antd";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase"; import { ClientResponseError } from "pocketbase";
import { WORKFLOW_TRIGGERS } from "@/domain/workflow"; import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun"; import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
import { list as listWorkflowRuns } from "@/repository/workflowRun"; import { list as listWorkflowRuns, remove as removeWorkflowRun } from "@/repository/workflowRun";
import { getErrMsg } from "@/utils/error"; import { getErrMsg } from "@/utils/error";
import WorkflowRunDetailDrawer from "./WorkflowRunDetailDrawer"; import WorkflowRunDetailDrawer from "./WorkflowRunDetailDrawer";
@ -27,6 +28,7 @@ export type WorkflowRunsProps = {
const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [modalApi, ModelContextHolder] = Modal.useModal();
const [notificationApi, NotificationContextHolder] = notification.useNotification(); const [notificationApi, NotificationContextHolder] = notification.useNotification();
const tableColumns: TableProps<WorkflowRunModel>["columns"] = [ const tableColumns: TableProps<WorkflowRunModel>["columns"] = [
@ -118,7 +120,27 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
width: 120, width: 120,
render: (_, record) => ( render: (_, record) => (
<Button.Group> <Button.Group>
<WorkflowRunDetailDrawer data={record} trigger={<Button color="primary" icon={<SelectOutlinedIcon />} variant="text" />} /> <WorkflowRunDetailDrawer
data={record}
trigger={
<Tooltip title={t("workflow_run.action.view")}>
<Button color="primary" icon={<SelectOutlinedIcon />} variant="text" />
</Tooltip>
}
/>
<Tooltip title={t("workflow_run.action.delete")}>
<Button
color="danger"
danger
disabled={record.status !== WORKFLOW_RUN_STATUSES.SUCCEEDED && record.status !== WORKFLOW_RUN_STATUSES.FAILED}
icon={<DeleteOutlinedIcon />}
variant="text"
onClick={() => {
handleDeleteClick(record);
}}
/>
</Tooltip>
</Button.Group> </Button.Group>
), ),
}, },
@ -129,7 +151,11 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
const [page, setPage] = useState<number>(1); const [page, setPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10); const [pageSize, setPageSize] = useState<number>(10);
const { loading, error: loadedError } = useRequest( const {
loading,
error: loadedError,
run: refreshData,
} = useRequest(
() => { () => {
return listWorkflowRuns({ return listWorkflowRuns({
workflowId: workflowId, workflowId: workflowId,
@ -156,8 +182,28 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
} }
); );
const handleDeleteClick = (workflowRun: WorkflowRunModel) => {
modalApi.confirm({
title: t("workflow_run.action.delete"),
content: t("workflow_run.action.delete.confirm"),
onOk: async () => {
try {
const resp = await removeWorkflowRun(workflowRun);
if (resp) {
setTableData((prev) => prev.filter((item) => item.id !== workflowRun.id));
refreshData();
}
} catch (err) {
console.error(err);
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
}
},
});
};
return ( return (
<> <>
{ModelContextHolder}
{NotificationContextHolder} {NotificationContextHolder}
<div className={className} style={style}> <div className={className} style={style}>

View File

@ -1,4 +1,8 @@
{ {
"workflow_run.action.view": "View detail",
"workflow_run.action.delete": "Delete history run",
"workflow_run.action.delete.confirm": "Are you sure to delete this history run?",
"workflow_run.props.id": "ID", "workflow_run.props.id": "ID",
"workflow_run.props.status": "Status", "workflow_run.props.status": "Status",
"workflow_run.props.status.pending": "Pending", "workflow_run.props.status.pending": "Pending",

View File

@ -1,4 +1,8 @@
{ {
"workflow_run.action.edit": "查看详情",
"workflow_run.action.delete": "删除执行历史",
"workflow_run.action.delete.confirm": "确定要删除此执行历史吗?",
"workflow_run.props.id": "ID", "workflow_run.props.id": "ID",
"workflow_run.props.status": "状态", "workflow_run.props.status": "状态",
"workflow_run.props.status.pending": "等待执行", "workflow_run.props.status.pending": "等待执行",

View File

@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { import {
CheckCircleOutlined as CheckCircleOutlinedIcon, CheckCircleOutlined as CheckCircleOutlinedIcon,
ClockCircleOutlined as ClockCircleOutlinedIcon,
CloseCircleOutlined as CloseCircleOutlinedIcon, CloseCircleOutlined as CloseCircleOutlinedIcon,
DeleteOutlined as DeleteOutlinedIcon, DeleteOutlined as DeleteOutlinedIcon,
EditOutlined as EditOutlinedIcon, EditOutlined as EditOutlinedIcon,
@ -13,7 +14,6 @@ import {
import { PageHeader } from "@ant-design/pro-components"; import { PageHeader } from "@ant-design/pro-components";
import { useRequest } from "ahooks"; import { useRequest } from "ahooks";
import { import {
Badge,
Button, Button,
Divider, Divider,
Empty, Empty,
@ -159,32 +159,23 @@ const WorkflowList = () => {
key: "lastRun", key: "lastRun",
title: t("workflow.props.last_run_at"), title: t("workflow.props.last_run_at"),
render: (_, record) => { render: (_, record) => {
if (record.lastRunId) { let icon = <></>;
if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.RUNNING) { if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.PENDING) {
return ( icon = <ClockCircleOutlinedIcon style={{ color: themeToken.colorTextSecondary }} />;
<Space> } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.RUNNING) {
<Badge status="processing" count={<SyncOutlinedIcon style={{ color: themeToken.colorInfo }} />} /> icon = <SyncOutlinedIcon style={{ color: themeToken.colorInfo }} spin />;
<Typography.Text>{dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss")}</Typography.Text> } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.SUCCEEDED) {
</Space> icon = <CheckCircleOutlinedIcon style={{ color: themeToken.colorSuccess }} />;
); } else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.FAILED) {
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.SUCCEEDED) { icon = <CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />;
return (
<Space>
<Badge status="success" count={<CheckCircleOutlinedIcon style={{ color: themeToken.colorSuccess }} />} />
<Typography.Text>{dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss")}</Typography.Text>
</Space>
);
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.FAILED) {
return (
<Space>
<Badge status="error" count={<CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />} />
<Typography.Text>{dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss")}</Typography.Text>
</Space>
);
}
} }
return <></>; return (
<Space>
{icon}
<Typography.Text>{record.lastRunTime ? dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss") : ""}</Typography.Text>
</Space>
);
}, },
}, },
{ {

View File

@ -31,3 +31,7 @@ export const list = async (request: ListWorkflowRunsRequest) => {
expand: request.expand ? "workflowId" : undefined, expand: request.expand ? "workflowId" : undefined,
}); });
}; };
export const remove = async (record: MaybeModelRecordWithId<WorkflowRunModel>) => {
return await getPocketBase().collection(COLLECTION_NAME).delete(record.id);
};