Merge pull request #573 from fudiwei/main
Support configuring independent CA for each workflow
This commit is contained in:
118
ui/src/components/access/AccessEditDrawer.tsx
Normal file
118
ui/src/components/access/AccessEditDrawer.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useControllableValue } from "ahooks";
|
||||
import { Button, Drawer, Space, notification } from "antd";
|
||||
|
||||
import { type AccessModel } from "@/domain/access";
|
||||
import { useTriggerElement, useZustandShallowSelector } from "@/hooks";
|
||||
import { useAccessesStore } from "@/stores/access";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
import AccessForm, { type AccessFormInstance, type AccessFormProps } from "./AccessForm";
|
||||
|
||||
export type AccessEditDrawerProps = {
|
||||
data?: AccessFormProps["initialValues"];
|
||||
loading?: boolean;
|
||||
open?: boolean;
|
||||
range?: AccessFormProps["range"];
|
||||
scene: AccessFormProps["scene"];
|
||||
trigger?: React.ReactNode;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
afterSubmit?: (record: AccessModel) => void;
|
||||
};
|
||||
|
||||
const AccessEditDrawer = ({ data, loading, trigger, scene, range, afterSubmit, ...props }: AccessEditDrawerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
|
||||
const { createAccess, updateAccess } = useAccessesStore(useZustandShallowSelector(["createAccess", "updateAccess"]));
|
||||
|
||||
const [open, setOpen] = useControllableValue<boolean>(props, {
|
||||
valuePropName: "open",
|
||||
defaultValuePropName: "defaultOpen",
|
||||
trigger: "onOpenChange",
|
||||
});
|
||||
|
||||
const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) });
|
||||
|
||||
const formRef = useRef<AccessFormInstance>(null);
|
||||
const [formPending, setFormPending] = useState(false);
|
||||
|
||||
const handleOkClick = async () => {
|
||||
setFormPending(true);
|
||||
try {
|
||||
await formRef.current!.validateFields();
|
||||
} catch (err) {
|
||||
setFormPending(false);
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
let values: AccessModel = formRef.current!.getFieldsValue();
|
||||
|
||||
if (scene === "add") {
|
||||
if (data?.id) {
|
||||
throw "Invalid props: `data`";
|
||||
}
|
||||
|
||||
values = await createAccess(values);
|
||||
} else if (scene === "edit") {
|
||||
if (!data?.id) {
|
||||
throw "Invalid props: `data`";
|
||||
}
|
||||
|
||||
values = await updateAccess({ ...data, ...values });
|
||||
} else {
|
||||
throw "Invalid props: `scene`";
|
||||
}
|
||||
|
||||
afterSubmit?.(values);
|
||||
setOpen(false);
|
||||
} catch (err) {
|
||||
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
|
||||
throw err;
|
||||
} finally {
|
||||
setFormPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelClick = () => {
|
||||
if (formPending) return;
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{NotificationContextHolder}
|
||||
|
||||
{triggerEl}
|
||||
|
||||
<Drawer
|
||||
afterOpenChange={setOpen}
|
||||
closable={!formPending}
|
||||
destroyOnClose
|
||||
footer={
|
||||
<Space className="w-full justify-end">
|
||||
<Button onClick={handleCancelClick}>{t("common.button.cancel")}</Button>
|
||||
<Button loading={formPending} type="primary" onClick={handleOkClick}>
|
||||
{scene === "edit" ? t("common.button.save") : t("common.button.submit")}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
loading={loading}
|
||||
maskClosable={!formPending}
|
||||
open={open}
|
||||
title={t(`access.action.${scene}`)}
|
||||
width={720}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<AccessForm ref={formRef} initialValues={data} range={range} scene={scene === "add" ? "add" : "edit"} />
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessEditDrawer;
|
||||
@@ -14,13 +14,14 @@ export type AccessEditModalProps = {
|
||||
data?: AccessFormProps["initialValues"];
|
||||
loading?: boolean;
|
||||
open?: boolean;
|
||||
preset: AccessFormProps["preset"];
|
||||
range?: AccessFormProps["range"];
|
||||
scene: AccessFormProps["scene"];
|
||||
trigger?: React.ReactNode;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
afterSubmit?: (record: AccessModel) => void;
|
||||
};
|
||||
|
||||
const AccessEditModal = ({ data, loading, trigger, preset, afterSubmit, ...props }: AccessEditModalProps) => {
|
||||
const AccessEditModal = ({ data, loading, trigger, scene, range, afterSubmit, ...props }: AccessEditModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
@@ -50,13 +51,13 @@ const AccessEditModal = ({ data, loading, trigger, preset, afterSubmit, ...props
|
||||
try {
|
||||
let values: AccessModel = formRef.current!.getFieldsValue();
|
||||
|
||||
if (preset === "add") {
|
||||
if (scene === "add") {
|
||||
if (data?.id) {
|
||||
throw "Invalid props: `data`";
|
||||
}
|
||||
|
||||
values = await createAccess(values);
|
||||
} else if (preset === "edit") {
|
||||
} else if (scene === "edit") {
|
||||
if (!data?.id) {
|
||||
throw "Invalid props: `data`";
|
||||
}
|
||||
@@ -96,15 +97,15 @@ const AccessEditModal = ({ data, loading, trigger, preset, afterSubmit, ...props
|
||||
confirmLoading={formPending}
|
||||
destroyOnClose
|
||||
loading={loading}
|
||||
okText={preset === "edit" ? t("common.button.save") : t("common.button.submit")}
|
||||
okText={scene === "edit" ? t("common.button.save") : t("common.button.submit")}
|
||||
open={open}
|
||||
title={t(`access.action.${preset}`)}
|
||||
title={t(`access.action.${scene}`)}
|
||||
width={480}
|
||||
onOk={handleOkClick}
|
||||
onCancel={handleCancelClick}
|
||||
>
|
||||
<div className="pb-2 pt-4">
|
||||
<AccessForm ref={formRef} initialValues={data} preset={preset === "add" ? "add" : "edit"} />
|
||||
<AccessForm ref={formRef} initialValues={data} range={range} scene={scene === "add" ? "add" : "edit"} />
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { z } from "zod";
|
||||
|
||||
import AccessProviderSelect from "@/components/provider/AccessProviderSelect";
|
||||
import { type AccessModel } from "@/domain/access";
|
||||
import { ACCESS_PROVIDERS } from "@/domain/provider";
|
||||
import { ACCESS_PROVIDERS, ACCESS_USAGES } from "@/domain/provider";
|
||||
import { useAntdForm, useAntdFormName } from "@/hooks";
|
||||
|
||||
import AccessForm1PanelConfig from "./AccessForm1PanelConfig";
|
||||
@@ -31,10 +31,10 @@ import AccessFormEdgioConfig from "./AccessFormEdgioConfig";
|
||||
import AccessFormGcoreConfig from "./AccessFormGcoreConfig";
|
||||
import AccessFormGnameConfig from "./AccessFormGnameConfig";
|
||||
import AccessFormGoDaddyConfig from "./AccessFormGoDaddyConfig";
|
||||
import AccessFormGoogleTrustServicesConfig from "./AccessFormGoogleTrustServicesConfig";
|
||||
import AccessFormHuaweiCloudConfig from "./AccessFormHuaweiCloudConfig";
|
||||
import AccessFormJDCloudConfig from "./AccessFormJDCloudConfig";
|
||||
import AccessFormKubernetesConfig from "./AccessFormKubernetesConfig";
|
||||
import AccessFormLocalConfig from "./AccessFormLocalConfig";
|
||||
import AccessFormNamecheapConfig from "./AccessFormNamecheapConfig";
|
||||
import AccessFormNameDotComConfig from "./AccessFormNameDotComConfig";
|
||||
import AccessFormNameSiloConfig from "./AccessFormNameSiloConfig";
|
||||
@@ -45,6 +45,7 @@ import AccessFormQiniuConfig from "./AccessFormQiniuConfig";
|
||||
import AccessFormRainYunConfig from "./AccessFormRainYunConfig";
|
||||
import AccessFormSafeLineConfig from "./AccessFormSafeLineConfig";
|
||||
import AccessFormSSHConfig from "./AccessFormSSHConfig";
|
||||
import AccessFormSSLComConfig from "./AccessFormSSLComConfig";
|
||||
import AccessFormTencentCloudConfig from "./AccessFormTencentCloudConfig";
|
||||
import AccessFormUCloudConfig from "./AccessFormUCloudConfig";
|
||||
import AccessFormUpyunConfig from "./AccessFormUpyunConfig";
|
||||
@@ -52,16 +53,19 @@ import AccessFormVercelConfig from "./AccessFormVercelConfig";
|
||||
import AccessFormVolcEngineConfig from "./AccessFormVolcEngineConfig";
|
||||
import AccessFormWebhookConfig from "./AccessFormWebhookConfig";
|
||||
import AccessFormWestcnConfig from "./AccessFormWestcnConfig";
|
||||
import AccessFormZeroSSLConfig from "./AccessFormZeroSSLConfig";
|
||||
|
||||
type AccessFormFieldValues = Partial<MaybeModelRecord<AccessModel>>;
|
||||
type AccessFormPresets = "add" | "edit";
|
||||
type AccessFormRanges = "both-dns-hosting" | "ca-only" | "notify-only";
|
||||
type AccessFormScenes = "add" | "edit";
|
||||
|
||||
export type AccessFormProps = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
disabled?: boolean;
|
||||
initialValues?: AccessFormFieldValues;
|
||||
preset: AccessFormPresets;
|
||||
range?: AccessFormRanges;
|
||||
scene: AccessFormScenes;
|
||||
onValuesChange?: (values: AccessFormFieldValues) => void;
|
||||
};
|
||||
|
||||
@@ -71,7 +75,7 @@ export type AccessFormInstance = {
|
||||
validateFields: FormInstance<AccessFormFieldValues>["validateFields"];
|
||||
};
|
||||
|
||||
const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className, style, disabled, initialValues, preset, onValuesChange }, ref) => {
|
||||
const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className, style, disabled, initialValues, range, scene, onValuesChange }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -80,7 +84,14 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
|
||||
.min(1, t("access.form.name.placeholder"))
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 }))
|
||||
.trim(),
|
||||
provider: z.nativeEnum(ACCESS_PROVIDERS, { message: t("access.form.provider.placeholder") }),
|
||||
provider: z.nativeEnum(ACCESS_PROVIDERS, {
|
||||
message:
|
||||
range === "ca-only"
|
||||
? t("access.form.certificate_authority.placeholder")
|
||||
: range === "notify-only"
|
||||
? t("access.form.notification_channel.placeholder")
|
||||
: t("access.form.provider.placeholder"),
|
||||
}),
|
||||
config: z.any(),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
@@ -88,6 +99,35 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
|
||||
initialValues: initialValues,
|
||||
});
|
||||
|
||||
const providerLabel = useMemo(() => {
|
||||
switch (range) {
|
||||
case "ca-only":
|
||||
return t("access.form.certificate_authority.label");
|
||||
case "notify-only":
|
||||
return t("access.form.notification_channel.label");
|
||||
}
|
||||
|
||||
return t("access.form.provider.label");
|
||||
}, [range]);
|
||||
const providerPlaceholder = useMemo(() => {
|
||||
switch (range) {
|
||||
case "ca-only":
|
||||
return t("access.form.certificate_authority.placeholder");
|
||||
case "notify-only":
|
||||
return t("access.form.notification_channel.placeholder");
|
||||
}
|
||||
|
||||
return t("access.form.provider.placeholder");
|
||||
}, [range]);
|
||||
const providerTooltip = useMemo(() => {
|
||||
switch (range) {
|
||||
case "both-dns-hosting":
|
||||
return <span dangerouslySetInnerHTML={{ __html: t("access.form.provider.tooltip") }}></span>;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [range]);
|
||||
|
||||
const fieldProvider = Form.useWatch("provider", formInst);
|
||||
|
||||
const [nestedFormInst] = Form.useForm();
|
||||
@@ -147,6 +187,8 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
|
||||
return <AccessFormGnameConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.GODADDY:
|
||||
return <AccessFormGoDaddyConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.GOOGLETRUSTSERVICES:
|
||||
return <AccessFormGoogleTrustServicesConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.EDGIO:
|
||||
return <AccessFormEdgioConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.HUAWEICLOUD:
|
||||
@@ -155,8 +197,6 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
|
||||
return <AccessFormJDCloudConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.KUBERNETES:
|
||||
return <AccessFormKubernetesConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.LOCAL:
|
||||
return <AccessFormLocalConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.NAMECHEAP:
|
||||
return <AccessFormNamecheapConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.NAMEDOTCOM:
|
||||
@@ -177,6 +217,8 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
|
||||
return <AccessFormSafeLineConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.SSH:
|
||||
return <AccessFormSSHConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.SSLCOM:
|
||||
return <AccessFormSSLComConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.TENCENTCLOUD:
|
||||
return <AccessFormTencentCloudConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.UCLOUD:
|
||||
@@ -191,6 +233,8 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
|
||||
return <AccessFormWebhookConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.WESTCN:
|
||||
return <AccessFormWestcnConfig {...nestedFormProps} />;
|
||||
case ACCESS_PROVIDERS.ZEROSSL:
|
||||
return <AccessFormZeroSSLConfig {...nestedFormProps} />;
|
||||
}
|
||||
}, [disabled, initialValues?.config, fieldProvider, nestedFormInst, nestedFormName]);
|
||||
|
||||
@@ -235,13 +279,25 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
|
||||
<Input placeholder={t("access.form.name.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="provider"
|
||||
label={t("access.form.provider.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.provider.tooltip") }}></span>}
|
||||
>
|
||||
<AccessProviderSelect disabled={preset !== "add"} placeholder={t("access.form.provider.placeholder")} showSearch={!disabled} />
|
||||
<Form.Item name="provider" label={providerLabel} rules={[formRule]} tooltip={providerTooltip}>
|
||||
<AccessProviderSelect
|
||||
filter={(record) => {
|
||||
if (range == null) return true;
|
||||
|
||||
switch (range) {
|
||||
case "both-dns-hosting":
|
||||
return record.usages.includes(ACCESS_USAGES.DNS) || record.usages.includes(ACCESS_USAGES.HOSTING);
|
||||
case "ca-only":
|
||||
return record.usages.includes(ACCESS_USAGES.CA);
|
||||
case "notify-only":
|
||||
return record.usages.includes(ACCESS_USAGES.NOTIFICATION);
|
||||
}
|
||||
}}
|
||||
disabled={scene !== "add"}
|
||||
placeholder={providerPlaceholder}
|
||||
showOptionTags={range == null || (range === "both-dns-hosting" ? { [ACCESS_USAGES.DNS]: true, [ACCESS_USAGES.HOSTING]: true } : false)}
|
||||
showSearch={!disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, type FormInstance, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { type AccessConfigForGoogleTrustServices } from "@/domain/access";
|
||||
|
||||
type AccessFormGoogleTrustServicesConfigFieldValues = Nullish<AccessConfigForGoogleTrustServices>;
|
||||
|
||||
export type AccessFormGoogleTrustServicesConfigProps = {
|
||||
form: FormInstance;
|
||||
formName: string;
|
||||
disabled?: boolean;
|
||||
initialValues?: AccessFormGoogleTrustServicesConfigFieldValues;
|
||||
onValuesChange?: (values: AccessFormGoogleTrustServicesConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const initFormModel = (): AccessFormGoogleTrustServicesConfigFieldValues => {
|
||||
return {
|
||||
eabKid: "",
|
||||
eabHmacKey: "",
|
||||
};
|
||||
};
|
||||
|
||||
const AccessFormGoogleTrustServicesConfig = ({
|
||||
form: formInst,
|
||||
formName,
|
||||
disabled,
|
||||
initialValues,
|
||||
onValuesChange,
|
||||
}: AccessFormGoogleTrustServicesConfigProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
eabKid: z
|
||||
.string()
|
||||
.min(1, t("access.form.googletrustservices_eab_kid.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
.trim(),
|
||||
eabHmacKey: z
|
||||
.string()
|
||||
.min(1, t("access.form.googletrustservices_eab_hmac_key.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
.trim(),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
|
||||
onValuesChange?.(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={formInst}
|
||||
disabled={disabled}
|
||||
initialValues={initialValues ?? initFormModel()}
|
||||
layout="vertical"
|
||||
name={formName}
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
<Form.Item
|
||||
name="eabKid"
|
||||
label={t("access.form.googletrustservices_eab_kid.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.googletrustservices_eab_kid.tooltip") }}></span>}
|
||||
>
|
||||
<Input autoComplete="new-password" placeholder={t("access.form.googletrustservices_eab_kid.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="eabHmacKey"
|
||||
label={t("access.form.googletrustservices_eab_hmac_key.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.googletrustservices_eab_hmac_key.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("access.form.googletrustservices_eab_hmac_key.placeholder")} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessFormGoogleTrustServicesConfig;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Form, type FormInstance } from "antd";
|
||||
|
||||
import { type AccessConfigForLocal } from "@/domain/access";
|
||||
|
||||
type AccessFormLocalConfigFieldValues = Nullish<AccessConfigForLocal>;
|
||||
|
||||
export type AccessFormLocalConfigProps = {
|
||||
form: FormInstance;
|
||||
formName: string;
|
||||
disabled?: boolean;
|
||||
initialValues?: AccessFormLocalConfigFieldValues;
|
||||
onValuesChange?: (values: AccessFormLocalConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const initFormModel = (): AccessFormLocalConfigFieldValues => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const AccessFormLocalConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormLocalConfigProps) => {
|
||||
const handleFormChange = (_: unknown, values: any) => {
|
||||
onValuesChange?.(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={formInst}
|
||||
disabled={disabled}
|
||||
initialValues={initialValues ?? initFormModel()}
|
||||
layout="vertical"
|
||||
name={formName}
|
||||
onValuesChange={handleFormChange}
|
||||
></Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessFormLocalConfig;
|
||||
@@ -7,7 +7,7 @@ import { z } from "zod";
|
||||
|
||||
import { type AccessConfigForSSH } from "@/domain/access";
|
||||
import { readFileContent } from "@/utils/file";
|
||||
import { validDomainName, validIPv4Address, validIPv6Address } from "@/utils/validators";
|
||||
import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators";
|
||||
|
||||
type AccessFormSSHConfigFieldValues = Nullish<AccessConfigForSSH>;
|
||||
|
||||
@@ -34,11 +34,13 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
|
||||
host: z
|
||||
.string({ message: t("access.form.ssh_host.placeholder") })
|
||||
.refine((v) => validDomainName(v) || validIPv4Address(v) || validIPv6Address(v), t("common.errmsg.host_invalid")),
|
||||
port: z
|
||||
.number({ message: t("access.form.ssh_port.placeholder") })
|
||||
.int()
|
||||
.gte(1, t("common.errmsg.port_invalid"))
|
||||
.lte(65535, t("common.errmsg.port_invalid")),
|
||||
port: z.preprocess(
|
||||
(v) => Number(v),
|
||||
z
|
||||
.number({ message: t("access.form.ssh_port.placeholder") })
|
||||
.int(t("access.form.ssh_port.placeholder"))
|
||||
.refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
|
||||
),
|
||||
username: z
|
||||
.string()
|
||||
.min(1, "access.form.ssh_username.placeholder")
|
||||
|
||||
76
ui/src/components/access/AccessFormSSLComConfig.tsx
Normal file
76
ui/src/components/access/AccessFormSSLComConfig.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, type FormInstance, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { type AccessConfigForSSLCom } from "@/domain/access";
|
||||
|
||||
type AccessFormSSLComConfigFieldValues = Nullish<AccessConfigForSSLCom>;
|
||||
|
||||
export type AccessFormSSLComConfigProps = {
|
||||
form: FormInstance;
|
||||
formName: string;
|
||||
disabled?: boolean;
|
||||
initialValues?: AccessFormSSLComConfigFieldValues;
|
||||
onValuesChange?: (values: AccessFormSSLComConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const initFormModel = (): AccessFormSSLComConfigFieldValues => {
|
||||
return {
|
||||
eabKid: "",
|
||||
eabHmacKey: "",
|
||||
};
|
||||
};
|
||||
|
||||
const AccessFormSSLComConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormSSLComConfigProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
eabKid: z
|
||||
.string()
|
||||
.min(1, t("access.form.sslcom_eab_kid.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
.trim(),
|
||||
eabHmacKey: z
|
||||
.string()
|
||||
.min(1, t("access.form.sslcom_eab_hmac_key.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
.trim(),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
|
||||
onValuesChange?.(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={formInst}
|
||||
disabled={disabled}
|
||||
initialValues={initialValues ?? initFormModel()}
|
||||
layout="vertical"
|
||||
name={formName}
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
<Form.Item
|
||||
name="eabKid"
|
||||
label={t("access.form.sslcom_eab_kid.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.sslcom_eab_kid.tooltip") }}></span>}
|
||||
>
|
||||
<Input autoComplete="new-password" placeholder={t("access.form.sslcom_eab_kid.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="eabHmacKey"
|
||||
label={t("access.form.sslcom_eab_hmac_key.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.sslcom_eab_hmac_key.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("access.form.sslcom_eab_hmac_key.placeholder")} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessFormSSLComConfig;
|
||||
76
ui/src/components/access/AccessFormZeroSSLConfig.tsx
Normal file
76
ui/src/components/access/AccessFormZeroSSLConfig.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, type FormInstance, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { type AccessConfigForZeroSSL } from "@/domain/access";
|
||||
|
||||
type AccessFormZeroSSLConfigFieldValues = Nullish<AccessConfigForZeroSSL>;
|
||||
|
||||
export type AccessFormZeroSSLConfigProps = {
|
||||
form: FormInstance;
|
||||
formName: string;
|
||||
disabled?: boolean;
|
||||
initialValues?: AccessFormZeroSSLConfigFieldValues;
|
||||
onValuesChange?: (values: AccessFormZeroSSLConfigFieldValues) => void;
|
||||
};
|
||||
|
||||
const initFormModel = (): AccessFormZeroSSLConfigFieldValues => {
|
||||
return {
|
||||
eabKid: "",
|
||||
eabHmacKey: "",
|
||||
};
|
||||
};
|
||||
|
||||
const AccessFormZeroSSLConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormZeroSSLConfigProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
eabKid: z
|
||||
.string()
|
||||
.min(1, t("access.form.zerossl_eab_kid.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
.trim(),
|
||||
eabHmacKey: z
|
||||
.string()
|
||||
.min(1, t("access.form.zerossl_eab_hmac_key.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 }))
|
||||
.trim(),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
|
||||
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
|
||||
onValuesChange?.(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={formInst}
|
||||
disabled={disabled}
|
||||
initialValues={initialValues ?? initFormModel()}
|
||||
layout="vertical"
|
||||
name={formName}
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
<Form.Item
|
||||
name="eabKid"
|
||||
label={t("access.form.zerossl_eab_kid.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.zerossl_eab_kid.tooltip") }}></span>}
|
||||
>
|
||||
<Input autoComplete="new-password" placeholder={t("access.form.zerossl_eab_kid.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="eabHmacKey"
|
||||
label={t("access.form.zerossl_eab_hmac_key.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.zerossl_eab_hmac_key.tooltip") }}></span>}
|
||||
>
|
||||
<Input.Password autoComplete="new-password" placeholder={t("access.form.zerossl_eab_hmac_key.placeholder")} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessFormZeroSSLConfig;
|
||||
@@ -29,7 +29,6 @@ const CertificateDetailDrawer = ({ data, loading, trigger, ...props }: Certifica
|
||||
|
||||
<Drawer
|
||||
afterOpenChange={setOpen}
|
||||
closable
|
||||
destroyOnClose
|
||||
open={open}
|
||||
loading={loading}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Form, Input, InputNumber, Switch } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { validPortNumber } from "@/utils/validators";
|
||||
|
||||
const NotifyChannelEditFormEmailFields = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -11,11 +13,13 @@ const NotifyChannelEditFormEmailFields = () => {
|
||||
.string({ message: t("settings.notification.channel.form.email_smtp_host.placeholder") })
|
||||
.min(1, t("settings.notification.channel.form.email_smtp_host.placeholder"))
|
||||
.max(256, t("common.errmsg.string_max", { max: 256 })),
|
||||
smtpPort: z
|
||||
.number({ message: t("settings.notification.channel.form.email_smtp_port.placeholder") })
|
||||
.int()
|
||||
.gte(1, t("common.errmsg.port_invalid"))
|
||||
.lte(65535, t("common.errmsg.port_invalid")),
|
||||
smtpPort: z.preprocess(
|
||||
(v) => Number(v),
|
||||
z
|
||||
.number({ message: t("settings.notification.channel.form.email_smtp_port.placeholder") })
|
||||
.int(t("settings.notification.channel.form.email_smtp_port.placeholder"))
|
||||
.refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
|
||||
),
|
||||
smtpTLS: z.boolean().nullish(),
|
||||
username: z
|
||||
.string({ message: t("settings.notification.channel.form.email_username.placeholder") })
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { memo, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Avatar, Select, type SelectProps, Space, Tag, Typography } from "antd";
|
||||
|
||||
import { ACCESS_USAGES, type AccessProvider, accessProvidersMap } from "@/domain/provider";
|
||||
import Show from "@/components/Show";
|
||||
import { ACCESS_USAGES, type AccessProvider, type AccessUsageType, accessProvidersMap } from "@/domain/provider";
|
||||
|
||||
export type AccessProviderSelectProps = Omit<
|
||||
SelectProps,
|
||||
"filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"
|
||||
> & {
|
||||
filter?: (record: AccessProvider) => boolean;
|
||||
showOptionTags?: boolean | { [key in AccessUsageType]?: boolean };
|
||||
};
|
||||
|
||||
const AccessProviderSelect = ({ filter, ...props }: AccessProviderSelectProps) => {
|
||||
const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProviderSelectProps = { showOptionTags: true }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: AccessProvider }>>([]);
|
||||
@@ -23,33 +25,51 @@ const AccessProviderSelect = ({ filter, ...props }: AccessProviderSelectProps) =
|
||||
key: item.type,
|
||||
value: item.type,
|
||||
label: t(item.name),
|
||||
disabled: item.builtin,
|
||||
data: item,
|
||||
}))
|
||||
);
|
||||
}, [filter]);
|
||||
|
||||
const showOptionTagForDNS = useMemo(() => {
|
||||
return typeof showOptionTags === "object" ? !!showOptionTags[ACCESS_USAGES.DNS] : !!showOptionTags;
|
||||
}, [showOptionTags]);
|
||||
const showOptionTagForHosting = useMemo(() => {
|
||||
return typeof showOptionTags === "object" ? !!showOptionTags[ACCESS_USAGES.HOSTING] : !!showOptionTags;
|
||||
}, [showOptionTags]);
|
||||
const showOptionTagForCA = useMemo(() => {
|
||||
return typeof showOptionTags === "object" ? !!showOptionTags[ACCESS_USAGES.CA] : !!showOptionTags;
|
||||
}, [showOptionTags]);
|
||||
const showOptionTagForNotification = useMemo(() => {
|
||||
return typeof showOptionTags === "object" ? !!showOptionTags[ACCESS_USAGES.NOTIFICATION] : !!showOptionTags;
|
||||
}, [showOptionTags]);
|
||||
|
||||
const renderOption = (key: string) => {
|
||||
const provider = accessProvidersMap.get(key);
|
||||
const provider = accessProvidersMap.get(key) ?? ({ type: "", name: "", icon: "", usages: [] } as unknown as AccessProvider);
|
||||
return (
|
||||
<div className="flex max-w-full items-center justify-between gap-4 overflow-hidden">
|
||||
<Space className="max-w-full grow truncate" size={4}>
|
||||
<Avatar src={provider?.icon} size="small" />
|
||||
<Typography.Text className="leading-loose" ellipsis>
|
||||
{t(provider?.name ?? "")}
|
||||
<Avatar src={provider.icon} size="small" />
|
||||
<Typography.Text className="leading-loose" type={provider.builtin ? "secondary" : undefined} delete={provider.builtin ? true : undefined} ellipsis>
|
||||
{t(provider.name)}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
<div>
|
||||
{provider?.usages?.includes(ACCESS_USAGES.APPLY) && (
|
||||
<>
|
||||
<Tag color="orange">{t("access.props.provider.usage.dns")}</Tag>
|
||||
</>
|
||||
)}
|
||||
{provider?.usages?.includes(ACCESS_USAGES.DEPLOY) && (
|
||||
<>
|
||||
<Tag color="blue">{t("access.props.provider.usage.host")}</Tag>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{showOptionTags && (
|
||||
<div>
|
||||
<Show when={showOptionTagForDNS && provider.usages.includes(ACCESS_USAGES.DNS)}>
|
||||
<Tag color="peru">{t("access.props.provider.usage.dns")}</Tag>
|
||||
</Show>
|
||||
<Show when={showOptionTagForHosting && provider.usages.includes(ACCESS_USAGES.HOSTING)}>
|
||||
<Tag color="royalblue">{t("access.props.provider.usage.hosting")}</Tag>
|
||||
</Show>
|
||||
<Show when={showOptionTagForCA && provider.usages.includes(ACCESS_USAGES.CA)}>
|
||||
<Tag color="crimson">{t("access.props.provider.usage.ca")}</Tag>
|
||||
</Show>
|
||||
<Show when={showOptionTagForNotification && provider.usages.includes(ACCESS_USAGES.NOTIFICATION)}>
|
||||
<Tag color="mediumaquamarine">{t("access.props.provider.usage.notification")}</Tag>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
83
ui/src/components/provider/ApplyCAProviderSelect.tsx
Normal file
83
ui/src/components/provider/ApplyCAProviderSelect.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Avatar, Select, type SelectProps, Space, Typography } from "antd";
|
||||
|
||||
import { type ApplyCAProvider, applyCAProvidersMap } from "@/domain/provider";
|
||||
|
||||
export type ApplyCAProviderSelectProps = Omit<
|
||||
SelectProps,
|
||||
"filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"
|
||||
> & {
|
||||
filter?: (record: ApplyCAProvider) => boolean;
|
||||
};
|
||||
|
||||
const ApplyCAProviderSelect = ({ filter, ...props }: ApplyCAProviderSelectProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: ApplyCAProvider }>>([]);
|
||||
useEffect(() => {
|
||||
const allItems = Array.from(applyCAProvidersMap.values());
|
||||
const filteredItems = filter != null ? allItems.filter(filter) : allItems;
|
||||
setOptions([
|
||||
{
|
||||
key: "",
|
||||
value: "",
|
||||
label: "provider.default_ca_provider.label",
|
||||
data: {} as ApplyCAProvider,
|
||||
},
|
||||
...filteredItems.map((item) => ({
|
||||
key: item.type,
|
||||
value: item.type,
|
||||
label: t(item.name),
|
||||
data: item,
|
||||
})),
|
||||
]);
|
||||
}, [filter]);
|
||||
|
||||
const renderOption = (key: string) => {
|
||||
if (key === "") {
|
||||
return (
|
||||
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
|
||||
<Typography.Text className="italic leading-loose" type="secondary" ellipsis italic>
|
||||
{t("provider.default_ca_provider.label")}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
const provider = applyCAProvidersMap.get(key);
|
||||
return (
|
||||
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
|
||||
<Avatar src={provider?.icon} size="small" />
|
||||
<Typography.Text className="leading-loose" ellipsis>
|
||||
{t(provider?.name ?? "")}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
{...props}
|
||||
filterOption={(inputValue, option) => {
|
||||
if (!option) return false;
|
||||
|
||||
const value = inputValue.toLowerCase();
|
||||
return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value);
|
||||
}}
|
||||
labelRender={({ label, value }) => {
|
||||
if (!label) {
|
||||
return <Typography.Text type="secondary">{props.placeholder || t("provider.default_ca_provider.label")}</Typography.Text>;
|
||||
}
|
||||
|
||||
return renderOption(value as string);
|
||||
}}
|
||||
options={options}
|
||||
optionFilterProp={undefined}
|
||||
optionLabelProp={undefined}
|
||||
optionRender={(option) => renderOption(option.data.value)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ApplyCAProviderSelect);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
import { memo, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Avatar, Card, Col, Empty, Flex, Input, type InputRef, Row, Typography } from "antd";
|
||||
|
||||
@@ -24,15 +24,16 @@ const ApplyDNSProviderPicker = ({ className, style, autoFocus, placeholder, onSe
|
||||
}
|
||||
}, []);
|
||||
|
||||
const providers = Array.from(applyDNSProvidersMap.values());
|
||||
const filteredProviders = providers.filter((provider) => {
|
||||
if (keyword) {
|
||||
const value = keyword.toLowerCase();
|
||||
return provider.type.toLowerCase().includes(value) || t(provider.name).toLowerCase().includes(value);
|
||||
}
|
||||
const providers = useMemo(() => {
|
||||
return Array.from(applyDNSProvidersMap.values()).filter((provider) => {
|
||||
if (keyword) {
|
||||
const value = keyword.toLowerCase();
|
||||
return provider.type.toLowerCase().includes(value) || t(provider.name).toLowerCase().includes(value);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}, [keyword]);
|
||||
|
||||
const handleProviderTypeSelect = (value: string) => {
|
||||
onSelect?.(value);
|
||||
@@ -40,12 +41,12 @@ const ApplyDNSProviderPicker = ({ className, style, autoFocus, placeholder, onSe
|
||||
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<Input.Search ref={keywordInputRef} placeholder={placeholder} onChange={(e) => setKeyword(e.target.value.trim())} />
|
||||
<Input.Search ref={keywordInputRef} placeholder={placeholder ?? t("common.text.search")} onChange={(e) => setKeyword(e.target.value.trim())} />
|
||||
|
||||
<div className="mt-4">
|
||||
<Show when={filteredProviders.length > 0} fallback={<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}>
|
||||
<Show when={providers.length > 0} fallback={<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{filteredProviders.map((provider, index) => {
|
||||
{providers.map((provider, index) => {
|
||||
return (
|
||||
<Col key={index} xs={24} md={12} span={12}>
|
||||
<Card
|
||||
|
||||
@@ -16,6 +16,8 @@ export type DeployProviderPickerProps = {
|
||||
const DeployProviderPicker = ({ className, style, autoFocus, placeholder, onSelect }: DeployProviderPickerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [category, setCategory] = useState<string>(DEPLOY_CATEGORIES.ALL);
|
||||
|
||||
const [keyword, setKeyword] = useState<string>();
|
||||
const keywordInputRef = useRef<InputRef>(null);
|
||||
useEffect(() => {
|
||||
@@ -24,26 +26,24 @@ const DeployProviderPicker = ({ className, style, autoFocus, placeholder, onSele
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [category, setCategory] = useState<string>(DEPLOY_CATEGORIES.ALL);
|
||||
|
||||
const providers = useMemo(() => {
|
||||
return Array.from(deployProvidersMap.values())
|
||||
.filter((provider) => {
|
||||
if (category && category !== DEPLOY_CATEGORIES.ALL) {
|
||||
return provider.category === category;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.filter((provider) => {
|
||||
if (keyword) {
|
||||
const value = keyword.toLowerCase();
|
||||
return provider.type.toLowerCase().includes(value) || t(provider.name).toLowerCase().includes(value);
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.filter((provider) => {
|
||||
if (category && category !== DEPLOY_CATEGORIES.ALL) {
|
||||
return provider.category === category;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [keyword, category]);
|
||||
}, [category, keyword]);
|
||||
|
||||
const handleProviderTypeSelect = (value: string) => {
|
||||
onSelect?.(value);
|
||||
@@ -51,7 +51,7 @@ const DeployProviderPicker = ({ className, style, autoFocus, placeholder, onSele
|
||||
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<Input.Search ref={keywordInputRef} placeholder={placeholder} onChange={(e) => setKeyword(e.target.value.trim())} />
|
||||
<Input.Search ref={keywordInputRef} placeholder={placeholder ?? t("common.text.search")} onChange={(e) => setKeyword(e.target.value.trim())} />
|
||||
|
||||
<div className="mt-4">
|
||||
<Flex>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
CheckOutlined as CheckOutlinedIcon,
|
||||
ClockCircleOutlined as ClockCircleOutlinedIcon,
|
||||
CloseCircleOutlined as CloseCircleOutlinedIcon,
|
||||
DownloadOutlined as DownloadOutlinedIcon,
|
||||
RightOutlined as RightOutlinedIcon,
|
||||
SelectOutlined as SelectOutlinedIcon,
|
||||
SettingOutlined as SettingOutlinedIcon,
|
||||
@@ -188,6 +189,34 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
|
||||
);
|
||||
};
|
||||
|
||||
const handleDownloadClick = () => {
|
||||
const NEWLINE = "\n";
|
||||
const logstr = listData
|
||||
.map((group) => {
|
||||
return (
|
||||
group.name +
|
||||
NEWLINE +
|
||||
group.records
|
||||
.map((record) => {
|
||||
const datetime = dayjs(record.timestamp).format("YYYY-MM-DDTHH:mm:ss.SSSZ");
|
||||
const level = record.level;
|
||||
const message = record.message.trim().replaceAll("\r", "\\r").replaceAll("\n", "\\n");
|
||||
return `[${datetime}] [${level}] ${message}`;
|
||||
})
|
||||
.join(NEWLINE)
|
||||
);
|
||||
})
|
||||
.join(NEWLINE + NEWLINE);
|
||||
const blob = new Blob([logstr], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `certimate_workflow_run_#${runId}_logs.txt`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography.Title level={5}>{t("workflow_run.logs")}</Typography.Title>
|
||||
@@ -210,6 +239,15 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
|
||||
icon: <CheckOutlinedIcon className={showWhitespace ? "visible" : "invisible"} />,
|
||||
onClick: () => setShowWhitespace(!showWhitespace),
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
key: "download-logs",
|
||||
label: t("workflow_run.logs.menu.download_logs"),
|
||||
icon: <DownloadOutlinedIcon className="invisible" />,
|
||||
onClick: handleDownloadClick,
|
||||
},
|
||||
],
|
||||
}}
|
||||
trigger={["click"]}
|
||||
|
||||
@@ -30,7 +30,6 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR
|
||||
|
||||
<Drawer
|
||||
afterOpenChange={setOpen}
|
||||
closable
|
||||
destroyOnClose
|
||||
open={open}
|
||||
loading={loading}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormOutlined as FormOutlinedIcon, PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
|
||||
import { Link } from "react-router";
|
||||
import {
|
||||
FormOutlined as FormOutlinedIcon,
|
||||
PlusOutlined as PlusOutlinedIcon,
|
||||
QuestionCircleOutlined as QuestionCircleOutlinedIcon,
|
||||
RightOutlined as RightOutlinedIcon,
|
||||
} from "@ant-design/icons";
|
||||
import { useControllableValue } from "ahooks";
|
||||
import {
|
||||
AutoComplete,
|
||||
@@ -25,8 +31,9 @@ import AccessEditModal from "@/components/access/AccessEditModal";
|
||||
import AccessSelect from "@/components/access/AccessSelect";
|
||||
import ModalForm from "@/components/ModalForm";
|
||||
import MultipleInput from "@/components/MultipleInput";
|
||||
import ApplyCAProviderSelect from "@/components/provider/ApplyCAProviderSelect";
|
||||
import ApplyDNSProviderSelect from "@/components/provider/ApplyDNSProviderSelect";
|
||||
import { ACCESS_USAGES, APPLY_DNS_PROVIDERS, accessProvidersMap, applyDNSProvidersMap } from "@/domain/provider";
|
||||
import { ACCESS_USAGES, APPLY_DNS_PROVIDERS, accessProvidersMap, applyCAProvidersMap, applyDNSProvidersMap } from "@/domain/provider";
|
||||
import { type WorkflowNodeConfigForApply } from "@/domain/workflow";
|
||||
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
|
||||
import { useAccessesStore } from "@/stores/access";
|
||||
@@ -60,7 +67,7 @@ const initFormModel = (): ApplyNodeConfigFormFieldValues => {
|
||||
return {
|
||||
challengeType: "dns-01",
|
||||
keyAlgorithm: "RSA2048",
|
||||
skipBeforeExpiryDays: 20,
|
||||
skipBeforeExpiryDays: 30,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -83,7 +90,18 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
providerAccessId: z
|
||||
.string({ message: t("workflow_node.apply.form.provider_access.placeholder") })
|
||||
.min(1, t("workflow_node.apply.form.provider_access.placeholder")),
|
||||
providerConfig: z.any(),
|
||||
providerConfig: z.any().nullish(),
|
||||
caProvider: z.string({ message: t("workflow_node.apply.form.ca_provider.placeholder") }).nullish(),
|
||||
caProviderAccessId: z
|
||||
.string({ message: t("workflow_node.apply.form.ca_provider_access.placeholder") })
|
||||
.nullish()
|
||||
.refine((v) => {
|
||||
if (!fieldCAProvider) return true;
|
||||
|
||||
const provider = applyCAProvidersMap.get(fieldCAProvider);
|
||||
return !!provider?.builtin || !!v;
|
||||
}, t("workflow_node.apply.form.ca_provider_access.placeholder")),
|
||||
caProviderConfig: z.any().nullish(),
|
||||
keyAlgorithm: z
|
||||
.string({ message: t("workflow_node.apply.form.key_algorithm.placeholder") })
|
||||
.nonempty(t("workflow_node.apply.form.key_algorithm.placeholder")),
|
||||
@@ -96,24 +114,27 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
.split(MULTIPLE_INPUT_DELIMITER)
|
||||
.every((e) => validIPv4Address(e) || validIPv6Address(e) || validDomainName(e));
|
||||
}, t("common.errmsg.host_invalid")),
|
||||
dnsPropagationTimeout: z
|
||||
.union([
|
||||
z.number().int().gte(1, t("workflow_node.apply.form.dns_propagation_timeout.placeholder")),
|
||||
z.string().refine((v) => !v || /^[1-9]\d*$/.test(v), t("workflow_node.apply.form.dns_propagation_timeout.placeholder")),
|
||||
])
|
||||
.nullish(),
|
||||
dnsTTL: z
|
||||
.union([
|
||||
z.number().int().gte(1, t("workflow_node.apply.form.dns_ttl.placeholder")),
|
||||
z.string().refine((v) => !v || /^[1-9]\d*$/.test(v), t("workflow_node.apply.form.dns_ttl.placeholder")),
|
||||
])
|
||||
.nullish(),
|
||||
dnsPropagationTimeout: z.preprocess(
|
||||
(v) => (v == null || v === "" ? undefined : Number(v)),
|
||||
z
|
||||
.number()
|
||||
.int(t("workflow_node.apply.form.dns_propagation_timeout.placeholder"))
|
||||
.gte(1, t("workflow_node.apply.form.dns_propagation_timeout.placeholder"))
|
||||
.nullish()
|
||||
),
|
||||
dnsTTL: z.preprocess(
|
||||
(v) => (v == null || v === "" ? undefined : Number(v)),
|
||||
z.number().int(t("workflow_node.apply.form.dns_ttl.placeholder")).gte(1, t("workflow_node.apply.form.dns_ttl.placeholder")).nullish()
|
||||
),
|
||||
disableFollowCNAME: z.boolean().nullish(),
|
||||
disableARI: z.boolean().nullish(),
|
||||
skipBeforeExpiryDays: z
|
||||
.number({ message: t("workflow_node.apply.form.skip_before_expiry_days.placeholder") })
|
||||
.int(t("workflow_node.apply.form.skip_before_expiry_days.placeholder"))
|
||||
.gte(1, t("workflow_node.apply.form.skip_before_expiry_days.placeholder")),
|
||||
skipBeforeExpiryDays: z.preprocess(
|
||||
(v) => Number(v),
|
||||
z
|
||||
.number({ message: t("workflow_node.apply.form.skip_before_expiry_days.placeholder") })
|
||||
.int(t("workflow_node.apply.form.skip_before_expiry_days.placeholder"))
|
||||
.gte(1, t("workflow_node.apply.form.skip_before_expiry_days.placeholder"))
|
||||
),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
const { form: formInst, formProps } = useAntdForm({
|
||||
@@ -121,9 +142,10 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
initialValues: initialValues ?? initFormModel(),
|
||||
});
|
||||
|
||||
const fieldDomains = Form.useWatch<string>("domains", formInst);
|
||||
const fieldProvider = Form.useWatch<string>("provider", { form: formInst, preserve: true });
|
||||
const fieldProviderAccessId = Form.useWatch<string>("providerAccessId", formInst);
|
||||
const fieldDomains = Form.useWatch<string>("domains", formInst);
|
||||
const fieldCAProvider = Form.useWatch<string>("caProvider", formInst);
|
||||
const fieldNameservers = Form.useWatch<string>("nameservers", formInst);
|
||||
|
||||
const [showProvider, setShowProvider] = useState(false);
|
||||
@@ -139,6 +161,17 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
}
|
||||
}, [accesses, fieldProviderAccessId]);
|
||||
|
||||
const [showCAProviderAccess, setShowCAProviderAccess] = useState(false);
|
||||
useEffect(() => {
|
||||
// 内置的 CA 提供商(如 Let's Encrypt)无需显示授权信息字段
|
||||
if (fieldCAProvider) {
|
||||
const provider = applyCAProvidersMap.get(fieldCAProvider);
|
||||
setShowCAProviderAccess(!provider?.builtin);
|
||||
} else {
|
||||
setShowCAProviderAccess(false);
|
||||
}
|
||||
}, [fieldCAProvider]);
|
||||
|
||||
const [nestedFormInst] = Form.useForm();
|
||||
const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeApplyConfigFormProviderConfigForm" });
|
||||
const nestedFormEl = useMemo(() => {
|
||||
@@ -195,6 +228,27 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
}
|
||||
};
|
||||
|
||||
const handleCAProviderSelect = (value?: string | undefined) => {
|
||||
if (fieldCAProvider === value) return;
|
||||
|
||||
// 切换 CA 提供商时联动授权信息
|
||||
if (value === "") {
|
||||
setTimeout(() => {
|
||||
formInst.setFieldValue("caProvider", undefined);
|
||||
formInst.setFieldValue("caProviderAccessId", undefined);
|
||||
onValuesChange?.(formInst.getFieldsValue(true));
|
||||
}, 1);
|
||||
} else if (initialValues?.caProvider === value) {
|
||||
formInst.setFieldValue("caProviderAccessId", initialValues?.caProviderAccessId);
|
||||
onValuesChange?.(formInst.getFieldsValue(true));
|
||||
} else {
|
||||
if (applyCAProvidersMap.get(fieldCAProvider)?.provider !== applyCAProvidersMap.get(value!)?.provider) {
|
||||
formInst.setFieldValue("caProviderAccessId", undefined);
|
||||
onValuesChange?.(formInst.getFieldsValue(true));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormProviderChange = (name: string) => {
|
||||
if (name === nestedFormName) {
|
||||
formInst.setFieldValue("providerConfig", nestedFormInst.getFieldsValue());
|
||||
@@ -301,16 +355,17 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<AccessEditModal
|
||||
preset="add"
|
||||
range="both-dns-hosting"
|
||||
scene="add"
|
||||
trigger={
|
||||
<Button size="small" type="link">
|
||||
<PlusOutlinedIcon />
|
||||
{t("workflow_node.apply.form.provider_access.button")}
|
||||
<PlusOutlinedIcon className="text-xs" />
|
||||
</Button>
|
||||
}
|
||||
afterSubmit={(record) => {
|
||||
const provider = accessProvidersMap.get(record.provider);
|
||||
if (provider?.usages?.includes(ACCESS_USAGES.APPLY)) {
|
||||
if (provider?.usages?.includes(ACCESS_USAGES.DNS)) {
|
||||
formInst.setFieldValue("providerAccessId", record.id);
|
||||
}
|
||||
}}
|
||||
@@ -322,7 +377,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
<AccessSelect
|
||||
filter={(record) => {
|
||||
const provider = accessProvidersMap.get(record.provider);
|
||||
return !!provider?.usages?.includes(ACCESS_USAGES.APPLY);
|
||||
return !!provider?.usages?.includes(ACCESS_USAGES.DNS);
|
||||
}}
|
||||
placeholder={t("workflow_node.apply.form.provider_access.placeholder")}
|
||||
onChange={handleProviderAccessSelect}
|
||||
@@ -340,6 +395,73 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
</Divider>
|
||||
|
||||
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
|
||||
<Form.Item className="mb-0">
|
||||
<label className="mb-1 block">
|
||||
<div className="flex w-full items-center justify-between gap-4">
|
||||
<div className="max-w-full grow truncate">{t("workflow_node.apply.form.ca_provider.label")}</div>
|
||||
<div className="text-right">
|
||||
<Link className="ant-typography" to="/settings/ssl-provider" target="_blank">
|
||||
<Button size="small" type="link">
|
||||
{t("workflow_node.apply.form.ca_provider.button")}
|
||||
<RightOutlinedIcon className="text-xs" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<Form.Item name="caProvider" rules={[formRule]}>
|
||||
<ApplyCAProviderSelect
|
||||
allowClear
|
||||
placeholder={t("workflow_node.apply.form.ca_provider.placeholder")}
|
||||
showSearch
|
||||
onSelect={handleCAProviderSelect}
|
||||
onClear={handleCAProviderSelect}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item className="mb-0" hidden={!showCAProviderAccess}>
|
||||
<label className="mb-1 block">
|
||||
<div className="flex w-full items-center justify-between gap-4">
|
||||
<div className="max-w-full grow truncate">
|
||||
<span>{t("workflow_node.apply.form.ca_provider_access.label")}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<AccessEditModal
|
||||
data={{ provider: applyCAProvidersMap.get(fieldCAProvider!)?.provider }}
|
||||
range="ca-only"
|
||||
scene="add"
|
||||
trigger={
|
||||
<Button size="small" type="link">
|
||||
{t("workflow_node.apply.form.ca_provider_access.button")}
|
||||
<PlusOutlinedIcon className="text-xs" />
|
||||
</Button>
|
||||
}
|
||||
afterSubmit={(record) => {
|
||||
const provider = accessProvidersMap.get(record.provider);
|
||||
if (provider?.usages?.includes(ACCESS_USAGES.CA)) {
|
||||
formInst.setFieldValue("caProviderAccessId", record.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<Form.Item name="caProviderAccessId" rules={[formRule]}>
|
||||
<AccessSelect
|
||||
filter={(record) => {
|
||||
if (fieldCAProvider) {
|
||||
return applyCAProvidersMap.get(fieldCAProvider)?.provider === record.provider;
|
||||
}
|
||||
|
||||
const provider = accessProvidersMap.get(record.provider);
|
||||
return !!provider?.usages?.includes(ACCESS_USAGES.CA);
|
||||
}}
|
||||
placeholder={t("workflow_node.apply.form.ca_provider_access.placeholder")}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="keyAlgorithm" label={t("workflow_node.apply.form.key_algorithm.label")} rules={[formRule]}>
|
||||
<Select
|
||||
options={["RSA2048", "RSA3072", "RSA4096", "RSA8192", "EC256", "EC384"].map((e) => ({
|
||||
@@ -364,6 +486,9 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
onChange={(e) => {
|
||||
formInst.setFieldValue("nameservers", e.target.value);
|
||||
}}
|
||||
onClear={() => {
|
||||
formInst.setFieldValue("nameservers", undefined);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<NameserversModalInput
|
||||
@@ -448,7 +573,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
<InputNumber
|
||||
className="w-36"
|
||||
min={1}
|
||||
max={90}
|
||||
max={365}
|
||||
placeholder={t("workflow_node.apply.form.skip_before_expiry_days.placeholder")}
|
||||
addonAfter={t("workflow_node.apply.form.skip_before_expiry_days.unit")}
|
||||
/>
|
||||
|
||||
@@ -7,8 +7,8 @@ import { z } from "zod";
|
||||
|
||||
import AccessEditModal from "@/components/access/AccessEditModal";
|
||||
import AccessSelect from "@/components/access/AccessSelect";
|
||||
import DeployProviderPicker from "@/components/provider/DeployProviderPicker";
|
||||
import DeployProviderSelect from "@/components/provider/DeployProviderSelect";
|
||||
import DeployProviderPicker from "@/components/provider/DeployProviderPicker.tsx";
|
||||
import DeployProviderSelect from "@/components/provider/DeployProviderSelect.tsx";
|
||||
import Show from "@/components/Show";
|
||||
import { ACCESS_USAGES, DEPLOY_PROVIDERS, accessProvidersMap, deployProvidersMap } from "@/domain/provider";
|
||||
import { type WorkflowNode, type WorkflowNodeConfigForDeploy } from "@/domain/workflow";
|
||||
@@ -125,8 +125,14 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
||||
provider: z.string({ message: t("workflow_node.deploy.form.provider.placeholder") }).nonempty(t("workflow_node.deploy.form.provider.placeholder")),
|
||||
providerAccessId: z
|
||||
.string({ message: t("workflow_node.deploy.form.provider_access.placeholder") })
|
||||
.nonempty(t("workflow_node.deploy.form.provider_access.placeholder")),
|
||||
providerConfig: z.any(),
|
||||
.nullish()
|
||||
.refine((v) => {
|
||||
if (!fieldProvider) return true;
|
||||
|
||||
const provider = deployProvidersMap.get(fieldProvider);
|
||||
return !!provider?.builtin || !!v;
|
||||
}, t("workflow_node.deploy.form.provider_access.placeholder")),
|
||||
providerConfig: z.any().nullish(),
|
||||
skipOnLastSucceeded: z.boolean().nullish(),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
@@ -137,6 +143,17 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
||||
|
||||
const fieldProvider = Form.useWatch("provider", { form: formInst, preserve: true });
|
||||
|
||||
const [showProviderAccess, setShowProviderAccess] = useState(false);
|
||||
useEffect(() => {
|
||||
// 内置的部署提供商(如本地部署)无需显示授权信息字段
|
||||
if (fieldProvider) {
|
||||
const provider = deployProvidersMap.get(fieldProvider);
|
||||
setShowProviderAccess(!provider?.builtin);
|
||||
} else {
|
||||
setShowProviderAccess(false);
|
||||
}
|
||||
}, [fieldProvider]);
|
||||
|
||||
const [nestedFormInst] = Form.useForm();
|
||||
const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeDeployConfigFormProviderConfigForm" });
|
||||
const nestedFormEl = useMemo(() => {
|
||||
@@ -292,7 +309,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
||||
onValuesChange?.(formInst.getFieldsValue(true));
|
||||
};
|
||||
|
||||
const handleProviderSelect = (value: string) => {
|
||||
const handleProviderSelect = (value?: string | undefined) => {
|
||||
if (fieldProvider === value) return;
|
||||
|
||||
// 切换部署目标时重置表单,避免其他部署目标的配置字段影响当前部署目标
|
||||
@@ -310,7 +327,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
||||
}
|
||||
formInst.setFieldsValue(newValues);
|
||||
|
||||
if (deployProvidersMap.get(fieldProvider)?.provider !== deployProvidersMap.get(value)?.provider) {
|
||||
if (deployProvidersMap.get(fieldProvider)?.provider !== deployProvidersMap.get(value!)?.provider) {
|
||||
formInst.setFieldValue("providerAccessId", undefined);
|
||||
onValuesChange?.(formInst.getFieldsValue(true));
|
||||
}
|
||||
@@ -364,10 +381,11 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
||||
placeholder={t("workflow_node.deploy.form.provider.placeholder")}
|
||||
showSearch
|
||||
onSelect={handleProviderSelect}
|
||||
onClear={handleProviderSelect}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item className="mb-0">
|
||||
<Form.Item className="mb-0" hidden={!showProviderAccess}>
|
||||
<label className="mb-1 block">
|
||||
<div className="flex w-full items-center justify-between gap-4">
|
||||
<div className="max-w-full grow truncate">
|
||||
@@ -381,16 +399,17 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
||||
<div className="text-right">
|
||||
<AccessEditModal
|
||||
data={{ provider: deployProvidersMap.get(fieldProvider!)?.provider }}
|
||||
preset="add"
|
||||
range="both-dns-hosting"
|
||||
scene="add"
|
||||
trigger={
|
||||
<Button size="small" type="link">
|
||||
<PlusOutlinedIcon />
|
||||
{t("workflow_node.deploy.form.provider_access.button")}
|
||||
<PlusOutlinedIcon className="text-xs" />
|
||||
</Button>
|
||||
}
|
||||
afterSubmit={(record) => {
|
||||
const provider = accessProvidersMap.get(record.provider);
|
||||
if (provider?.usages?.includes(ACCESS_USAGES.DEPLOY)) {
|
||||
if (provider?.usages?.includes(ACCESS_USAGES.HOSTING)) {
|
||||
formInst.setFieldValue("providerAccessId", record.id);
|
||||
}
|
||||
}}
|
||||
@@ -406,7 +425,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
||||
}
|
||||
|
||||
const provider = accessProvidersMap.get(record.provider);
|
||||
return !!provider?.usages?.includes(ACCESS_USAGES.DEPLOY);
|
||||
return !!provider?.usages?.includes(ACCESS_USAGES.HOSTING);
|
||||
}}
|
||||
placeholder={t("workflow_node.deploy.form.provider_access.placeholder")}
|
||||
/>
|
||||
|
||||
@@ -100,6 +100,9 @@ const DeployNodeConfigFormAliyunCASDeployConfig = ({
|
||||
onChange={(e) => {
|
||||
formInst.setFieldValue("resourceIds", e.target.value);
|
||||
}}
|
||||
onClear={() => {
|
||||
formInst.setFieldValue("resourceIds", "");
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<ResourceIdsModalInput
|
||||
@@ -130,6 +133,9 @@ const DeployNodeConfigFormAliyunCASDeployConfig = ({
|
||||
onChange={(e) => {
|
||||
formInst.setFieldValue("contactIds", e.target.value);
|
||||
}}
|
||||
onClear={() => {
|
||||
formInst.setFieldValue("contactIds", "");
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<ContactIdsModalInput
|
||||
|
||||
@@ -10,7 +10,7 @@ type DeployNodeConfigFormAliyunCLBConfigFieldValues = Nullish<{
|
||||
resourceType: string;
|
||||
region: string;
|
||||
loadbalancerId?: string;
|
||||
listenerPort?: string | number;
|
||||
listenerPort?: number;
|
||||
domain?: string;
|
||||
}>;
|
||||
|
||||
@@ -53,10 +53,13 @@ const DeployNodeConfigFormAliyunCLBConfig = ({
|
||||
.min(1, t("workflow_node.deploy.form.aliyun_clb_loadbalancer_id.placeholder"))
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 }))
|
||||
.trim(),
|
||||
listenerPort: z
|
||||
.union([z.number(), z.string()])
|
||||
.refine((v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v), t("workflow_node.deploy.form.aliyun_clb_listener_port.placeholder"))
|
||||
.nullish(),
|
||||
listenerPort: z.preprocess(
|
||||
(v) => (v == null || v === "" ? undefined : Number(v)),
|
||||
z
|
||||
.number()
|
||||
.nullish()
|
||||
.refine((v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v!), t("workflow_node.deploy.form.aliyun_clb_listener_port.placeholder"))
|
||||
),
|
||||
domain: z
|
||||
.string()
|
||||
.nullish()
|
||||
|
||||
@@ -10,7 +10,7 @@ type DeployNodeConfigFormBaiduCloudAppBLBConfigFieldValues = Nullish<{
|
||||
resourceType: string;
|
||||
region: string;
|
||||
loadbalancerId?: string;
|
||||
listenerPort?: string | number;
|
||||
listenerPort?: number;
|
||||
domain?: string;
|
||||
}>;
|
||||
|
||||
@@ -53,13 +53,16 @@ const DeployNodeConfigFormBaiduCloudAppBLBConfig = ({
|
||||
.min(1, t("workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder"))
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 }))
|
||||
.trim(),
|
||||
listenerPort: z
|
||||
.union([z.number(), z.string()])
|
||||
.refine(
|
||||
(v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v),
|
||||
t("workflow_node.deploy.form.baiducloud_appblb_listener_port.placeholder")
|
||||
)
|
||||
.nullish(),
|
||||
listenerPort: z.preprocess(
|
||||
(v) => (v == null || v === "" ? undefined : Number(v)),
|
||||
z
|
||||
.number()
|
||||
.refine(
|
||||
(v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v!),
|
||||
t("workflow_node.deploy.form.baiducloud_appblb_listener_port.placeholder")
|
||||
)
|
||||
.nullish()
|
||||
),
|
||||
domain: z
|
||||
.string()
|
||||
.nullish()
|
||||
|
||||
@@ -10,7 +10,7 @@ type DeployNodeConfigFormBaiduCloudBLBConfigFieldValues = Nullish<{
|
||||
resourceType: string;
|
||||
region: string;
|
||||
loadbalancerId?: string;
|
||||
listenerPort?: string | number;
|
||||
listenerPort?: number;
|
||||
domain?: string;
|
||||
}>;
|
||||
|
||||
@@ -53,13 +53,16 @@ const DeployNodeConfigFormBaiduCloudBLBConfig = ({
|
||||
.min(1, t("workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder"))
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 }))
|
||||
.trim(),
|
||||
listenerPort: z
|
||||
.union([z.number(), z.string()])
|
||||
.refine(
|
||||
(v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v),
|
||||
t("workflow_node.deploy.form.baiducloud_blb_listener_port.placeholder")
|
||||
)
|
||||
.nullish(),
|
||||
listenerPort: z.preprocess(
|
||||
(v) => (v == null || v === "" ? undefined : Number(v)),
|
||||
z
|
||||
.number()
|
||||
.nullish()
|
||||
.refine(
|
||||
(v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v!),
|
||||
t("workflow_node.deploy.form.baiducloud_blb_listener_port.placeholder")
|
||||
)
|
||||
),
|
||||
domain: z
|
||||
.string()
|
||||
.nullish()
|
||||
|
||||
@@ -123,6 +123,9 @@ const DeployNodeConfigFormBaotaPanelSiteConfig = ({
|
||||
onChange={(e) => {
|
||||
formInst.setFieldValue("siteNames", e.target.value);
|
||||
}}
|
||||
onClear={() => {
|
||||
formInst.setFieldValue("siteNames", "");
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<SiteNamesModalInput
|
||||
|
||||
@@ -107,6 +107,9 @@ const DeployNodeConfigFormTencentCloudSSLDeployConfig = ({
|
||||
onChange={(e) => {
|
||||
formInst.setFieldValue("resourceIds", e.target.value);
|
||||
}}
|
||||
onClear={() => {
|
||||
formInst.setFieldValue("resourceIds", "");
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<ResourceIdsModalInput
|
||||
|
||||
@@ -283,7 +283,8 @@ const SharedNodeConfigDrawer = ({
|
||||
{ModelContextHolder}
|
||||
|
||||
<Drawer
|
||||
afterOpenChange={(open) => setOpen(open)}
|
||||
afterOpenChange={setOpen}
|
||||
closable={!pending}
|
||||
destroyOnClose
|
||||
extra={
|
||||
<SharedNodeMenu
|
||||
@@ -306,6 +307,7 @@ const SharedNodeConfigDrawer = ({
|
||||
)
|
||||
}
|
||||
loading={loading}
|
||||
maskClosable={!pending}
|
||||
open={open}
|
||||
title={<div className="max-w-[480px] truncate">{node.name}</div>}
|
||||
width={720}
|
||||
|
||||
Reference in New Issue
Block a user