feat(ui): AccessProviderPicker

This commit is contained in:
Fu Diwei 2025-05-13 00:28:58 +08:00
parent 07037e8d49
commit 0c42bb845d
28 changed files with 246 additions and 89 deletions

View File

@ -66,7 +66,7 @@ const DrawerForm = <T extends NonNullable<unknown> = any>({
}); });
const mergedFormProps: FormProps = { const mergedFormProps: FormProps = {
clearOnDestroy: drawerProps?.destroyOnClose ? true : undefined, clearOnDestroy: drawerProps?.destroyOnHidden ? true : undefined,
...formProps, ...formProps,
...props, ...props,
}; };

View File

@ -75,7 +75,7 @@ const ModalForm = <T extends NonNullable<unknown> = any>({
}); });
const mergedFormProps: FormProps = { const mergedFormProps: FormProps = {
clearOnDestroy: modalProps?.destroyOnClose ? true : undefined, clearOnDestroy: modalProps?.destroyOnHidden ? true : undefined,
...formProps, ...formProps,
...props, ...props,
}; };

View File

@ -93,7 +93,7 @@ const AccessEditDrawer = ({ data, loading, trigger, scene, usage, afterSubmit, .
<Drawer <Drawer
afterOpenChange={setOpen} afterOpenChange={setOpen}
closable={!formPending} closable={!formPending}
destroyOnClose destroyOnHidden
footer={ footer={
<Space className="w-full justify-end"> <Space className="w-full justify-end">
<Button onClick={handleCancelClick}>{t("common.button.cancel")}</Button> <Button onClick={handleCancelClick}>{t("common.button.cancel")}</Button>

View File

@ -95,7 +95,7 @@ const AccessEditModal = ({ data, loading, trigger, scene, usage, afterSubmit, ..
cancelButtonProps={{ disabled: formPending }} cancelButtonProps={{ disabled: formPending }}
closable closable
confirmLoading={formPending} confirmLoading={formPending}
destroyOnClose destroyOnHidden
loading={loading} loading={loading}
okText={scene === "edit" ? t("common.button.save") : t("common.button.submit")} okText={scene === "edit" ? t("common.button.save") : t("common.button.submit")}
open={open} open={open}

View File

@ -1,12 +1,14 @@
import { forwardRef, useImperativeHandle, useMemo } from "react"; import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd"; import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod"; import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod"; import { z } from "zod";
import AccessProviderPicker from "@/components/provider/AccessProviderPicker";
import AccessProviderSelect from "@/components/provider/AccessProviderSelect"; import AccessProviderSelect from "@/components/provider/AccessProviderSelect";
import Show from "@/components/Show";
import { type AccessModel } from "@/domain/access"; import { type AccessModel } from "@/domain/access";
import { ACCESS_PROVIDERS, ACCESS_USAGES } from "@/domain/provider"; import { ACCESS_PROVIDERS, ACCESS_USAGES, type AccessProvider } from "@/domain/provider";
import { useAntdForm, useAntdFormName } from "@/hooks"; import { useAntdForm, useAntdFormName } from "@/hooks";
import AccessForm1PanelConfig from "./AccessForm1PanelConfig"; import AccessForm1PanelConfig from "./AccessForm1PanelConfig";
@ -107,9 +109,22 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
}); });
const formRule = createSchemaFieldRule(formSchema); const formRule = createSchemaFieldRule(formSchema);
const { form: formInst, formProps } = useAntdForm({ const { form: formInst, formProps } = useAntdForm({
name: "accessForm",
initialValues: initialValues, initialValues: initialValues,
}); });
const providerFilter = useMemo(() => {
switch (usage) {
case "both-dns-hosting":
return (record: AccessProvider) => record.usages.includes(ACCESS_USAGES.DNS) || record.usages.includes(ACCESS_USAGES.HOSTING);
case "ca-only":
return (record: AccessProvider) => record.usages.includes(ACCESS_USAGES.CA);
case "notification-only":
return (record: AccessProvider) => record.usages.includes(ACCESS_USAGES.NOTIFICATION);
}
return undefined;
}, [usage]);
const providerLabel = useMemo(() => { const providerLabel = useMemo(() => {
switch (usage) { switch (usage) {
case "ca-only": case "ca-only":
@ -139,10 +154,11 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
return undefined; return undefined;
}, [usage]); }, [usage]);
const fieldProvider = Form.useWatch("provider", formInst); const fieldProvider = Form.useWatch<z.infer<typeof formSchema>["provider"]>("provider", formInst);
const [fieldProviderPicked, setFieldProviderPicked] = useState<string>(initialValues?.provider); // bugfix: Form.useWatch 在条件渲染下不生效,这里用单独的变量存放 Picker 组件选择的值
const [nestedFormInst] = Form.useForm(); const [nestedFormInst] = Form.useForm();
const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "accessEditFormConfigForm" }); const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "accessConfigForm" });
const nestedFormEl = useMemo(() => { const nestedFormEl = useMemo(() => {
const nestedFormProps = { const nestedFormProps = {
form: nestedFormInst, form: nestedFormInst,
@ -272,7 +288,13 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
case ACCESS_PROVIDERS.ZEROSSL: case ACCESS_PROVIDERS.ZEROSSL:
return <AccessFormZeroSSLConfig {...nestedFormProps} />; return <AccessFormZeroSSLConfig {...nestedFormProps} />;
} }
}, [disabled, initialValues?.config, fieldProvider, nestedFormInst, nestedFormName]); }, [usage, disabled, initialValues?.config, fieldProvider, nestedFormInst, nestedFormName]);
const handleProviderPick = (value: string) => {
setFieldProviderPicked(value);
formInst.setFieldValue("provider", value);
onValuesChange?.(formInst.getFieldsValue(true));
};
const handleFormProviderChange = (name: string) => { const handleFormProviderChange = (name: string) => {
if (name === nestedFormName) { if (name === nestedFormName) {
@ -312,30 +334,32 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
<Form.Provider onFormChange={handleFormProviderChange}> <Form.Provider onFormChange={handleFormProviderChange}>
<div className={className} style={style}> <div className={className} style={style}>
<Form {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}> <Form {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
<Form.Item name="name" label={t("access.form.name.label")} rules={[formRule]}> <Show
<Input placeholder={t("access.form.name.placeholder")} /> when={!!fieldProvider || !!fieldProviderPicked}
</Form.Item> fallback={
<AccessProviderPicker
autoFocus
filter={providerFilter}
placeholder={t("access.form.provider.search.placeholder")}
showOptionTags={usage == null || (usage === "both-dns-hosting" ? { [ACCESS_USAGES.DNS]: true, [ACCESS_USAGES.HOSTING]: true } : false)}
onSelect={handleProviderPick}
/>
}
>
<Form.Item name="name" label={t("access.form.name.label")} rules={[formRule]}>
<Input placeholder={t("access.form.name.placeholder")} />
</Form.Item>
<Form.Item name="provider" label={providerLabel} rules={[formRule]} tooltip={providerTooltip}> <Form.Item name="provider" label={providerLabel} rules={[formRule]} tooltip={providerTooltip}>
<AccessProviderSelect <AccessProviderSelect
filter={(record) => { filter={providerFilter}
if (usage == null) return true; disabled={scene !== "add"}
placeholder={providerPlaceholder}
switch (usage) { showOptionTags={usage == null || (usage === "both-dns-hosting" ? { [ACCESS_USAGES.DNS]: true, [ACCESS_USAGES.HOSTING]: true } : false)}
case "both-dns-hosting": showSearch={!disabled}
return record.usages.includes(ACCESS_USAGES.DNS) || record.usages.includes(ACCESS_USAGES.HOSTING); />
case "ca-only": </Form.Item>
return record.usages.includes(ACCESS_USAGES.CA); </Show>
case "notification-only":
return record.usages.includes(ACCESS_USAGES.NOTIFICATION);
}
}}
disabled={scene !== "add"}
placeholder={providerPlaceholder}
showOptionTags={usage == null || (usage === "both-dns-hosting" ? { [ACCESS_USAGES.DNS]: true, [ACCESS_USAGES.HOSTING]: true } : false)}
showSearch={!disabled}
/>
</Form.Item>
</Form> </Form>
{nestedFormEl} {nestedFormEl}

View File

@ -283,7 +283,7 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa
</Form.Item> </Form.Item>
<Show when={!usage || usage === "deployment"}> <Show when={!usage || usage === "deployment"}>
<Form.Item className="mb-0"> <Form.Item className="mb-0" htmlFor="null">
<label className="mb-1 block"> <label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4"> <div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate"> <div className="max-w-full grow truncate">
@ -312,7 +312,7 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa
</Show> </Show>
<Show when={!usage || usage === "notification"}> <Show when={!usage || usage === "notification"}>
<Form.Item className="mb-0"> <Form.Item className="mb-0" htmlFor="null">
<label className="mb-1 block"> <label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4"> <div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate"> <div className="max-w-full grow truncate">

View File

@ -29,7 +29,7 @@ const CertificateDetailDrawer = ({ data, loading, trigger, ...props }: Certifica
<Drawer <Drawer
afterOpenChange={setOpen} afterOpenChange={setOpen}
destroyOnClose destroyOnHidden
open={open} open={open}
loading={loading} loading={loading}
placement="right" placement="right"

View File

@ -3,17 +3,18 @@ import { useTranslation } from "react-i18next";
import { Avatar, Card, Col, Empty, Flex, Input, type InputRef, Row, Typography } from "antd"; import { Avatar, Card, Col, Empty, Flex, Input, type InputRef, Row, Typography } from "antd";
import Show from "@/components/Show"; import Show from "@/components/Show";
import { acmeDns01ProvidersMap } from "@/domain/provider"; import { type ACMEDns01Provider, acmeDns01ProvidersMap } from "@/domain/provider";
export type ACMEDns01ProviderPickerProps = { export type ACMEDns01ProviderPickerProps = {
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
autoFocus?: boolean; autoFocus?: boolean;
filter?: (record: ACMEDns01Provider) => boolean;
placeholder?: string; placeholder?: string;
onSelect?: (value: string) => void; onSelect?: (value: string) => void;
}; };
const ACMEDns01ProviderPicker = ({ className, style, autoFocus, placeholder, onSelect }: ACMEDns01ProviderPickerProps) => { const ACMEDns01ProviderPicker = ({ className, style, autoFocus, filter, placeholder, onSelect }: ACMEDns01ProviderPickerProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [keyword, setKeyword] = useState<string>(); const [keyword, setKeyword] = useState<string>();
@ -25,15 +26,23 @@ const ACMEDns01ProviderPicker = ({ className, style, autoFocus, placeholder, onS
}, []); }, []);
const providers = useMemo(() => { const providers = useMemo(() => {
return Array.from(acmeDns01ProvidersMap.values()).filter((provider) => { return Array.from(acmeDns01ProvidersMap.values())
if (keyword) { .filter((provider) => {
const value = keyword.toLowerCase(); if (filter) {
return provider.type.toLowerCase().includes(value) || t(provider.name).toLowerCase().includes(value); return filter(provider);
} }
return true; return true;
}); })
}, [keyword]); .filter((provider) => {
if (keyword) {
const value = keyword.toLowerCase();
return provider.type.toLowerCase().includes(value) || t(provider.name).toLowerCase().includes(value);
}
return true;
});
}, [filter, keyword]);
const handleProviderTypeSelect = (value: string) => { const handleProviderTypeSelect = (value: string) => {
onSelect?.(value); onSelect?.(value);

View File

@ -0,0 +1,117 @@
import { memo, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Avatar, Card, Col, Empty, Flex, Input, type InputRef, Row, Tag, Typography } from "antd";
import Show from "@/components/Show";
import { ACCESS_USAGES, type AccessProvider, type AccessUsageType, accessProvidersMap } from "@/domain/provider";
export type AccessProviderPickerProps = {
className?: string;
style?: React.CSSProperties;
autoFocus?: boolean;
filter?: (record: AccessProvider) => boolean;
placeholder?: string;
showOptionTags?: boolean | { [key in AccessUsageType]?: boolean };
onSelect?: (value: string) => void;
};
const AccessProviderPicker = ({ className, style, autoFocus, filter, placeholder, showOptionTags, onSelect }: AccessProviderPickerProps) => {
const { t } = useTranslation();
const [keyword, setKeyword] = useState<string>();
const keywordInputRef = useRef<InputRef>(null);
useEffect(() => {
if (autoFocus) {
setTimeout(() => keywordInputRef.current?.focus(), 1);
}
}, []);
const providers = useMemo(() => {
return Array.from(accessProvidersMap.values())
.filter((provider) => {
if (filter) {
return filter(provider);
}
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, keyword]);
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 handleProviderTypeSelect = (value: string) => {
onSelect?.(value);
};
return (
<div className={className} style={style}>
<Input.Search ref={keywordInputRef} placeholder={placeholder ?? t("common.text.search")} onChange={(e) => setKeyword(e.target.value.trim())} />
<div className="mt-4">
<Show when={providers.length > 0} fallback={<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}>
<Row gutter={[16, 16]}>
{providers.map((provider, index) => {
return (
<Col key={index} xs={24} md={12} span={8}>
<Card
className="h-20 w-full overflow-hidden shadow-sm"
styles={{ body: { height: "100%", padding: "0.5rem 1rem" } }}
hoverable
onClick={() => {
handleProviderTypeSelect(provider.type);
}}
>
<Flex className="size-full overflow-hidden" align="center" gap={8}>
<Avatar src={provider.icon} size="small" />
<div className="flex-1 overflow-hidden">
<Typography.Text className="mb-1 line-clamp-1">{t(provider.name)}</Typography.Text>
<div className="mx-[-30px] scale-[80%]">
<Show when={provider.builtin}>
<Tag>{t("access.props.provider.builtin")}</Tag>
</Show>
<Show when={showOptionTagForDNS && provider.usages.includes(ACCESS_USAGES.DNS)}>
<Tag color="orange">{t("access.props.provider.usage.dns")}</Tag>
</Show>
<Show when={showOptionTagForHosting && provider.usages.includes(ACCESS_USAGES.HOSTING)}>
<Tag color="geekblue">{t("access.props.provider.usage.hosting")}</Tag>
</Show>
<Show when={showOptionTagForCA && provider.usages.includes(ACCESS_USAGES.CA)}>
<Tag color="magenta">{t("access.props.provider.usage.ca")}</Tag>
</Show>
<Show when={showOptionTagForNotification && provider.usages.includes(ACCESS_USAGES.NOTIFICATION)}>
<Tag color="cyan">{t("access.props.provider.usage.notification")}</Tag>
</Show>
</div>
</div>
</Flex>
</Card>
</Col>
);
})}
</Row>
</Show>
</div>
</div>
);
};
export default memo(AccessProviderPicker);

View File

@ -56,19 +56,19 @@ const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProvid
</Space> </Space>
<div> <div>
<Show when={provider.builtin}> <Show when={provider.builtin}>
<Tag color="grey">{t("access.props.provider.builtin")}</Tag> <Tag>{t("access.props.provider.builtin")}</Tag>
</Show> </Show>
<Show when={showOptionTagForDNS && provider.usages.includes(ACCESS_USAGES.DNS)}> <Show when={showOptionTagForDNS && provider.usages.includes(ACCESS_USAGES.DNS)}>
<Tag color="peru">{t("access.props.provider.usage.dns")}</Tag> <Tag color="orange">{t("access.props.provider.usage.dns")}</Tag>
</Show> </Show>
<Show when={showOptionTagForHosting && provider.usages.includes(ACCESS_USAGES.HOSTING)}> <Show when={showOptionTagForHosting && provider.usages.includes(ACCESS_USAGES.HOSTING)}>
<Tag color="royalblue">{t("access.props.provider.usage.hosting")}</Tag> <Tag color="geekblue">{t("access.props.provider.usage.hosting")}</Tag>
</Show> </Show>
<Show when={showOptionTagForCA && provider.usages.includes(ACCESS_USAGES.CA)}> <Show when={showOptionTagForCA && provider.usages.includes(ACCESS_USAGES.CA)}>
<Tag color="crimson">{t("access.props.provider.usage.ca")}</Tag> <Tag color="magenta">{t("access.props.provider.usage.ca")}</Tag>
</Show> </Show>
<Show when={showOptionTagForNotification && provider.usages.includes(ACCESS_USAGES.NOTIFICATION)}> <Show when={showOptionTagForNotification && provider.usages.includes(ACCESS_USAGES.NOTIFICATION)}>
<Tag color="mediumaquamarine">{t("access.props.provider.usage.notification")}</Tag> <Tag color="cyan">{t("access.props.provider.usage.notification")}</Tag>
</Show> </Show>
</div> </div>
</div> </div>

View File

@ -3,17 +3,18 @@ import { useTranslation } from "react-i18next";
import { Avatar, Card, Col, Empty, Flex, Input, type InputRef, Row, Tabs, Tooltip, Typography } from "antd"; import { Avatar, Card, Col, Empty, Flex, Input, type InputRef, Row, Tabs, Tooltip, Typography } from "antd";
import Show from "@/components/Show"; import Show from "@/components/Show";
import { DEPLOYMENT_CATEGORIES, deploymentProvidersMap } from "@/domain/provider"; import { DEPLOYMENT_CATEGORIES, type DeploymentProvider, deploymentProvidersMap } from "@/domain/provider";
export type DeploymentProviderPickerProps = { export type DeploymentProviderPickerProps = {
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
autoFocus?: boolean; autoFocus?: boolean;
filter?: (record: DeploymentProvider) => boolean;
placeholder?: string; placeholder?: string;
onSelect?: (value: string) => void; onSelect?: (value: string) => void;
}; };
const DeploymentProviderPicker = ({ className, style, autoFocus, placeholder, onSelect }: DeploymentProviderPickerProps) => { const DeploymentProviderPicker = ({ className, style, autoFocus, filter, placeholder, onSelect }: DeploymentProviderPickerProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [category, setCategory] = useState<string>(DEPLOYMENT_CATEGORIES.ALL); const [category, setCategory] = useState<string>(DEPLOYMENT_CATEGORIES.ALL);
@ -28,6 +29,13 @@ const DeploymentProviderPicker = ({ className, style, autoFocus, placeholder, on
const providers = useMemo(() => { const providers = useMemo(() => {
return Array.from(deploymentProvidersMap.values()) return Array.from(deploymentProvidersMap.values())
.filter((provider) => {
if (filter) {
return filter(provider);
}
return true;
})
.filter((provider) => { .filter((provider) => {
if (category && category !== DEPLOYMENT_CATEGORIES.ALL) { if (category && category !== DEPLOYMENT_CATEGORIES.ALL) {
return provider.category === category; return provider.category === category;
@ -43,7 +51,7 @@ const DeploymentProviderPicker = ({ className, style, autoFocus, placeholder, on
return true; return true;
}); });
}, [category, keyword]); }, [filter, category, keyword]);
const handleProviderTypeSelect = (value: string) => { const handleProviderTypeSelect = (value: string) => {
onSelect?.(value); onSelect?.(value);

View File

@ -30,7 +30,7 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR
<Drawer <Drawer
afterOpenChange={setOpen} afterOpenChange={setOpen}
destroyOnClose destroyOnHidden
open={open} open={open}
loading={loading} loading={loading}
placement="right" placement="right"

View File

@ -350,7 +350,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
/> />
</Form.Item> </Form.Item>
<Form.Item className="mb-0"> <Form.Item className="mb-0" htmlFor="null">
<label className="mb-1 block"> <label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4"> <div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate"> <div className="max-w-full grow truncate">
@ -406,7 +406,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
</Divider> </Divider>
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}> <Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
<Form.Item className="mb-0"> <Form.Item className="mb-0" htmlFor="null">
<label className="mb-1 block"> <label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4"> <div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate"> <div className="max-w-full grow truncate">
@ -435,7 +435,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
</Form.Item> </Form.Item>
</Form.Item> </Form.Item>
<Form.Item className="mb-0" hidden={!showCAProviderAccess}> <Form.Item className="mb-0" htmlFor="null" hidden={!showCAProviderAccess}>
<label className="mb-1 block"> <label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4"> <div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate"> <div className="max-w-full grow truncate">
@ -703,7 +703,7 @@ const DomainsModalInput = memo(({ value, trigger, onChange }: { value?: string;
{...formProps} {...formProps}
layout="vertical" layout="vertical"
form={formInst} form={formInst}
modalProps={{ destroyOnClose: true }} modalProps={{ destroyOnHidden: true }}
title={t("workflow_node.apply.form.domains.multiple_input_modal.title")} title={t("workflow_node.apply.form.domains.multiple_input_modal.title")}
trigger={trigger} trigger={trigger}
validateTrigger="onSubmit" validateTrigger="onSubmit"
@ -743,7 +743,7 @@ const NameserversModalInput = memo(({ trigger, value, onChange }: { trigger?: Re
{...formProps} {...formProps}
layout="vertical" layout="vertical"
form={formInst} form={formInst}
modalProps={{ destroyOnClose: true }} modalProps={{ destroyOnHidden: true }}
title={t("workflow_node.apply.form.nameservers.multiple_input_modal.title")} title={t("workflow_node.apply.form.nameservers.multiple_input_modal.title")}
trigger={trigger} trigger={trigger}
validateTrigger="onSubmit" validateTrigger="onSubmit"

View File

@ -391,7 +391,9 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}> <Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
<Show <Show
when={!!fieldProvider} when={!!fieldProvider}
fallback={<DeploymentProviderPicker autoFocus placeholder={t("workflow_node.deploy.search.provider.placeholder")} onSelect={handleProviderPick} />} fallback={
<DeploymentProviderPicker autoFocus placeholder={t("workflow_node.deploy.form.provider.search.placeholder")} onSelect={handleProviderPick} />
}
> >
<Form.Item name="provider" label={t("workflow_node.deploy.form.provider.label")} rules={[formRule]}> <Form.Item name="provider" label={t("workflow_node.deploy.form.provider.label")} rules={[formRule]}>
<DeploymentProviderSelect <DeploymentProviderSelect
@ -404,7 +406,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
/> />
</Form.Item> </Form.Item>
<Form.Item className="mb-0" hidden={!showProviderAccess}> <Form.Item className="mb-0" htmlFor="null" hidden={!showProviderAccess}>
<label className="mb-1 block"> <label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4"> <div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate"> <div className="max-w-full grow truncate">
@ -450,15 +452,6 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
</Form.Item> </Form.Item>
</Form.Item> </Form.Item>
<Show when={fieldProvider === DEPLOYMENT_PROVIDERS.LOCAL}>
<Form.Item>
<Alert
type="info"
message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.provider_access.guide_for_local") }}></span>}
/>
</Form.Item>
</Show>
<Form.Item <Form.Item
name="certificate" name="certificate"
label={t("workflow_node.deploy.form.certificate.label")} label={t("workflow_node.deploy.form.certificate.label")}

View File

@ -186,7 +186,7 @@ const ResourceIdsModalInput = memo(({ value, trigger, onChange }: { value?: stri
{...formProps} {...formProps}
layout="vertical" layout="vertical"
form={formInst} form={formInst}
modalProps={{ destroyOnClose: true }} modalProps={{ destroyOnHidden: true }}
title={t("workflow_node.deploy.form.aliyun_cas_deploy_resource_ids.multiple_input_modal.title")} title={t("workflow_node.deploy.form.aliyun_cas_deploy_resource_ids.multiple_input_modal.title")}
trigger={trigger} trigger={trigger}
validateTrigger="onSubmit" validateTrigger="onSubmit"
@ -226,7 +226,7 @@ const ContactIdsModalInput = memo(({ value, trigger, onChange }: { value?: strin
{...formProps} {...formProps}
layout="vertical" layout="vertical"
form={formInst} form={formInst}
modalProps={{ destroyOnClose: true }} modalProps={{ destroyOnHidden: true }}
title={t("workflow_node.deploy.form.aliyun_cas_deploy_contact_ids.multiple_input_modal.title")} title={t("workflow_node.deploy.form.aliyun_cas_deploy_contact_ids.multiple_input_modal.title")}
trigger={trigger} trigger={trigger}
validateTrigger="onSubmit" validateTrigger="onSubmit"

View File

@ -173,7 +173,7 @@ const SiteNamesModalInput = memo(({ value, trigger, onChange }: { value?: string
{...formProps} {...formProps}
layout="vertical" layout="vertical"
form={formInst} form={formInst}
modalProps={{ destroyOnClose: true }} modalProps={{ destroyOnHidden: true }}
title={t("workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.title")} title={t("workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.title")}
trigger={trigger} trigger={trigger}
validateTrigger="onSubmit" validateTrigger="onSubmit"

View File

@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DownOutlined as DownOutlinedIcon } from "@ant-design/icons"; import { DownOutlined as DownOutlinedIcon } from "@ant-design/icons";
import { Button, Dropdown, Form, type FormInstance, Input, Select } from "antd"; import { Alert, Button, Dropdown, Form, type FormInstance, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod"; import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod"; import { z } from "zod";
@ -289,6 +289,10 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i
name={formName} name={formName}
onValuesChange={handleFormChange} onValuesChange={handleFormChange}
> >
<Form.Item>
<Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.local.guide") }}></span>} />
</Form.Item>
<Form.Item name="format" label={t("workflow_node.deploy.form.local_format.label")} rules={[formRule]}> <Form.Item name="format" label={t("workflow_node.deploy.form.local_format.label")} rules={[formRule]}>
<Select placeholder={t("workflow_node.deploy.form.local_format.placeholder")} onSelect={handleFormatSelect}> <Select placeholder={t("workflow_node.deploy.form.local_format.placeholder")} onSelect={handleFormatSelect}>
<Select.Option key={FORMAT_PEM} value={FORMAT_PEM}> <Select.Option key={FORMAT_PEM} value={FORMAT_PEM}>
@ -377,7 +381,7 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item className="mb-0"> <Form.Item className="mb-0" htmlFor="null">
<label className="mb-1 block"> <label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4"> <div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate"> <div className="max-w-full grow truncate">
@ -407,7 +411,7 @@ const DeployNodeConfigFormLocalConfig = ({ form: formInst, formName, disabled, i
</Form.Item> </Form.Item>
</Form.Item> </Form.Item>
<Form.Item className="mb-0"> <Form.Item className="mb-0" htmlFor="null">
<label className="mb-1 block"> <label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4"> <div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate"> <div className="max-w-full grow truncate">

View File

@ -252,7 +252,7 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
<Select options={[{ value: t("workflow_node.deploy.form.ssh_shell_env.value") }]} value={t("workflow_node.deploy.form.ssh_shell_env.value")} /> <Select options={[{ value: t("workflow_node.deploy.form.ssh_shell_env.value") }]} value={t("workflow_node.deploy.form.ssh_shell_env.value")} />
</Form.Item> </Form.Item>
<Form.Item className="mb-0"> <Form.Item className="mb-0" htmlFor="null">
<label className="mb-1 block"> <label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4"> <div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate"> <div className="max-w-full grow truncate">
@ -282,7 +282,7 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
</Form.Item> </Form.Item>
</Form.Item> </Form.Item>
<Form.Item className="mb-0"> <Form.Item className="mb-0" htmlFor="null">
<label className="mb-1 block"> <label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4"> <div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate"> <div className="max-w-full grow truncate">

View File

@ -160,7 +160,7 @@ const ResourceIdsModalInput = memo(({ value, trigger, onChange }: { value?: stri
{...formProps} {...formProps}
layout="vertical" layout="vertical"
form={formInst} form={formInst}
modalProps={{ destroyOnClose: true }} modalProps={{ destroyOnHidden: true }}
title={t("workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.multiple_input_modal.title")} title={t("workflow_node.deploy.form.tencentcloud_ssl_deploy_resource_ids.multiple_input_modal.title")}
trigger={trigger} trigger={trigger}
validateTrigger="onSubmit" validateTrigger="onSubmit"

View File

@ -180,7 +180,7 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
<Input.TextArea autoSize={{ minRows: 3, maxRows: 5 }} placeholder={t("workflow_node.notify.form.message.placeholder")} /> <Input.TextArea autoSize={{ minRows: 3, maxRows: 5 }} placeholder={t("workflow_node.notify.form.message.placeholder")} />
</Form.Item> </Form.Item>
<Form.Item className="mb-0"> <Form.Item className="mb-0" htmlFor="null">
<label className="mb-1 block"> <label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4"> <div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate line-through">{t("workflow_node.notify.form.channel.label")}</div> <div className="max-w-full grow truncate line-through">{t("workflow_node.notify.form.channel.label")}</div>
@ -224,7 +224,7 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
/> />
</Form.Item> </Form.Item>
<Form.Item className="mb-0"> <Form.Item className="mb-0" htmlFor="null">
<label className="mb-1 block"> <label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4"> <div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate"> <div className="max-w-full grow truncate">

View File

@ -285,7 +285,7 @@ const SharedNodeConfigDrawer = ({
<Drawer <Drawer
afterOpenChange={setOpen} afterOpenChange={setOpen}
closable={!pending} closable={!pending}
destroyOnClose destroyOnHidden
extra={ extra={
<SharedNodeMenu <SharedNodeMenu
node={node} node={node}

View File

@ -29,6 +29,7 @@
"access.form.provider.label": "Provider", "access.form.provider.label": "Provider",
"access.form.provider.placeholder": "Please select a provider", "access.form.provider.placeholder": "Please select a provider",
"access.form.provider.tooltip": "DNS provider: The provider that hosts your domain names and manages your DNS records.<br>Hosting provider: The provider that hosts your servers or cloud services for deploying certificates.<br><br><i>Cannot be edited after saving.</i>", "access.form.provider.tooltip": "DNS provider: The provider that hosts your domain names and manages your DNS records.<br>Hosting provider: The provider that hosts your servers or cloud services for deploying certificates.<br><br><i>Cannot be edited after saving.</i>",
"access.form.provider.search.placeholder": "Search provider ...",
"access.form.certificate_authority.label": "Certificate authority", "access.form.certificate_authority.label": "Certificate authority",
"access.form.certificate_authority.placeholder": "Please select a certificate authority", "access.form.certificate_authority.placeholder": "Please select a certificate authority",
"access.form.notification_channel.label": "Notification channel", "access.form.notification_channel.label": "Notification channel",

View File

@ -98,14 +98,13 @@
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "Be careful not to exceed the validity period limit of the issued certificate, otherwise the certificate may never be renewed.", "workflow_node.apply.form.skip_before_expiry_days.tooltip": "Be careful not to exceed the validity period limit of the issued certificate, otherwise the certificate may never be renewed.",
"workflow_node.deploy.label": "Deployment", "workflow_node.deploy.label": "Deployment",
"workflow_node.deploy.search.provider.placeholder": "Search deploy target ...",
"workflow_node.deploy.form.provider.label": "Deploy target", "workflow_node.deploy.form.provider.label": "Deploy target",
"workflow_node.deploy.form.provider.placeholder": "Please select deploy target", "workflow_node.deploy.form.provider.placeholder": "Please select deploy target",
"workflow_node.deploy.form.provider.search.placeholder": "Search deploy target ...",
"workflow_node.deploy.form.provider_access.label": "Hosting provider authorization", "workflow_node.deploy.form.provider_access.label": "Hosting provider authorization",
"workflow_node.deploy.form.provider_access.placeholder": "Please select an authorization of Hosting provider", "workflow_node.deploy.form.provider_access.placeholder": "Please select an authorization of Hosting provider",
"workflow_node.deploy.form.provider_access.tooltip": "Used to invoke API during deployment.", "workflow_node.deploy.form.provider_access.tooltip": "Used to invoke API during deployment.",
"workflow_node.deploy.form.provider_access.button": "Create", "workflow_node.deploy.form.provider_access.button": "Create",
"workflow_node.deploy.form.provider_access.guide_for_local": "Tips: If you are running Certimate in Docker, the \"Local\" refers to the container rather than the host.",
"workflow_node.deploy.form.certificate.label": "Certificate", "workflow_node.deploy.form.certificate.label": "Certificate",
"workflow_node.deploy.form.certificate.placeholder": "Please select certificate", "workflow_node.deploy.form.certificate.placeholder": "Please select certificate",
"workflow_node.deploy.form.certificate.tooltip": "The certificate to be deployed comes from the previous nodes of application or upload.", "workflow_node.deploy.form.certificate.tooltip": "The certificate to be deployed comes from the previous nodes of application or upload.",
@ -441,6 +440,7 @@
"workflow_node.deploy.form.k8s_secret_data_key_for_key.label": "Kubernetes Secret data key for private key", "workflow_node.deploy.form.k8s_secret_data_key_for_key.label": "Kubernetes Secret data key for private key",
"workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder": "Please enter Kubernetes Secret data key for private key", "workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder": "Please enter Kubernetes Secret data key for private key",
"workflow_node.deploy.form.k8s_secret_data_key_for_key.tooltip": "For more information, see <a href=\"https://kubernetes.io/docs/concepts/configuration/secret/\" target=\"_blank\">https://kubernetes.io/docs/concepts/configuration/secret/</a>", "workflow_node.deploy.form.k8s_secret_data_key_for_key.tooltip": "For more information, see <a href=\"https://kubernetes.io/docs/concepts/configuration/secret/\" target=\"_blank\">https://kubernetes.io/docs/concepts/configuration/secret/</a>",
"workflow_node.deploy.form.local.guide": "Tips: If you are running Certimate in Docker, the \"Local\" refers to the container rather than the host.",
"workflow_node.deploy.form.local_format.label": "File format", "workflow_node.deploy.form.local_format.label": "File format",
"workflow_node.deploy.form.local_format.placeholder": "Please select file format", "workflow_node.deploy.form.local_format.placeholder": "Please select file format",
"workflow_node.deploy.form.local_format.option.pem.label": "PEM (*.pem, *.crt, *.key)", "workflow_node.deploy.form.local_format.option.pem.label": "PEM (*.pem, *.crt, *.key)",

View File

@ -29,6 +29,7 @@
"access.form.provider.label": "提供商", "access.form.provider.label": "提供商",
"access.form.provider.placeholder": "请选择提供商", "access.form.provider.placeholder": "请选择提供商",
"access.form.provider.tooltip": "提供商分为两种类型:<br>【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理您的域名解析记录。<br>【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。<br><br>该字段保存后不可修改。", "access.form.provider.tooltip": "提供商分为两种类型:<br>【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理您的域名解析记录。<br>【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。<br><br>该字段保存后不可修改。",
"access.form.provider.search.placeholder": "搜索提供商……",
"access.form.certificate_authority.label": "证书颁发机构", "access.form.certificate_authority.label": "证书颁发机构",
"access.form.certificate_authority.placeholder": "请选择证书颁发机构", "access.form.certificate_authority.placeholder": "请选择证书颁发机构",
"access.form.notification_channel.label": "通知渠道", "access.form.notification_channel.label": "通知渠道",

View File

@ -97,14 +97,13 @@
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "注意不要超过颁发的证书最大有效期,否则证书可能永远不会续期。", "workflow_node.apply.form.skip_before_expiry_days.tooltip": "注意不要超过颁发的证书最大有效期,否则证书可能永远不会续期。",
"workflow_node.deploy.label": "部署证书", "workflow_node.deploy.label": "部署证书",
"workflow_node.deploy.search.provider.placeholder": "搜索部署目标……",
"workflow_node.deploy.form.provider.label": "部署目标", "workflow_node.deploy.form.provider.label": "部署目标",
"workflow_node.deploy.form.provider.placeholder": "请选择部署目标", "workflow_node.deploy.form.provider.placeholder": "请选择部署目标",
"workflow_node.deploy.form.provider.search.placeholder": "搜索部署目标……",
"workflow_node.deploy.form.provider_access.label": "主机提供商授权", "workflow_node.deploy.form.provider_access.label": "主机提供商授权",
"workflow_node.deploy.form.provider_access.placeholder": "请选择主机提供商授权", "workflow_node.deploy.form.provider_access.placeholder": "请选择主机提供商授权",
"workflow_node.deploy.form.provider_access.tooltip": "用于部署证书时调用相关 API注意与申请阶段所需的 DNS 提供商相区分。", "workflow_node.deploy.form.provider_access.tooltip": "用于部署证书时调用相关 API注意与申请阶段所需的 DNS 提供商相区分。",
"workflow_node.deploy.form.provider_access.button": "新建", "workflow_node.deploy.form.provider_access.button": "新建",
"workflow_node.deploy.form.provider_access.guide_for_local": "小贴士:如果你正在使用 Docker 运行 Certimate“本地”指的是容器内而非宿主机。",
"workflow_node.deploy.form.certificate.label": "待部署证书", "workflow_node.deploy.form.certificate.label": "待部署证书",
"workflow_node.deploy.form.certificate.placeholder": "请选择待部署证书", "workflow_node.deploy.form.certificate.placeholder": "请选择待部署证书",
"workflow_node.deploy.form.certificate.tooltip": "待部署证书来自之前的申请或上传节点。如果选项为空请先确保前序节点配置正确。", "workflow_node.deploy.form.certificate.tooltip": "待部署证书来自之前的申请或上传节点。如果选项为空请先确保前序节点配置正确。",
@ -440,6 +439,7 @@
"workflow_node.deploy.form.k8s_secret_data_key_for_key.label": "Kubernetes Secret 数据键(用于存放私钥的字段)", "workflow_node.deploy.form.k8s_secret_data_key_for_key.label": "Kubernetes Secret 数据键(用于存放私钥的字段)",
"workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder": "请输入 Kubernetes Secret 中用于存放私钥的数据键", "workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder": "请输入 Kubernetes Secret 中用于存放私钥的数据键",
"workflow_node.deploy.form.k8s_secret_data_key_for_key.tooltip": "这是什么?请参阅 <a href=\"https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/\" target=\"_blank\">https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/</a>", "workflow_node.deploy.form.k8s_secret_data_key_for_key.tooltip": "这是什么?请参阅 <a href=\"https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/\" target=\"_blank\">https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/</a>",
"workflow_node.deploy.form.local.guide": "小贴士:如果你正在使用 Docker 运行 Certimate“本地”指的是容器内而非宿主机。",
"workflow_node.deploy.form.local_format.label": "文件格式", "workflow_node.deploy.form.local_format.label": "文件格式",
"workflow_node.deploy.form.local_format.placeholder": "请选择文件格式", "workflow_node.deploy.form.local_format.placeholder": "请选择文件格式",
"workflow_node.deploy.form.local_format.option.pem.label": "PEM 格式(*.pem, *.crt, *.key", "workflow_node.deploy.form.local_format.option.pem.label": "PEM 格式(*.pem, *.crt, *.key",

View File

@ -162,7 +162,7 @@ const SiderMenuDrawer = memo(({ trigger }: { trigger: React.ReactNode }) => {
<Drawer <Drawer
closable={false} closable={false}
destroyOnClose destroyOnHidden
open={siderOpen} open={siderOpen}
placement="left" placement="left"
styles={{ styles={{

View File

@ -374,7 +374,7 @@ const WorkflowBaseInfoModal = ({ trigger }: { trigger?: React.ReactNode }) => {
disabled={formPending} disabled={formPending}
layout="vertical" layout="vertical"
form={formInst} form={formInst}
modalProps={{ destroyOnClose: true }} modalProps={{ destroyOnHidden: true }}
okText={t("common.button.save")} okText={t("common.button.save")}
title={t(`workflow.detail.baseinfo.modal.title`)} title={t(`workflow.detail.baseinfo.modal.title`)}
trigger={trigger} trigger={trigger}

View File

@ -165,7 +165,7 @@ const WorkflowNew = () => {
disabled={formPending} disabled={formPending}
layout="vertical" layout="vertical"
form={formInst} form={formInst}
modalProps={{ destroyOnClose: true }} modalProps={{ destroyOnHidden: true }}
okText={t("common.button.submit")} okText={t("common.button.submit")}
open={formModalOpen} open={formModalOpen}
title={t(`workflow.new.modal.title`)} title={t(`workflow.new.modal.title`)}