mirror of
https://github.com/usual2970/certimate.git
synced 2025-10-05 14:04:54 +00:00
Merge branch 'feat/new-workflow' of github.com:fudiwei/certimate into next
This commit is contained in:
@@ -3,10 +3,14 @@ import { ClientResponseError } from "pocketbase";
|
||||
import { type CertificateFormatType } from "@/domain/certificate";
|
||||
import { getPocketBase } from "@/repository/_pocketbase";
|
||||
|
||||
type ArchiveRespData = {
|
||||
fileBytes: string;
|
||||
};
|
||||
|
||||
export const archive = async (certificateId: string, format?: CertificateFormatType) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
const resp = await pb.send<BaseResponse<string>>(`/api/certificates/${encodeURIComponent(certificateId)}/archive`, {
|
||||
const resp = await pb.send<BaseResponse<ArchiveRespData>>(`/api/certificates/${encodeURIComponent(certificateId)}/archive`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -24,6 +28,7 @@ export const archive = async (certificateId: string, format?: CertificateFormatT
|
||||
};
|
||||
|
||||
type ValidateCertificateResp = {
|
||||
isValid: boolean;
|
||||
domains: string;
|
||||
};
|
||||
|
||||
@@ -46,9 +51,13 @@ export const validateCertificate = async (certificate: string) => {
|
||||
return resp;
|
||||
};
|
||||
|
||||
type ValidatePrivateKeyResp = {
|
||||
isValid: boolean;
|
||||
};
|
||||
|
||||
export const validatePrivateKey = async (privateKey: string) => {
|
||||
const pb = getPocketBase();
|
||||
const resp = await pb.send<BaseResponse>(`/api/certificates/validate/private-key`, {
|
||||
const resp = await pb.send<BaseResponse<ValidatePrivateKeyResp>>(`/api/certificates/validate/private-key`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
@@ -22,7 +22,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
|
||||
const handleDownloadClick = async (format: CertificateFormatType) => {
|
||||
try {
|
||||
const res = await archiveCertificate(data.id, format);
|
||||
const bstr = atob(res.data);
|
||||
const bstr = atob(res.data.fileBytes);
|
||||
const u8arr = Uint8Array.from(bstr, (ch) => ch.charCodeAt(0));
|
||||
const blob = new Blob([u8arr], { type: "application/zip" });
|
||||
saveAs(blob, `${data.id}-${data.subjectAltNames}.zip`);
|
||||
@@ -38,11 +38,27 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
|
||||
|
||||
<Form layout="vertical">
|
||||
<Form.Item label={t("certificate.props.subject_alt_names")}>
|
||||
<Input value={data.subjectAltNames} placeholder="" />
|
||||
<Input value={data.subjectAltNames} variant="filled" placeholder="" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("certificate.props.issuer")}>
|
||||
<Input value={data.issuer} variant="filled" placeholder="" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("certificate.props.validity")}>
|
||||
<Input value={`${dayjs(data.effectAt).format("YYYY-MM-DD HH:mm:ss")} ~ ${dayjs(data.expireAt).format("YYYY-MM-DD HH:mm:ss")}`} placeholder="" />
|
||||
<Input
|
||||
value={`${dayjs(data.effectAt).format("YYYY-MM-DD HH:mm:ss")} ~ ${dayjs(data.expireAt).format("YYYY-MM-DD HH:mm:ss")}`}
|
||||
variant="filled"
|
||||
placeholder=""
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("certificate.props.serial_number")}>
|
||||
<Input value={data.serialNumber} variant="filled" placeholder="" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("certificate.props.key_algorithm")}>
|
||||
<Input value={data.keyAlgorithm} variant="filled" placeholder="" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
@@ -59,7 +75,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input.TextArea value={data.certificate} rows={10} autoSize={{ maxRows: 10 }} readOnly />
|
||||
<Input.TextArea value={data.certificate} variant="filled" rows={5} autoSize={{ maxRows: 5 }} readOnly />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
@@ -76,7 +92,7 @@ const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => {
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Input.TextArea value={data.privateKey} rows={10} autoSize={{ maxRows: 10 }} readOnly />
|
||||
<Input.TextArea value={data.privateKey} variant="filled" rows={5} autoSize={{ maxRows: 5 }} readOnly />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
|
163
ui/src/components/workflow/WorkflowRunDetail.tsx
Normal file
163
ui/src/components/workflow/WorkflowRunDetail.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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;
|
||||
style?: React.CSSProperties;
|
||||
data: WorkflowRunModel;
|
||||
};
|
||||
|
||||
const WorkflowRunDetail = ({ data, ...props }: WorkflowRunDetailProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
<Show when={data.status === WORKFLOW_RUN_STATUSES.SUCCEEDED}>
|
||||
<Alert showIcon type="success" message={<Typography.Text type="success">{t("workflow_run.props.status.succeeded")}</Typography.Text>} />
|
||||
</Show>
|
||||
|
||||
<Show when={data.status === WORKFLOW_RUN_STATUSES.FAILED}>
|
||||
<Alert showIcon type="error" message={<Typography.Text type="danger">{t("workflow_run.props.status.failed")}</Typography.Text>} />
|
||||
</Show>
|
||||
|
||||
<div className="my-4">
|
||||
<Typography.Title level={5}>{t("workflow_run.logs")}</Typography.Title>
|
||||
<div className="rounded-md bg-black p-4 text-stone-200">
|
||||
<div className="flex flex-col space-y-4">
|
||||
{data.logs?.map((item, i) => {
|
||||
return (
|
||||
<div key={i} className="flex flex-col space-y-2">
|
||||
<div className="font-semibold">{item.nodeName}</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
{item.records?.map((output, j) => {
|
||||
return (
|
||||
<div key={j} className="flex space-x-2 text-sm" style={{ wordBreak: "break-word" }}>
|
||||
<div className="whitespace-nowrap">[{dayjs(output.time).format("YYYY-MM-DD HH:mm:ss")}]</div>
|
||||
{output.error ? <div className="text-red-500">{output.error}</div> : <div>{output.content}</div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</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;
|
@@ -1,12 +1,12 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useControllableValue } from "ahooks";
|
||||
import { Alert, Drawer, Typography } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { Drawer } from "antd";
|
||||
|
||||
import Show from "@/components/Show";
|
||||
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
|
||||
import { type WorkflowRunModel } from "@/domain/workflowRun";
|
||||
import { useTriggerElement } from "@/hooks";
|
||||
|
||||
import WorkflowRunDetail from "./WorkflowRunDetail";
|
||||
|
||||
export type WorkflowRunDetailDrawerProps = {
|
||||
data?: WorkflowRunModel;
|
||||
loading?: boolean;
|
||||
@@ -16,8 +16,6 @@ export type WorkflowRunDetailDrawerProps = {
|
||||
};
|
||||
|
||||
const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowRunDetailDrawerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [open, setOpen] = useControllableValue<boolean>(props, {
|
||||
valuePropName: "open",
|
||||
defaultValuePropName: "defaultOpen",
|
||||
@@ -30,37 +28,19 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR
|
||||
<>
|
||||
{triggerEl}
|
||||
|
||||
<Drawer destroyOnClose open={open} loading={loading} placement="right" title={`WorkflowRun #${data?.id}`} width={640} onClose={() => setOpen(false)}>
|
||||
<Drawer
|
||||
afterOpenChange={setOpen}
|
||||
closable
|
||||
destroyOnClose
|
||||
open={open}
|
||||
loading={loading}
|
||||
placement="right"
|
||||
title={`WorkflowRun #${data?.id}`}
|
||||
width={640}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<Show when={!!data}>
|
||||
<Show when={data!.status === WORKFLOW_RUN_STATUSES.SUCCEEDED}>
|
||||
<Alert showIcon type="success" message={<Typography.Text type="success">{t("workflow_run.props.status.succeeded")}</Typography.Text>} />
|
||||
</Show>
|
||||
|
||||
<Show when={data!.status === WORKFLOW_RUN_STATUSES.FAILED}>
|
||||
<Alert showIcon type="error" message={<Typography.Text type="danger">{t("workflow_run.props.status.failed")}</Typography.Text>} />
|
||||
</Show>
|
||||
|
||||
<div className="mt-4 rounded-md bg-black p-4 text-stone-200">
|
||||
<div className="flex flex-col space-y-4">
|
||||
{data!.logs?.map((item, i) => {
|
||||
return (
|
||||
<div key={i} className="flex flex-col space-y-2">
|
||||
<div className="font-semibold">{item.nodeName}</div>
|
||||
<div className="flex flex-col space-y-1">
|
||||
{item.outputs?.map((output, j) => {
|
||||
return (
|
||||
<div key={j} className="flex space-x-2 text-sm" style={{ wordBreak: "break-word" }}>
|
||||
<div className="whitespace-nowrap">[{dayjs(output.time).format("YYYY-MM-DD HH:mm:ss")}]</div>
|
||||
{output.error ? <div className="text-red-500">{output.error}</div> : <div>{output.content}</div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<WorkflowRunDetail data={data!} />
|
||||
</Show>
|
||||
</Drawer>
|
||||
</>
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
CheckCircleOutlined as CheckCircleOutlinedIcon,
|
||||
ClockCircleOutlined as ClockCircleOutlinedIcon,
|
||||
CloseCircleOutlined as CloseCircleOutlinedIcon,
|
||||
DeleteOutlined as DeleteOutlinedIcon,
|
||||
PauseCircleOutlined as PauseCircleOutlinedIcon,
|
||||
PauseOutlined as PauseOutlinedIcon,
|
||||
SelectOutlined as SelectOutlinedIcon,
|
||||
StopOutlined as StopOutlinedIcon,
|
||||
SyncOutlined as SyncOutlinedIcon,
|
||||
} from "@ant-design/icons";
|
||||
import { useRequest } from "ahooks";
|
||||
@@ -18,7 +18,12 @@ import { ClientResponseError } from "pocketbase";
|
||||
import { cancelRun as cancelWorkflowRun } from "@/api/workflows";
|
||||
import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
|
||||
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
|
||||
import { list as listWorkflowRuns, remove as removeWorkflowRun } from "@/repository/workflowRun";
|
||||
import {
|
||||
list as listWorkflowRuns,
|
||||
remove as removeWorkflowRun,
|
||||
subscribe as subscribeWorkflowRun,
|
||||
unsubscribe as unsubscribeWorkflowRun,
|
||||
} from "@/repository/workflowRun";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
import WorkflowRunDetailDrawer from "./WorkflowRunDetailDrawer";
|
||||
|
||||
@@ -75,7 +80,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
);
|
||||
} else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) {
|
||||
return (
|
||||
<Tag icon={<PauseCircleOutlinedIcon />} color="warning">
|
||||
<Tag icon={<StopOutlinedIcon />} color="warning">
|
||||
{t("workflow_run.props.status.canceled")}
|
||||
</Tag>
|
||||
);
|
||||
@@ -211,6 +216,31 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const items = tableData.filter((e) => e.status === WORKFLOW_RUN_STATUSES.PENDING || e.status === WORKFLOW_RUN_STATUSES.RUNNING);
|
||||
for (const item of items) {
|
||||
subscribeWorkflowRun(item.id, (cb) => {
|
||||
setTableData((prev) => {
|
||||
const index = prev.findIndex((e) => e.id === item.id);
|
||||
if (index !== -1) {
|
||||
prev[index] = cb.record;
|
||||
}
|
||||
return [...prev];
|
||||
});
|
||||
|
||||
if (cb.record.status !== WORKFLOW_RUN_STATUSES.PENDING && cb.record.status !== WORKFLOW_RUN_STATUSES.RUNNING) {
|
||||
unsubscribeWorkflowRun(item.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const item of items) {
|
||||
unsubscribeWorkflowRun(item.id);
|
||||
}
|
||||
};
|
||||
}, [tableData]);
|
||||
|
||||
const handleCancelClick = (workflowRun: WorkflowRunModel) => {
|
||||
modalApi.confirm({
|
||||
title: t("workflow_run.action.cancel"),
|
||||
@@ -275,7 +305,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
rowKey={(record: WorkflowRunModel) => record.id}
|
||||
rowKey={(record) => record.id}
|
||||
scroll={{ x: "max(100%, 960px)" }}
|
||||
/>
|
||||
</div>
|
||||
|
@@ -56,6 +56,7 @@ const ApplyNode = ({ node, disabled }: ApplyNodeProps) => {
|
||||
const newNode = produce(node, (draft) => {
|
||||
draft.config = {
|
||||
...newValues,
|
||||
challengeType: newValues.challengeType || "dns-01", // 默认使用 DNS-01 认证
|
||||
};
|
||||
draft.validated = true;
|
||||
});
|
||||
|
@@ -56,6 +56,7 @@ const MULTIPLE_INPUT_DELIMITER = ";";
|
||||
|
||||
const initFormModel = (): ApplyNodeConfigFormFieldValues => {
|
||||
return {
|
||||
challengeType: "dns-01",
|
||||
keyAlgorithm: "RSA2048",
|
||||
skipBeforeExpiryDays: 20,
|
||||
};
|
||||
@@ -74,6 +75,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
.every((e) => validDomainName(e, { allowWildcard: true }));
|
||||
}, t("common.errmsg.domain_invalid")),
|
||||
contactEmail: z.string({ message: t("workflow_node.apply.form.contact_email.placeholder") }).email(t("common.errmsg.email_invalid")),
|
||||
challengeType: z.string().nullish(),
|
||||
provider: z.string({ message: t("workflow_node.apply.form.provider.placeholder") }).nonempty(t("workflow_node.apply.form.provider.placeholder")),
|
||||
providerAccessId: z
|
||||
.string({ message: t("workflow_node.apply.form.provider_access.placeholder") })
|
||||
@@ -235,6 +237,16 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
<EmailInput placeholder={t("workflow_node.apply.form.contact_email.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="challengeType" label={t("workflow_node.apply.form.challenge_type.label")} rules={[formRule]} hidden>
|
||||
<Select
|
||||
options={["DNS-01"].map((e) => ({
|
||||
label: e,
|
||||
value: e.toLowerCase(),
|
||||
}))}
|
||||
placeholder={t("workflow_node.apply.form.challenge_type.placeholder")}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="provider" label={t("workflow_node.apply.form.provider.label")} hidden rules={[formRule]}>
|
||||
<ApplyDNSProviderSelect
|
||||
allowClear
|
||||
|
@@ -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";
|
||||
|
||||
|
@@ -137,11 +137,11 @@ const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNode
|
||||
return (
|
||||
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
|
||||
<Form.Item name="domains" label={t("workflow_node.upload.form.domains.label")} rules={[formRule]}>
|
||||
<Input placeholder={t("workflow_node.upload.form.domains.placeholder")} readOnly />
|
||||
<Input variant="filled" placeholder={t("workflow_node.upload.form.domains.placeholder")} readOnly />
|
||||
</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>
|
||||
|
@@ -3,8 +3,11 @@ import { type WorkflowModel } from "./workflow";
|
||||
export interface CertificateModel extends BaseModel {
|
||||
source: string;
|
||||
subjectAltNames: string;
|
||||
serialNumber: string;
|
||||
certificate: string;
|
||||
privateKey: string;
|
||||
issuer: string;
|
||||
keyAlgorithm: string;
|
||||
effectAt: ISO8601String;
|
||||
expireAt: ISO8601String;
|
||||
workflowId: string;
|
||||
|
@@ -122,6 +122,7 @@ export type WorkflowNodeConfigForStart = {
|
||||
export type WorkflowNodeConfigForApply = {
|
||||
domains: string;
|
||||
contactEmail: string;
|
||||
challengeType: string;
|
||||
provider: string;
|
||||
providerAccessId: string;
|
||||
providerConfig?: Record<string, unknown>;
|
||||
@@ -276,21 +277,21 @@ export const updateNode = (node: WorkflowNode, targetNode: WorkflowNode) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const addNode = (node: WorkflowNode, preId: string, targetNode: WorkflowNode) => {
|
||||
export const addNode = (node: WorkflowNode, previousNodeId: string, targetNode: WorkflowNode) => {
|
||||
return produce(node, (draft) => {
|
||||
let current = draft;
|
||||
while (current) {
|
||||
if (current.id === preId && targetNode.type !== WorkflowNodeType.Branch && targetNode.type !== WorkflowNodeType.ExecuteResultBranch) {
|
||||
if (current.id === previousNodeId && targetNode.type !== WorkflowNodeType.Branch && targetNode.type !== WorkflowNodeType.ExecuteResultBranch) {
|
||||
targetNode.next = current.next;
|
||||
current.next = targetNode;
|
||||
break;
|
||||
} else if (current.id === preId && (targetNode.type === WorkflowNodeType.Branch || targetNode.type === WorkflowNodeType.ExecuteResultBranch)) {
|
||||
} else if (current.id === previousNodeId && (targetNode.type === WorkflowNodeType.Branch || targetNode.type === WorkflowNodeType.ExecuteResultBranch)) {
|
||||
targetNode.branches![0].next = current.next;
|
||||
current.next = targetNode;
|
||||
break;
|
||||
}
|
||||
if (current.type === WorkflowNodeType.Branch || current.type === WorkflowNodeType.ExecuteResultBranch) {
|
||||
current.branches = current.branches!.map((branch) => addNode(branch, preId, targetNode));
|
||||
current.branches = current.branches!.map((branch) => addNode(branch, previousNodeId, targetNode));
|
||||
}
|
||||
current = current.next as WorkflowNode;
|
||||
}
|
||||
@@ -382,15 +383,15 @@ export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchInd
|
||||
});
|
||||
};
|
||||
|
||||
// 1 个分支的节点,不应该能获取到相邻分支上节点的输出
|
||||
export const getWorkflowOutputBeforeId = (node: WorkflowNode, id: string, type: string): WorkflowNode[] => {
|
||||
export const getWorkflowOutputBeforeId = (root: WorkflowNode, nodeId: string, type: string): WorkflowNode[] => {
|
||||
// 1 个分支的节点,不应该能获取到相邻分支上节点的输出
|
||||
const output: WorkflowNode[] = [];
|
||||
|
||||
const traverse = (current: WorkflowNode, output: WorkflowNode[]) => {
|
||||
if (!current) {
|
||||
return false;
|
||||
}
|
||||
if (current.id === id) {
|
||||
if (current.id === nodeId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -422,7 +423,7 @@ export const getWorkflowOutputBeforeId = (node: WorkflowNode, id: string, type:
|
||||
return traverse(current.next as WorkflowNode, output);
|
||||
};
|
||||
|
||||
traverse(node, output);
|
||||
traverse(root, output);
|
||||
return output;
|
||||
};
|
||||
|
||||
@@ -446,21 +447,3 @@ export const isAllNodesValidated = (node: WorkflowNode): boolean => {
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export const getExecuteMethod = (node: WorkflowNode): { trigger: string; triggerCron: string } => {
|
||||
if (node.type === WorkflowNodeType.Start) {
|
||||
const config = node.config as WorkflowNodeConfigForStart;
|
||||
return {
|
||||
trigger: config.trigger ?? "",
|
||||
triggerCron: config.triggerCron ?? "",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
trigger: "",
|
||||
triggerCron: "",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { WorkflowModel } from "./workflow";
|
||||
import { type WorkflowModel } from "./workflow";
|
||||
|
||||
export interface WorkflowRunModel extends BaseModel {
|
||||
workflowId: string;
|
||||
@@ -16,13 +16,13 @@ export interface WorkflowRunModel extends BaseModel {
|
||||
export type WorkflowRunLog = {
|
||||
nodeId: string;
|
||||
nodeName: string;
|
||||
outputs?: WorkflowRunLogOutput[];
|
||||
records?: WorkflowRunLogRecord[];
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type WorkflowRunLogOutput = {
|
||||
export type WorkflowRunLogRecord = {
|
||||
time: ISO8601String;
|
||||
title: string;
|
||||
level: string;
|
||||
content: string;
|
||||
error?: string;
|
||||
};
|
||||
|
@@ -15,11 +15,15 @@
|
||||
"certificate.props.validity.expiration": "Expire on {{date}}",
|
||||
"certificate.props.validity.filter.expire_soon": "Expire soon",
|
||||
"certificate.props.validity.filter.expired": "Expired",
|
||||
"certificate.props.brand": "Brand",
|
||||
"certificate.props.source": "Source",
|
||||
"certificate.props.source.workflow": "Workflow",
|
||||
"certificate.props.source.upload": "Upload",
|
||||
"certificate.props.certificate": "Certificate chain",
|
||||
"certificate.props.private_key": "Private key",
|
||||
"certificate.props.serial_number": "Serial number",
|
||||
"certificate.props.key_algorithm": "Key algorithm",
|
||||
"certificate.props.issuer": "Issuer",
|
||||
"certificate.props.created_at": "Created at",
|
||||
"certificate.props.updated_at": "Updated at"
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@
|
||||
"workflow_node.start.form.trigger_cron.label": "Cron expression",
|
||||
"workflow_node.start.form.trigger_cron.placeholder": "Please enter cron expression",
|
||||
"workflow_node.start.form.trigger_cron.errmsg.invalid": "Please enter a valid cron expression",
|
||||
"workflow_node.start.form.trigger_cron.tooltip": "Time zone is based on the server.",
|
||||
"workflow_node.start.form.trigger_cron.tooltip": "Exactly 5 space separated segments. Time zone is based on the server.",
|
||||
"workflow_node.start.form.trigger_cron.extra": "Expected execution time for the last 5 times:",
|
||||
"workflow_node.start.form.trigger_cron.guide": "Tips: If you have multiple workflows, it is recommended to set them to run at multiple times of the day instead of always running at specific times.<br><br>Reference links:<br>1. <a href=\"https://letsencrypt.org/docs/rate-limits/\" target=\"_blank\">Let’s Encrypt rate limits</a><br>2. <a href=\"https://letsencrypt.org/docs/faq/#why-should-my-let-s-encrypt-acme-client-run-at-a-random-time\" target=\"_blank\">Why should my Let’s Encrypt (ACME) client run at a random time?</a>",
|
||||
|
||||
|
@@ -16,5 +16,12 @@
|
||||
"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.logs": "Logs",
|
||||
"workflow_run.artifacts": "Artifacts",
|
||||
|
||||
"workflow_run_artifact.props.type": "Type",
|
||||
"workflow_run_artifact.props.type.certificate": "Certificate",
|
||||
"workflow_run_artifact.props.name": "Name"
|
||||
}
|
||||
|
@@ -15,11 +15,15 @@
|
||||
"certificate.props.validity.expiration": "{{date}} 到期",
|
||||
"certificate.props.validity.filter.expire_soon": "即将到期",
|
||||
"certificate.props.validity.filter.expired": "已到期",
|
||||
"certificate.props.brand": "证书品牌",
|
||||
"certificate.props.source": "来源",
|
||||
"certificate.props.source.workflow": "工作流",
|
||||
"certificate.props.source.upload": "用户上传",
|
||||
"certificate.props.certificate": "证书内容",
|
||||
"certificate.props.private_key": "私钥内容",
|
||||
"certificate.props.serial_number": "证书序列号",
|
||||
"certificate.props.key_algorithm": "证书算法",
|
||||
"certificate.props.issuer": "颁发者",
|
||||
"certificate.props.created_at": "创建时间",
|
||||
"certificate.props.updated_at": "更新时间"
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@
|
||||
"workflow_node.start.form.trigger_cron.label": "Cron 表达式",
|
||||
"workflow_node.start.form.trigger_cron.placeholder": "请输入 Cron 表达式",
|
||||
"workflow_node.start.form.trigger_cron.errmsg.invalid": "请输入正确的 Cron 表达式",
|
||||
"workflow_node.start.form.trigger_cron.tooltip": "支持使用任意值(即 <strong>*</strong>)、值列表分隔符(即 <strong>,</strong>)、值的范围(即 <strong>-</strong>)、步骤值(即 <strong>/</strong>)等四种表达式,时区以服务器设置为准。",
|
||||
"workflow_node.start.form.trigger_cron.tooltip": "五段式表达式,支持使用任意值(即 <strong>*</strong>)、值列表分隔符(即 <strong>,</strong>)、值的范围(即 <strong>-</strong>)、步骤值(即 <strong>/</strong>)等四种表达式。时区以服务器设置为准。",
|
||||
"workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:",
|
||||
"workflow_node.start.form.trigger_cron.guide": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。<br><br>参考链接:<br>1. <a href=\"https://letsencrypt.org/zh-cn/docs/rate-limits/\" target=\"_blank\">Let’s Encrypt 速率限制</a><br>2. <a href=\"https://letsencrypt.org/zh-cn/docs/faq/#%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E7%9A%84-let-s-encrypt-acme-%E5%AE%A2%E6%88%B7%E7%AB%AF%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E5%BA%94%E5%BD%93%E9%9A%8F%E6%9C%BA\" target=\"_blank\">为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?</a>",
|
||||
|
||||
|
@@ -16,5 +16,12 @@
|
||||
"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.logs": "日志",
|
||||
"workflow_run.artifacts": "输出产物",
|
||||
|
||||
"workflow_run_artifact.props.type": "类型",
|
||||
"workflow_run_artifact.props.type.certificate": "证书",
|
||||
"workflow_run_artifact.props.name": "名称"
|
||||
}
|
||||
|
@@ -1,13 +1,8 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
line-height: 1.5;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@@ -15,8 +10,7 @@
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import dayjsUtc from "dayjs/plugin/utc";
|
||||
|
||||
import App from "./App";
|
||||
import "./i18n";
|
||||
import "./index.css";
|
||||
import "./global.css";
|
||||
|
||||
dayjs.extend(dayjsUtc);
|
||||
|
@@ -207,7 +207,7 @@ const AccessList = () => {
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
rowKey={(record: AccessModel) => record.id}
|
||||
rowKey={(record) => record.id}
|
||||
scroll={{ x: "max(100%, 960px)" }}
|
||||
/>
|
||||
</div>
|
||||
|
@@ -107,6 +107,16 @@ const CertificateList = () => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "issuer",
|
||||
title: t("certificate.props.brand"),
|
||||
render: (_, record) => (
|
||||
<Space className="max-w-full" direction="vertical" size={4}>
|
||||
<Typography.Text>{record.issuer}</Typography.Text>
|
||||
<Typography.Text>{record.keyAlgorithm}</Typography.Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "source",
|
||||
title: t("certificate.props.source"),
|
||||
@@ -250,7 +260,7 @@ const CertificateList = () => {
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={loadedError ? getErrMsg(loadedError) : t("certificate.nodata")} />,
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={getErrMsg(loadedError ?? t("certificate.nodata"))} />,
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
@@ -266,7 +276,7 @@ const CertificateList = () => {
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
rowKey={(record: CertificateModel) => record.id}
|
||||
rowKey={(record) => record.id}
|
||||
scroll={{ x: "max(100%, 960px)" }}
|
||||
/>
|
||||
</div>
|
||||
|
@@ -7,16 +7,15 @@ import {
|
||||
ClockCircleOutlined as ClockCircleOutlinedIcon,
|
||||
CloseCircleOutlined as CloseCircleOutlinedIcon,
|
||||
LockOutlined as LockOutlinedIcon,
|
||||
PauseCircleOutlined as PauseCircleOutlinedIcon,
|
||||
PlusOutlined as PlusOutlinedIcon,
|
||||
SelectOutlined as SelectOutlinedIcon,
|
||||
SendOutlined as SendOutlinedIcon,
|
||||
StopOutlined as StopOutlinedIcon,
|
||||
SyncOutlined as SyncOutlinedIcon,
|
||||
} 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,
|
||||
@@ -89,7 +88,6 @@ const Dashboard = () => {
|
||||
const workflow = record.expand?.workflowId;
|
||||
return (
|
||||
<Typography.Link
|
||||
type="secondary"
|
||||
ellipsis
|
||||
onClick={() => {
|
||||
if (workflow) {
|
||||
@@ -129,7 +127,7 @@ const Dashboard = () => {
|
||||
);
|
||||
} else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) {
|
||||
return (
|
||||
<Tag icon={<PauseCircleOutlinedIcon />} color="warning">
|
||||
<Tag icon={<StopOutlinedIcon />} color="warning">
|
||||
{t("workflow_run.props.status.canceled")}
|
||||
</Tag>
|
||||
);
|
||||
@@ -178,7 +176,7 @@ const Dashboard = () => {
|
||||
() => {
|
||||
return listWorkflowRuns({
|
||||
page: 1,
|
||||
perPage: 5,
|
||||
perPage: 9,
|
||||
expand: true,
|
||||
});
|
||||
},
|
||||
@@ -286,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>
|
||||
|
@@ -34,7 +34,7 @@ const SettingsPassword = () => {
|
||||
onSubmit: async (values) => {
|
||||
try {
|
||||
await authWithPassword(getAuthStore().record!.email, values.oldPassword);
|
||||
await saveAdmin({ password: values.newPassword });
|
||||
await saveAdmin({ password: values.newPassword, passwordConfirm: values.confirmPassword });
|
||||
|
||||
messageApi.success(t("common.text.operation_succeeded"));
|
||||
|
||||
|
@@ -42,7 +42,6 @@ const WorkflowDetail = () => {
|
||||
useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled", "release", "discard"])
|
||||
);
|
||||
useEffect(() => {
|
||||
// TODO: loading & error
|
||||
workflowState.init(workflowId!);
|
||||
|
||||
return () => {
|
||||
@@ -52,7 +51,7 @@ const WorkflowDetail = () => {
|
||||
|
||||
const [tabValue, setTabValue] = useState<"orchestration" | "runs">("orchestration");
|
||||
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [isPendingOrRunning, setIsPendingOrRunning] = useState(false);
|
||||
const lastRunStatus = useMemo(() => workflow.lastRunStatus, [workflow]);
|
||||
|
||||
const [allowDiscard, setAllowDiscard] = useState(false);
|
||||
@@ -60,14 +59,14 @@ const WorkflowDetail = () => {
|
||||
const [allowRun, setAllowRun] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING);
|
||||
setIsPendingOrRunning(lastRunStatus == WORKFLOW_RUN_STATUSES.PENDING || lastRunStatus == WORKFLOW_RUN_STATUSES.RUNNING);
|
||||
}, [lastRunStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!!workflowId && isRunning) {
|
||||
subscribeWorkflow(workflowId, (e) => {
|
||||
if (e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) {
|
||||
setIsRunning(false);
|
||||
if (!!workflowId && isPendingOrRunning) {
|
||||
subscribeWorkflow(workflowId, (cb) => {
|
||||
if (cb.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.PENDING && cb.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) {
|
||||
setIsPendingOrRunning(false);
|
||||
unsubscribeWorkflow(workflowId);
|
||||
}
|
||||
});
|
||||
@@ -76,15 +75,15 @@ const WorkflowDetail = () => {
|
||||
unsubscribeWorkflow(workflowId);
|
||||
};
|
||||
}
|
||||
}, [workflowId, isRunning]);
|
||||
}, [workflowId, isPendingOrRunning]);
|
||||
|
||||
useEffect(() => {
|
||||
const hasReleased = !!workflow.content;
|
||||
const hasChanges = workflow.hasDraft! || !isEqual(workflow.draft, workflow.content);
|
||||
setAllowDiscard(!isRunning && hasReleased && hasChanges);
|
||||
setAllowRelease(!isRunning && hasChanges);
|
||||
setAllowDiscard(!isPendingOrRunning && hasReleased && hasChanges);
|
||||
setAllowRelease(!isPendingOrRunning && hasChanges);
|
||||
setAllowRun(hasReleased);
|
||||
}, [workflow.content, workflow.draft, workflow.hasDraft, isRunning]);
|
||||
}, [workflow.content, workflow.draft, workflow.hasDraft, isPendingOrRunning]);
|
||||
|
||||
const handleEnableChange = async () => {
|
||||
if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) {
|
||||
@@ -174,12 +173,12 @@ const WorkflowDetail = () => {
|
||||
let unsubscribeFn: Awaited<ReturnType<typeof subscribeWorkflow>> | undefined = undefined;
|
||||
|
||||
try {
|
||||
setIsRunning(true);
|
||||
setIsPendingOrRunning(true);
|
||||
|
||||
// subscribe before running workflow
|
||||
unsubscribeFn = await subscribeWorkflow(workflowId!, (e) => {
|
||||
if (e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) {
|
||||
setIsRunning(false);
|
||||
if (e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.PENDING && e.record.lastRunStatus !== WORKFLOW_RUN_STATUSES.RUNNING) {
|
||||
setIsPendingOrRunning(false);
|
||||
unsubscribeFn?.();
|
||||
}
|
||||
});
|
||||
@@ -188,7 +187,7 @@ const WorkflowDetail = () => {
|
||||
|
||||
messageApi.info(t("workflow.detail.orchestration.action.run.prompt"));
|
||||
} catch (err) {
|
||||
setIsRunning(false);
|
||||
setIsPendingOrRunning(false);
|
||||
unsubscribeFn?.();
|
||||
|
||||
console.error(err);
|
||||
@@ -279,7 +278,7 @@ const WorkflowDetail = () => {
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Space>
|
||||
<Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={isRunning} type="primary" onClick={handleRunClick}>
|
||||
<Button disabled={!allowRun} icon={<CaretRightOutlinedIcon />} loading={isPendingOrRunning} type="primary" onClick={handleRunClick}>
|
||||
{t("workflow.detail.orchestration.action.run")}
|
||||
</Button>
|
||||
|
||||
|
@@ -7,8 +7,8 @@ import {
|
||||
CloseCircleOutlined as CloseCircleOutlinedIcon,
|
||||
DeleteOutlined as DeleteOutlinedIcon,
|
||||
EditOutlined as EditOutlinedIcon,
|
||||
PauseCircleOutlined as PauseCircleOutlinedIcon,
|
||||
PlusOutlined as PlusOutlinedIcon,
|
||||
StopOutlined as StopOutlinedIcon,
|
||||
SyncOutlined as SyncOutlinedIcon,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
@@ -170,7 +170,7 @@ const WorkflowList = () => {
|
||||
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.FAILED) {
|
||||
icon = <CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />;
|
||||
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.CANCELED) {
|
||||
icon = <PauseCircleOutlinedIcon style={{ color: themeToken.colorWarning }} />;
|
||||
icon = <StopOutlinedIcon style={{ color: themeToken.colorWarning }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -350,7 +350,7 @@ const WorkflowList = () => {
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={loadedError ? getErrMsg(loadedError) : t("workflow.nodata")} />,
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={getErrMsg(loadedError ?? t("workflow.nodata"))} />,
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
@@ -366,7 +366,7 @@ const WorkflowList = () => {
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
rowKey={(record: WorkflowModel) => record.id}
|
||||
rowKey={(record) => record.id}
|
||||
scroll={{ x: "max(100%, 960px)" }}
|
||||
/>
|
||||
</div>
|
||||
|
@@ -6,3 +6,11 @@ export const getPocketBase = () => {
|
||||
pb = new PocketBase("/");
|
||||
return pb;
|
||||
};
|
||||
|
||||
export const COLLECTION_NAME_ADMIN = "_superusers";
|
||||
export const COLLECTION_NAME_ACCESS = "access";
|
||||
export const COLLECTION_NAME_CERTIFICATE = "certificate";
|
||||
export const COLLECTION_NAME_SETTINGS = "settings";
|
||||
export const COLLECTION_NAME_WORKFLOW = "workflow";
|
||||
export const COLLECTION_NAME_WORKFLOW_RUN = "workflow_run";
|
||||
export const COLLECTION_NAME_WORKFLOW_OUTPUT = "workflow_output";
|
||||
|
@@ -1,12 +1,10 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { type AccessModel } from "@/domain/access";
|
||||
import { getPocketBase } from "./_pocketbase";
|
||||
|
||||
const COLLECTION_NAME = "access";
|
||||
import { COLLECTION_NAME_ACCESS, getPocketBase } from "./_pocketbase";
|
||||
|
||||
export const list = async () => {
|
||||
return await getPocketBase().collection(COLLECTION_NAME).getFullList<AccessModel>({
|
||||
return await getPocketBase().collection(COLLECTION_NAME_ACCESS).getFullList<AccessModel>({
|
||||
filter: "deleted=null",
|
||||
sort: "-created",
|
||||
requestKey: null,
|
||||
@@ -15,15 +13,15 @@ export const list = async () => {
|
||||
|
||||
export const save = async (record: MaybeModelRecord<AccessModel>) => {
|
||||
if (record.id) {
|
||||
return await getPocketBase().collection(COLLECTION_NAME).update<AccessModel>(record.id, record);
|
||||
return await getPocketBase().collection(COLLECTION_NAME_ACCESS).update<AccessModel>(record.id, record);
|
||||
}
|
||||
|
||||
return await getPocketBase().collection(COLLECTION_NAME).create<AccessModel>(record);
|
||||
return await getPocketBase().collection(COLLECTION_NAME_ACCESS).create<AccessModel>(record);
|
||||
};
|
||||
|
||||
export const remove = async (record: MaybeModelRecordWithId<AccessModel>) => {
|
||||
await getPocketBase()
|
||||
.collection(COLLECTION_NAME)
|
||||
.collection(COLLECTION_NAME_ACCESS)
|
||||
.update<AccessModel>(record.id!, { deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") });
|
||||
return true;
|
||||
};
|
||||
|
@@ -1,17 +1,15 @@
|
||||
import { getPocketBase } from "./_pocketbase";
|
||||
|
||||
const COLLECTION_NAME = "_superusers";
|
||||
import { COLLECTION_NAME_ADMIN, getPocketBase } from "./_pocketbase";
|
||||
|
||||
export const authWithPassword = (username: string, password: string) => {
|
||||
return getPocketBase().collection(COLLECTION_NAME).authWithPassword(username, password);
|
||||
return getPocketBase().collection(COLLECTION_NAME_ADMIN).authWithPassword(username, password);
|
||||
};
|
||||
|
||||
export const getAuthStore = () => {
|
||||
return getPocketBase().authStore;
|
||||
};
|
||||
|
||||
export const save = (data: { email: string } | { password: string }) => {
|
||||
export const save = (data: { email: string } | { password: string; passwordConfirm: string }) => {
|
||||
return getPocketBase()
|
||||
.collection(COLLECTION_NAME)
|
||||
.collection(COLLECTION_NAME_ADMIN)
|
||||
.update(getAuthStore().record?.id || "", data);
|
||||
};
|
||||
|
@@ -2,9 +2,7 @@ import dayjs from "dayjs";
|
||||
import { type RecordListOptions } from "pocketbase";
|
||||
|
||||
import { type CertificateModel } from "@/domain/certificate";
|
||||
import { getPocketBase } from "./_pocketbase";
|
||||
|
||||
const COLLECTION_NAME = "certificate";
|
||||
import { COLLECTION_NAME_CERTIFICATE, getPocketBase } from "./_pocketbase";
|
||||
|
||||
export type ListCertificateRequest = {
|
||||
page?: number;
|
||||
@@ -35,12 +33,29 @@ export const list = async (request: ListCertificateRequest) => {
|
||||
});
|
||||
}
|
||||
|
||||
return pb.collection(COLLECTION_NAME).getList<CertificateModel>(page, perPage, options);
|
||||
return pb.collection(COLLECTION_NAME_CERTIFICATE).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_CERTIFICATE).getFullList<CertificateModel>(options);
|
||||
return {
|
||||
totalItems: items.length,
|
||||
items: items,
|
||||
};
|
||||
};
|
||||
|
||||
export const remove = async (record: MaybeModelRecordWithId<CertificateModel>) => {
|
||||
await getPocketBase()
|
||||
.collection(COLLECTION_NAME)
|
||||
.collection(COLLECTION_NAME_CERTIFICATE)
|
||||
.update<CertificateModel>(record.id!, { deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") });
|
||||
return true;
|
||||
};
|
||||
|
@@ -1,13 +1,11 @@
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import { type SettingsModel, type SettingsNames } from "@/domain/settings";
|
||||
import { getPocketBase } from "./_pocketbase";
|
||||
|
||||
const COLLECTION_NAME = "settings";
|
||||
import { COLLECTION_NAME_SETTINGS, getPocketBase } from "./_pocketbase";
|
||||
|
||||
export const get = async <T extends NonNullable<unknown>>(name: SettingsNames) => {
|
||||
try {
|
||||
const resp = await getPocketBase().collection(COLLECTION_NAME).getFirstListItem<SettingsModel<T>>(`name='${name}'`, {
|
||||
const resp = await getPocketBase().collection(COLLECTION_NAME_SETTINGS).getFirstListItem<SettingsModel<T>>(`name='${name}'`, {
|
||||
requestKey: null,
|
||||
});
|
||||
return resp;
|
||||
@@ -25,8 +23,8 @@ export const get = async <T extends NonNullable<unknown>>(name: SettingsNames) =
|
||||
|
||||
export const save = async <T extends NonNullable<unknown>>(record: MaybeModelRecordWithId<SettingsModel<T>>) => {
|
||||
if (record.id) {
|
||||
return await getPocketBase().collection(COLLECTION_NAME).update<SettingsModel<T>>(record.id, record);
|
||||
return await getPocketBase().collection(COLLECTION_NAME_SETTINGS).update<SettingsModel<T>>(record.id, record);
|
||||
}
|
||||
|
||||
return await getPocketBase().collection(COLLECTION_NAME).create<SettingsModel<T>>(record);
|
||||
return await getPocketBase().collection(COLLECTION_NAME_SETTINGS).create<SettingsModel<T>>(record);
|
||||
};
|
||||
|
@@ -1,9 +1,7 @@
|
||||
import { type RecordListOptions, type RecordSubscription } from "pocketbase";
|
||||
|
||||
import { type WorkflowModel } from "@/domain/workflow";
|
||||
import { getPocketBase } from "./_pocketbase";
|
||||
|
||||
const COLLECTION_NAME = "workflow";
|
||||
import { COLLECTION_NAME_WORKFLOW, getPocketBase } from "./_pocketbase";
|
||||
|
||||
export type ListWorkflowRequest = {
|
||||
page?: number;
|
||||
@@ -26,11 +24,11 @@ export const list = async (request: ListWorkflowRequest) => {
|
||||
options.filter = pb.filter("enabled={:enabled}", { enabled: request.enabled });
|
||||
}
|
||||
|
||||
return await pb.collection(COLLECTION_NAME).getList<WorkflowModel>(page, perPage, options);
|
||||
return await pb.collection(COLLECTION_NAME_WORKFLOW).getList<WorkflowModel>(page, perPage, options);
|
||||
};
|
||||
|
||||
export const get = async (id: string) => {
|
||||
return await getPocketBase().collection(COLLECTION_NAME).getOne<WorkflowModel>(id, {
|
||||
return await getPocketBase().collection(COLLECTION_NAME_WORKFLOW).getOne<WorkflowModel>(id, {
|
||||
requestKey: null,
|
||||
});
|
||||
};
|
||||
@@ -38,25 +36,21 @@ export const get = async (id: string) => {
|
||||
export const save = async (record: MaybeModelRecord<WorkflowModel>) => {
|
||||
if (record.id) {
|
||||
return await getPocketBase()
|
||||
.collection(COLLECTION_NAME)
|
||||
.collection(COLLECTION_NAME_WORKFLOW)
|
||||
.update<WorkflowModel>(record.id as string, record);
|
||||
}
|
||||
|
||||
return await getPocketBase().collection(COLLECTION_NAME).create<WorkflowModel>(record);
|
||||
return await getPocketBase().collection(COLLECTION_NAME_WORKFLOW).create<WorkflowModel>(record);
|
||||
};
|
||||
|
||||
export const remove = async (record: MaybeModelRecordWithId<WorkflowModel>) => {
|
||||
return await getPocketBase().collection(COLLECTION_NAME).delete(record.id);
|
||||
return await getPocketBase().collection(COLLECTION_NAME_WORKFLOW).delete(record.id);
|
||||
};
|
||||
|
||||
export const subscribe = async (id: string, cb: (e: RecordSubscription<WorkflowModel>) => void) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
return pb.collection("workflow").subscribe(id, cb);
|
||||
return getPocketBase().collection(COLLECTION_NAME_WORKFLOW).subscribe(id, cb);
|
||||
};
|
||||
|
||||
export const unsubscribe = async (id: string) => {
|
||||
const pb = getPocketBase();
|
||||
|
||||
return pb.collection("workflow").unsubscribe(id);
|
||||
return getPocketBase().collection(COLLECTION_NAME_WORKFLOW).unsubscribe(id);
|
||||
};
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { type WorkflowRunModel } from "@/domain/workflowRun";
|
||||
import { type RecordSubscription } from "pocketbase";
|
||||
|
||||
import { getPocketBase } from "./_pocketbase";
|
||||
import { type WorkflowRunModel } from "@/domain/workflowRun";
|
||||
|
||||
const COLLECTION_NAME = "workflow_run";
|
||||
import { COLLECTION_NAME_WORKFLOW_RUN, getPocketBase } from "./_pocketbase";
|
||||
|
||||
export type ListWorkflowRunsRequest = {
|
||||
workflowId?: string;
|
||||
@@ -23,7 +23,7 @@ export const list = async (request: ListWorkflowRunsRequest) => {
|
||||
}
|
||||
|
||||
return await getPocketBase()
|
||||
.collection(COLLECTION_NAME)
|
||||
.collection(COLLECTION_NAME_WORKFLOW_RUN)
|
||||
.getList<WorkflowRunModel>(page, perPage, {
|
||||
filter: getPocketBase().filter(filter, params),
|
||||
sort: "-created",
|
||||
@@ -33,5 +33,13 @@ export const list = async (request: ListWorkflowRunsRequest) => {
|
||||
};
|
||||
|
||||
export const remove = async (record: MaybeModelRecordWithId<WorkflowRunModel>) => {
|
||||
return await getPocketBase().collection(COLLECTION_NAME).delete(record.id);
|
||||
return await getPocketBase().collection(COLLECTION_NAME_WORKFLOW_RUN).delete(record.id);
|
||||
};
|
||||
|
||||
export const subscribe = async (id: string, cb: (e: RecordSubscription<WorkflowRunModel>) => void) => {
|
||||
return getPocketBase().collection(COLLECTION_NAME_WORKFLOW_RUN).subscribe(id, cb);
|
||||
};
|
||||
|
||||
export const unsubscribe = async (id: string) => {
|
||||
return getPocketBase().collection(COLLECTION_NAME_WORKFLOW_RUN).unsubscribe(id);
|
||||
};
|
||||
|
@@ -25,14 +25,14 @@ export type WorkflowState = {
|
||||
discard(): void;
|
||||
destroy(): void;
|
||||
|
||||
addNode: (node: WorkflowNode, preId: string) => void;
|
||||
addNode: (node: WorkflowNode, previousNodeId: string) => void;
|
||||
updateNode: (node: WorkflowNode) => void;
|
||||
removeNode: (nodeId: string) => void;
|
||||
|
||||
addBranch: (branchId: string) => void;
|
||||
removeBranch: (branchId: string, index: number) => void;
|
||||
|
||||
getWorkflowOuptutBeforeId: (id: string, type: string) => WorkflowNode[];
|
||||
getWorkflowOuptutBeforeId: (nodeId: string, type: string) => WorkflowNode[];
|
||||
};
|
||||
|
||||
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||
@@ -143,10 +143,10 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
addNode: async (node: WorkflowNode, preId: string) => {
|
||||
addNode: async (node: WorkflowNode, previousNodeId: string) => {
|
||||
if (!get().initialized) throw "Workflow not initialized yet";
|
||||
|
||||
const root = addNode(get().workflow.draft!, preId, node);
|
||||
const root = addNode(get().workflow.draft!, previousNodeId, node);
|
||||
const resp = await saveWorkflow({
|
||||
id: get().workflow.id!,
|
||||
draft: root,
|
||||
@@ -243,7 +243,7 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
getWorkflowOuptutBeforeId: (id: string, type: string) => {
|
||||
return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type);
|
||||
getWorkflowOuptutBeforeId: (nodeId: string, type: string) => {
|
||||
return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, nodeId, type);
|
||||
},
|
||||
}));
|
||||
|
@@ -3,6 +3,8 @@
|
||||
export const validCronExpression = (expr: string): boolean => {
|
||||
try {
|
||||
parseExpression(expr);
|
||||
|
||||
if (expr.trim().split(" ").length !== 5) return false; // pocketbase 后端仅支持五段式的表达式
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -10,19 +12,15 @@ export const validCronExpression = (expr: string): boolean => {
|
||||
};
|
||||
|
||||
export const getNextCronExecutions = (expr: string, times = 1): Date[] => {
|
||||
if (!expr) return [];
|
||||
if (!validCronExpression(expr)) return [];
|
||||
|
||||
try {
|
||||
const now = new Date();
|
||||
const cron = parseExpression(expr, { currentDate: now, iterator: true });
|
||||
const now = new Date();
|
||||
const cron = parseExpression(expr, { currentDate: now, iterator: true });
|
||||
|
||||
const result: Date[] = [];
|
||||
for (let i = 0; i < times; i++) {
|
||||
const next = cron.next();
|
||||
result.push(next.value.toDate());
|
||||
}
|
||||
return result;
|
||||
} catch {
|
||||
return [];
|
||||
const result: Date[] = [];
|
||||
for (let i = 0; i < times; i++) {
|
||||
const next = cron.next();
|
||||
result.push(next.value.toDate());
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
@@ -7,13 +7,13 @@ export const getErrMsg = (error: unknown): string => {
|
||||
return error.message;
|
||||
} else if (typeof error === "object" && error != null) {
|
||||
if ("message" in error) {
|
||||
return String(error.message);
|
||||
return getErrMsg(error.message);
|
||||
} else if ("msg" in error) {
|
||||
return String(error.msg);
|
||||
return getErrMsg(error.msg);
|
||||
}
|
||||
} else if (typeof error === "string") {
|
||||
return error;
|
||||
return error || "Unknown error";
|
||||
}
|
||||
|
||||
return String(error ?? "Unknown error");
|
||||
return "Unknown error";
|
||||
};
|
||||
|
Reference in New Issue
Block a user