From e256d36cd14f15c0fceded7e2bcd2e02a891d8c3 Mon Sep 17 00:00:00 2001
From: Fu Diwei <fudiwei@sina.com>
Date: Thu, 2 Jan 2025 10:04:23 +0800
Subject: [PATCH] refactor(ui): clean code

---
 ui/src/components/access/AccessEditForm.tsx   |   7 +-
 ...ypeSelect.tsx => AccessProviderSelect.tsx} |   9 +-
 ui/src/components/workflow/Node.tsx           |  75 ++++---
 .../workflow/node/ApplyNodeForm.tsx           |   3 +-
 .../workflow/node/DeployNodeForm.tsx          |  49 ++---
 .../workflow/run/WorkflowRunDetailDrawer.tsx  |   2 +-
 .../components/workflow/run/WorkflowRuns.tsx  |   2 +-
 ui/src/domain/access.ts                       |  58 +----
 ui/src/domain/base.ts                         |   6 -
 ui/src/domain/provider.ts                     | 207 ++++++++++++------
 ui/src/domain/settings.ts                     |  56 ++---
 ui/src/pages/workflows/WorkflowDetail.tsx     |   2 +-
 12 files changed, 238 insertions(+), 238 deletions(-)
 rename ui/src/components/access/{AccessTypeSelect.tsx => AccessProviderSelect.tsx} (89%)
 delete mode 100644 ui/src/domain/base.ts

diff --git a/ui/src/components/access/AccessEditForm.tsx b/ui/src/components/access/AccessEditForm.tsx
index c765b58b..396405d0 100644
--- a/ui/src/components/access/AccessEditForm.tsx
+++ b/ui/src/components/access/AccessEditForm.tsx
@@ -5,7 +5,8 @@ import { Form, Input, type FormInstance } from "antd";
 import { createSchemaFieldRule } from "antd-zod";
 import { z } from "zod";
 
-import { ACCESS_PROVIDERS, type AccessModel } from "@/domain/access";
+import { type AccessModel } from "@/domain/access";
+import { ACCESS_PROVIDERS } from "@/domain/provider";
 import { useAntdForm } from "@/hooks";
 
 import AccessEditFormACMEHttpReqConfig from "./AccessEditFormACMEHttpReqConfig";
@@ -27,7 +28,7 @@ import AccessEditFormSSHConfig from "./AccessEditFormSSHConfig";
 import AccessEditFormTencentCloudConfig from "./AccessEditFormTencentCloudConfig";
 import AccessEditFormVolcEngineConfig from "./AccessEditFormVolcEngineConfig";
 import AccessEditFormWebhookConfig from "./AccessEditFormWebhookConfig";
-import AccessTypeSelect from "./AccessTypeSelect";
+import AccessProviderSelect from "./AccessProviderSelect";
 
 type AccessEditFormFieldValues = Partial<MaybeModelRecord<AccessModel>>;
 type AccessEditFormPresets = "add" | "edit";
@@ -164,7 +165,7 @@ const AccessEditForm = forwardRef<AccessEditFormInstance, AccessEditFormProps>((
             rules={[formRule]}
             tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.type.tooltip") }}></span>}
           >
-            <AccessTypeSelect disabled={preset !== "add"} placeholder={t("access.form.type.placeholder")} showSearch={!disabled} />
+            <AccessProviderSelect disabled={preset !== "add"} placeholder={t("access.form.type.placeholder")} showSearch={!disabled} />
           </Form.Item>
         </Form>
 
diff --git a/ui/src/components/access/AccessTypeSelect.tsx b/ui/src/components/access/AccessProviderSelect.tsx
similarity index 89%
rename from ui/src/components/access/AccessTypeSelect.tsx
rename to ui/src/components/access/AccessProviderSelect.tsx
index 9b83cd32..59d9c24f 100644
--- a/ui/src/components/access/AccessTypeSelect.tsx
+++ b/ui/src/components/access/AccessProviderSelect.tsx
@@ -2,15 +2,14 @@ import { memo } from "react";
 import { useTranslation } from "react-i18next";
 import { Avatar, Select, Space, Tag, Typography, type SelectProps } from "antd";
 
-import { ACCESS_USAGES } from "@/domain/access";
-import { accessProvidersMap } from "@/domain/provider";
+import { ACCESS_USAGES, accessProvidersMap } from "@/domain/provider";
 
-export type AccessTypeSelectProps = Omit<
+export type AccessProviderSelectProps = Omit<
   SelectProps,
   "filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"
 >;
 
-const AccessTypeSelect = (props: AccessTypeSelectProps) => {
+const AccessProviderSelect = (props: AccessProviderSelectProps) => {
   const { t } = useTranslation();
 
   const options = Array.from(accessProvidersMap.values()).map((item) => ({
@@ -69,4 +68,4 @@ const AccessTypeSelect = (props: AccessTypeSelectProps) => {
   );
 };
 
-export default memo(AccessTypeSelect);
+export default memo(AccessProviderSelect);
diff --git a/ui/src/components/workflow/Node.tsx b/ui/src/components/workflow/Node.tsx
index def5d30d..d8dfce78 100644
--- a/ui/src/components/workflow/Node.tsx
+++ b/ui/src/components/workflow/Node.tsx
@@ -1,6 +1,7 @@
 import { useTranslation } from "react-i18next";
 import { DeleteOutlined as DeleteOutlinedIcon, EllipsisOutlined as EllipsisOutlinedIcon } from "@ant-design/icons";
-import { Dropdown } from "antd";
+import { Button, Dropdown } from "antd";
+import { produce } from "immer";
 
 import Show from "@/components/Show";
 import { deployProvidersMap } from "@/domain/provider";
@@ -20,14 +21,24 @@ type NodeProps = {
 const i18nPrefix = "workflow.node";
 
 const Node = ({ data }: NodeProps) => {
-  const { updateNode, removeNode } = useWorkflowStore(useZustandShallowSelector(["updateNode", "removeNode"]));
-  const handleNameBlur = (e: React.FocusEvent<HTMLDivElement>) => {
-    updateNode({ ...data, name: e.target.innerText });
-  };
+  const { t } = useTranslation();
 
+  const { updateNode, removeNode } = useWorkflowStore(useZustandShallowSelector(["updateNode", "removeNode"]));
   const { showPanel } = usePanel();
 
-  const { t } = useTranslation();
+  const handleNameBlur = (e: React.FocusEvent<HTMLDivElement>) => {
+    const oldName = data.name;
+    const newName = e.target.innerText.trim();
+    if (oldName === newName) {
+      return;
+    }
+
+    updateNode(
+      produce(data, (draft) => {
+        draft.name = e.target.innerText;
+      })
+    );
+  };
 
   const handleNodeSettingClick = () => {
     showPanel({
@@ -83,48 +94,48 @@ const Node = ({ data }: NodeProps) => {
 
   return (
     <>
-      <div className="rounded-md shadow-md w-[260px] relative">
-        {data.type != WorkflowNodeType.Start && (
-          <>
-            <Dropdown
-              menu={{
-                items: [
-                  {
-                    key: "delete",
-                    label: t(`${i18nPrefix}.delete.label`),
-                    icon: <DeleteOutlinedIcon />,
-                    danger: true,
-                    onClick: () => {
-                      removeNode(data.id);
-                    },
+      <div className="relative w-[256px] rounded-md shadow-md overflow-hidden">
+        <Show when={data.type != WorkflowNodeType.Start}>
+          <Dropdown
+            menu={{
+              items: [
+                {
+                  key: "delete",
+                  label: t(`${i18nPrefix}.delete.label`),
+                  icon: <DeleteOutlinedIcon />,
+                  danger: true,
+                  onClick: () => {
+                    removeNode(data.id);
                   },
-                ],
-              }}
-              trigger={["click"]}
-            >
-              <div className="absolute right-2 top-1 cursor-pointer">
-                <EllipsisOutlinedIcon className="text-white" size={17} />
-              </div>
-            </Dropdown>
-          </>
-        )}
+                },
+              ],
+            }}
+            trigger={["click"]}
+          >
+            <div className="absolute right-2 top-1">
+              <Button icon={<EllipsisOutlinedIcon style={{ color: "white" }} />} size="small" type="text" />
+            </div>
+          </Dropdown>
+        </Show>
 
-        <div className="w-[260px] h-[60px] flex flex-col justify-center items-center bg-primary text-white rounded-t-md px-5">
+        <div className="w-[256px] h-[48px] flex flex-col justify-center items-center bg-primary text-white rounded-t-md px-4 line-clamp-2">
           <div
+            className="w-full text-center outline-none focus:bg-white focus:text-stone-600 focus:rounded-sm"
             contentEditable
             suppressContentEditableWarning
             onBlur={handleNameBlur}
-            className="w-full text-center outline-none focus:bg-white focus:text-stone-600 focus:rounded-sm"
           >
             {data.name}
           </div>
         </div>
+
         <div className="p-2 text-sm text-primary flex flex-col justify-center bg-white">
           <div className="leading-7 text-primary cursor-pointer" onClick={handleNodeSettingClick}>
             {getSetting()}
           </div>
         </div>
       </div>
+
       <AddNode data={data} />
     </>
   );
diff --git a/ui/src/components/workflow/node/ApplyNodeForm.tsx b/ui/src/components/workflow/node/ApplyNodeForm.tsx
index de013ec7..2c115e5a 100644
--- a/ui/src/components/workflow/node/ApplyNodeForm.tsx
+++ b/ui/src/components/workflow/node/ApplyNodeForm.tsx
@@ -11,8 +11,7 @@ import AccessEditModal from "@/components/access/AccessEditModal";
 import AccessSelect from "@/components/access/AccessSelect";
 import ModalForm from "@/components/core/ModalForm";
 import MultipleInput from "@/components/core/MultipleInput";
-import { ACCESS_USAGES } from "@/domain/access";
-import { accessProvidersMap } from "@/domain/provider";
+import { ACCESS_USAGES, accessProvidersMap } from "@/domain/provider";
 import { type WorkflowNode, type WorkflowNodeConfig } from "@/domain/workflow";
 import { useAntdForm, useZustandShallowSelector } from "@/hooks";
 import { useContactStore } from "@/stores/contact";
diff --git a/ui/src/components/workflow/node/DeployNodeForm.tsx b/ui/src/components/workflow/node/DeployNodeForm.tsx
index d50f31ec..33615b05 100644
--- a/ui/src/components/workflow/node/DeployNodeForm.tsx
+++ b/ui/src/components/workflow/node/DeployNodeForm.tsx
@@ -8,8 +8,7 @@ import { z } from "zod";
 
 import AccessEditModal from "@/components/access/AccessEditModal";
 import AccessSelect from "@/components/access/AccessSelect";
-import { ACCESS_USAGES } from "@/domain/access";
-import { accessProvidersMap, deployProvidersMap } from "@/domain/provider";
+import { ACCESS_USAGES, accessProvidersMap, DEPLOY_PROVIDERS, deployProvidersMap } from "@/domain/provider";
 import { type WorkflowNode, type WorkflowNodeConfig } from "@/domain/workflow";
 import { useAntdForm, useZustandShallowSelector } from "@/hooks";
 import { useWorkflowStore } from "@/stores/workflow";
@@ -96,51 +95,51 @@ const DeployNodeForm = ({ data, defaultProivderType }: DeployFormProps) => {
       NOTICE: If you add new child component, please keep ASCII order.
      */
     switch (fieldProviderType) {
-      case "aliyun-alb":
+      case DEPLOY_PROVIDERS.ALIYUN_ALB:
         return <DeployNodeFormAliyunALBFields />;
-      case "aliyun-clb":
+      case DEPLOY_PROVIDERS.ALIYUN_CLB:
         return <DeployNodeFormAliyunCLBFields />;
-      case "aliyun-cdn":
+      case DEPLOY_PROVIDERS.ALIYUN_CDN:
         return <DeployNodeFormAliyunCDNFields />;
-      case "aliyun-dcdn":
+      case DEPLOY_PROVIDERS.ALIYUN_DCDN:
         return <DeployNodeFormAliyunDCDNFields />;
-      case "aliyun-nlb":
+      case DEPLOY_PROVIDERS.ALIYUN_NLB:
         return <DeployNodeFormAliyunNLBFields />;
-      case "aliyun-oss":
+      case DEPLOY_PROVIDERS.ALIYUN_OSS:
         return <DeployNodeFormAliyunOSSFields />;
-      case "baiducloud-cdn":
+      case DEPLOY_PROVIDERS.BAIDUCLOUD_CDN:
         return <DeployNodeFormBaiduCloudCDNFields />;
-      case "byteplus-cdn":
+      case DEPLOY_PROVIDERS.BYTEPLUS_CDN:
         return <DeployNodeFormBytePlusCDNFields />;
-      case "dogecloud-cdn":
+      case DEPLOY_PROVIDERS.DOGECLOUD_CDN:
         return <DeployNodeFormDogeCloudCDNFields />;
-      case "huaweicloud-cdn":
+      case DEPLOY_PROVIDERS.HUAWEICLOUD_CDN:
         return <DeployNodeFormHuaweiCloudCDNFields />;
-      case "huaweicloud-elb":
+      case DEPLOY_PROVIDERS.HUAWEICLOUD_ELB:
         return <DeployNodeFormHuaweiCloudELBFields />;
-      case "k8s-secret":
+      case DEPLOY_PROVIDERS.KUBERNETES_SECRET:
         return <DeployNodeFormKubernetesSecretFields />;
-      case "local":
+      case DEPLOY_PROVIDERS.LOCAL:
         return <DeployNodeFormLocalFields />;
-      case "qiniu-cdn":
+      case DEPLOY_PROVIDERS.QINIU_CDN:
         return <DeployNodeFormQiniuCDNFields />;
-      case "ssh":
+      case DEPLOY_PROVIDERS.SSH:
         return <DeployNodeFormSSHFields />;
-      case "tencentcloud-cdn":
+      case DEPLOY_PROVIDERS.TENCENTCLOUD_CDN:
         return <DeployNodeFormTencentCloudCDNFields />;
-      case "tencentcloud-clb":
+      case DEPLOY_PROVIDERS.TENCENTCLOUD_CLB:
         return <DeployNodeFormTencentCloudCLBFields />;
-      case "tencentcloud-cos":
+      case DEPLOY_PROVIDERS.TENCENTCLOUD_COS:
         return <DeployNodeFormTencentCloudCOSFields />;
-      case "tencentcloud-ecdn":
+      case DEPLOY_PROVIDERS.TENCENTCLOUD_ECDN:
         return <DeployNodeFormTencentCloudECDNFields />;
-      case "tencentcloud-eo":
+      case DEPLOY_PROVIDERS.TENCENTCLOUD_EO:
         return <DeployNodeFormTencentCloudEOFields />;
-      case "volcengine-cdn":
+      case DEPLOY_PROVIDERS.VOLCENGINE_CDN:
         return <DeployNodeFormVolcEngineCDNFields />;
-      case "volcengine-live":
+      case DEPLOY_PROVIDERS.VOLCENGINE_LIVE:
         return <DeployNodeFormVolcEngineLiveFields />;
-      case "webhook":
+      case DEPLOY_PROVIDERS.WEBHOOK:
         return <DeployNodeFormWebhookFields />;
     }
   }, [fieldProviderType]);
diff --git a/ui/src/components/workflow/run/WorkflowRunDetailDrawer.tsx b/ui/src/components/workflow/run/WorkflowRunDetailDrawer.tsx
index fc530980..99346a4a 100644
--- a/ui/src/components/workflow/run/WorkflowRunDetailDrawer.tsx
+++ b/ui/src/components/workflow/run/WorkflowRunDetailDrawer.tsx
@@ -30,7 +30,7 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR
     <>
       {triggerDom}
 
-      <Drawer closable destroyOnClose open={open} loading={loading} placement="right" title={`runlog-${data?.id}`} width={640} onClose={() => setOpen(false)}>
+      <Drawer destroyOnClose open={open} loading={loading} placement="right" title={`runlog-${data?.id}`} width={640} onClose={() => setOpen(false)}>
         <Show when={!!data}>
           <Show when={data!.succeed}>
             <Alert showIcon type="success" message={<Typography.Text type="success">{t("workflow_run.props.status.succeeded")}</Typography.Text>} />
diff --git a/ui/src/components/workflow/run/WorkflowRuns.tsx b/ui/src/components/workflow/run/WorkflowRuns.tsx
index 77273ae5..8b3dd552 100644
--- a/ui/src/components/workflow/run/WorkflowRuns.tsx
+++ b/ui/src/components/workflow/run/WorkflowRuns.tsx
@@ -39,7 +39,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
       key: "id",
       title: t("workflow_run.props.id"),
       ellipsis: true,
-      render: (_, record) => record.id,
+      render: (_, record) => <span className="font-mono">{record.id}</span>,
     },
     {
       key: "status",
diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts
index 4bd3b363..fa17f6d2 100644
--- a/ui/src/domain/access.ts
+++ b/ui/src/domain/access.ts
@@ -1,60 +1,4 @@
-/*
-  注意:如果追加新的常量值,请保持以 ASCII 排序。
-  NOTICE: If you add new constant, please keep ASCII order.
- */
-export const ACCESS_PROVIDER_ACMEHTTPREQ = "acmehttpreq" as const;
-export const ACCESS_PROVIDER_ALIYUN = "aliyun" as const;
-export const ACCESS_PROVIDER_AWS = "aws" as const;
-export const ACCESS_PROVIDER_BAIDUCLOUD = "baiducloud" as const;
-export const ACCESS_PROVIDER_BYTEPLUS = "byteplus" as const;
-export const ACCESS_PROVIDER_CLOUDFLARE = "cloudflare" as const;
-export const ACCESS_PROVIDER_DOGECLOUD = "dogecloud" as const;
-export const ACCESS_PROVIDER_GODADDY = "godaddy" as const;
-export const ACCESS_PROVIDER_HUAWEICLOUD = "huaweicloud" as const;
-export const ACCESS_PROVIDER_KUBERNETES = "k8s" as const;
-export const ACCESS_PROVIDER_LOCAL = "local" as const;
-export const ACCESS_PROVIDER_NAMEDOTCOM = "namedotcom" as const;
-export const ACCESS_PROVIDER_NAMESILO = "namesilo" as const;
-export const ACCESS_PROVIDER_POWERDNS = "powerdns" as const;
-export const ACCESS_PROVIDER_QINIU = "qiniu" as const;
-export const ACCESS_PROVIDER_SSH = "ssh" as const;
-export const ACCESS_PROVIDER_TENCENTCLOUD = "tencentcloud" as const;
-export const ACCESS_PROVIDER_VOLCENGINE = "volcengine" as const;
-export const ACCESS_PROVIDER_WEBHOOK = "webhook" as const;
-export const ACCESS_PROVIDERS = Object.freeze({
-  ACMEHTTPREQ: ACCESS_PROVIDER_ACMEHTTPREQ,
-  ALIYUN: ACCESS_PROVIDER_ALIYUN,
-  AWS: ACCESS_PROVIDER_AWS,
-  BAIDUCLOUD: ACCESS_PROVIDER_BAIDUCLOUD,
-  BYTEPLUS: ACCESS_PROVIDER_BYTEPLUS,
-  CLOUDFLARE: ACCESS_PROVIDER_CLOUDFLARE,
-  DOGECLOUD: ACCESS_PROVIDER_DOGECLOUD,
-  GODADDY: ACCESS_PROVIDER_GODADDY,
-  HUAWEICLOUD: ACCESS_PROVIDER_HUAWEICLOUD,
-  KUBERNETES: ACCESS_PROVIDER_KUBERNETES,
-  LOCAL: ACCESS_PROVIDER_LOCAL,
-  NAMEDOTCOM: ACCESS_PROVIDER_NAMEDOTCOM,
-  NAMESILO: ACCESS_PROVIDER_NAMESILO,
-  POWERDNS: ACCESS_PROVIDER_POWERDNS,
-  QINIU: ACCESS_PROVIDER_QINIU,
-  SSH: ACCESS_PROVIDER_SSH,
-  TENCENTCLOUD: ACCESS_PROVIDER_TENCENTCLOUD,
-  VOLCENGINE: ACCESS_PROVIDER_VOLCENGINE,
-  WEBHOOK: ACCESS_PROVIDER_WEBHOOK,
-} as const);
-
-export type AccessProviderType = (typeof ACCESS_PROVIDERS)[keyof typeof ACCESS_PROVIDERS];
-
-export const ACCESS_USAGE_ALL = "all" as const;
-export const ACCESS_USAGE_APPLY = "apply" as const;
-export const ACCESS_USAGE_DEPLOY = "deploy" as const;
-export const ACCESS_USAGES = Object.freeze({
-  ALL: ACCESS_USAGE_ALL,
-  APPLY: ACCESS_USAGE_APPLY,
-  DEPLOY: ACCESS_USAGE_DEPLOY,
-} as const);
-
-export type AccessUsageType = (typeof ACCESS_USAGES)[keyof typeof ACCESS_USAGES];
+import { type AccessUsageType } from "./provider";
 
 // #region AccessModel
 export interface AccessModel extends BaseModel {
diff --git a/ui/src/domain/base.ts b/ui/src/domain/base.ts
deleted file mode 100644
index 9ff977c5..00000000
--- a/ui/src/domain/base.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export type PbErrorData = {
-  [key: string]: {
-    message: string;
-    code: string;
-  };
-};
diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts
index 61172540..73eb805e 100644
--- a/ui/src/domain/provider.ts
+++ b/ui/src/domain/provider.ts
@@ -1,96 +1,157 @@
-import {
-  ACCESS_PROVIDER_ACMEHTTPREQ,
-  ACCESS_PROVIDER_ALIYUN,
-  ACCESS_PROVIDER_AWS,
-  ACCESS_PROVIDER_BAIDUCLOUD,
-  ACCESS_PROVIDER_BYTEPLUS,
-  ACCESS_PROVIDER_CLOUDFLARE,
-  ACCESS_PROVIDER_DOGECLOUD,
-  ACCESS_PROVIDER_HUAWEICLOUD,
-  ACCESS_PROVIDER_KUBERNETES,
-  ACCESS_PROVIDER_LOCAL,
-  ACCESS_PROVIDER_NAMEDOTCOM,
-  ACCESS_PROVIDER_NAMESILO,
-  ACCESS_PROVIDER_GODADDY,
-  ACCESS_PROVIDER_POWERDNS,
-  ACCESS_PROVIDER_QINIU,
-  ACCESS_PROVIDER_SSH,
-  ACCESS_PROVIDER_TENCENTCLOUD,
-  ACCESS_PROVIDER_VOLCENGINE,
-  ACCESS_PROVIDER_WEBHOOK,
-  type AccessUsageType,
-} from "./access";
+/*
+  注意:如果追加新的常量值,请保持以 ASCII 排序。
+  NOTICE: If you add new constant, please keep ASCII order.
+ */
+export const ACCESS_PROVIDERS = Object.freeze({
+  ACMEHTTPREQ: "acmehttpreq",
+  ALIYUN: "aliyun",
+  AWS: "aws",
+  BAIDUCLOUD: "baiducloud",
+  BYTEPLUS: "byteplus",
+  CLOUDFLARE: "cloudflare",
+  DOGECLOUD: "dogecloud",
+  GODADDY: "godaddy",
+  HUAWEICLOUD: "huaweicloud",
+  KUBERNETES: "k8s",
+  LOCAL: "local",
+  NAMEDOTCOM: "namedotcom",
+  NAMESILO: "namesilo",
+  POWERDNS: "powerdns",
+  QINIU: "qiniu",
+  SSH: "ssh",
+  TENCENTCLOUD: "tencentcloud",
+  VOLCENGINE: "volcengine",
+  WEBHOOK: "webhook",
+} as const);
+
+export type AccessProviderType = (typeof ACCESS_PROVIDERS)[keyof typeof ACCESS_PROVIDERS];
+
+export const ACCESS_USAGES = Object.freeze({
+  ALL: "all",
+  APPLY: "apply",
+  DEPLOY: "deploy",
+} as const);
+
+export type AccessUsageType = (typeof ACCESS_USAGES)[keyof typeof ACCESS_USAGES];
 
 export type AccessProvider = {
-  type: string;
+  type: AccessProviderType;
   name: string;
   icon: string;
   usage: AccessUsageType;
 };
 
-export const accessProvidersMap: Map<AccessProvider["type"], AccessProvider> = new Map(
+export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProvider> = new Map(
   /*
    注意:此处的顺序决定显示在前端的顺序。
    NOTICE: The following order determines the order displayed at the frontend.
   */
   [
-    [ACCESS_PROVIDER_LOCAL, "common.provider.local", "/imgs/providers/local.svg", "deploy"],
-    [ACCESS_PROVIDER_SSH, "common.provider.ssh", "/imgs/providers/ssh.svg", "deploy"],
-    [ACCESS_PROVIDER_WEBHOOK, "common.provider.webhook", "/imgs/providers/webhook.svg", "deploy"],
-    [ACCESS_PROVIDER_KUBERNETES, "common.provider.kubernetes", "/imgs/providers/kubernetes.svg", "deploy"],
-    [ACCESS_PROVIDER_ALIYUN, "common.provider.aliyun", "/imgs/providers/aliyun.svg", "all"],
-    [ACCESS_PROVIDER_TENCENTCLOUD, "common.provider.tencentcloud", "/imgs/providers/tencentcloud.svg", "all"],
-    [ACCESS_PROVIDER_HUAWEICLOUD, "common.provider.huaweicloud", "/imgs/providers/huaweicloud.svg", "all"],
-    [ACCESS_PROVIDER_BAIDUCLOUD, "common.provider.baiducloud", "/imgs/providers/baiducloud.svg", "all"],
-    [ACCESS_PROVIDER_QINIU, "common.provider.qiniu", "/imgs/providers/qiniu.svg", "deploy"],
-    [ACCESS_PROVIDER_DOGECLOUD, "common.provider.dogecloud", "/imgs/providers/dogecloud.svg", "deploy"],
-    [ACCESS_PROVIDER_VOLCENGINE, "common.provider.volcengine", "/imgs/providers/volcengine.svg", "all"],
-    [ACCESS_PROVIDER_BYTEPLUS, "common.provider.byteplus", "/imgs/providers/byteplus.svg", "all"],
-    [ACCESS_PROVIDER_AWS, "common.provider.aws", "/imgs/providers/aws.svg", "apply"],
-    [ACCESS_PROVIDER_CLOUDFLARE, "common.provider.cloudflare", "/imgs/providers/cloudflare.svg", "apply"],
-    [ACCESS_PROVIDER_NAMEDOTCOM, "common.provider.namedotcom", "/imgs/providers/namedotcom.svg", "apply"],
-    [ACCESS_PROVIDER_NAMESILO, "common.provider.namesilo", "/imgs/providers/namesilo.svg", "apply"],
-    [ACCESS_PROVIDER_GODADDY, "common.provider.godaddy", "/imgs/providers/godaddy.svg", "apply"],
-    [ACCESS_PROVIDER_POWERDNS, "common.provider.powerdns", "/imgs/providers/powerdns.svg", "apply"],
-    [ACCESS_PROVIDER_ACMEHTTPREQ, "common.provider.acmehttpreq", "/imgs/providers/acmehttpreq.svg", "apply"],
-  ].map(([type, name, icon, usage]) => [type, { type, name, icon, usage: usage as AccessUsageType }])
+    [ACCESS_PROVIDERS.LOCAL, "common.provider.local", "/imgs/providers/local.svg", "deploy"],
+    [ACCESS_PROVIDERS.SSH, "common.provider.ssh", "/imgs/providers/ssh.svg", "deploy"],
+    [ACCESS_PROVIDERS.WEBHOOK, "common.provider.webhook", "/imgs/providers/webhook.svg", "deploy"],
+    [ACCESS_PROVIDERS.KUBERNETES, "common.provider.kubernetes", "/imgs/providers/kubernetes.svg", "deploy"],
+    [ACCESS_PROVIDERS.ALIYUN, "common.provider.aliyun", "/imgs/providers/aliyun.svg", "all"],
+    [ACCESS_PROVIDERS.TENCENTCLOUD, "common.provider.tencentcloud", "/imgs/providers/tencentcloud.svg", "all"],
+    [ACCESS_PROVIDERS.HUAWEICLOUD, "common.provider.huaweicloud", "/imgs/providers/huaweicloud.svg", "all"],
+    [ACCESS_PROVIDERS.BAIDUCLOUD, "common.provider.baiducloud", "/imgs/providers/baiducloud.svg", "all"],
+    [ACCESS_PROVIDERS.QINIU, "common.provider.qiniu", "/imgs/providers/qiniu.svg", "deploy"],
+    [ACCESS_PROVIDERS.DOGECLOUD, "common.provider.dogecloud", "/imgs/providers/dogecloud.svg", "deploy"],
+    [ACCESS_PROVIDERS.VOLCENGINE, "common.provider.volcengine", "/imgs/providers/volcengine.svg", "all"],
+    [ACCESS_PROVIDERS.BYTEPLUS, "common.provider.byteplus", "/imgs/providers/byteplus.svg", "all"],
+    [ACCESS_PROVIDERS.AWS, "common.provider.aws", "/imgs/providers/aws.svg", "apply"],
+    [ACCESS_PROVIDERS.CLOUDFLARE, "common.provider.cloudflare", "/imgs/providers/cloudflare.svg", "apply"],
+    [ACCESS_PROVIDERS.NAMEDOTCOM, "common.provider.namedotcom", "/imgs/providers/namedotcom.svg", "apply"],
+    [ACCESS_PROVIDERS.NAMESILO, "common.provider.namesilo", "/imgs/providers/namesilo.svg", "apply"],
+    [ACCESS_PROVIDERS.GODADDY, "common.provider.godaddy", "/imgs/providers/godaddy.svg", "apply"],
+    [ACCESS_PROVIDERS.POWERDNS, "common.provider.powerdns", "/imgs/providers/powerdns.svg", "apply"],
+    [ACCESS_PROVIDERS.ACMEHTTPREQ, "common.provider.acmehttpreq", "/imgs/providers/acmehttpreq.svg", "apply"],
+  ].map(([type, name, icon, usage]) => [
+    type,
+    {
+      type: type as AccessProviderType,
+      name: name,
+      icon: icon,
+      usage: usage as AccessUsageType,
+    },
+  ])
 );
 
+/*
+  注意:如果追加新的常量值,请保持以 ASCII 排序。
+  NOTICE: If you add new constant, please keep ASCII order.
+ */
+export const DEPLOY_PROVIDERS = Object.freeze({
+  ALIYUN_ALB: `${ACCESS_PROVIDERS.ALIYUN}-alb`,
+  ALIYUN_CDN: `${ACCESS_PROVIDERS.ALIYUN}-cdn`,
+  ALIYUN_CLB: `${ACCESS_PROVIDERS.ALIYUN}-clb`,
+  ALIYUN_DCDN: `${ACCESS_PROVIDERS.ALIYUN}-dcdn`,
+  ALIYUN_NLB: `${ACCESS_PROVIDERS.ALIYUN}-nlb`,
+  ALIYUN_OSS: `${ACCESS_PROVIDERS.ALIYUN}-oss`,
+  BAIDUCLOUD_CDN: `${ACCESS_PROVIDERS.BAIDUCLOUD}-cdn`,
+  BYTEPLUS_CDN: `${ACCESS_PROVIDERS.BYTEPLUS}-cdn`,
+  DOGECLOUD_CDN: `${ACCESS_PROVIDERS.DOGECLOUD}-cdn`,
+  HUAWEICLOUD_CDN: `${ACCESS_PROVIDERS.HUAWEICLOUD}-cdn`,
+  HUAWEICLOUD_ELB: `${ACCESS_PROVIDERS.HUAWEICLOUD}-elb`,
+  KUBERNETES_SECRET: `${ACCESS_PROVIDERS.KUBERNETES}-secret`,
+  LOCAL: `${ACCESS_PROVIDERS.LOCAL}`,
+  QINIU_CDN: `${ACCESS_PROVIDERS.QINIU}-cdn`,
+  SSH: `${ACCESS_PROVIDERS.SSH}`,
+  TENCENTCLOUD_CDN: `${ACCESS_PROVIDERS.TENCENTCLOUD}-cdn`,
+  TENCENTCLOUD_CLB: `${ACCESS_PROVIDERS.TENCENTCLOUD}-clb`,
+  TENCENTCLOUD_COS: `${ACCESS_PROVIDERS.TENCENTCLOUD}-cos`,
+  TENCENTCLOUD_ECDN: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ecdn`,
+  TENCENTCLOUD_EO: `${ACCESS_PROVIDERS.TENCENTCLOUD}-eo`,
+  VOLCENGINE_CDN: `${ACCESS_PROVIDERS.VOLCENGINE}-cdn`,
+  VOLCENGINE_LIVE: `${ACCESS_PROVIDERS.VOLCENGINE}-live`,
+  WEBHOOK: `${ACCESS_PROVIDERS.WEBHOOK}`,
+} as const);
+
+export type DeployProviderType = (typeof DEPLOY_PROVIDERS)[keyof typeof DEPLOY_PROVIDERS];
+
 export type DeployProvider = {
-  type: string;
+  type: DeployProviderType;
   name: string;
   icon: string;
-  provider: AccessProvider["type"];
+  provider: AccessProviderType;
 };
 
-export const deployProvidersMap: Map<DeployProvider["type"], DeployProvider> = new Map(
-  [
-    /*
+export const deployProvidersMap: Map<DeployProvider["type"] | string, DeployProvider> = new Map(
+  /*
    注意:此处的顺序决定显示在前端的顺序。
    NOTICE: The following order determines the order displayed at the frontend.
   */
-    [`${ACCESS_PROVIDER_LOCAL}`, "common.provider.local"],
-    [`${ACCESS_PROVIDER_SSH}`, "common.provider.ssh"],
-    [`${ACCESS_PROVIDER_WEBHOOK}`, "common.provider.webhook"],
-    [`${ACCESS_PROVIDER_KUBERNETES}-secret`, "common.provider.kubernetes.secret"],
-    [`${ACCESS_PROVIDER_ALIYUN}-oss`, "common.provider.aliyun.oss"],
-    [`${ACCESS_PROVIDER_ALIYUN}-cdn`, "common.provider.aliyun.cdn"],
-    [`${ACCESS_PROVIDER_ALIYUN}-dcdn`, "common.provider.aliyun.dcdn"],
-    [`${ACCESS_PROVIDER_ALIYUN}-clb`, "common.provider.aliyun.clb"],
-    [`${ACCESS_PROVIDER_ALIYUN}-alb`, "common.provider.aliyun.alb"],
-    [`${ACCESS_PROVIDER_ALIYUN}-nlb`, "common.provider.aliyun.nlb"],
-    [`${ACCESS_PROVIDER_TENCENTCLOUD}-cdn`, "common.provider.tencentcloud.cdn"],
-    [`${ACCESS_PROVIDER_TENCENTCLOUD}-ecdn`, "common.provider.tencentcloud.ecdn"],
-    [`${ACCESS_PROVIDER_TENCENTCLOUD}-clb`, "common.provider.tencentcloud.clb"],
-    [`${ACCESS_PROVIDER_TENCENTCLOUD}-cos`, "common.provider.tencentcloud.cos"],
-    [`${ACCESS_PROVIDER_TENCENTCLOUD}-eo`, "common.provider.tencentcloud.eo"],
-    [`${ACCESS_PROVIDER_HUAWEICLOUD}-cdn`, "common.provider.huaweicloud.cdn"],
-    [`${ACCESS_PROVIDER_HUAWEICLOUD}-elb`, "common.provider.huaweicloud.elb"],
-    [`${ACCESS_PROVIDER_BAIDUCLOUD}-cdn`, "common.provider.baiducloud.cdn"],
-    [`${ACCESS_PROVIDER_VOLCENGINE}-cdn`, "common.provider.volcengine.cdn"],
-    [`${ACCESS_PROVIDER_VOLCENGINE}-live`, "common.provider.volcengine.live"],
-    [`${ACCESS_PROVIDER_QINIU}-cdn`, "common.provider.qiniu.cdn"],
-    [`${ACCESS_PROVIDER_DOGECLOUD}-cdn`, "common.provider.dogecloud.cdn"],
-    [`${ACCESS_PROVIDER_BYTEPLUS}-cdn`, "common.provider.byteplus.cdn"],
-  ].map(([type, name]) => [type, { type, name, icon: accessProvidersMap.get(type.split("-")[0])!.icon, provider: type.split("-")[0] }])
+  [
+    [DEPLOY_PROVIDERS.LOCAL, "common.provider.local"],
+    [DEPLOY_PROVIDERS.SSH, "common.provider.ssh"],
+    [DEPLOY_PROVIDERS.WEBHOOK, "common.provider.webhook"],
+    [DEPLOY_PROVIDERS.KUBERNETES_SECRET, "common.provider.kubernetes.secret"],
+    [DEPLOY_PROVIDERS.ALIYUN_OSS, "common.provider.aliyun.oss"],
+    [DEPLOY_PROVIDERS.ALIYUN_CDN, "common.provider.aliyun.cdn"],
+    [DEPLOY_PROVIDERS.ALIYUN_DCDN, "common.provider.aliyun.dcdn"],
+    [DEPLOY_PROVIDERS.ALIYUN_CLB, "common.provider.aliyun.clb"],
+    [DEPLOY_PROVIDERS.ALIYUN_ALB, "common.provider.aliyun.alb"],
+    [DEPLOY_PROVIDERS.ALIYUN_NLB, "common.provider.aliyun.nlb"],
+    [DEPLOY_PROVIDERS.TENCENTCLOUD_CDN, "common.provider.tencentcloud.cdn"],
+    [DEPLOY_PROVIDERS.TENCENTCLOUD_ECDN, "common.provider.tencentcloud.ecdn"],
+    [DEPLOY_PROVIDERS.TENCENTCLOUD_CLB, "common.provider.tencentcloud.clb"],
+    [DEPLOY_PROVIDERS.TENCENTCLOUD_COS, "common.provider.tencentcloud.cos"],
+    [DEPLOY_PROVIDERS.TENCENTCLOUD_EO, "common.provider.tencentcloud.eo"],
+    [DEPLOY_PROVIDERS.HUAWEICLOUD_CDN, "common.provider.huaweicloud.cdn"],
+    [DEPLOY_PROVIDERS.HUAWEICLOUD_ELB, "common.provider.huaweicloud.elb"],
+    [DEPLOY_PROVIDERS.BAIDUCLOUD_CDN, "common.provider.baiducloud.cdn"],
+    [DEPLOY_PROVIDERS.VOLCENGINE_CDN, "common.provider.volcengine.cdn"],
+    [DEPLOY_PROVIDERS.VOLCENGINE_LIVE, "common.provider.volcengine.live"],
+    [DEPLOY_PROVIDERS.QINIU_CDN, "common.provider.qiniu.cdn"],
+    [DEPLOY_PROVIDERS.DOGECLOUD_CDN, "common.provider.dogecloud.cdn"],
+    [DEPLOY_PROVIDERS.BYTEPLUS_CDN, "common.provider.byteplus.cdn"],
+  ].map(([type, name]) => [
+    type,
+    {
+      type: type as DeployProviderType,
+      name: name,
+      icon: accessProvidersMap.get(type.split("-")[0])!.icon,
+      provider: type.split("-")[0] as AccessProviderType,
+    },
+  ])
 );
diff --git a/ui/src/domain/settings.ts b/ui/src/domain/settings.ts
index 1c041e37..b2412a7d 100644
--- a/ui/src/domain/settings.ts
+++ b/ui/src/domain/settings.ts
@@ -39,23 +39,15 @@ export const defaultNotifyTemplate: NotifyTemplate = {
 // #endregion
 
 // #region Settings: NotifyChannels
-export const NOTIFY_CHANNEL_BARK = "bark" as const;
-export const NOTIFY_CHANNEL_DINGTALK = "dingtalk" as const;
-export const NOTIFY_CHANNEL_EMAIL = "email" as const;
-export const NOTIFY_CHANNEL_LARK = "lark" as const;
-export const NOTIFY_CHANNEL_SERVERCHAN = "serverchan" as const;
-export const NOTIFY_CHANNEL_TELEGRAM = "telegram" as const;
-export const NOTIFY_CHANNEL_WEBHOOK = "webhook" as const;
-export const NOTIFY_CHANNEL_WECOM = "wecom" as const;
 export const NOTIFY_CHANNELS = Object.freeze({
-  BARK: NOTIFY_CHANNEL_BARK,
-  DINGTALK: NOTIFY_CHANNEL_DINGTALK,
-  EMAIL: NOTIFY_CHANNEL_EMAIL,
-  LARK: NOTIFY_CHANNEL_LARK,
-  SERVERCHAN: NOTIFY_CHANNEL_SERVERCHAN,
-  TELEGRAM: NOTIFY_CHANNEL_TELEGRAM,
-  WEBHOOK: NOTIFY_CHANNEL_WEBHOOK,
-  WECOM: NOTIFY_CHANNEL_WECOM,
+  BARK: "bark",
+  DINGTALK: "dingtalk",
+  EMAIL: "email",
+  LARK: "lark",
+  SERVERCHAN: "serverchan",
+  TELEGRAM: "telegram",
+  WEBHOOK: "webhook",
+  WECOM: "wecom",
 } as const);
 
 export type NotifyChannels = (typeof NOTIFY_CHANNELS)[keyof typeof NOTIFY_CHANNELS];
@@ -66,14 +58,14 @@ export type NotifyChannelsSettingsContent = {
     NOTICE: If you add new type, please keep ASCII order.
   */
   [key: string]: ({ enabled?: boolean } & Record<string, unknown>) | undefined;
-  [NOTIFY_CHANNEL_BARK]?: BarkNotifyChannelConfig;
-  [NOTIFY_CHANNEL_DINGTALK]?: DingTalkNotifyChannelConfig;
-  [NOTIFY_CHANNEL_EMAIL]?: EmailNotifyChannelConfig;
-  [NOTIFY_CHANNEL_LARK]?: LarkNotifyChannelConfig;
-  [NOTIFY_CHANNEL_SERVERCHAN]?: ServerChanNotifyChannelConfig;
-  [NOTIFY_CHANNEL_TELEGRAM]?: TelegramNotifyChannelConfig;
-  [NOTIFY_CHANNEL_WEBHOOK]?: WebhookNotifyChannelConfig;
-  [NOTIFY_CHANNEL_WECOM]?: WeComNotifyChannelConfig;
+  [NOTIFY_CHANNELS.BARK]?: BarkNotifyChannelConfig;
+  [NOTIFY_CHANNELS.DINGTALK]?: DingTalkNotifyChannelConfig;
+  [NOTIFY_CHANNELS.EMAIL]?: EmailNotifyChannelConfig;
+  [NOTIFY_CHANNELS.LARK]?: LarkNotifyChannelConfig;
+  [NOTIFY_CHANNELS.SERVERCHAN]?: ServerChanNotifyChannelConfig;
+  [NOTIFY_CHANNELS.TELEGRAM]?: TelegramNotifyChannelConfig;
+  [NOTIFY_CHANNELS.WEBHOOK]?: WebhookNotifyChannelConfig;
+  [NOTIFY_CHANNELS.WECOM]?: WeComNotifyChannelConfig;
 };
 
 export type BarkNotifyChannelConfig = {
@@ -132,14 +124,14 @@ export type NotifyChannel = {
 
 export const notifyChannelsMap: Map<NotifyChannel["type"], NotifyChannel> = new Map(
   [
-    ["email", "common.notifier.email"],
-    ["dingtalk", "common.notifier.dingtalk"],
-    ["lark", "common.notifier.lark"],
-    ["wecom", "common.notifier.wecom"],
-    ["telegram", "common.notifier.telegram"],
-    ["serverchan", "common.notifier.serverchan"],
-    ["bark", "common.notifier.bark"],
-    ["webhook", "common.notifier.webhook"],
+    [NOTIFY_CHANNELS.EMAIL, "common.notifier.email"],
+    [NOTIFY_CHANNELS.DINGTALK, "common.notifier.dingtalk"],
+    [NOTIFY_CHANNELS.LARK, "common.notifier.lark"],
+    [NOTIFY_CHANNELS.WECOM, "common.notifier.wecom"],
+    [NOTIFY_CHANNELS.TELEGRAM, "common.notifier.telegram"],
+    [NOTIFY_CHANNELS.SERVERCHAN, "common.notifier.serverchan"],
+    [NOTIFY_CHANNELS.BARK, "common.notifier.bark"],
+    [NOTIFY_CHANNELS.WEBHOOK, "common.notifier.webhook"],
   ].map(([type, name]) => [type, { type, name }])
 );
 // #endregion
diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx
index 1360d886..c390c615 100644
--- a/ui/src/pages/workflows/WorkflowDetail.tsx
+++ b/ui/src/pages/workflows/WorkflowDetail.tsx
@@ -249,7 +249,7 @@ const WorkflowDetail = () => {
                       }}
                       trigger={["click"]}
                     >
-                      <Button color="primary" icon={<EllipsisOutlinedIcon />} variant="outlined" />
+                      <Button color="primary" disabled={!allowDiscard} icon={<EllipsisOutlinedIcon />} variant="outlined" />
                     </Dropdown>
                   </Button.Group>
                 </Space>