mirror of
https://github.com/certimate-go/certimate.git
synced 2026-06-16 21:09:50 +08:00
feat(ui): new WorkflowList page
This commit is contained in:
parent
004d8dbd0a
commit
b34ddf3e56
@ -38,11 +38,9 @@ const Empty = (props: EmptyProps) => {
|
||||
</Show>
|
||||
<div className="my-2">
|
||||
<Show when={!!title}>
|
||||
{isPrimitive(title) ? (
|
||||
<Typography.Title level={4}>{title}</Typography.Title>
|
||||
) : (
|
||||
<h4 style={{ color: themeToken.colorTextHeading }}>{title}</h4>
|
||||
)}
|
||||
<div className="pb-2 text-xl" style={{ color: themeToken.colorTextLabel }}>
|
||||
{title}
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!!description}>
|
||||
{isPrimitive(description) ? (
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useControllableValue } from "ahooks";
|
||||
import { Button, Drawer, Space, notification } from "antd";
|
||||
import { App, Button, Drawer, Space } from "antd";
|
||||
|
||||
import { type AccessModel } from "@/domain/access";
|
||||
import { useTriggerElement, useZustandShallowSelector } from "@/hooks";
|
||||
@ -24,7 +24,7 @@ export type AccessEditDrawerProps = {
|
||||
const AccessEditDrawer = ({ data, loading, trigger, scene, usage, afterSubmit, ...props }: AccessEditDrawerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
const { notification } = App.useApp();
|
||||
|
||||
const { createAccess, updateAccess } = useAccessesStore(useZustandShallowSelector(["createAccess", "updateAccess"]));
|
||||
|
||||
@ -70,7 +70,7 @@ const AccessEditDrawer = ({ data, loading, trigger, scene, usage, afterSubmit, .
|
||||
afterSubmit?.(values);
|
||||
setOpen(false);
|
||||
} catch (err) {
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
|
||||
throw err;
|
||||
} finally {
|
||||
@ -86,8 +86,6 @@ const AccessEditDrawer = ({ data, loading, trigger, scene, usage, afterSubmit, .
|
||||
|
||||
return (
|
||||
<>
|
||||
{NotificationContextHolder}
|
||||
|
||||
{triggerEl}
|
||||
|
||||
<Drawer
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useControllableValue } from "ahooks";
|
||||
import { Modal, notification } from "antd";
|
||||
import { App, Modal } from "antd";
|
||||
|
||||
import { type AccessModel } from "@/domain/access";
|
||||
import { useTriggerElement, useZustandShallowSelector } from "@/hooks";
|
||||
@ -24,7 +24,7 @@ export type AccessEditModalProps = {
|
||||
const AccessEditModal = ({ data, loading, trigger, scene, usage, afterSubmit, ...props }: AccessEditModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
const { notification } = App.useApp();
|
||||
|
||||
const { createAccess, updateAccess } = useAccessesStore(useZustandShallowSelector(["createAccess", "updateAccess"]));
|
||||
|
||||
@ -70,7 +70,7 @@ const AccessEditModal = ({ data, loading, trigger, scene, usage, afterSubmit, ..
|
||||
afterSubmit?.(values);
|
||||
setOpen(false);
|
||||
} catch (err) {
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
|
||||
throw err;
|
||||
} finally {
|
||||
@ -86,8 +86,6 @@ const AccessEditModal = ({ data, loading, trigger, scene, usage, afterSubmit, ..
|
||||
|
||||
return (
|
||||
<>
|
||||
{NotificationContextHolder}
|
||||
|
||||
{triggerEl}
|
||||
|
||||
<Modal
|
||||
@ -112,7 +110,7 @@ const AccessEditModal = ({ data, loading, trigger, scene, usage, afterSubmit, ..
|
||||
onOk={handleOkClick}
|
||||
onCancel={handleCancelClick}
|
||||
>
|
||||
<div className="pb-2 pt-4">
|
||||
<div className="pt-4 pb-2">
|
||||
<AccessForm ref={formRef} initialValues={data} scene={scene === "create" ? "create" : "edit"} usage={usage} />
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconBrowserShare, IconCheck, IconChevronRight, IconDownload, IconSettings2 } from "@tabler/icons-react";
|
||||
import { useRequest } from "ahooks";
|
||||
import { Button, Collapse, Divider, Dropdown, Empty, Flex, Skeleton, Spin, Table, type TableProps, Tooltip, Typography, notification, theme } from "antd";
|
||||
import { App, Button, Collapse, Divider, Dropdown, Empty, Flex, Skeleton, Spin, Table, type TableProps, Tooltip, Typography, theme } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
@ -276,7 +276,7 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
|
||||
const WorkflowRunArtifacts = ({ runId }: { runId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
const { notification } = App.useApp();
|
||||
|
||||
const tableColumns: TableProps<CertificateModel>["columns"] = [
|
||||
{
|
||||
@ -337,7 +337,7 @@ const WorkflowRunArtifacts = ({ runId }: { runId: string }) => {
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
|
||||
throw err;
|
||||
},
|
||||
@ -346,8 +346,6 @@ const WorkflowRunArtifacts = ({ runId }: { runId: string }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{NotificationContextHolder}
|
||||
|
||||
<Typography.Title level={5}>{t("workflow_run.artifacts")}</Typography.Title>
|
||||
<Table<CertificateModel>
|
||||
columns={tableColumns}
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconBrowserShare, IconPlayerPause, IconTrash } from "@tabler/icons-react";
|
||||
import { IconBrowserShare, IconHistory, IconPlayerPause, IconTrash } from "@tabler/icons-react";
|
||||
import { useRequest } from "ahooks";
|
||||
import { Alert, Button, Empty, Modal, Table, type TableProps, Tooltip, notification } from "antd";
|
||||
import { Alert, App, Button, Skeleton, Table, type TableProps, Tooltip } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import { cancelRun as cancelWorkflowRun } from "@/api/workflows";
|
||||
import Empty from "@/components/Empty";
|
||||
import WorkflowStatusTag from "@/components/workflow/WorkflowStatusTag";
|
||||
import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
|
||||
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
|
||||
@ -28,8 +29,7 @@ export type WorkflowRunsProps = {
|
||||
const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [modalApi, ModelContextHolder] = Modal.useModal();
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
const { modal, notification } = App.useApp();
|
||||
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [pageSize, setPageSize] = useState<number>(10);
|
||||
@ -108,27 +108,29 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<WorkflowRunDetailDrawer
|
||||
data={record}
|
||||
trigger={
|
||||
<Tooltip title={t("workflow_run.action.view")}>
|
||||
<Button color="primary" icon={<IconBrowserShare size="1.25em" />} variant="text" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
|
||||
<Tooltip title={t("workflow_run.action.view")}>
|
||||
<Button
|
||||
color="primary"
|
||||
icon={<IconBrowserShare size="1.25em" />}
|
||||
variant="text"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRecordDetailClick(record);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("workflow_run.action.cancel")}>
|
||||
<Button
|
||||
color="default"
|
||||
disabled={!allowCancel}
|
||||
icon={<IconPlayerPause size="1.25em" />}
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
handleCancelClick(record);
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRecordCancelClick(record);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={t("workflow_run.action.delete")}>
|
||||
<Button
|
||||
color="danger"
|
||||
@ -136,8 +138,9 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
disabled={!aloowDelete}
|
||||
icon={<IconTrash size="1.25em" />}
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
handleDeleteClick(record);
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRecordDeleteClick(record);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
@ -173,7 +176,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
|
||||
throw err;
|
||||
},
|
||||
@ -205,8 +208,16 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
};
|
||||
}, [tableData]);
|
||||
|
||||
const handleCancelClick = (workflowRun: WorkflowRunModel) => {
|
||||
modalApi.confirm({
|
||||
const [detailRecord, setDetailRecord] = useState<WorkflowRunModel>();
|
||||
const [detailOpen, setDetailOpen] = useState<boolean>(false);
|
||||
|
||||
const handleRecordDetailClick = (workflowRun: WorkflowRunModel) => {
|
||||
setDetailRecord(workflowRun);
|
||||
setDetailOpen(true);
|
||||
};
|
||||
|
||||
const handleRecordCancelClick = (workflowRun: WorkflowRunModel) => {
|
||||
modal.confirm({
|
||||
title: t("workflow_run.action.cancel"),
|
||||
content: t("workflow_run.action.cancel.confirm"),
|
||||
onOk: async () => {
|
||||
@ -217,14 +228,14 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteClick = (workflowRun: WorkflowRunModel) => {
|
||||
modalApi.confirm({
|
||||
const handleRecordDeleteClick = (workflowRun: WorkflowRunModel) => {
|
||||
modal.confirm({
|
||||
title: <span className="text-error">{t("workflow_run.action.delete")}</span>,
|
||||
content: (
|
||||
<span
|
||||
@ -247,46 +258,49 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{ModelContextHolder}
|
||||
{NotificationContextHolder}
|
||||
<div className={className} style={style}>
|
||||
<Alert className="mb-4" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_run.table.alert") }}></span>} showIcon type="info" />
|
||||
|
||||
<div className={className} style={style}>
|
||||
<Alert className="mb-4" type="warning" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_run.table.alert") }}></span>} />
|
||||
<Table<WorkflowRunModel>
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: loading ? <Skeleton /> : <Empty title={t("common.text.nodata")} icon={<IconHistory size={24} />} />,
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: pageSize,
|
||||
total: tableTotal,
|
||||
showSizeChanger: true,
|
||||
onChange: (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
onShowSizeChange: (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
rowClassName="cursor-pointer"
|
||||
rowKey={(record) => record.id}
|
||||
scroll={{ x: "max(100%, 960px)" }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => {
|
||||
handleRecordDetailClick(record);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
<Table<WorkflowRunModel>
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={loadedError ? getErrMsg(loadedError) : undefined} />,
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: pageSize,
|
||||
total: tableTotal,
|
||||
showSizeChanger: true,
|
||||
onChange: (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
onShowSizeChange: (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
rowKey={(record) => record.id}
|
||||
scroll={{ x: "max(100%, 960px)" }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<WorkflowRunDetailDrawer data={detailRecord} open={detailOpen} onOpenChange={setDetailOpen} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { memo, useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconCopy, IconDotsVertical, IconLabel, IconTrashX } from "@tabler/icons-react";
|
||||
import { useControllableValue } from "ahooks";
|
||||
import { Button, Card, Drawer, Dropdown, Input, type InputRef, type MenuProps, Modal, Popover, Space } from "antd";
|
||||
import { App, Button, Card, Drawer, Dropdown, Input, type InputRef, type MenuProps, Popover, Space } from "antd";
|
||||
import { produce } from "immer";
|
||||
import { isEqual } from "radash";
|
||||
|
||||
@ -95,12 +95,12 @@ const isNodeUnremovable = (node: WorkflowNode) => {
|
||||
const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex, afterUpdate, afterDelete }: SharedNodeMenuProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { modal } = App.useApp();
|
||||
|
||||
const { duplicateNode, updateNode, removeNode, duplicateBranch, removeBranch } = useWorkflowStore(
|
||||
useZustandShallowSelector(["duplicateNode", "updateNode", "removeNode", "duplicateBranch", "removeBranch"])
|
||||
);
|
||||
|
||||
const [modalApi, ModelContextHolder] = Modal.useModal();
|
||||
|
||||
const nameInputRef = useRef<InputRef>(null);
|
||||
const nameRef = useRef<string>();
|
||||
|
||||
@ -152,7 +152,7 @@ const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex,
|
||||
onClick: () => {
|
||||
nameRef.current = node.name;
|
||||
|
||||
const dialog = modalApi.confirm({
|
||||
const dialog = modal.confirm({
|
||||
title: isNodeBranchLike(node) ? t("workflow_node.action.rename_branch") : t("workflow_node.action.rename_node"),
|
||||
content: (
|
||||
<div className="pt-4 pb-2">
|
||||
@ -221,18 +221,14 @@ const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex,
|
||||
}, [disabled, node]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ModelContextHolder}
|
||||
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: menuItems,
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
{trigger}
|
||||
</Dropdown>
|
||||
</>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: menuItems,
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
{trigger}
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
// #endregion
|
||||
@ -305,7 +301,7 @@ const SharedNodeConfigDrawer = ({
|
||||
}: SharedNodeEditDrawerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [modalApi, ModelContextHolder] = Modal.useModal();
|
||||
const { modal } = App.useApp();
|
||||
|
||||
const [open, setOpen] = useControllableValue<boolean>(props, {
|
||||
valuePropName: "open",
|
||||
@ -333,7 +329,7 @@ const SharedNodeConfigDrawer = ({
|
||||
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
if (changed) {
|
||||
modalApi.confirm({
|
||||
modal.confirm({
|
||||
title: t("common.text.operation_confirm"),
|
||||
content: t("workflow_node.unsaved_changes.confirm"),
|
||||
onOk: () => resolve(void 0),
|
||||
@ -347,44 +343,40 @@ const SharedNodeConfigDrawer = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{ModelContextHolder}
|
||||
|
||||
<Drawer
|
||||
afterOpenChange={setOpen}
|
||||
closable={!pending}
|
||||
destroyOnHidden
|
||||
extra={
|
||||
<SharedNodeMenu
|
||||
menus={["rename", "remove"]}
|
||||
node={node}
|
||||
disabled={disabled}
|
||||
trigger={<Button icon={<IconDotsVertical size="1.25em" />} type="text" />}
|
||||
afterDelete={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
!!footer && (
|
||||
<Space className="w-full justify-end">
|
||||
<Button onClick={handleCancelClick}>{t("common.button.cancel")}</Button>
|
||||
<Button disabled={disabled} loading={pending} type="primary" onClick={handleConfirmClick}>
|
||||
{t("common.button.save")}
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
loading={loading}
|
||||
maskClosable={!pending}
|
||||
open={open}
|
||||
title={<div className="max-w-[480px] truncate">{node.name}</div>}
|
||||
width={720}
|
||||
onClose={handleClose}
|
||||
>
|
||||
{children}
|
||||
</Drawer>
|
||||
</>
|
||||
<Drawer
|
||||
afterOpenChange={setOpen}
|
||||
closable={!pending}
|
||||
destroyOnHidden
|
||||
extra={
|
||||
<SharedNodeMenu
|
||||
menus={["rename", "remove"]}
|
||||
node={node}
|
||||
disabled={disabled}
|
||||
trigger={<Button icon={<IconDotsVertical size="1.25em" />} type="text" />}
|
||||
afterDelete={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
!!footer && (
|
||||
<Space className="w-full justify-end">
|
||||
<Button onClick={handleCancelClick}>{t("common.button.cancel")}</Button>
|
||||
<Button disabled={disabled} loading={pending} type="primary" onClick={handleConfirmClick}>
|
||||
{t("common.button.save")}
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
loading={loading}
|
||||
maskClosable={!pending}
|
||||
open={open}
|
||||
title={<div className="max-w-[480px] truncate">{node.name}</div>}
|
||||
width={720}
|
||||
onClose={handleClose}
|
||||
>
|
||||
{children}
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
// #endregion
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
"settings.sslprovider.tab": "Certificate authority",
|
||||
"settings.sslprovider.ca.title": "System-wide CA",
|
||||
"settings.sslprovider.ca.tips": "The certificate validity lifetime, certificate algorithm, domain names count, and support for wildcard domain names are allowed may vary among different providers. After switching service providers, please check whether the configuration of the workflows needs to be adjusted.",
|
||||
"settings.sslprovider.ca.tips": "You can set different CAs for each workflow as well.<br>The certificate validity lifetime, certificate algorithm, domain names count, and support for wildcard domain names are allowed may vary among different providers. After switching service providers, please check whether the configuration of the workflows needs to be adjusted.",
|
||||
"settings.sslprovider.form.provider.label": "ACME CA provider",
|
||||
"settings.sslprovider.form.letsencryptstaging_alert": "The staging environment can reduce the chance of your running up against rate limits.<br><br>Learn more:<br><a href=\"https://letsencrypt.org/docs/staging-environment/\" target=\"_blank\">https://letsencrypt.org/docs/staging-environment/</a>",
|
||||
"settings.sslprovider.form.zerossl_eab_kid.label": "EAB KID",
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
"workflow.page.title": "Workflows",
|
||||
"workflow.page.subtitle": "Workflows are collections of nodes that automate a process. Workflows begin execution when a trigger condition occurs and execute sequentially to achieve complex tasks.",
|
||||
|
||||
"workflow.nodata": "No workflows. Please create a workflow to generate certificates! 😀",
|
||||
"workflow.nodata.title": "No workflows",
|
||||
"workflow.nodata.description": "It looks like you don't have any workflows. Get started by adding one.",
|
||||
"workflow.nodata.button": "Create workflow",
|
||||
|
||||
"workflow.search.placeholder": "Search by workflow name ...",
|
||||
|
||||
@ -22,7 +24,8 @@
|
||||
"workflow.props.trigger.auto": "Scheduled",
|
||||
"workflow.props.trigger.manual": "Manual",
|
||||
"workflow.props.last_run_at": "Last run at",
|
||||
"workflow.props.state": "State",
|
||||
"workflow.props.state": "Active",
|
||||
"workflow.props.state.filter.all": "All",
|
||||
"workflow.props.state.filter.enabled": "Active",
|
||||
"workflow.props.state.filter.disabled": "Inactive",
|
||||
"workflow.props.created_at": "Created at",
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
"workflow_run.action.delete": "Delete run",
|
||||
"workflow_run.action.delete.confirm": "Are you sure want to delete this \"{{name}}\" workflow run?<br>This action cannot be undone.",
|
||||
|
||||
"workflow_run.table.alert": "Attention: The workflow run contains the execution results of each node. Deleting it may trigger re-application or re-deployment of certificates due to the inability to find the previous execution result. Please do not delete unless necessary. It is recommended to keep it for at least 180 days.",
|
||||
"workflow_run.table.alert": "The workflow run contains the execution results of each node. Deleting it may trigger re-application or re-deployment of certificates due to the inability to find the previous execution result. Please do not delete unless necessary. It is recommended to keep it for at least 180 days.",
|
||||
|
||||
"workflow_run.props.id": "ID",
|
||||
"workflow_run.props.status": "Status",
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
"settings.sslprovider.tab": "证书颁发机构",
|
||||
"settings.sslprovider.ca.title": "全局证书颁发机构",
|
||||
"settings.sslprovider.ca.tips": "不同服务商所支持的证书有效期、证书算法、多域名数量上限、是否允许泛域名等可能不同,切换服务商后请注意检查已有工作流的配置是否需要调整。",
|
||||
"settings.sslprovider.ca.tips": "你也可以在每个工作流中设置不同的证书颁发机构。<br>不同服务商所支持的证书有效期、证书算法、多域名数量上限、是否允许泛域名等可能不同,切换服务商后请注意检查已有工作流的配置是否需要调整。",
|
||||
"settings.sslprovider.form.provider.label": "证书颁发机构提供商",
|
||||
"settings.sslprovider.form.letsencryptstaging_alert": "测试环境比生产环境有更宽松的速率限制,可进行测试性部署。<br><br>点击下方链接了解更多:<br><a href=\"https://letsencrypt.org/zh-cn/docs/staging-environment/\" target=\"_blank\">https://letsencrypt.org/zh-cn/docs/staging-environment/</a>",
|
||||
"settings.sslprovider.form.zerossl_eab_kid.label": "EAB KID",
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
{
|
||||
"workflow.page.title": "工作流",
|
||||
"workflow.page.subtitle": "工作流是自动化流程的节点集合。当满足触发条件时,工作流开始顺序执行各节点以完成复杂的任务(如证书申请、部署等)。",
|
||||
"workflow.page.subtitle": "工作流是自动化流程的节点集合。当满足触发条件时,工作流开始顺序执行各节点以完成复杂的任务。",
|
||||
|
||||
"workflow.nodata": "暂无工作流,请先新建工作流",
|
||||
"workflow.nodata.title": "暂无工作流",
|
||||
"workflow.nodata.description": "请先新建工作流以执行证书申请、部署等任务",
|
||||
"workflow.nodata.button": "新建工作流",
|
||||
|
||||
"workflow.search.placeholder": "按工作流名称搜索……",
|
||||
|
||||
@ -22,7 +24,8 @@
|
||||
"workflow.props.trigger.auto": "定时",
|
||||
"workflow.props.trigger.manual": "手动",
|
||||
"workflow.props.last_run_at": "最近执行时间",
|
||||
"workflow.props.state": "启用状态",
|
||||
"workflow.props.state": "启用",
|
||||
"workflow.props.state.filter.all": "全部",
|
||||
"workflow.props.state.filter.enabled": "启用",
|
||||
"workflow.props.state.filter.disabled": "未启用",
|
||||
"workflow.props.created_at": "创建时间",
|
||||
|
||||
@ -52,7 +52,7 @@ const AccessList = () => {
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<div className="flex max-w-full items-center gap-4 truncate overflow-hidden">
|
||||
<Avatar shape="square" src={accessProvidersMap.get(record.provider)?.icon} size="large" />
|
||||
<Avatar shape="square" src={accessProvidersMap.get(record.provider)?.icon} size={28} />
|
||||
<div className="flex max-w-full flex-col gap-1">
|
||||
<Typography.Text ellipsis>{record.name}</Typography.Text>
|
||||
<Typography.Text ellipsis type="secondary">
|
||||
|
||||
@ -428,7 +428,9 @@ const SettingsSSLProvider = () => {
|
||||
<Show when={!loading} fallback={<Skeleton active />}>
|
||||
<Form form={formInst} disabled={formPending} layout="vertical" initialValues={{ provider: providerType }}>
|
||||
<div className="mb-2">
|
||||
<Typography.Text type="secondary">{t("settings.sslprovider.ca.tips")}</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
<span dangerouslySetInnerHTML={{ __html: t("settings.sslprovider.ca.tips") }}></span>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<Form.Item name="provider" label={t("settings.sslprovider.form.provider.label")}>
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { PageHeader } from "@ant-design/pro-components";
|
||||
import { IconArrowBackUp, IconChevronDown, IconDots, IconHistory, IconPlayerPlay, IconRobot, IconTrash } from "@tabler/icons-react";
|
||||
import { Alert, Button, Card, Dropdown, Form, Input, Modal, Space, Tabs, Typography, message, notification } from "antd";
|
||||
import { Alert, App, Button, Card, Dropdown, Flex, Form, Input, Space, Tabs } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { isEqual } from "radash";
|
||||
import { z } from "zod/v4";
|
||||
@ -25,9 +24,7 @@ const WorkflowDetail = () => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [messageApi, MessageContextHolder] = message.useMessage();
|
||||
const [modalApi, ModalContextHolder] = Modal.useModal();
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
const { message, modal, notification } = App.useApp();
|
||||
|
||||
const { id: workflowId } = useParams();
|
||||
const { workflow, initialized, ...workflowState } = useWorkflowStore(
|
||||
@ -79,7 +76,7 @@ const WorkflowDetail = () => {
|
||||
|
||||
const handleEnableChange = async () => {
|
||||
if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) {
|
||||
messageApi.warning(t("workflow.action.enable.failed.uncompleted"));
|
||||
message.warning(t("workflow.action.enable.failed.uncompleted"));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -87,12 +84,12 @@ const WorkflowDetail = () => {
|
||||
await workflowState.setEnabled(!workflow.enabled);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
modalApi.confirm({
|
||||
modal.confirm({
|
||||
title: <span className="text-error">{t("workflow.action.delete")}</span>,
|
||||
content: <span dangerouslySetInnerHTML={{ __html: t("workflow.action.delete.confirm", { name: workflow.name }) }} />,
|
||||
icon: (
|
||||
@ -110,24 +107,24 @@ const WorkflowDetail = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDiscardClick = () => {
|
||||
modalApi.confirm({
|
||||
modal.confirm({
|
||||
title: t("workflow.detail.orchestration.action.discard"),
|
||||
content: t("workflow.detail.orchestration.action.discard.confirm"),
|
||||
onOk: async () => {
|
||||
try {
|
||||
await workflowState.discard();
|
||||
|
||||
messageApi.success(t("common.text.operation_succeeded"));
|
||||
message.success(t("common.text.operation_succeeded"));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -135,21 +132,21 @@ const WorkflowDetail = () => {
|
||||
|
||||
const handleReleaseClick = () => {
|
||||
if (!isAllNodesValidated(workflow.draft!)) {
|
||||
messageApi.warning(t("workflow.detail.orchestration.action.release.failed.uncompleted"));
|
||||
message.warning(t("workflow.detail.orchestration.action.release.failed.uncompleted"));
|
||||
return;
|
||||
}
|
||||
|
||||
modalApi.confirm({
|
||||
modal.confirm({
|
||||
title: t("workflow.detail.orchestration.action.release"),
|
||||
content: t("workflow.detail.orchestration.action.release.confirm"),
|
||||
onOk: async () => {
|
||||
try {
|
||||
await workflowState.release();
|
||||
|
||||
messageApi.success(t("common.text.operation_succeeded"));
|
||||
message.success(t("common.text.operation_succeeded"));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -158,7 +155,7 @@ const WorkflowDetail = () => {
|
||||
const handleRunClick = () => {
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
if (workflow.hasDraft) {
|
||||
modalApi.confirm({
|
||||
modal.confirm({
|
||||
title: t("workflow.detail.orchestration.action.run"),
|
||||
content: t("workflow.detail.orchestration.action.run.confirm"),
|
||||
onOk: () => resolve(void 0),
|
||||
@ -184,63 +181,62 @@ const WorkflowDetail = () => {
|
||||
|
||||
await startWorkflowRun(workflowId!);
|
||||
|
||||
messageApi.info(t("workflow.detail.orchestration.action.run.prompt"));
|
||||
message.info(t("workflow.detail.orchestration.action.run.prompt"));
|
||||
} catch (err) {
|
||||
setIsPendingOrRunning(false);
|
||||
unsubscribeFn?.();
|
||||
|
||||
console.error(err);
|
||||
messageApi.warning(t("common.text.operation_failed"));
|
||||
message.warning(t("common.text.operation_failed"));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col">
|
||||
{MessageContextHolder}
|
||||
{ModalContextHolder}
|
||||
{NotificationContextHolder}
|
||||
<Card styles={{ body: { padding: 0 } }}>
|
||||
<div className="px-6 py-4">
|
||||
<div className="mx-auto max-w-320">
|
||||
<div className="flex justify-between gap-2">
|
||||
<div>
|
||||
<h1>{workflow.name || " "}</h1>
|
||||
<p className="mb-0 text-base text-gray-500">{workflow.description || " "}</p>
|
||||
</div>
|
||||
<Flex gap="small">
|
||||
{initialized
|
||||
? [
|
||||
<WorkflowBaseInfoModal key="edit" trigger={<Button>{t("common.button.edit")}</Button>} />,
|
||||
|
||||
<div>
|
||||
<Card styles={{ body: { padding: "0.5rem", paddingBottom: 0 } }} style={{ borderRadius: 0 }}>
|
||||
<PageHeader
|
||||
style={{ paddingBottom: 0 }}
|
||||
title={workflow.name}
|
||||
extra={
|
||||
initialized
|
||||
? [
|
||||
<WorkflowBaseInfoModal key="edit" trigger={<Button>{t("common.button.edit")}</Button>} />,
|
||||
<Button key="enable" onClick={handleEnableChange}>
|
||||
{workflow.enabled ? t("workflow.action.disable") : t("workflow.action.enable")}
|
||||
</Button>,
|
||||
|
||||
<Button key="enable" onClick={handleEnableChange}>
|
||||
{workflow.enabled ? t("workflow.action.disable") : t("workflow.action.enable")}
|
||||
</Button>,
|
||||
|
||||
<Dropdown
|
||||
key="more"
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "delete",
|
||||
label: t("workflow.action.delete"),
|
||||
danger: true,
|
||||
icon: <IconTrash size="1.25em" />,
|
||||
onClick: () => {
|
||||
handleDeleteClick();
|
||||
<Dropdown
|
||||
key="more"
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "delete",
|
||||
label: t("workflow.action.delete"),
|
||||
danger: true,
|
||||
icon: <IconTrash size="1.25em" />,
|
||||
onClick: () => {
|
||||
handleDeleteClick();
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button icon={<IconChevronDown size="1.25em" />} iconPosition="end">
|
||||
{t("common.button.more")}
|
||||
</Button>
|
||||
</Dropdown>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
>
|
||||
<Typography.Paragraph type="secondary">{workflow.description}</Typography.Paragraph>
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
>
|
||||
<Button icon={<IconChevronDown size="1.25em" />} iconPosition="end">
|
||||
{t("common.button.more")}
|
||||
</Button>
|
||||
</Dropdown>,
|
||||
]
|
||||
: []}
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
activeKey={tabValue}
|
||||
defaultActiveKey="orchestration"
|
||||
@ -268,9 +264,9 @@ const WorkflowDetail = () => {
|
||||
tabBarStyle={{ border: "none" }}
|
||||
onChange={(key) => setTabValue(key as typeof tabValue)}
|
||||
/>
|
||||
</PageHeader>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Show when={tabValue === "orchestration"}>
|
||||
<div className="min-h-[360px] flex-1 overflow-hidden p-4">
|
||||
@ -285,7 +281,7 @@ const WorkflowDetail = () => {
|
||||
}}
|
||||
loading={!initialized}
|
||||
>
|
||||
<div className="absolute inset-x-6 top-4 z-2 flex items-center justify-between gap-4">
|
||||
<div className="absolute inset-x-6 top-4 z-2 mx-auto flex max-w-320 items-center justify-between gap-4">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Show when={workflow.hasDraft!}>
|
||||
<Alert banner message={<div className="truncate">{t("workflow.detail.orchestration.draft.alert")}</div>} type="warning" />
|
||||
@ -330,9 +326,9 @@ const WorkflowDetail = () => {
|
||||
|
||||
<Show when={tabValue === "runs"}>
|
||||
<div className="p-4">
|
||||
<Card loading={!initialized}>
|
||||
<div className="mx-auto max-w-320">
|
||||
<WorkflowRuns workflowId={workflowId!} />
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
@ -342,7 +338,7 @@ const WorkflowDetail = () => {
|
||||
const WorkflowBaseInfoModal = ({ trigger }: { trigger?: React.ReactNode }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
const { notification } = App.useApp();
|
||||
|
||||
const { workflow, ...workflowState } = useWorkflowStore(useZustandShallowSelector(["workflow", "setBaseInfo"]));
|
||||
|
||||
@ -368,7 +364,7 @@ const WorkflowBaseInfoModal = ({ trigger }: { trigger?: React.ReactNode }) => {
|
||||
try {
|
||||
await workflowState.setBaseInfo(values.name!, values.description!);
|
||||
} catch (err) {
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
|
||||
throw err;
|
||||
}
|
||||
@ -381,8 +377,6 @@ const WorkflowBaseInfoModal = ({ trigger }: { trigger?: React.ReactNode }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{NotificationContextHolder}
|
||||
|
||||
<ModalForm
|
||||
disabled={formPending}
|
||||
layout="vertical"
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { IconCopy, IconEdit, IconPlus, IconReload, IconTrash } from "@tabler/icons-react";
|
||||
import { IconCirclePlus, IconCopy, IconEdit, IconPlus, IconReload, IconSchema, IconTrash } from "@tabler/icons-react";
|
||||
import { useRequest } from "ahooks";
|
||||
import { App, Button, Divider, Empty, Input, Menu, type MenuProps, Radio, Space, Switch, Table, type TableProps, Tooltip, Typography, theme } from "antd";
|
||||
import { App, Button, Flex, Input, Segmented, Skeleton, Switch, Table, type TableProps, Tooltip, Typography } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import Empty from "@/components/Empty";
|
||||
import WorkflowStatusIcon from "@/components/workflow/WorkflowStatusIcon";
|
||||
import { WORKFLOW_TRIGGERS, type WorkflowModel, cloneNode, initWorkflow, isAllNodesValidated } from "@/domain/workflow";
|
||||
import { list as listWorkflows, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow";
|
||||
@ -19,7 +20,6 @@ const WorkflowList = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { message, modal, notification } = App.useApp();
|
||||
const { token: themeToken } = theme.useToken();
|
||||
|
||||
const [filters, setFilters] = useState<Record<string, unknown>>(() => {
|
||||
return {
|
||||
@ -43,12 +43,12 @@ const WorkflowList = () => {
|
||||
title: t("workflow.props.name"),
|
||||
ellipsis: true,
|
||||
render: (_, record) => (
|
||||
<Space className="max-w-full" direction="vertical" size={4}>
|
||||
<div className="flex max-w-full flex-col gap-1">
|
||||
<Typography.Text ellipsis>{record.name}</Typography.Text>
|
||||
<Typography.Text type="secondary" ellipsis>
|
||||
{record.description}
|
||||
<Typography.Text ellipsis type="secondary">
|
||||
{record.description || " "}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
@ -63,10 +63,10 @@ const WorkflowList = () => {
|
||||
return <Typography.Text>{t("workflow.props.trigger.manual")}</Typography.Text>;
|
||||
} else if (trigger === WORKFLOW_TRIGGERS.AUTO) {
|
||||
return (
|
||||
<Space className="max-w-full" direction="vertical" size={4}>
|
||||
<div className="flex max-w-full flex-col gap-1">
|
||||
<Typography.Text>{t("workflow.props.trigger.auto")}</Typography.Text>
|
||||
<Typography.Text type="secondary">{record.triggerCron ?? ""}</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
@ -75,62 +75,21 @@ const WorkflowList = () => {
|
||||
key: "state",
|
||||
title: t("workflow.props.state"),
|
||||
defaultFilteredValue: searchParams.has("state") ? [searchParams.get("state") as string] : undefined,
|
||||
filterDropdown: ({ setSelectedKeys, confirm, clearFilters }) => {
|
||||
const items: Required<MenuProps>["items"] = [
|
||||
["enabled", "workflow.props.state.filter.enabled"],
|
||||
["disabled", "workflow.props.state.filter.disabled"],
|
||||
].map(([key, label]) => {
|
||||
return {
|
||||
key,
|
||||
label: <Radio checked={filters["state"] === key}>{t(label)}</Radio>,
|
||||
onClick: () => {
|
||||
if (filters["state"] !== key) {
|
||||
setPage(1);
|
||||
setFilters((prev) => ({ ...prev, state: key }));
|
||||
setSelectedKeys([key]);
|
||||
}
|
||||
|
||||
confirm({ closeDropdown: true });
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const handleResetClick = () => {
|
||||
setPage(1);
|
||||
setFilters((prev) => ({ ...prev, state: undefined }));
|
||||
setSelectedKeys([]);
|
||||
clearFilters?.();
|
||||
confirm();
|
||||
};
|
||||
|
||||
const handleConfirmClick = () => {
|
||||
confirm();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 0 }}>
|
||||
<Menu items={items} selectable={false} />
|
||||
<Divider className="my-0" />
|
||||
<Space className="w-full justify-end" style={{ padding: themeToken.paddingSM }}>
|
||||
<Button size="small" disabled={!filters.state} onClick={handleResetClick}>
|
||||
{t("common.button.reset")}
|
||||
</Button>
|
||||
<Button type="primary" size="small" onClick={handleConfirmClick}>
|
||||
{t("common.button.ok")}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
render: (_, record) => {
|
||||
const enabled = record.enabled;
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
handleRecordActiveChange(record);
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
handleRecordActiveChange(record);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
@ -139,10 +98,10 @@ const WorkflowList = () => {
|
||||
title: t("workflow.props.last_run_at"),
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<Space>
|
||||
<Flex gap="small">
|
||||
<WorkflowStatusIcon color={true} status={record.lastRunStatus!} />
|
||||
<Typography.Text>{record.lastRunTime ? dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss") : ""}</Typography.Text>
|
||||
</Space>
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
},
|
||||
@ -154,14 +113,6 @@ const WorkflowList = () => {
|
||||
return dayjs(record.created!).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "updatedAt",
|
||||
title: t("workflow.props.updated_at"),
|
||||
ellipsis: true,
|
||||
render: (_, record) => {
|
||||
return dayjs(record.updated!).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "$action",
|
||||
align: "end",
|
||||
@ -174,8 +125,9 @@ const WorkflowList = () => {
|
||||
color="primary"
|
||||
icon={<IconEdit size="1.25em" />}
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
navigate(`/workflows/${record.id}`);
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRecordDetailClick(record);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
@ -186,6 +138,7 @@ const WorkflowList = () => {
|
||||
icon={<IconCopy size="1.25em" />}
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
e.stopPropagation();
|
||||
handleRecordDuplicateClick(record);
|
||||
}}
|
||||
/>
|
||||
@ -197,7 +150,8 @@ const WorkflowList = () => {
|
||||
danger
|
||||
icon={<IconTrash size="1.25em" />}
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRecordDeleteClick(record);
|
||||
}}
|
||||
/>
|
||||
@ -256,6 +210,10 @@ const WorkflowList = () => {
|
||||
refreshData();
|
||||
};
|
||||
|
||||
const handleRecordDetailClick = (workflow: WorkflowModel) => {
|
||||
navigate(`/workflows/${workflow.id}`);
|
||||
};
|
||||
|
||||
const handleRecordActiveChange = async (workflow: WorkflowModel) => {
|
||||
try {
|
||||
if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) {
|
||||
@ -347,6 +305,22 @@ const WorkflowList = () => {
|
||||
|
||||
<div className="flex items-center justify-between gap-x-2 gap-y-3 not-md:flex-col-reverse not-md:items-start not-md:justify-normal">
|
||||
<div className="flex w-full flex-1 items-center gap-x-2 md:max-w-200">
|
||||
<div>
|
||||
<Segmented
|
||||
className="shadow-xs"
|
||||
options={[
|
||||
{ label: <span className="text-sm">{t("workflow.props.state.filter.all")}</span>, value: "" },
|
||||
{ label: <span className="text-sm">{t("workflow.props.state.filter.enabled")}</span>, value: "enabled" },
|
||||
{ label: <span className="text-sm">{t("workflow.props.state.filter.disabled")}</span>, value: "disabled" },
|
||||
]}
|
||||
size="large"
|
||||
value={(filters["state"] as string) || ""}
|
||||
onChange={(value) => {
|
||||
setPage(1);
|
||||
setFilters((prev) => ({ ...prev, state: value }));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Input.Search
|
||||
className="text-sm placeholder:text-sm"
|
||||
@ -374,7 +348,26 @@ const WorkflowList = () => {
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={getErrMsg(loadedError ?? t("workflow.nodata"))} />,
|
||||
emptyText: loading ? (
|
||||
<Skeleton />
|
||||
) : (
|
||||
<Empty
|
||||
title={t("workflow.nodata.title")}
|
||||
description={getErrMsg(loadedError ?? t("workflow.nodata.description"))}
|
||||
icon={<IconSchema size={24} />}
|
||||
extra={
|
||||
loadedError ? (
|
||||
<Button icon={<IconReload size="1.25em" />} type="primary" onClick={handleReloadClick}>
|
||||
{t("common.button.reload")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button icon={<IconCirclePlus size="1.25em" />} type="primary" onClick={handleCreateClick}>
|
||||
{t("workflow.nodata.button")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
@ -390,8 +383,14 @@ const WorkflowList = () => {
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
rowClassName="cursor-pointer"
|
||||
rowKey={(record) => record.id}
|
||||
scroll={{ x: "max(100%, 960px)" }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => {
|
||||
handleRecordDetailClick(record);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -123,7 +123,7 @@ const WorkflowNew = () => {
|
||||
</Card>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="mx-auto max-w-320 px-2">
|
||||
<div className="mx-auto max-w-320">
|
||||
<Typography.Text type="secondary">
|
||||
<div className="mt-4 mb-8 text-xl">{t("workflow.new.templates.title")}</div>
|
||||
</Typography.Text>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user