mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-24 23:10:13 +08:00
feat(ui): new AccessNew page
This commit is contained in:
+1
-1
@@ -10,7 +10,7 @@ import "dayjs/locale/zh-cn";
|
||||
|
||||
import { useBrowserTheme } from "@/hooks";
|
||||
import { localeNames } from "@/i18n";
|
||||
import { router } from "@/router.tsx";
|
||||
import { router } from "@/routers";
|
||||
|
||||
const antdLocalesMap: Record<string, Locale> = {
|
||||
[localeNames.ZH]: AntdLocaleZhCN,
|
||||
|
||||
@@ -11,9 +11,9 @@ import { getErrMsg } from "@/utils/error";
|
||||
import AccessForm, { type AccessFormInstance, type AccessFormProps } from "./AccessForm";
|
||||
|
||||
export interface AccessEditDrawerProps {
|
||||
action: AccessFormProps["action"];
|
||||
data?: AccessFormProps["initialValues"];
|
||||
loading?: boolean;
|
||||
mode: AccessFormProps["mode"];
|
||||
open?: boolean;
|
||||
trigger?: React.ReactNode;
|
||||
usage?: AccessFormProps["usage"];
|
||||
@@ -21,7 +21,7 @@ export interface AccessEditDrawerProps {
|
||||
afterSubmit?: (record: AccessModel) => void;
|
||||
}
|
||||
|
||||
const AccessEditDrawer = ({ action, data, loading, trigger, usage, afterSubmit, ...props }: AccessEditDrawerProps) => {
|
||||
const AccessEditDrawer = ({ mode, data, loading, trigger, usage, afterSubmit, ...props }: AccessEditDrawerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { notification } = App.useApp();
|
||||
@@ -56,13 +56,13 @@ const AccessEditDrawer = ({ action, data, loading, trigger, usage, afterSubmit,
|
||||
try {
|
||||
let values: AccessModel = formRef.current!.getFieldsValue();
|
||||
|
||||
if (action === "create") {
|
||||
if (mode === "create") {
|
||||
if (data?.id) {
|
||||
throw "Invalid props: `data`";
|
||||
}
|
||||
|
||||
values = await createAccess(values);
|
||||
} else if (action === "edit") {
|
||||
} else if (mode === "edit") {
|
||||
if (!data?.id) {
|
||||
throw "Invalid props: `data`";
|
||||
}
|
||||
@@ -106,7 +106,7 @@ const AccessEditDrawer = ({ action, data, loading, trigger, usage, afterSubmit,
|
||||
<Flex justify="end" gap="small">
|
||||
<Button onClick={handleCancelClick}>{t("common.button.cancel")}</Button>
|
||||
<Button loading={formPending} type="primary" onClick={handleOkClick}>
|
||||
{action === "edit" ? t("common.button.save") : t("common.button.submit")}
|
||||
{mode === "edit" ? t("common.button.save") : t("common.button.submit")}
|
||||
</Button>
|
||||
</Flex>
|
||||
) : (
|
||||
@@ -116,11 +116,11 @@ const AccessEditDrawer = ({ action, data, loading, trigger, usage, afterSubmit,
|
||||
loading={loading}
|
||||
maskClosable={!formPending}
|
||||
open={open}
|
||||
title={t(`access.action.${action}.modal.title`)}
|
||||
title={t(`access.action.${mode}.modal.title`)}
|
||||
width={720}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<AccessForm ref={formRef} initialValues={data} action={action === "create" ? "create" : "edit"} usage={usage} onValuesChange={handleFormValuesChange} />
|
||||
<AccessForm ref={formRef} disabled={formPending} initialValues={data} mode={mode} usage={usage} onValuesChange={handleFormValuesChange} />
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useControllableValue } from "ahooks";
|
||||
import { App, Modal } 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 interface AccessEditModalProps {
|
||||
action: AccessFormProps["action"];
|
||||
data?: AccessFormProps["initialValues"];
|
||||
loading?: boolean;
|
||||
open?: boolean;
|
||||
usage?: AccessFormProps["usage"];
|
||||
trigger?: React.ReactNode;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
afterSubmit?: (record: AccessModel) => void;
|
||||
}
|
||||
|
||||
const AccessEditModal = ({ action, data, loading, trigger, usage, afterSubmit, ...props }: AccessEditModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { notification } = App.useApp();
|
||||
|
||||
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 [footerShow, setFooterShow] = useState(!!data?.provider);
|
||||
useEffect(() => {
|
||||
setFooterShow(!!data?.provider);
|
||||
}, [data?.provider]);
|
||||
|
||||
const handleOkClick = async () => {
|
||||
setFormPending(true);
|
||||
try {
|
||||
await formRef.current!.validateFields();
|
||||
} catch (err) {
|
||||
setFormPending(false);
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
let values: AccessModel = formRef.current!.getFieldsValue();
|
||||
|
||||
if (action === "create") {
|
||||
if (data?.id) {
|
||||
throw "Invalid props: `data`";
|
||||
}
|
||||
|
||||
values = await createAccess(values);
|
||||
} else if (action === "edit") {
|
||||
if (!data?.id) {
|
||||
throw "Invalid props: `data`";
|
||||
}
|
||||
|
||||
values = await updateAccess({ ...data, ...values });
|
||||
} else {
|
||||
throw "Invalid props: `action`";
|
||||
}
|
||||
|
||||
afterSubmit?.(values);
|
||||
setOpen(false);
|
||||
} catch (err) {
|
||||
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
|
||||
throw err;
|
||||
} finally {
|
||||
setFormPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelClick = () => {
|
||||
if (formPending) return;
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleFormValuesChange: AccessFormProps["onValuesChange"] = (values) => {
|
||||
setFooterShow(!!values.provider);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{triggerEl}
|
||||
|
||||
<Modal
|
||||
styles={{
|
||||
body: {
|
||||
maxHeight: "calc(80vh - 64px)",
|
||||
paddingTop: "1em",
|
||||
paddingBottom: "1em",
|
||||
overflowX: "hidden",
|
||||
overflowY: "auto",
|
||||
},
|
||||
}}
|
||||
afterClose={() => setOpen(false)}
|
||||
cancelButtonProps={{ disabled: formPending }}
|
||||
cancelText={t("common.button.cancel")}
|
||||
closable
|
||||
confirmLoading={formPending}
|
||||
destroyOnHidden
|
||||
footer={footerShow ? undefined : false}
|
||||
loading={loading}
|
||||
okText={action === "edit" ? t("common.button.save") : t("common.button.submit")}
|
||||
open={open}
|
||||
title={t(`access.action.${action}.modal.title`)}
|
||||
width={480}
|
||||
onOk={handleOkClick}
|
||||
onCancel={handleCancelClick}
|
||||
>
|
||||
<AccessForm ref={formRef} initialValues={data} action={action === "create" ? "create" : "edit"} usage={usage} onValuesChange={handleFormValuesChange} />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessEditModal;
|
||||
@@ -86,13 +86,13 @@ import AccessFormWestcnConfig from "./AccessFormWestcnConfig";
|
||||
import AccessFormZeroSSLConfig from "./AccessFormZeroSSLConfig";
|
||||
|
||||
type AccessFormFieldValues = Partial<MaybeModelRecord<AccessModel>>;
|
||||
type AccessFormActions = "create" | "edit";
|
||||
type AccessFormModes = "create" | "edit";
|
||||
type AccessFormUsages = "dns" | "hosting" | "dns-hosting" | "ca" | "notification";
|
||||
|
||||
export interface AccessFormProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
action: AccessFormActions;
|
||||
mode: AccessFormModes;
|
||||
disabled?: boolean;
|
||||
initialValues?: AccessFormFieldValues;
|
||||
usage?: AccessFormUsages;
|
||||
@@ -105,7 +105,7 @@ export interface AccessFormInstance {
|
||||
validateFields: FormInstance<AccessFormFieldValues>["validateFields"];
|
||||
}
|
||||
|
||||
const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className, style, action, disabled, initialValues, usage, onValuesChange }, ref) => {
|
||||
const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className, style, mode, disabled, initialValues, usage, onValuesChange }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -383,7 +383,7 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
|
||||
<Form.Item name="provider" label={t("access.form.provider.label")} rules={[formRule]} tooltip={providerTooltip}>
|
||||
<AccessProviderSelect
|
||||
filter={providerFilter}
|
||||
disabled={action !== "create"}
|
||||
disabled={mode !== "create"}
|
||||
placeholder={t("access.form.provider.placeholder")}
|
||||
showOptionTags={usage == null || (usage === "dns-hosting" ? { [ACCESS_USAGES.DNS]: true, [ACCESS_USAGES.HOSTING]: true } : false)}
|
||||
showSearch={!disabled}
|
||||
|
||||
@@ -12,12 +12,15 @@ export interface AccessProviderPickerProps {
|
||||
style?: React.CSSProperties;
|
||||
autoFocus?: boolean;
|
||||
filter?: (record: AccessProvider) => boolean;
|
||||
gap?: number | "small" | "middle" | "large";
|
||||
placeholder?: string;
|
||||
showOptionTags?: boolean | { [key in AccessUsageType]?: boolean };
|
||||
onSelect?: (value: string) => void;
|
||||
}
|
||||
|
||||
const AccessProviderPicker = ({ className, style, autoFocus, filter, placeholder, showOptionTags, onSelect }: AccessProviderPickerProps) => {
|
||||
const AccessProviderPicker = ({ className, style, autoFocus, filter, placeholder, showOptionTags, onSelect, ...props }: AccessProviderPickerProps) => {
|
||||
const { gap = "middle" } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
@@ -31,6 +34,22 @@ const AccessProviderPicker = ({ className, style, autoFocus, filter, placeholder
|
||||
}
|
||||
}, []);
|
||||
|
||||
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 showOptionTagAnyhow = useMemo(() => {
|
||||
return showOptionTagForDNS || showOptionTagForHosting || showOptionTagForCA || showOptionTagForNotification;
|
||||
}, [showOptionTagForDNS, showOptionTagForHosting, showOptionTagForCA, showOptionTagForNotification]);
|
||||
|
||||
const providers = useMemo(() => {
|
||||
return Array.from(accessProvidersMap.values())
|
||||
.filter((provider) => {
|
||||
@@ -54,25 +73,9 @@ const AccessProviderPicker = ({ className, style, autoFocus, filter, placeholder
|
||||
return 1;
|
||||
}
|
||||
|
||||
const cols = Math.floor(wrapperSize.width / 200);
|
||||
const cols = Math.floor(wrapperSize.width / (showOptionTagAnyhow ? 240 : 200));
|
||||
return Math.min(9, Math.max(1, cols));
|
||||
}, [wrapperSize]);
|
||||
|
||||
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 showOptionTagAnyhow = useMemo(() => {
|
||||
return showOptionTagForDNS || showOptionTagForHosting || showOptionTagForCA || showOptionTagForNotification;
|
||||
}, [showOptionTagForDNS, showOptionTagForHosting, showOptionTagForCA, showOptionTagForNotification]);
|
||||
}, [wrapperSize, showOptionTagAnyhow]);
|
||||
|
||||
const handleProviderTypeSelect = (value: string) => {
|
||||
onSelect?.(value);
|
||||
@@ -84,12 +87,19 @@ const AccessProviderPicker = ({ className, style, autoFocus, filter, placeholder
|
||||
|
||||
<div className="mt-4">
|
||||
<Show when={providers.length > 0} fallback={<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}>
|
||||
<div className={mergeCls("grid w-full gap-2", `grid-cols-${providerCols}`)}>
|
||||
<div
|
||||
className={mergeCls("grid w-full gap-2", `grid-cols-${providerCols}`, {
|
||||
"gap-4": gap === "large",
|
||||
"gap-2": gap === "middle",
|
||||
"gap-1": gap === "small",
|
||||
[`gap-${+gap || "2"}`]: typeof gap === "number",
|
||||
})}
|
||||
>
|
||||
{providers.map((provider) => {
|
||||
return (
|
||||
<div key={provider.type}>
|
||||
<Card
|
||||
className={mergeCls("h-20 w-full overflow-hidden shadow-xs", provider.builtin ? " cursor-not-allowed" : "")}
|
||||
className={mergeCls("h-20 w-full overflow-hidden shadow", provider.builtin ? " cursor-not-allowed" : "")}
|
||||
styles={{ body: { height: "100%", padding: "0.5rem 1rem" } }}
|
||||
hoverable
|
||||
onClick={() => {
|
||||
|
||||
@@ -12,11 +12,14 @@ export interface DeploymentProviderPickerProps {
|
||||
style?: React.CSSProperties;
|
||||
autoFocus?: boolean;
|
||||
filter?: (record: DeploymentProvider) => boolean;
|
||||
gap?: number | "small" | "middle" | "large";
|
||||
placeholder?: string;
|
||||
onSelect?: (value: string) => void;
|
||||
}
|
||||
|
||||
const DeploymentProviderPicker = ({ className, style, autoFocus, filter, placeholder, onSelect }: DeploymentProviderPickerProps) => {
|
||||
const DeploymentProviderPicker = ({ className, style, autoFocus, filter, placeholder, onSelect, ...props }: DeploymentProviderPickerProps) => {
|
||||
const { gap = "middle" } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
@@ -103,13 +106,20 @@ const DeploymentProviderPicker = ({ className, style, autoFocus, filter, placeho
|
||||
/>
|
||||
|
||||
<div className="flex-1">
|
||||
<Show when={providers.length > 0} fallback={<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}>
|
||||
<div className={mergeCls("grid w-full gap-2", `grid-cols-${providerCols}`)}>
|
||||
<Show when={providers.length > 0} fallback={<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t("common.text.nodata")} />}>
|
||||
<div
|
||||
className={mergeCls("grid w-full gap-2", `grid-cols-${providerCols}`, {
|
||||
"gap-4": gap === "large",
|
||||
"gap-2": gap === "middle",
|
||||
"gap-1": gap === "small",
|
||||
[`gap-${+gap || "2"}`]: typeof gap === "number",
|
||||
})}
|
||||
>
|
||||
{providers.map((provider) => {
|
||||
return (
|
||||
<div key={provider.type}>
|
||||
<Card
|
||||
className="h-16 w-full overflow-hidden shadow-xs"
|
||||
className="h-16 w-full overflow-hidden shadow"
|
||||
styles={{ body: { height: "100%", padding: "0.5rem 1rem" } }}
|
||||
hoverable
|
||||
onClick={() => {
|
||||
@@ -120,7 +130,7 @@ const DeploymentProviderPicker = ({ className, style, autoFocus, filter, placeho
|
||||
<div className="flex size-full items-center gap-4 overflow-hidden">
|
||||
<Avatar className="bg-stone-100" icon={<img src={provider.icon} />} shape="square" size={28} />
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="mb-1 line-clamp-2 max-w-full">
|
||||
<div className="line-clamp-2 max-w-full">
|
||||
<Typography.Text>{t(provider.name) || "\u00A0"}</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AutoComplete, Button, Divider, Flex, Form, type FormInstance, Input, In
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import AccessEditModal from "@/components/access/AccessEditModal";
|
||||
import AccessEditDrawer from "@/components/access/AccessEditDrawer";
|
||||
import AccessSelect from "@/components/access/AccessSelect";
|
||||
import MultipleSplitValueInput from "@/components/MultipleSplitValueInput";
|
||||
import ACMEDns01ProviderSelect from "@/components/provider/ACMEDns01ProviderSelect";
|
||||
@@ -325,8 +325,8 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<AccessEditModal
|
||||
action="create"
|
||||
<AccessEditDrawer
|
||||
mode="create"
|
||||
trigger={
|
||||
<Button size="small" type="link">
|
||||
{t("workflow_node.apply.form.provider_access.button")}
|
||||
@@ -406,9 +406,9 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
|
||||
<span>{t("workflow_node.apply.form.ca_provider_access.label")}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<AccessEditModal
|
||||
<AccessEditDrawer
|
||||
data={{ provider: caProvidersMap.get(fieldCAProvider!)?.provider }}
|
||||
action="create"
|
||||
mode="create"
|
||||
trigger={
|
||||
<Button size="small" type="link">
|
||||
{t("workflow_node.apply.form.ca_provider_access.button")}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button, Divider, Flex, Form, type FormInstance, Select, Switch, Tooltip
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import AccessEditModal from "@/components/access/AccessEditModal";
|
||||
import AccessEditDrawer from "@/components/access/AccessEditDrawer";
|
||||
import AccessSelect from "@/components/access/AccessSelect";
|
||||
import DeploymentProviderPicker from "@/components/provider/DeploymentProviderPicker.tsx";
|
||||
import DeploymentProviderSelect from "@/components/provider/DeploymentProviderSelect.tsx";
|
||||
@@ -487,9 +487,9 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<AccessEditModal
|
||||
<AccessEditDrawer
|
||||
data={{ provider: deploymentProvidersMap.get(fieldProvider!)?.provider }}
|
||||
action="create"
|
||||
mode="create"
|
||||
trigger={
|
||||
<Button size="small" type="link">
|
||||
{t("workflow_node.deploy.form.provider_access.button")}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button, Divider, Flex, Form, type FormInstance, Input, Switch, Typograp
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import AccessEditModal from "@/components/access/AccessEditModal";
|
||||
import AccessEditDrawer from "@/components/access/AccessEditDrawer";
|
||||
import AccessSelect from "@/components/access/AccessSelect";
|
||||
import NotificationProviderSelect from "@/components/provider/NotificationProviderSelect";
|
||||
import Show from "@/components/Show";
|
||||
@@ -198,8 +198,8 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
|
||||
<span>{t("workflow_node.notify.form.provider_access.label")}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<AccessEditModal
|
||||
action="create"
|
||||
<AccessEditDrawer
|
||||
mode="create"
|
||||
trigger={
|
||||
<Button size="small" type="link">
|
||||
{t("workflow_node.notify.form.provider_access.button")}
|
||||
|
||||
@@ -20,7 +20,7 @@ const UnknownNode = ({ node, disabled }: MonitorNodeProps) => {
|
||||
return (
|
||||
<>
|
||||
<div className="relative w-[256px] overflow-hidden">
|
||||
<Card className="shadow-md" styles={{ body: { padding: 0 } }} hoverable variant="borderless">
|
||||
<Card className="shadow" styles={{ body: { padding: 0 } }} hoverable variant="borderless">
|
||||
<div className="cursor-pointer">
|
||||
<Alert
|
||||
type="error"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useMemo, useRef } from "react";
|
||||
import { useMemo, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconCopy, IconDotsVertical, IconLabel, IconTrashX } from "@tabler/icons-react";
|
||||
import { useControllableValue } from "ahooks";
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
"access.props.created_at": "Created at",
|
||||
"access.props.updated_at": "Updated at",
|
||||
|
||||
"access.new.title": "Create Credential",
|
||||
"access.new.subtitle": "Use this credential to connect with specific third-party apps and services.",
|
||||
|
||||
"access.form.name.label": "Name",
|
||||
"access.form.name.placeholder": "Please enter credential name",
|
||||
"access.form.provider.label": "Provider",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"access.page.title": "授权管理",
|
||||
"access.page.subtitle": "授权中存储有用于访问特定第三方应用程序和服务的身份验证信息(如账号密码、API 密钥、OAuth 令牌等)。",
|
||||
"access.page.subtitle": "授权中存储有用于访问特定第三方应用程序或服务的身份验证信息(如账号密码、API 密钥、OAuth 令牌等)。",
|
||||
|
||||
"access.nodata.title": "暂无授权",
|
||||
"access.nodata.description": "新建一个授权并存储身份验证信息吧~",
|
||||
@@ -27,6 +27,9 @@
|
||||
"access.props.created_at": "创建时间",
|
||||
"access.props.updated_at": "更新时间",
|
||||
|
||||
"access.new.title": "新建授权",
|
||||
"access.new.subtitle": "使用此授权访问特定的第三方应用程序或服务。",
|
||||
|
||||
"access.form.name.label": "名称",
|
||||
"access.form.name.placeholder": "请输入授权名称",
|
||||
"access.form.provider.label": "提供商",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { IconCirclePlus, IconCopy, IconEdit, IconFingerprint, IconPlus, IconReload, IconTrash } from "@tabler/icons-react";
|
||||
import { useRequest } from "ahooks";
|
||||
import { App, Avatar, Button, Input, Skeleton, Table, type TableProps, Tabs, Tooltip, Typography } from "antd";
|
||||
@@ -18,6 +18,7 @@ import { getErrMsg } from "@/utils/error";
|
||||
type AccessUsageProp = AccessEditDrawerProps["usage"];
|
||||
|
||||
const AccessList = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const { t } = useTranslation();
|
||||
@@ -182,19 +183,23 @@ const AccessList = () => {
|
||||
fetchAccesses();
|
||||
};
|
||||
|
||||
const handleCreateClick = () => {
|
||||
navigate(`/accesses/new?usage=${filters["usage"]}`);
|
||||
};
|
||||
|
||||
const [detailRecord, setDetailRecord] = useState<MaybeModelRecord<AccessModel>>();
|
||||
const [detailAction, setDetailAction] = useState<AccessEditDrawerProps["action"]>("create");
|
||||
const [detailMode, setDetailMode] = useState<AccessEditDrawerProps["mode"]>("create");
|
||||
const [detailOpen, setDetailOpen] = useState<boolean>(false);
|
||||
|
||||
const handleRecordDetailClick = (access: AccessModel) => {
|
||||
setDetailRecord(access);
|
||||
setDetailAction("edit");
|
||||
setDetailMode("edit");
|
||||
setDetailOpen(true);
|
||||
};
|
||||
|
||||
const handleRecordDuplicateClick = (access: AccessModel) => {
|
||||
setDetailRecord({ ...access, id: undefined, name: `${access.name}-copy` });
|
||||
setDetailAction("create");
|
||||
setDetailMode("create");
|
||||
setDetailOpen(true);
|
||||
};
|
||||
|
||||
@@ -245,15 +250,9 @@ const AccessList = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<AccessEditDrawer
|
||||
action="create"
|
||||
usage={filters["usage"] as AccessUsageProp}
|
||||
trigger={
|
||||
<Button className="text-sm" icon={<IconPlus size="1.25em" />} size="large" type="primary">
|
||||
{t("access.action.create.button")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Button className="text-sm" icon={<IconPlus size="1.25em" />} size="large" type="primary" onClick={handleCreateClick}>
|
||||
{t("access.action.create.button")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -291,15 +290,9 @@ const AccessList = () => {
|
||||
description={t("access.nodata.description")}
|
||||
icon={<IconFingerprint size={24} />}
|
||||
extra={
|
||||
<AccessEditDrawer
|
||||
action="create"
|
||||
usage={filters["usage"] as AccessUsageProp}
|
||||
trigger={
|
||||
<Button icon={<IconCirclePlus size="1.25em" />} type="primary">
|
||||
{t("access.action.create.button")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Button icon={<IconCirclePlus size="1.25em" />} type="primary" onClick={handleCreateClick}>
|
||||
{t("access.action.create.button")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
),
|
||||
@@ -328,13 +321,7 @@ const AccessList = () => {
|
||||
})}
|
||||
/>
|
||||
|
||||
<AccessEditDrawer
|
||||
data={detailRecord}
|
||||
open={detailOpen}
|
||||
action={detailAction}
|
||||
usage={filters["usage"] as AccessUsageProp}
|
||||
onOpenChange={setDetailOpen}
|
||||
/>
|
||||
<AccessEditDrawer data={detailRecord} open={detailOpen} mode={detailMode} usage={filters["usage"] as AccessUsageProp} onOpenChange={setDetailOpen} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { App, Button, Flex } from "antd";
|
||||
|
||||
import AccessForm, { type AccessFormInstance, type AccessFormProps } from "@/components/access/AccessForm";
|
||||
import AccessProviderPicker from "@/components/provider/AccessProviderPicker";
|
||||
import Show from "@/components/Show";
|
||||
import { type AccessModel } from "@/domain/access";
|
||||
import { ACCESS_USAGES, type AccessProvider } from "@/domain/provider";
|
||||
import { useZustandShallowSelector } from "@/hooks";
|
||||
import { useAccessesStore } from "@/stores/access";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
const AccessNew = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { notification } = App.useApp();
|
||||
|
||||
const { createAccess } = useAccessesStore(useZustandShallowSelector(["createAccess"]));
|
||||
|
||||
const formRef = useRef<AccessFormInstance>(null);
|
||||
const [formModel, setFormModel] = useState<MaybeModelRecord<AccessModel>>({});
|
||||
const [formPending, setFormPending] = useState(false);
|
||||
|
||||
const providerUsage = useMemo(() => {
|
||||
return searchParams.get("usage") as AccessFormProps["usage"];
|
||||
}, [searchParams]);
|
||||
const providerFilter = useMemo(() => {
|
||||
switch (providerUsage) {
|
||||
case "dns":
|
||||
return (record: AccessProvider) => record.usages.includes(ACCESS_USAGES.DNS);
|
||||
case "hosting":
|
||||
return (record: AccessProvider) => record.usages.includes(ACCESS_USAGES.HOSTING);
|
||||
case "dns-hosting":
|
||||
return (record: AccessProvider) => record.usages.includes(ACCESS_USAGES.DNS) || record.usages.includes(ACCESS_USAGES.HOSTING);
|
||||
case "ca":
|
||||
return (record: AccessProvider) => record.usages.includes(ACCESS_USAGES.CA);
|
||||
case "notification":
|
||||
return (record: AccessProvider) => record.usages.includes(ACCESS_USAGES.NOTIFICATION);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [providerUsage]);
|
||||
|
||||
const handleProviderPick = (value: string) => {
|
||||
setFormModel((prev) => {
|
||||
return { ...prev, provider: value, config: undefined };
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmitClick = async () => {
|
||||
setFormPending(true);
|
||||
try {
|
||||
await formRef.current!.validateFields();
|
||||
} catch (err) {
|
||||
setFormPending(false);
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
const values: AccessModel = formRef.current!.getFieldsValue();
|
||||
await createAccess(values);
|
||||
|
||||
navigate(`/accesses?usage=${providerUsage}`, { replace: true });
|
||||
} catch (err) {
|
||||
notification.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||
|
||||
throw err;
|
||||
} finally {
|
||||
setFormPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelClick = () => {
|
||||
setFormModel((prev) => {
|
||||
return { ...prev, provider: undefined, config: undefined, reserve: undefined };
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-6 py-4">
|
||||
<div className="mx-auto max-w-320">
|
||||
<h1>{t("access.new.title")}</h1>
|
||||
<p className="text-base text-gray-500">{t("access.new.subtitle")}</p>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-320">
|
||||
<Show
|
||||
when={!!formModel?.provider}
|
||||
fallback={
|
||||
<AccessProviderPicker
|
||||
autoFocus
|
||||
filter={providerFilter}
|
||||
gap="large"
|
||||
placeholder={t("access.form.provider.search.placeholder")}
|
||||
showOptionTags={providerUsage == null || (providerUsage === "dns-hosting" ? { [ACCESS_USAGES.DNS]: true, [ACCESS_USAGES.HOSTING]: true } : false)}
|
||||
onSelect={handleProviderPick}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<AccessForm
|
||||
className="md:max-w-160"
|
||||
ref={formRef}
|
||||
initialValues={formModel}
|
||||
disabled={formPending}
|
||||
mode="create"
|
||||
usage={providerUsage}
|
||||
onValuesChange={(values) => setFormModel(values)}
|
||||
/>
|
||||
<Flex gap="small">
|
||||
<Button type="primary" onClick={handleSubmitClick}>
|
||||
{t("common.button.submit")}
|
||||
</Button>
|
||||
<Button onClick={handleCancelClick}>{t("common.button.cancel")}</Button>
|
||||
</Flex>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessNew;
|
||||
@@ -258,7 +258,6 @@ const CertificateList = () => {
|
||||
<div className="flex w-full flex-1 items-center gap-x-2 md:max-w-200">
|
||||
<div>
|
||||
<Segmented
|
||||
className="shadow-xs"
|
||||
options={[
|
||||
{ label: <span className="text-sm">{t("certificate.props.validity.filter.all")}</span>, value: "" },
|
||||
{ label: <span className="text-sm">{t("certificate.props.validity.filter.expire_soon")}</span>, value: "expireSoon" },
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
IconUserShield,
|
||||
} from "@tabler/icons-react";
|
||||
import { useRequest } from "ahooks";
|
||||
import { App, Button, Card, Col, Divider, Empty, Flex, Grid, Row, Space, Statistic, Table, type TableProps, Typography, theme } from "antd";
|
||||
import { App, Button, Card, Col, Divider, Empty, Flex, Grid, Row, Statistic, Table, type TableProps, Typography, theme } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
|
||||
@@ -196,16 +196,16 @@ const WorkflowList = () => {
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const handleCreateClick = () => {
|
||||
navigate("/workflows/new");
|
||||
};
|
||||
|
||||
const handleReloadClick = () => {
|
||||
if (loading) return;
|
||||
|
||||
refreshData();
|
||||
};
|
||||
|
||||
const handleCreateClick = () => {
|
||||
navigate("/workflows/new");
|
||||
};
|
||||
|
||||
const handleRecordDetailClick = (workflow: WorkflowModel) => {
|
||||
navigate(`/workflows/${workflow.id}`);
|
||||
};
|
||||
@@ -303,7 +303,6 @@ const WorkflowList = () => {
|
||||
<div className="flex w-full flex-1 items-center gap-x-2 md:max-w-200">
|
||||
<div>
|
||||
<Segmented
|
||||
className="shadow-xs"
|
||||
options={[
|
||||
{ label: <span className="text-sm">{t("workflow.props.state.filter.all")}</span>, value: "" },
|
||||
{ label: <span className="text-sm">{t("workflow.props.state.filter.enabled")}</span>, value: "enabled" },
|
||||
|
||||
@@ -115,12 +115,12 @@ const WorkflowNew = () => {
|
||||
<div className="px-6 py-4">
|
||||
<div className="mx-auto max-w-320">
|
||||
<h1>{t("workflow.new.title")}</h1>
|
||||
<p className="mb-0 text-base text-gray-500">{t("workflow.new.subtitle")}</p>
|
||||
<p className="text-base text-gray-500">{t("workflow.new.subtitle")}</p>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-320">
|
||||
<Typography.Text type="secondary">
|
||||
<div className="mt-12 mb-8 text-center text-xl">{t("workflow.new.templates.title")}</div>
|
||||
<div className="mb-4 text-xl">{t("workflow.new.templates.title")}</div>
|
||||
</Typography.Text>
|
||||
|
||||
<Row className="justify-stretch" gutter={[16, 16]}>
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { createHashRouter } from "react-router-dom";
|
||||
import { createHashRouter } from "react-router-dom";
|
||||
|
||||
import AccessList from "./pages/accesses/AccessList";
|
||||
import AuthLayout from "./pages/AuthLayout";
|
||||
import CertificateList from "./pages/certificates/CertificateList";
|
||||
import ConsoleLayout from "./pages/ConsoleLayout";
|
||||
import Dashboard from "./pages/dashboard/Dashboard";
|
||||
import ErrorLayout from "./pages/ErrorLayout";
|
||||
import Login from "./pages/login/Login";
|
||||
import Settings from "./pages/settings/Settings";
|
||||
import SettingsAccount from "./pages/settings/SettingsAccount";
|
||||
import SettingsAppearance from "./pages/settings/SettingsAppearance";
|
||||
import SettingsPersistence from "./pages/settings/SettingsPersistence";
|
||||
import SettingsSSLProvider from "./pages/settings/SettingsSSLProvider";
|
||||
import WorkflowDetail from "./pages/workflows/WorkflowDetail";
|
||||
import WorkflowList from "./pages/workflows/WorkflowList";
|
||||
import WorkflowNew from "./pages/workflows/WorkflowNew";
|
||||
import AccessList from "@/pages/accesses/AccessList";
|
||||
import AccessNew from "@/pages/accesses/AccessNew";
|
||||
import AuthLayout from "@/pages/AuthLayout";
|
||||
import CertificateList from "@/pages/certificates/CertificateList";
|
||||
import ConsoleLayout from "@/pages/ConsoleLayout";
|
||||
import Dashboard from "@/pages/dashboard/Dashboard";
|
||||
import ErrorLayout from "@/pages/ErrorLayout";
|
||||
import Login from "@/pages/login/Login";
|
||||
import Settings from "@/pages/settings/Settings";
|
||||
import SettingsAccount from "@/pages/settings/SettingsAccount";
|
||||
import SettingsAppearance from "@/pages/settings/SettingsAppearance";
|
||||
import SettingsPersistence from "@/pages/settings/SettingsPersistence";
|
||||
import SettingsSSLProvider from "@/pages/settings/SettingsSSLProvider";
|
||||
import WorkflowDetail from "@/pages/workflows/WorkflowDetail";
|
||||
import WorkflowList from "@/pages/workflows/WorkflowList";
|
||||
import WorkflowNew from "@/pages/workflows/WorkflowNew";
|
||||
|
||||
export const router = createHashRouter([
|
||||
{
|
||||
@@ -29,6 +30,10 @@ export const router = createHashRouter([
|
||||
path: "/accesses",
|
||||
element: <AccessList />,
|
||||
},
|
||||
{
|
||||
path: "/accesses/new",
|
||||
element: <AccessNew />,
|
||||
},
|
||||
{
|
||||
path: "/certificates",
|
||||
element: <CertificateList />,
|
||||
Reference in New Issue
Block a user