diff --git a/ui/package-lock.json b/ui/package-lock.json index f4fd3895..e2b71062 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -19,7 +19,6 @@ "@radix-ui/react-radio-group": "^1.2.0", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.1.1", - "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", @@ -2559,28 +2558,6 @@ } } }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-separator/-/react-separator-1.1.0.tgz", - "integrity": "sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==", - "dependencies": { - "@radix-ui/react-primitive": "2.0.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-slot": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", diff --git a/ui/package.json b/ui/package.json index a08f888e..a7963f08 100644 --- a/ui/package.json +++ b/ui/package.json @@ -21,7 +21,6 @@ "@radix-ui/react-radio-group": "^1.2.0", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.1.1", - "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", diff --git a/ui/src/components/access/AccessProviderSelect.tsx b/ui/src/components/access/AccessProviderSelect.tsx deleted file mode 100644 index a7186ea9..00000000 --- a/ui/src/components/access/AccessProviderSelect.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { Avatar, Select, Space, Typography, type SelectProps } from "antd"; - -import { accessProvidersMap } from "@/domain/access"; - -export type AccessProviderSelectProps = Omit<SelectProps, "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"> & { - className?: string; -}; - -const AccessProviderSelect = React.memo((props: AccessProviderSelectProps) => { - const { t } = useTranslation(); - - const options = Array.from(accessProvidersMap.values()).map((item) => ({ - key: item.type, - value: item.type, - label: t(item.name), - })); - - return ( - <Select - {...props} - labelRender={({ label, value }) => { - if (label) { - return ( - <Space className="max-w-full truncate" align="center" size={4}> - <Avatar src={accessProvidersMap.get(String(value))?.icon} size="small" /> - {label} - </Space> - ); - } - - return <Typography.Text type="secondary">{props.placeholder}</Typography.Text>; - }} - options={options} - optionFilterProp={undefined} - optionLabelProp={undefined} - optionRender={(option) => ( - <Space className="max-w-full truncate" align="center" size={4}> - <Avatar src={accessProvidersMap.get(option.data.value)?.icon} size="small" /> - <Typography.Text ellipsis>{t(accessProvidersMap.get(option.data.value)?.name ?? "")}</Typography.Text> - </Space> - )} - /> - ); -}); - -export default AccessProviderSelect; diff --git a/ui/src/components/access/AccessTypeSelect.tsx b/ui/src/components/access/AccessTypeSelect.tsx new file mode 100644 index 00000000..711d1b22 --- /dev/null +++ b/ui/src/components/access/AccessTypeSelect.tsx @@ -0,0 +1,66 @@ +import { memo } from "react"; +import { useTranslation } from "react-i18next"; +import { Avatar, Select, Space, Tag, Typography, type SelectProps } from "antd"; + +import { accessProvidersMap } from "@/domain/access"; + +export type AccessTypeSelectProps = Omit<SelectProps, "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender">; + +const AccessTypeSelect = memo((props: AccessTypeSelectProps) => { + const { t } = useTranslation(); + + const options = Array.from(accessProvidersMap.values()).map((item) => ({ + key: item.type, + value: item.type, + label: t(item.name), + })); + + return ( + <Select + {...props} + labelRender={({ label, value }) => { + if (label) { + return ( + <Space className="max-w-full truncate" size={4}> + <Avatar src={accessProvidersMap.get(String(value))?.icon} size="small" /> + {label} + </Space> + ); + } + + return <Typography.Text type="secondary">{props.placeholder}</Typography.Text>; + }} + options={options} + optionFilterProp={undefined} + optionLabelProp={undefined} + optionRender={(option) => ( + <div className="flex items-center justify-between gap-4 max-w-full overflow-hidden"> + <Space className="flex-grow max-w-full truncate" size={4}> + <Avatar src={accessProvidersMap.get(option.data.value)?.icon} size="small" /> + <Typography.Text ellipsis>{t(accessProvidersMap.get(option.data.value)?.name ?? "")}</Typography.Text> + </Space> + <div> + {accessProvidersMap.get(option.data.value)?.usage === "apply" && ( + <> + <Tag color="orange">{t("access.props.provider.usage.dns")}</Tag> + </> + )} + {accessProvidersMap.get(option.data.value)?.usage === "deploy" && ( + <> + <Tag color="blue">{t("access.props.provider.usage.host")}</Tag> + </> + )} + {accessProvidersMap.get(option.data.value)?.usage === "all" && ( + <> + <Tag color="orange">{t("access.props.provider.usage.dns")}</Tag> + <Tag color="blue">{t("access.props.provider.usage.host")}</Tag> + </> + )} + </div> + </div> + )} + /> + ); +}); + +export default AccessTypeSelect; diff --git a/ui/src/components/certificate/CertificateDetail.tsx b/ui/src/components/certificate/CertificateDetail.tsx index 05b492d3..684784cb 100644 --- a/ui/src/components/certificate/CertificateDetail.tsx +++ b/ui/src/components/certificate/CertificateDetail.tsx @@ -8,10 +8,12 @@ import { type CertificateModel } from "@/domain/certificate"; import { saveFiles2Zip } from "@/utils/file"; type CertificateDetailProps = { + className?: string; + style?: React.CSSProperties; data: CertificateModel; }; -const CertificateDetail = ({ data }: CertificateDetailProps) => { +const CertificateDetail = ({ data, ...props }: CertificateDetailProps) => { const { t } = useTranslation(); const [messageApi, MessageContextHolder] = message.useMessage(); @@ -33,7 +35,7 @@ const CertificateDetail = ({ data }: CertificateDetailProps) => { }; return ( - <div> + <div {...props}> {MessageContextHolder} <Form layout="vertical"> diff --git a/ui/src/components/certificate/CertificateDetailDrawer.tsx b/ui/src/components/certificate/CertificateDetailDrawer.tsx index 6aefd7d6..4108cbee 100644 --- a/ui/src/components/certificate/CertificateDetailDrawer.tsx +++ b/ui/src/components/certificate/CertificateDetailDrawer.tsx @@ -1,4 +1,5 @@ -import { useEffect, useState } from "react"; +import { cloneElement, useEffect, useMemo, useState } from "react"; +import { useControllableValue } from "ahooks"; import { Drawer } from "antd"; import { type CertificateModel } from "@/domain/certificate"; @@ -7,19 +8,44 @@ import CertificateDetail from "./CertificateDetail"; type CertificateDetailDrawerProps = { data?: CertificateModel; open?: boolean; - onClose?: () => void; + trigger?: React.ReactElement; + onOpenChange?: (open: boolean) => void; }; -const CertificateDetailDrawer = ({ data, open, onClose }: CertificateDetailDrawerProps) => { +const CertificateDetailDrawer = ({ data, trigger, ...props }: CertificateDetailDrawerProps) => { + const [open, setOpen] = useControllableValue<boolean>(props, { + valuePropName: "open", + defaultValuePropName: "defaultOpen", + trigger: "onOpenChange", + }); + const [loading, setLoading] = useState(true); useEffect(() => { setLoading(data == null); }, [data]); + const triggerDom = useMemo(() => { + if (!trigger) { + return null; + } + + return cloneElement(trigger, { + ...trigger.props, + onClick: () => { + setOpen(true); + trigger.props?.onClick?.(); + }, + }); + }, [trigger, setOpen]); + return ( - <Drawer closable destroyOnClose open={open} loading={loading} placement="right" width={480} onClose={onClose}> - {data ? <CertificateDetail data={data} /> : <></>} - </Drawer> + <> + {triggerDom} + + <Drawer closable destroyOnClose open={open} loading={loading} placement="right" width={480} onClose={() => setOpen(false)}> + {data ? <CertificateDetail data={data} /> : <></>} + </Drawer> + </> ); }; diff --git a/ui/src/components/certimate/AccessEditDialog.tsx b/ui/src/components/certimate/AccessEditDialog.tsx index c4ac874d..36de6fce 100644 --- a/ui/src/components/certimate/AccessEditDialog.tsx +++ b/ui/src/components/certimate/AccessEditDialog.tsx @@ -5,7 +5,8 @@ import { cn } from "@/components/ui/utils"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; -import AccessProviderSelect from "@/components/access/AccessProviderSelect"; +import AccessEditForm from "@/components/access/AccessEditForm"; +import AccessTypeSelect from "@/components/access/AccessTypeSelect"; import AccessAliyunForm from "./AccessAliyunForm"; import AccessTencentForm from "./AccessTencentForm"; import AccessHuaweiCloudForm from "./AccessHuaweicloudForm"; @@ -281,10 +282,11 @@ const AccessEditDialog = ({ trigger, op, data, className, outConfigType }: Acces </DialogTitle> </DialogHeader> <ScrollArea className="max-h-[80vh]"> + <AccessEditForm data={data} /> <div className="container py-3"> <div> <Label>{t("access.authorization.form.type.label")}</Label> - <AccessProviderSelect + <AccessTypeSelect className="w-full mt-3" placeholder={t("access.authorization.form.type.placeholder")} value={configType} diff --git a/ui/src/components/ui/alert.tsx b/ui/src/components/ui/alert.tsx deleted file mode 100644 index e5e48af8..00000000 --- a/ui/src/components/ui/alert.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from "react"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "./utils"; - -const alertVariants = cva( - "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", - { - variants: { - variant: { - default: "bg-background text-foreground", - destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", - }, - }, - defaultVariants: { - variant: "default", - }, - } -); - -const Alert = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>>( - ({ className, variant, ...props }, ref) => <div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} /> -); -Alert.displayName = "Alert"; - -const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(({ className, ...props }, ref) => ( - <h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} /> -)); -AlertTitle.displayName = "AlertTitle"; - -const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(({ className, ...props }, ref) => ( - <div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} /> -)); -AlertDescription.displayName = "AlertDescription"; - -export { Alert, AlertTitle, AlertDescription }; diff --git a/ui/src/components/ui/separator.tsx b/ui/src/components/ui/separator.tsx deleted file mode 100644 index 57070755..00000000 --- a/ui/src/components/ui/separator.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from "react"; -import * as SeparatorPrimitive from "@radix-ui/react-separator"; - -import { cn } from "./utils"; - -const Separator = React.forwardRef<React.ElementRef<typeof SeparatorPrimitive.Root>, React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>>( - ({ className, orientation = "horizontal", decorative = true, ...props }, ref) => ( - <SeparatorPrimitive.Root - ref={ref} - decorative={decorative} - orientation={orientation} - className={cn("shrink-0 bg-border", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", className)} - {...props} - /> - ) -); -Separator.displayName = SeparatorPrimitive.Root.displayName; - -export { Separator }; diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index e36fba19..8fa38ae0 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -11,15 +11,17 @@ "access.props.name": "Name", "access.props.provider": "Provider", + "access.props.provider.usage.dns": "DNS Provider", + "access.props.provider.usage.host": "Hos Provider", "access.props.created_at": "Created At", "access.props.updated_at": "Updated At", + "access.authorization.form.name.label": "Name", + "access.authorization.form.name.placeholder": "Please enter authorization name", "access.authorization.form.type.label": "Provider", "access.authorization.form.type.placeholder": "Please select a provider", "access.authorization.form.type.search.notfound": "Provider not found", "access.authorization.form.type.list": "Authorization List", - "access.authorization.form.name.label": "Name", - "access.authorization.form.name.placeholder": "Please enter authorization name", "access.authorization.form.config.label": "Configuration Type", "access.authorization.form.region.label": "Region", "access.authorization.form.region.placeholder": "Please enter Region", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 3c27acaf..068f80a1 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -11,6 +11,8 @@ "access.props.name": "名称", "access.props.provider": "服务商", + "access.props.provider.usage.dns": "DNS 服务商", + "access.props.provider.usage.host": "主机服务商", "access.props.created_at": "创建时间", "access.props.updated_at": "更新时间", diff --git a/ui/src/pages/ConsoleLayout.tsx b/ui/src/pages/ConsoleLayout.tsx index c21ab1bf..9929aa5c 100644 --- a/ui/src/pages/ConsoleLayout.tsx +++ b/ui/src/pages/ConsoleLayout.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import { memo, useEffect, useState } from "react"; import { Link, Navigate, Outlet, useLocation, useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Button, Drawer, Dropdown, Layout, Menu, Tooltip, theme, type ButtonProps, type MenuProps } from "antd"; @@ -109,7 +109,7 @@ const ConsoleLayout = () => { ); }; -const SiderMenu = React.memo(({ onSelect }: { onSelect?: (key: string) => void }) => { +const SiderMenu = memo(({ onSelect }: { onSelect?: (key: string) => void }) => { const location = useLocation(); const navigate = useNavigate(); @@ -179,7 +179,7 @@ const SiderMenu = React.memo(({ onSelect }: { onSelect?: (key: string) => void } ); }); -const ThemeToggleButton = React.memo(({ size }: { size?: ButtonProps["size"] }) => { +const ThemeToggleButton = memo(({ size }: { size?: ButtonProps["size"] }) => { const { t } = useTranslation(); const { theme, setThemeMode } = useTheme(); @@ -206,7 +206,7 @@ const ThemeToggleButton = React.memo(({ size }: { size?: ButtonProps["size"] }) ); }); -const LocaleToggleButton = React.memo(({ size }: { size?: ButtonProps["size"] }) => { +const LocaleToggleButton = memo(({ size }: { size?: ButtonProps["size"] }) => { const { i18n } = useTranslation(); const items: Required<MenuProps>["items"] = Object.keys(i18n.store.data).map((key) => { diff --git a/ui/src/pages/accesses/AccessList.tsx b/ui/src/pages/accesses/AccessList.tsx index df915bcb..e29f357b 100644 --- a/ui/src/pages/accesses/AccessList.tsx +++ b/ui/src/pages/accesses/AccessList.tsx @@ -40,7 +40,7 @@ const AccessList = () => { ellipsis: true, render: (_, record) => { return ( - <Space className="max-w-full truncate" align="center" size={4}> + <Space className="max-w-full truncate" size={4}> <Avatar src={accessProvidersMap.get(record.configType)?.icon} size="small" /> <Typography.Text ellipsis>{t(accessProvidersMap.get(record.configType)?.name ?? "")}</Typography.Text> </Space> diff --git a/ui/src/pages/certificates/CertificateList.tsx b/ui/src/pages/certificates/CertificateList.tsx index aa744fff..5e533675 100644 --- a/ui/src/pages/certificates/CertificateList.tsx +++ b/ui/src/pages/certificates/CertificateList.tsx @@ -152,15 +152,14 @@ const CertificateList = () => { width: 120, render: (_, record) => ( <Space size={0}> - <Tooltip title={t("certificate.action.view")}> - <Button - type="link" - icon={<EyeIcon size={16} />} - onClick={() => { - handleViewClick(record); - }} - /> - </Tooltip> + <CertificateDetailDrawer + data={record} + trigger={ + <Tooltip title={t("certificate.action.view")}> + <Button type="link" icon={<EyeIcon size={16} />} /> + </Tooltip> + } + /> </Space> ), }, @@ -177,10 +176,6 @@ const CertificateList = () => { const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1); const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 10); - const [currentRecord, setCurrentRecord] = useState<CertificateModel>(); - - const [drawerOpen, setDrawerOpen] = useState(false); - const fetchTableData = useCallback(async () => { if (loading) return; setLoading(true); @@ -210,11 +205,6 @@ const CertificateList = () => { fetchTableData(); }, [fetchTableData]); - const handleViewClick = (certificate: CertificateModel) => { - setDrawerOpen(true); - setCurrentRecord(certificate); - }; - return ( <> {NotificationContextHolder} @@ -244,15 +234,6 @@ const CertificateList = () => { rowKey={(record: CertificateModel) => record.id} scroll={{ x: "max(100%, 960px)" }} /> - - <CertificateDetailDrawer - data={currentRecord} - open={drawerOpen} - onClose={() => { - setDrawerOpen(false); - setCurrentRecord(undefined); - }} - /> </> ); }; diff --git a/ui/src/pages/login/Login.tsx b/ui/src/pages/login/Login.tsx index 8ae9b371..ce316109 100644 --- a/ui/src/pages/login/Login.tsx +++ b/ui/src/pages/login/Login.tsx @@ -19,7 +19,7 @@ const Login = () => { password: z.string().min(10, t("login.password.errmsg.invalid")), }); const formRule = createSchemaFieldRule(formSchema); - const [form] = Form.useForm(); + const [form] = Form.useForm<z.infer<typeof formSchema>>(); const [isPending, setIsPending] = useState(false);