diff --git a/README.md b/README.md index f03cd297..dbfcd1ee 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ > [!WARNING] > 当前分支为 `next`,是 v0.3.x 的开发分支,目前还没有稳定,请勿在生产环境中使用。 -> -> 如需访问 v0.2.x 源码,请切换至 `main` 分支。 +> +> 如需访问之前的版本,请切换至 `main` 分支。 # 🔒Certimate @@ -84,11 +84,11 @@ make local.run | 华为云 | √ | √ | 可签发在华为云注册的域名;可部署到华为云 CDN、ELB | | 七牛云 | | √ | 可部署到七牛云 CDN | | 多吉云 | | √ | 可部署到多吉云 CDN | -| 火山引擎 | √ | √ | 可签发在火山引擎注册的域名;可部署到火山引擎 Live、CDN | +| 火山引擎 | √ | √ | 可签发在火山引擎注册的域名;可部署到火山引擎 Live、CDN | | AWS | √ | | 可签发在 AWS Route53 托管的域名 | | CloudFlare | √ | | 可签发在 CloudFlare 注册的域名;CloudFlare 服务自带 SSL 证书 | | GoDaddy | √ | | 可签发在 GoDaddy 注册的域名 | -| Namesilo | √ | | 可签发在 Namesilo 注册的域名 | +| NameSilo | √ | | 可签发在 NameSilo 注册的域名 | | PowerDNS | √ | | 可签发在 PowerDNS 托管的域名 | | HTTP 请求 | √ | | 可签发允许通过 HTTP 请求修改 DNS 的域名 | | 本地部署 | | √ | 可部署到本地服务器 | @@ -194,4 +194,3 @@ Certimate 是一个免费且开源的项目,采用 [MIT 开源协议](LICENSE. ## 十、Star 趋势图 [![Stargazers over time](https://starchart.cc/usual2970/certimate.svg?variant=adaptive)](https://starchart.cc/usual2970/certimate) - diff --git a/README_EN.md b/README_EN.md index 15c61cf7..88e03baf 100644 --- a/README_EN.md +++ b/README_EN.md @@ -2,7 +2,7 @@ > [!WARNING] > The current branch is `next`, which is the development branch for v0.3.x. It is currently unstable and should not be used in production environments. -> +> > To access the previous versions, please switch to the `main` branch. # 🔒Certimate @@ -76,7 +76,7 @@ password:1234567890 ## List of Supported Providers | Provider | Registration | Deployment | Remarks | -| :-----------: | :----------: | :--------: |-------------------------------------------------------------------------------------------------------------| +| :-----------: | :----------: | :--------: | ----------------------------------------------------------------------------------------------------------- | | Alibaba Cloud | √ | √ | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN,SLB | | Tencent Cloud | √ | √ | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud COS, CDN, ECDN, CLB, TEO | | Baidu Cloud | | √ | Supports deployment to Baidu Cloud CDN | @@ -87,7 +87,7 @@ password:1234567890 | AWS | √ | | Supports domains managed on AWS Route53 | | CloudFlare | √ | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates | | GoDaddy | √ | | Supports domains registered on GoDaddy | -| Namesilo | √ | | Supports domains registered on Namesilo | +| NameSilo | √ | | Supports domains registered on NameSilo | | PowerDNS | √ | | Supports domains managed on PowerDNS | | HTTP Request | √ | | Supports domains which allow managing DNS by HTTP request | | Local Deploy | | √ | Supports deployment to local servers | diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index 7fd00a84..33eb8779 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -39,7 +39,7 @@ const ( configTypeCloudflare = "cloudflare" configTypeGoDaddy = "godaddy" configTypeHuaweiCloud = "huaweicloud" - configTypeNamesilo = "namesilo" + configTypeNameSilo = "namesilo" configTypePowerDNS = "powerdns" configTypeTencentCloud = "tencentcloud" configTypeVolcEngine = "volcengine" @@ -219,7 +219,7 @@ func GetWithTypeOption(t string, option *ApplyOption) (Applicant, error) { return NewAws(option), nil case configTypeCloudflare: return NewCloudflare(option), nil - case configTypeNamesilo: + case configTypeNameSilo: return NewNamesilo(option), nil case configTypeGoDaddy: return NewGodaddy(option), nil diff --git a/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go b/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go index 9c7131d1..b305156b 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go @@ -22,7 +22,7 @@ type HuaweiCloudCDNDeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` - // 华为云地域。 + // 华为云区域。 Region string `json:"region"` // 加速域名(不支持泛域名)。 Domain string `json:"domain"` diff --git a/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go b/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go index 7cbc4f12..d3c142f0 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go @@ -27,7 +27,7 @@ type HuaweiCloudELBDeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` - // 华为云地域。 + // 华为云区域。 Region string `json:"region"` // 部署资源类型。 ResourceType DeployResourceType `json:"resourceType"` diff --git a/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go b/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go index 42c4747f..d8ea91c6 100644 --- a/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go @@ -26,7 +26,7 @@ type HuaweiCloudELBUploaderConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` - // 华为云地域。 + // 华为云区域。 Region string `json:"region"` } diff --git a/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go b/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go index 1c219c79..5662f9b1 100644 --- a/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go @@ -22,7 +22,7 @@ type HuaweiCloudSCMUploaderConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` - // 华为云地域。 + // 华为云区域。 Region string `json:"region"` } diff --git a/ui/src/components/certimate/Version.tsx b/ui/src/components/Version.tsx similarity index 100% rename from ui/src/components/certimate/Version.tsx rename to ui/src/components/Version.tsx diff --git a/ui/src/components/access/AccessEditForm.tsx b/ui/src/components/access/AccessEditForm.tsx new file mode 100644 index 00000000..049abd68 --- /dev/null +++ b/ui/src/components/access/AccessEditForm.tsx @@ -0,0 +1,193 @@ +import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { + ACCESS_PROVIDER_TYPES, + type AccessModel, + type ACMEHttpReqAccessConfig, + type AliyunAccessConfig, + type AWSAccessConfig, + type BaiduCloudAccessConfig, + type BytePlusAccessConfig, + type CloudflareAccessConfig, + type DogeCloudAccessConfig, + type GoDaddyAccessConfig, + type HuaweiCloudAccessConfig, + type KubernetesAccessConfig, + type LocalAccessConfig, + type NameSiloAccessConfig, + type PowerDNSAccessConfig, + type QiniuAccessConfig, + type SSHAccessConfig, + type TencentCloudAccessConfig, + type VolcEngineAccessConfig, + type WebhookAccessConfig, +} from "@/domain/access"; +import AccessTypeSelect from "./AccessTypeSelect"; +import AccessEditFormACMEHttpReqConfig from "./AccessEditFormACMEHttpReqConfig"; +import AccessEditFormAliyunConfig from "./AccessEditFormAliyunConfig"; +import AccessEditFormAWSConfig from "./AccessEditFormAWSConfig"; +import AccessEditFormBaiduCloudConfig from "./AccessEditFormBaiduCloudConfig"; +import AccessEditFormBytePlusConfig from "./AccessEditFormBytePlusConfig"; +import AccessEditFormCloudflareConfig from "./AccessEditFormCloudflareConfig"; +import AccessEditFormDogeCloudConfig from "./AccessEditFormDogeCloudConfig"; +import AccessEditFormGoDaddyConfig from "./AccessEditFormGoDaddyConfig"; +import AccessEditFormHuaweiCloudConfig from "./AccessEditFormHuaweiCloudConfig"; +import AccessEditFormKubernetesConfig from "./AccessEditFormKubernetesConfig"; +import AccessEditFormLocalConfig from "./AccessEditFormLocalConfig"; +import AccessEditFormNameSiloConfig from "./AccessEditFormNameSiloConfig"; +import AccessEditFormPowerDNSConfig from "./AccessEditFormPowerDNSConfig"; +import AccessEditFormQiniuConfig from "./AccessEditFormQiniuConfig"; +import AccessEditFormSSHConfig from "./AccessEditFormSSHConfig"; +import AccessEditFormTencentCloudConfig from "./AccessEditFormTencentCloudConfig"; +import AccessEditFormVolcEngineConfig from "./AccessEditFormVolcEngineConfig"; +import AccessEditFormWebhookConfig from "./AccessEditFormWebhookConfig"; + +type AccessEditFormModelType = Partial>; + +export type AccessEditFormProps = { + className?: string; + style?: React.CSSProperties; + disabled?: boolean; + loading?: boolean; + mode: "add" | "edit"; + model?: AccessEditFormModelType; + onModelChange?: (model: AccessEditFormModelType) => void; +}; + +export type AccessEditFormInstance = { + getFieldsValue: () => AccessEditFormModelType; + resetFields: () => void; + validateFields: () => Promise; +}; + +const AccessEditForm = forwardRef(({ className, style, disabled, loading, mode, model, onModelChange }, ref) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + name: z + .string() + .trim() + .min(1, t("access.form.name.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + configType: z.nativeEnum(ACCESS_PROVIDER_TYPES, { message: t("access.form.type.placeholder") }), + config: z.any(), + }); + const formRule = createSchemaFieldRule(formSchema); + const [form] = Form.useForm>(); + + const [initialValues, setInitialValues] = useState>>(model ?? {}); + useEffect(() => { + setInitialValues(model ?? {}); + }, [model]); + + const [configType, setConfigType] = useState(model?.configType); + useEffect(() => { + setConfigType(model?.configType); + }, [model?.configType]); + + const [configFormInst] = Form.useForm(); + const configForm = useMemo(() => { + /* + 注意:如果追加新的子组件,请保持以 ASCII 排序。 + NOTICE: If you add new child component, please keep ASCII order. + */ + switch (configType) { + case ACCESS_PROVIDER_TYPES.ACMEHTTPREQ: + return ; + case ACCESS_PROVIDER_TYPES.ALIYUN: + return ; + case ACCESS_PROVIDER_TYPES.AWS: + return ; + case ACCESS_PROVIDER_TYPES.BAIDUCLOUD: + return ; + case ACCESS_PROVIDER_TYPES.BYTEPLUS: + return ; + case ACCESS_PROVIDER_TYPES.CLOUDFLARE: + return ; + case ACCESS_PROVIDER_TYPES.DOGECLOUD: + return ; + case ACCESS_PROVIDER_TYPES.GODADDY: + return ; + case ACCESS_PROVIDER_TYPES.HUAWEICLOUD: + return ; + case ACCESS_PROVIDER_TYPES.KUBERNETES: + return ; + case ACCESS_PROVIDER_TYPES.LOCAL: + return ; + case ACCESS_PROVIDER_TYPES.NAMESILO: + return ; + case ACCESS_PROVIDER_TYPES.POWERDNS: + return ; + case ACCESS_PROVIDER_TYPES.QINIU: + return ; + case ACCESS_PROVIDER_TYPES.SSH: + return ; + case ACCESS_PROVIDER_TYPES.TENCENTCLOUD: + return ; + case ACCESS_PROVIDER_TYPES.VOLCENGINE: + return ; + case ACCESS_PROVIDER_TYPES.WEBHOOK: + return ; + } + }, [model, configType, configFormInst]); + + const handleFormProviderChange = (name: string) => { + if (name === "configForm") { + form.setFieldValue("config", configFormInst.getFieldsValue()); + onModelChange?.(form.getFieldsValue()); + } + }; + + const handleFormChange = (_: unknown, fields: AccessEditFormModelType) => { + if (fields.configType !== configType) { + setConfigType(fields.configType); + } + + onModelChange?.(fields); + }; + + useImperativeHandle(ref, () => ({ + getFieldsValue: () => { + return form.getFieldsValue(); + }, + resetFields: () => { + return form.resetFields(); + }, + validateFields: () => { + const t1 = form.validateFields(); + const t2 = configFormInst.validateFields(); + return Promise.all([t1, t2]).then(() => t1); + }, + })); + + return ( + +
+
+ + + + + } + > + + + +
+
+ ); +}); + +export default AccessEditForm; diff --git a/ui/src/components/access/AccessEditFormACMEHttpReqConfig.tsx b/ui/src/components/access/AccessEditFormACMEHttpReqConfig.tsx new file mode 100644 index 00000000..6f88c6dd --- /dev/null +++ b/ui/src/components/access/AccessEditFormACMEHttpReqConfig.tsx @@ -0,0 +1,103 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input, Select, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type ACMEHttpReqAccessConfig } from "@/domain/access"; + +type AccessEditFormACMEHttpReqConfigModelType = Partial; + +export type AccessEditFormACMEHttpReqConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormACMEHttpReqConfigModelType; + onModelChange?: (model: AccessEditFormACMEHttpReqConfigModelType) => void; +}; + +const initModel = () => { + return { + endpoint: "https://example.com/api/", + mode: "", + } as AccessEditFormACMEHttpReqConfigModelType; +}; + +const AccessEditFormACMEHttpReqConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormACMEHttpReqConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + endpoint: z.string().url(t("common.errmsg.url_invalid")), + mode: z.string().min(0, t("access.form.acmehttpreq_mode.placeholder")).nullish(), + username: z + .string() + .trim() + .min(0, t("access.form.acmehttpreq_username.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })) + .nullish(), + password: z + .string() + .trim() + .min(0, t("access.form.acmehttpreq_password.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })) + .nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormACMEHttpReqConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormACMEHttpReqConfig; diff --git a/ui/src/components/access/AccessEditFormAWSConfig.tsx b/ui/src/components/access/AccessEditFormAWSConfig.tsx new file mode 100644 index 00000000..d153c734 --- /dev/null +++ b/ui/src/components/access/AccessEditFormAWSConfig.tsx @@ -0,0 +1,106 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AWSAccessConfig } from "@/domain/access"; + +type AccessEditFormAWSConfigModelType = Partial; + +export type AccessEditFormAWSConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormAWSConfigModelType; + onModelChange?: (model: AccessEditFormAWSConfigModelType) => void; +}; + +const initModel = () => { + return { + region: "us-east-1", + } as AccessEditFormAWSConfigModelType; +}; + +const AccessEditFormAWSConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormAWSConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKeyId: z + .string() + .trim() + .min(1, t("access.form.aws_access_key_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretAccessKey: z + .string() + .trim() + .min(1, t("access.form.aws_secret_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + // TODO: 该字段仅用于申请证书,后续迁移到工作流表单中 + region: z + .string() + .trim() + .min(0, t("access.form.aws_region.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .nullish(), + // TODO: 该字段仅用于申请证书,后续迁移到工作流表单中 + hostedZoneId: z + .string() + .trim() + .min(0, t("access.form.aws_hosted_zone_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormAWSConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + + + } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormAWSConfig; diff --git a/ui/src/components/access/AccessEditFormAliyunConfig.tsx b/ui/src/components/access/AccessEditFormAliyunConfig.tsx new file mode 100644 index 00000000..9d965d4c --- /dev/null +++ b/ui/src/components/access/AccessEditFormAliyunConfig.tsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AliyunAccessConfig } from "@/domain/access"; + +type AccessEditFormAliyunConfigModelType = Partial; + +export type AccessEditFormAliyunConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormAliyunConfigModelType; + onModelChange?: (model: AccessEditFormAliyunConfigModelType) => void; +}; + +const initModel = () => { + return {} as AccessEditFormAliyunConfigModelType; +}; + +const AccessEditFormAliyunConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormAliyunConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKeyId: z + .string() + .trim() + .min(1, t("access.form.aliyun_access_key_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + accessKeySecret: z + .string() + .trim() + .min(1, t("access.form.aliyun_access_key_secret.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormAliyunConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormAliyunConfig; diff --git a/ui/src/components/access/AccessEditFormBaiduCloudConfig.tsx b/ui/src/components/access/AccessEditFormBaiduCloudConfig.tsx new file mode 100644 index 00000000..b66d55c5 --- /dev/null +++ b/ui/src/components/access/AccessEditFormBaiduCloudConfig.tsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type BaiduCloudAccessConfig } from "@/domain/access"; + +type AccessEditFormBaiduCloudConfigModelType = Partial; + +export type AccessEditFormBaiduCloudConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormBaiduCloudConfigModelType; + onModelChange?: (model: AccessEditFormBaiduCloudConfigModelType) => void; +}; + +const initModel = () => { + return {} as AccessEditFormBaiduCloudConfigModelType; +}; + +const AccessEditFormBaiduCloudConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormBaiduCloudConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKeyId: z + .string() + .trim() + .min(1, t("access.form.baiducloud_access_key_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretAccessKey: z + .string() + .trim() + .min(1, t("access.form.baiducloud_secret_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormBaiduCloudConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormBaiduCloudConfig; diff --git a/ui/src/components/access/AccessEditFormBytePlusConfig.tsx b/ui/src/components/access/AccessEditFormBytePlusConfig.tsx new file mode 100644 index 00000000..d55e17b7 --- /dev/null +++ b/ui/src/components/access/AccessEditFormBytePlusConfig.tsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type BytePlusAccessConfig } from "@/domain/access"; + +type AccessEditFormBytePlusConfigModelType = Partial; + +export type AccessEditFormBytePlusConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormBytePlusConfigModelType; + onModelChange?: (model: AccessEditFormBytePlusConfigModelType) => void; +}; + +const initModel = () => { + return {} as AccessEditFormBytePlusConfigModelType; +}; + +const AccessEditFormBytePlusConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormBytePlusConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKey: z + .string() + .trim() + .min(1, t("access.form.byteplus_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretKey: z + .string() + .trim() + .min(1, t("access.form.byteplus_secret_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormBytePlusConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormBytePlusConfig; diff --git a/ui/src/components/access/AccessEditFormCloudflareConfig.tsx b/ui/src/components/access/AccessEditFormCloudflareConfig.tsx new file mode 100644 index 00000000..40a3167b --- /dev/null +++ b/ui/src/components/access/AccessEditFormCloudflareConfig.tsx @@ -0,0 +1,58 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type CloudflareAccessConfig } from "@/domain/access"; + +type AccessEditFormCloudflareConfigModelType = Partial; + +export type AccessEditFormCloudflareConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormCloudflareConfigModelType; + onModelChange?: (model: AccessEditFormCloudflareConfigModelType) => void; +}; + +const initModel = () => { + return {} as AccessEditFormCloudflareConfigModelType; +}; + +const AccessEditFormCloudflareConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormCloudflareConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + dnsApiToken: z + .string() + .trim() + .min(1, t("access.form.cloudflare_dns_api_token.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormCloudflareConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + +
+ ); +}; + +export default AccessEditFormCloudflareConfig; diff --git a/ui/src/components/access/AccessEditFormDogeCloudConfig.tsx b/ui/src/components/access/AccessEditFormDogeCloudConfig.tsx new file mode 100644 index 00000000..d8dac295 --- /dev/null +++ b/ui/src/components/access/AccessEditFormDogeCloudConfig.tsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type DogeCloudAccessConfig } from "@/domain/access"; + +type AccessEditFormDogeCloudConfigModelType = Partial; + +export type AccessEditFormDogeCloudConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormDogeCloudConfigModelType; + onModelChange?: (model: AccessEditFormDogeCloudConfigModelType) => void; +}; + +const initModel = () => { + return {} as AccessEditFormDogeCloudConfigModelType; +}; + +const AccessEditFormDogeCloudConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormDogeCloudConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKey: z + .string() + .trim() + .min(1, t("access.form.dogecloud_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretKey: z + .string() + .trim() + .min(1, t("access.form.dogecloud_secret_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormDogeCloudConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormDogeCloudConfig; diff --git a/ui/src/components/access/AccessEditFormGoDaddyConfig.tsx b/ui/src/components/access/AccessEditFormGoDaddyConfig.tsx new file mode 100644 index 00000000..3c71b752 --- /dev/null +++ b/ui/src/components/access/AccessEditFormGoDaddyConfig.tsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type GoDaddyAccessConfig } from "@/domain/access"; + +type AccessEditFormGoDaddyConfigModelType = Partial; + +export type AccessEditFormGoDaddyConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormGoDaddyConfigModelType; + onModelChange?: (model: AccessEditFormGoDaddyConfigModelType) => void; +}; + +const initModel = () => { + return {} as AccessEditFormGoDaddyConfigModelType; +}; + +const AccessEditFormGoDaddyConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormGoDaddyConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + apiKey: z + .string() + .trim() + .min(1, t("access.form.godaddy_api_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + apiSecret: z + .string() + .trim() + .min(1, t("access.form.godaddy_api_secret.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormGoDaddyConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormGoDaddyConfig; diff --git a/ui/src/components/access/AccessEditFormHuaweiCloudConfig.tsx b/ui/src/components/access/AccessEditFormHuaweiCloudConfig.tsx new file mode 100644 index 00000000..fc0e422d --- /dev/null +++ b/ui/src/components/access/AccessEditFormHuaweiCloudConfig.tsx @@ -0,0 +1,90 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type HuaweiCloudAccessConfig } from "@/domain/access"; + +type AccessEditFormHuaweiCloudConfigModelType = Partial; + +export type AccessEditFormHuaweiCloudConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormHuaweiCloudConfigModelType; + onModelChange?: (model: AccessEditFormHuaweiCloudConfigModelType) => void; +}; + +const initModel = () => { + return { + region: "cn-north-1", + } as AccessEditFormHuaweiCloudConfigModelType; +}; + +const AccessEditFormHuaweiCloudConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormHuaweiCloudConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKeyId: z + .string() + .trim() + .min(1, t("access.form.huaweicloud_access_key_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretAccessKey: z + .string() + .trim() + .min(1, t("access.form.huaweicloud_secret_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + // TODO: 该字段仅用于申请证书,后续迁移到工作流表单中 + region: z + .string() + .trim() + .min(0, t("access.form.huaweicloud_region.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormHuaweiCloudConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormHuaweiCloudConfig; diff --git a/ui/src/components/access/AccessEditFormKubernetesConfig.tsx b/ui/src/components/access/AccessEditFormKubernetesConfig.tsx new file mode 100644 index 00000000..04adee75 --- /dev/null +++ b/ui/src/components/access/AccessEditFormKubernetesConfig.tsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from "react"; +import { flushSync } from "react-dom"; +import { useTranslation } from "react-i18next"; +import { Button, Form, Input, Upload, type FormInstance, type UploadFile, type UploadProps } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; +import { Upload as UploadIcon } from "lucide-react"; + +import { type KubernetesAccessConfig } from "@/domain/access"; +import { readFileContent } from "@/utils/file"; + +type AccessEditFormKubernetesConfigModelType = Partial; + +export type AccessEditFormKubernetesConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormKubernetesConfigModelType; + onModelChange?: (model: AccessEditFormKubernetesConfigModelType) => void; +}; + +const initModel = () => { + return {} as AccessEditFormKubernetesConfigModelType; +}; + +const AccessEditFormKubernetesConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormKubernetesConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + kubeConfig: z + .string() + .trim() + .min(0, t("access.form.k8s_kubeconfig.placeholder")) + .max(20480, t("common.errmsg.string_max", { max: 20480 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + setKubeFileList(model?.kubeConfig?.trim() ? [{ uid: "-1", name: "kubeconfig", status: "done" }] : []); + }, [model]); + + const [kubeFileList, setKubeFileList] = useState([]); + + const handleFormChange = (_: unknown, fields: AccessEditFormKubernetesConfigModelType) => { + onModelChange?.(fields); + }; + + const handleUploadChange: UploadProps["onChange"] = async ({ file }) => { + if (file && file.status !== "removed") { + form.setFieldValue("kubeConfig", (await readFileContent(file.originFileObj ?? (file as unknown as File))).trim()); + setKubeFileList([file]); + } else { + form.setFieldValue("kubeConfig", ""); + setKubeFileList([]); + } + + flushSync(() => onModelChange?.(form.getFieldsValue())); + }; + + return ( +
+ } + > + +
+ ); +}; + +export default AccessEditFormKubernetesConfig; diff --git a/ui/src/components/access/AccessEditFormLocalConfig.tsx b/ui/src/components/access/AccessEditFormLocalConfig.tsx new file mode 100644 index 00000000..918908a8 --- /dev/null +++ b/ui/src/components/access/AccessEditFormLocalConfig.tsx @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; +import { Form, type FormInstance } from "antd"; + +import { type LocalAccessConfig } from "@/domain/access"; + +type AccessEditFormLocalConfigModelType = Partial; + +export type AccessEditFormLocalConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormLocalConfigModelType; + onModelChange?: (model: AccessEditFormLocalConfigModelType) => void; +}; + +const initModel = () => { + return {} as AccessEditFormLocalConfigModelType; +}; + +const AccessEditFormLocalConfig = ({ form, disabled, loading, model }: AccessEditFormLocalConfigProps) => { + const [initialValues, setInitialValues] = useState(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + return
; +}; + +export default AccessEditFormLocalConfig; diff --git a/ui/src/components/access/AccessEditFormNameSiloConfig.tsx b/ui/src/components/access/AccessEditFormNameSiloConfig.tsx new file mode 100644 index 00000000..54b77870 --- /dev/null +++ b/ui/src/components/access/AccessEditFormNameSiloConfig.tsx @@ -0,0 +1,58 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type NameSiloAccessConfig } from "@/domain/access"; + +type AccessEditFormNameSiloConfigModelType = Partial; + +export type AccessEditFormNameSiloConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormNameSiloConfigModelType; + onModelChange?: (model: AccessEditFormNameSiloConfigModelType) => void; +}; + +const initModel = () => { + return {} as AccessEditFormNameSiloConfigModelType; +}; + +const AccessEditFormNameSiloConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormNameSiloConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + apiKey: z + .string() + .trim() + .min(1, t("access.form.namesilo_api_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormNameSiloConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + +
+ ); +}; + +export default AccessEditFormNameSiloConfig; diff --git a/ui/src/components/access/AccessEditFormPowerDNSConfig.tsx b/ui/src/components/access/AccessEditFormPowerDNSConfig.tsx new file mode 100644 index 00000000..5cbc52a2 --- /dev/null +++ b/ui/src/components/access/AccessEditFormPowerDNSConfig.tsx @@ -0,0 +1,68 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type PowerDNSAccessConfig } from "@/domain/access"; + +type AccessEditFormPowerDNSConfigModelType = Partial; + +export type AccessEditFormPowerDNSConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormPowerDNSConfigModelType; + onModelChange?: (model: AccessEditFormPowerDNSConfigModelType) => void; +}; + +const initModel = () => { + return {} as AccessEditFormPowerDNSConfigModelType; +}; + +const AccessEditFormPowerDNSConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormPowerDNSConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + apiUrl: z.string().url(t("common.errmsg.url_invalid")), + apiKey: z + .string() + .trim() + .min(1, t("access.form.powerdns_api_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormPowerDNSConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormPowerDNSConfig; diff --git a/ui/src/components/access/AccessEditFormQiniuConfig.tsx b/ui/src/components/access/AccessEditFormQiniuConfig.tsx new file mode 100644 index 00000000..9bb3f1c2 --- /dev/null +++ b/ui/src/components/access/AccessEditFormQiniuConfig.tsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type QiniuAccessConfig } from "@/domain/access"; + +type AccessEditFormQiniuConfigModelType = Partial; + +export type AccessEditFormQiniuConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormQiniuConfigModelType; + onModelChange?: (model: AccessEditFormQiniuConfigModelType) => void; +}; + +const initModel = () => { + return {} as AccessEditFormQiniuConfigModelType; +}; + +const AccessEditFormQiniuConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormQiniuConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKey: z + .string() + .trim() + .min(1, t("access.form.qiniu_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretKey: z + .string() + .trim() + .min(1, t("access.form.qiniu_secret_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormQiniuConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormQiniuConfig; diff --git a/ui/src/components/access/AccessEditFormSSHConfig.tsx b/ui/src/components/access/AccessEditFormSSHConfig.tsx new file mode 100644 index 00000000..4d9c8d9b --- /dev/null +++ b/ui/src/components/access/AccessEditFormSSHConfig.tsx @@ -0,0 +1,162 @@ +import { useEffect, useState } from "react"; +import { flushSync } from "react-dom"; +import { useTranslation } from "react-i18next"; +import { Button, Form, Input, InputNumber, Upload, type FormInstance, type UploadFile, type UploadProps } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; +import { Upload as UploadIcon } from "lucide-react"; + +import { type SSHAccessConfig } from "@/domain/access"; +import { readFileContent } from "@/utils/file"; + +type AccessEditFormSSHConfigModelType = Partial; + +export type AccessEditFormSSHConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormSSHConfigModelType; + onModelChange?: (model: AccessEditFormSSHConfigModelType) => void; +}; + +const initModel = () => { + return { + host: "127.0.0.1", + port: 22, + username: "root", + } as AccessEditFormSSHConfigModelType; +}; + +const AccessEditFormSSHConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormSSHConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + host: z.string().refine( + (str) => { + const reIpv4 = + /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + const reIpv6 = + /^([\da-fA-F]{1,4}:){6}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)|::([\da−fA−F]1,4:)0,4((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|::([\da−fA−F]1,4:)0,4((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|^([\da-fA-F]{1,4}:):([\da-fA-F]{1,4}:){0,3}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)|([\da−fA−F]1,4:)2:([\da−fA−F]1,4:)0,2((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|([\da−fA−F]1,4:)2:([\da−fA−F]1,4:)0,2((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|^([\da-fA-F]{1,4}:){3}:([\da-fA-F]{1,4}:){0,1}((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)|([\da−fA−F]1,4:)4:((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|([\da−fA−F]1,4:)4:((25[0−5]|2[0−4]\d|[01]?\d\d?)\.)3(25[0−5]|2[0−4]\d|[01]?\d\d?)|^([\da-fA-F]{1,4}:){7}[\da-fA-F]{1,4}|:((:[\da−fA−F]1,4)1,6|:)|:((:[\da−fA−F]1,4)1,6|:)|^[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,5}|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|([\da−fA−F]1,4:)2((:[\da−fA−F]1,4)1,4|:)|^([\da-fA-F]{1,4}:){3}((:[\da-fA-F]{1,4}){1,3}|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|([\da−fA−F]1,4:)4((:[\da−fA−F]1,4)1,2|:)|^([\da-fA-F]{1,4}:){5}:([\da-fA-F]{1,4})?|([\da−fA−F]1,4:)6:|([\da−fA−F]1,4:)6:/; + const reDomain = /^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/; + return reIpv4.test(str) || reIpv6.test(str) || reDomain.test(str); + }, + { message: t("common.errmsg.host_invalid") } + ), + port: z + .number() + .int() + .gte(1, t("common.errmsg.port_invalid")) + .lte(65535, t("common.errmsg.port_invalid")) + .transform((v) => +v), + username: z + .string() + .min(1, "access.form.ssh_username.placeholder") + .max(64, t("common.errmsg.string_max", { max: 64 })), + password: z + .string() + .min(0, "access.form.ssh_password.placeholder") + .max(64, t("common.errmsg.string_max", { max: 64 })) + .nullish(), + key: z + .string() + .min(0, "access.form.ssh_key.placeholder") + .max(20480, t("common.errmsg.string_max", { max: 20480 })) + .nullish(), + keyPassphrase: z + .string() + .min(0, "access.form.ssh_key_passphrase.placeholder") + .max(20480, t("common.errmsg.string_max", { max: 20480 })) + .nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + setKeyFileList(model?.key?.trim() ? [{ uid: "-1", name: "sshkey", status: "done" }] : []); + }, [model]); + + const [keyFileList, setKeyFileList] = useState([]); + + const handleFormChange = (_: unknown, fields: AccessEditFormSSHConfigModelType) => { + onModelChange?.(fields); + }; + + const handleUploadChange: UploadProps["onChange"] = async ({ file }) => { + if (file && file.status !== "removed") { + form.setFieldValue("kubeConfig", (await readFileContent(file.originFileObj ?? (file as unknown as File))).trim()); + setKeyFileList([file]); + } else { + form.setFieldValue("kubeConfig", ""); + setKeyFileList([]); + } + + flushSync(() => onModelChange?.(form.getFieldsValue())); + }; + + return ( +
+
+
+ + + +
+ +
+ + + +
+
+ +
+
+ + + +
+ +
+ } + > + + +
+
+ +
+
+ } + > + +
+ +
+ } + > + + +
+
+
+ ); +}; + +export default AccessEditFormSSHConfig; diff --git a/ui/src/components/access/AccessEditFormTencentCloudConfig.tsx b/ui/src/components/access/AccessEditFormTencentCloudConfig.tsx new file mode 100644 index 00000000..b710635b --- /dev/null +++ b/ui/src/components/access/AccessEditFormTencentCloudConfig.tsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type TencentCloudAccessConfig } from "@/domain/access"; + +type AccessEditFormTencentCloudConfigModelType = Partial; + +export type AccessEditFormTencentCloudConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormTencentCloudConfigModelType; + onModelChange?: (model: AccessEditFormTencentCloudConfigModelType) => void; +}; + +const initModel = () => { + return {} as AccessEditFormTencentCloudConfigModelType; +}; + +const AccessEditFormTencentCloudConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormTencentCloudConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + secretId: z + .string() + .trim() + .min(1, t("access.form.tencentcloud_secret_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretKey: z + .string() + .trim() + .min(1, t("access.form.tencentcloud_secret_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormTencentCloudConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormTencentCloudConfig; diff --git a/ui/src/components/access/AccessEditFormVolcEngineConfig.tsx b/ui/src/components/access/AccessEditFormVolcEngineConfig.tsx new file mode 100644 index 00000000..173bed42 --- /dev/null +++ b/ui/src/components/access/AccessEditFormVolcEngineConfig.tsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type VolcEngineAccessConfig } from "@/domain/access"; + +type AccessEditFormVolcEngineConfigModelType = Partial; + +export type AccessEditFormVolcEngineConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormVolcEngineConfigModelType; + onModelChange?: (model: AccessEditFormVolcEngineConfigModelType) => void; +}; + +const initModel = () => { + return {} as AccessEditFormVolcEngineConfigModelType; +}; + +const AccessEditFormVolcEngineConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormVolcEngineConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKeyId: z + .string() + .trim() + .min(1, t("access.form.volcengine_access_key_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + secretAccessKey: z + .string() + .trim() + .min(1, t("access.form.volcengine_secret_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormVolcEngineConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessEditFormVolcEngineConfig; diff --git a/ui/src/components/access/AccessEditFormWebhookConfig.tsx b/ui/src/components/access/AccessEditFormWebhookConfig.tsx new file mode 100644 index 00000000..8b2649a5 --- /dev/null +++ b/ui/src/components/access/AccessEditFormWebhookConfig.tsx @@ -0,0 +1,49 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, Input, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type WebhookAccessConfig } from "@/domain/access"; + +type AccessEditFormWebhookConfigModelType = Partial; + +export type AccessEditFormWebhookConfigProps = { + form: FormInstance; + disabled?: boolean; + loading?: boolean; + model?: AccessEditFormWebhookConfigModelType; + onModelChange?: (model: AccessEditFormWebhookConfigModelType) => void; +}; + +const initModel = () => { + return {} as AccessEditFormWebhookConfigModelType; +}; + +const AccessEditFormWebhookConfig = ({ form, disabled, loading, model, onModelChange }: AccessEditFormWebhookConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + url: z.string().url(t("common.errmsg.url_invalid")), + }); + const formRule = createSchemaFieldRule(formSchema); + + const [initialValues, setInitialValues] = useState>>(model ?? initModel()); + useEffect(() => { + setInitialValues(model ?? initModel()); + }, [model]); + + const handleFormChange = (_: unknown, fields: AccessEditFormWebhookConfigModelType) => { + onModelChange?.(fields); + }; + + return ( +
+ + + +
+ ); +}; + +export default AccessEditFormWebhookConfig; diff --git a/ui/src/components/access/AccessEditModal.tsx b/ui/src/components/access/AccessEditModal.tsx new file mode 100644 index 00000000..c9baec52 --- /dev/null +++ b/ui/src/components/access/AccessEditModal.tsx @@ -0,0 +1,108 @@ +import { cloneElement, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useControllableValue } from "ahooks"; +import { Modal, notification } from "antd"; + +import { type AccessModel } from "@/domain/access"; +import { useAccessStore } from "@/stores/access"; +import AccessEditForm, { type AccessEditFormInstance } from "./AccessEditForm"; + +export type AccessEditModalProps = { + data?: Partial; + loading?: boolean; + mode: "add" | "edit" | "copy"; + open?: boolean; + trigger?: React.ReactElement; + onOpenChange?: (open: boolean) => void; +}; + +const AccessEditModal = ({ data, loading, mode, trigger, ...props }: AccessEditModalProps) => { + const { t } = useTranslation(); + + const [notificationApi, NotificationContextHolder] = notification.useNotification(); + + const { createAccess, updateAccess } = useAccessStore(); + + const [open, setOpen] = useControllableValue(props, { + valuePropName: "open", + defaultValuePropName: "defaultOpen", + trigger: "onOpenChange", + }); + + const triggerEl = useMemo(() => { + if (!trigger) { + return null; + } + + return cloneElement(trigger, { + ...trigger.props, + onClick: () => { + setOpen(true); + trigger.props?.onClick?.(); + }, + }); + }, [trigger, setOpen]); + + const formRef = useRef(null); + const [formPending, setFormPending] = useState(false); + + const handleClickOk = async () => { + setFormPending(true); + try { + await formRef.current!.validateFields(); + } catch (err) { + setFormPending(false); + return Promise.reject(); + } + + try { + if (mode === "add") { + await createAccess(formRef.current!.getFieldsValue() as AccessModel); + } else { + await updateAccess({ ...data, ...formRef.current!.getFieldsValue() } as AccessModel); + } + + setOpen(false); + } catch (err) { + notificationApi.error({ message: t("common.text.request_error"), description: <>{String(err)} }); + + throw err; + } finally { + setFormPending(false); + } + }; + + const handleClickCancel = () => { + if (formPending) return Promise.reject(); + + setOpen(false); + }; + + return ( + <> + {NotificationContextHolder} + + {triggerEl} + + setOpen(false)} + cancelButtonProps={{ disabled: formPending }} + closable + confirmLoading={formPending} + destroyOnClose + loading={loading} + okText={mode === "edit" ? t("common.button.save") : t("common.button.submit")} + open={open} + title={t(`access.action.${mode}`)} + onOk={handleClickOk} + onCancel={handleClickCancel} + > +
+ +
+
+ + ); +}; + +export default AccessEditModal; diff --git a/ui/src/components/access/AccessTypeSelect.tsx b/ui/src/components/access/AccessTypeSelect.tsx index 711d1b22..bc52a709 100644 --- a/ui/src/components/access/AccessTypeSelect.tsx +++ b/ui/src/components/access/AccessTypeSelect.tsx @@ -15,17 +15,44 @@ const AccessTypeSelect = memo((props: AccessTypeSelectProps) => { label: t(item.name), })); + const renderOption = (key: string) => { + const provider = accessProvidersMap.get(key); + return ( +
+ + + + {t(provider?.name ?? "")} + + +
+ {provider?.usage === "apply" && ( + <> + {t("access.props.provider.usage.dns")} + + )} + {provider?.usage === "deploy" && ( + <> + {t("access.props.provider.usage.host")} + + )} + {provider?.usage === "all" && ( + <> + {t("access.props.provider.usage.dns")} + {t("access.props.provider.usage.host")} + + )} +
+
+ ); + }; + return ( @@ -55,7 +54,7 @@ const Login = () => { - diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index 9196aad6..8559b73c 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -313,6 +313,7 @@ const WorkflowList = () => { current: page, pageSize: pageSize, total: tableTotal, + showSizeChanger: true, onChange: (page: number, pageSize: number) => { setPage(page); setPageSize(pageSize); diff --git a/ui/src/repository/access.ts b/ui/src/repository/access.ts index bc4ab8c8..2d114a45 100644 --- a/ui/src/repository/access.ts +++ b/ui/src/repository/access.ts @@ -12,7 +12,7 @@ export const list = async () => { }); }; -export const save = async (record: AccessModel) => { +export const save = async (record: Partial) => { if (record.id) { return await getPocketBase().collection(COLLECTION_NAME).update(record.id, record); } @@ -20,7 +20,7 @@ export const save = async (record: AccessModel) => { return await getPocketBase().collection(COLLECTION_NAME).create(record); }; -export const remove = async (record: AccessModel) => { +export const remove = async (record: Partial) => { record = { ...record, deleted: dayjs.utc().format("YYYY-MM-DD HH:mm:ss") }; await getPocketBase().collection(COLLECTION_NAME).update(record.id, record); }; diff --git a/ui/src/stores/access/index.ts b/ui/src/stores/access/index.ts index c67167e7..510167c8 100644 --- a/ui/src/stores/access/index.ts +++ b/ui/src/stores/access/index.ts @@ -6,7 +6,7 @@ import { list as listAccess, save as saveAccess, remove as removeAccess } from " export interface AccessState { accesses: AccessModel[]; - createAccess: (access: AccessModel) => void; + createAccess: (access: Omit) => void; updateAccess: (access: AccessModel) => void; deleteAccess: (access: AccessModel) => void; fetchAccesses: () => Promise; @@ -17,22 +17,24 @@ export const useAccessStore = create((set) => { accesses: [], createAccess: async (access) => { - access = await saveAccess(access); + const record = await saveAccess(access); set( produce((state: AccessState) => { - state.accesses.unshift(access); + state.accesses.unshift(record); }) ); }, updateAccess: async (access) => { - access = await saveAccess(access); + const record = await saveAccess(access); set( produce((state: AccessState) => { - const index = state.accesses.findIndex((e) => e.id === access.id); - state.accesses[index] = access; + const index = state.accesses.findIndex((e) => e.id === record.id); + if (index !== -1) { + state.accesses[index] = record; + } }) ); }, diff --git a/ui/src/utils/file.ts b/ui/src/utils/file.ts index 41283994..00b7beef 100644 --- a/ui/src/utils/file.ts +++ b/ui/src/utils/file.ts @@ -14,7 +14,7 @@ export function readFileContent(file: File): Promise { reader.onerror = () => reject(reader.error); - reader.readAsText(file); + reader.readAsText(file, "utf-8"); }); }