feat(ui): new AccessNew page

This commit is contained in:
Fu Diwei
2025-07-22 21:09:13 +08:00
parent 0dc3cb79bd
commit 065e3e96fa
20 changed files with 248 additions and 237 deletions
+1 -1
View File
@@ -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;
+4 -4
View File
@@ -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";
+3
View File
@@ -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",
+4 -1
View File
@@ -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": "提供商",
+16 -29
View File
@@ -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>
);
+126
View File
@@ -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" },
+1 -1
View File
@@ -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";
+4 -5
View File
@@ -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" },
+2 -2
View File
@@ -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]}>
+21 -16
View File
@@ -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 />,