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

@@ -3,17 +3,18 @@ import { useTranslation } from "react-i18next";
import { Avatar, Card, Col, Empty, Flex, Input, type InputRef, Row, Typography } from "antd";
import Show from "@/components/Show";
import { acmeDns01ProvidersMap } from "@/domain/provider";
import { type ACMEDns01Provider, acmeDns01ProvidersMap } from "@/domain/provider";
export type ACMEDns01ProviderPickerProps = {
className?: string;
style?: React.CSSProperties;
autoFocus?: boolean;
filter?: (record: ACMEDns01Provider) => boolean;
placeholder?: string;
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 [keyword, setKeyword] = useState<string>();
@@ -25,15 +26,23 @@ const ACMEDns01ProviderPicker = ({ className, style, autoFocus, placeholder, onS
}, []);
const providers = useMemo(() => {
return Array.from(acmeDns01ProvidersMap.values()).filter((provider) => {
if (keyword) {
const value = keyword.toLowerCase();
return provider.type.toLowerCase().includes(value) || t(provider.name).toLowerCase().includes(value);
}
return Array.from(acmeDns01ProvidersMap.values())
.filter((provider) => {
if (filter) {
return filter(provider);
}
return true;
});
}, [keyword]);
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 handleProviderTypeSelect = (value: string) => {
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>
<div>
<Show when={provider.builtin}>
<Tag color="grey">{t("access.props.provider.builtin")}</Tag>
<Tag>{t("access.props.provider.builtin")}</Tag>
</Show>
<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 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 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 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>
</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 Show from "@/components/Show";
import { DEPLOYMENT_CATEGORIES, deploymentProvidersMap } from "@/domain/provider";
import { DEPLOYMENT_CATEGORIES, type DeploymentProvider, deploymentProvidersMap } from "@/domain/provider";
export type DeploymentProviderPickerProps = {
className?: string;
style?: React.CSSProperties;
autoFocus?: boolean;
filter?: (record: DeploymentProvider) => boolean;
placeholder?: string;
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 [category, setCategory] = useState<string>(DEPLOYMENT_CATEGORIES.ALL);
@@ -28,6 +29,13 @@ const DeploymentProviderPicker = ({ className, style, autoFocus, placeholder, on
const providers = useMemo(() => {
return Array.from(deploymentProvidersMap.values())
.filter((provider) => {
if (filter) {
return filter(provider);
}
return true;
})
.filter((provider) => {
if (category && category !== DEPLOYMENT_CATEGORIES.ALL) {
return provider.category === category;
@@ -43,7 +51,7 @@ const DeploymentProviderPicker = ({ className, style, autoFocus, placeholder, on
return true;
});
}, [category, keyword]);
}, [filter, category, keyword]);
const handleProviderTypeSelect = (value: string) => {
onSelect?.(value);