mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
chore: format
This commit is contained in:
@@ -298,9 +298,7 @@ function AddToIPGroupPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className='text-sm text-muted-foreground'>
|
<p className='text-sm text-muted-foreground'>{t('notInAny')}</p>
|
||||||
{t('notInAny')}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ const MAX_CUSTOM_RANGE_MS = 30 * 24 * 3_600_000;
|
|||||||
export function validateCustomTimeRange(
|
export function validateCustomTimeRange(
|
||||||
sinceLocal: string,
|
sinceLocal: string,
|
||||||
untilLocal: string,
|
untilLocal: string,
|
||||||
t?: (key: 'ip.needRange' | 'ip.invalidTime' | 'ip.endAfterStart' | 'ip.maxRange') => string,
|
t?: (
|
||||||
|
key: 'ip.needRange' | 'ip.invalidTime' | 'ip.endAfterStart' | 'ip.maxRange',
|
||||||
|
) => string,
|
||||||
): string | null {
|
): string | null {
|
||||||
if (!sinceLocal.trim() || !untilLocal.trim()) {
|
if (!sinceLocal.trim() || !untilLocal.trim()) {
|
||||||
return t ? t('ip.needRange') : 'needRange';
|
return t ? t('ip.needRange') : 'needRange';
|
||||||
@@ -306,7 +308,9 @@ export function IpTab({
|
|||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<div className='flex flex-wrap items-end gap-3'>
|
<div className='flex flex-wrap items-end gap-3'>
|
||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
<p className='text-xs text-muted-foreground'>{t('ip.start')}</p>
|
<p className='text-xs text-muted-foreground'>
|
||||||
|
{t('ip.start')}
|
||||||
|
</p>
|
||||||
<Input
|
<Input
|
||||||
type='datetime-local'
|
type='datetime-local'
|
||||||
className='h-9 w-52 text-xs'
|
className='h-9 w-52 text-xs'
|
||||||
@@ -364,7 +368,9 @@ export function IpTab({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
<p className='text-xs text-muted-foreground'>{t('ip.pageSize')}</p>
|
<p className='text-xs text-muted-foreground'>
|
||||||
|
{t('ip.pageSize')}
|
||||||
|
</p>
|
||||||
<Select
|
<Select
|
||||||
value={String(pageSize)}
|
value={String(pageSize)}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
|
|||||||
@@ -66,22 +66,25 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
|
|||||||
const [lruEnabled, setLruEnabled] = useState<boolean>(true);
|
const [lruEnabled, setLruEnabled] = useState<boolean>(true);
|
||||||
|
|
||||||
// 获取磁盘缓存状态
|
// 获取磁盘缓存状态
|
||||||
const fetchCacheStatus = useCallback(async (isSilent = false) => {
|
const fetchCacheStatus = useCallback(
|
||||||
if (!isSilent) setLoadingCache(true);
|
async (isSilent = false) => {
|
||||||
try {
|
if (!isSilent) setLoadingCache(true);
|
||||||
const data = await services.adminCache.getCacheStatus();
|
try {
|
||||||
setCacheStatus(data);
|
const data = await services.adminCache.getCacheStatus();
|
||||||
setMaxSizeMB(data.max_size_mb.toString());
|
setCacheStatus(data);
|
||||||
setTtlMinutes(data.ttl_minutes.toString());
|
setMaxSizeMB(data.max_size_mb.toString());
|
||||||
setLruEnabled(data.lru_enabled);
|
setTtlMinutes(data.ttl_minutes.toString());
|
||||||
} catch (err) {
|
setLruEnabled(data.lru_enabled);
|
||||||
toast.error(t('fetchCacheStatusFailed'), {
|
} catch (err) {
|
||||||
description: err instanceof Error ? err.message : t('unknownError'),
|
toast.error(t('fetchCacheStatusFailed'), {
|
||||||
});
|
description: err instanceof Error ? err.message : t('unknownError'),
|
||||||
} finally {
|
});
|
||||||
setLoadingCache(false);
|
} finally {
|
||||||
}
|
setLoadingCache(false);
|
||||||
}, [t]);
|
}
|
||||||
|
},
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
// 保存配置
|
// 保存配置
|
||||||
const handleSaveConfig = async (e: React.FormEvent) => {
|
const handleSaveConfig = async (e: React.FormEvent) => {
|
||||||
|
|||||||
@@ -349,9 +349,7 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {
|
|||||||
{!executingSQL && !sqlResult && !sqlError && (
|
{!executingSQL && !sqlResult && !sqlError && (
|
||||||
<div className='flex flex-col items-center justify-center h-full py-10 text-muted-foreground opacity-60'>
|
<div className='flex flex-col items-center justify-center h-full py-10 text-muted-foreground opacity-60'>
|
||||||
<Terminal className='size-8 mb-2' />
|
<Terminal className='size-8 mb-2' />
|
||||||
<span className='text-xs'>
|
<span className='text-xs'>{t('editorReady')}</span>
|
||||||
{t('editorReady')}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,19 +71,22 @@ export function DatabasePageClient() {
|
|||||||
const [refreshTrigger, setRefreshTrigger] = useState<number>(0);
|
const [refreshTrigger, setRefreshTrigger] = useState<number>(0);
|
||||||
|
|
||||||
// 1. 获取运行概览
|
// 1. 获取运行概览
|
||||||
const fetchOverview = useCallback(async (isSilent = false) => {
|
const fetchOverview = useCallback(
|
||||||
if (!isSilent) setLoadingOverview(true);
|
async (isSilent = false) => {
|
||||||
try {
|
if (!isSilent) setLoadingOverview(true);
|
||||||
const data = await services.dbManage.getOverview();
|
try {
|
||||||
setOverview(data);
|
const data = await services.dbManage.getOverview();
|
||||||
} catch (err) {
|
setOverview(data);
|
||||||
toast.error(t('fetchOverviewFailed'), {
|
} catch (err) {
|
||||||
description: err instanceof Error ? err.message : t('unknownError'),
|
toast.error(t('fetchOverviewFailed'), {
|
||||||
});
|
description: err instanceof Error ? err.message : t('unknownError'),
|
||||||
} finally {
|
});
|
||||||
setLoadingOverview(false);
|
} finally {
|
||||||
}
|
setLoadingOverview(false);
|
||||||
}, [t]);
|
}
|
||||||
|
},
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
// 2. 获取表列表
|
// 2. 获取表列表
|
||||||
const fetchTables = useCallback(async () => {
|
const fetchTables = useCallback(async () => {
|
||||||
|
|||||||
@@ -77,9 +77,7 @@ export function DashboardTab() {
|
|||||||
<span className='font-semibold text-foreground'>34</span>
|
<span className='font-semibold text-foreground'>34</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className='text-[10px] text-muted-foreground'>
|
<p className='text-[10px] text-muted-foreground'>{t('dbStatus')}</p>
|
||||||
{t('dbStatus')}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -478,9 +478,7 @@ export function FileList() {
|
|||||||
<SheetContent className='w-full sm:max-w-md overflow-y-auto p-6'>
|
<SheetContent className='w-full sm:max-w-md overflow-y-auto p-6'>
|
||||||
<SheetHeader className='border-b pb-4 px-0'>
|
<SheetHeader className='border-b pb-4 px-0'>
|
||||||
<SheetTitle>{t('list.detailTitle')}</SheetTitle>
|
<SheetTitle>{t('list.detailTitle')}</SheetTitle>
|
||||||
<SheetDescription>
|
<SheetDescription>{t('list.detailDesc')}</SheetDescription>
|
||||||
{t('list.detailDesc')}
|
|
||||||
</SheetDescription>
|
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
{detailTarget && (
|
{detailTarget && (
|
||||||
@@ -542,8 +540,7 @@ export function FileList() {
|
|||||||
</span>
|
</span>
|
||||||
<span className='col-span-2 text-foreground/90'>
|
<span className='col-span-2 text-foreground/90'>
|
||||||
{formatFileSize(detailTarget.file_size)} (
|
{formatFileSize(detailTarget.file_size)} (
|
||||||
{detailTarget.file_size.toLocaleString()} {t('list.bytes')}
|
{detailTarget.file_size.toLocaleString()} {t('list.bytes')})
|
||||||
)
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -397,8 +397,7 @@ export function AccessAnalytics() {
|
|||||||
{user.nickname
|
{user.nickname
|
||||||
? `(${user.nickname})`
|
? `(${user.nickname})`
|
||||||
: t('noNickname')}{' '}
|
: t('noNickname')}{' '}
|
||||||
|
|
| ID: {user.user_id}
|
||||||
ID: {user.user_id}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -174,13 +174,16 @@ export function AppLogs() {
|
|||||||
}, [logs, autoScroll]);
|
}, [logs, autoScroll]);
|
||||||
|
|
||||||
// ---- Data fetching ---------------------------------------------------
|
// ---- Data fetching ---------------------------------------------------
|
||||||
const fetchLogs = useCallback(async (cursor: number = 0) => {
|
const fetchLogs = useCallback(
|
||||||
try {
|
async (cursor: number = 0) => {
|
||||||
return await services.adminLog.getLogs(cursor);
|
try {
|
||||||
} catch (err) {
|
return await services.adminLog.getLogs(cursor);
|
||||||
throw err instanceof Error ? err : new Error(t('fetchFailed'));
|
} catch (err) {
|
||||||
}
|
throw err instanceof Error ? err : new Error(t('fetchFailed'));
|
||||||
}, [t]);
|
}
|
||||||
|
},
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
const loadHistory = useCallback(
|
const loadHistory = useCallback(
|
||||||
async (cursor: number = 0) => {
|
async (cursor: number = 0) => {
|
||||||
|
|||||||
@@ -315,7 +315,10 @@ export function EventsTab() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{eventsQuery.isLoading ? (
|
{eventsQuery.isLoading ? (
|
||||||
<LoadingStateWithBorder icon={Layers} description={t('loadingEvents')} />
|
<LoadingStateWithBorder
|
||||||
|
icon={Layers}
|
||||||
|
description={t('loadingEvents')}
|
||||||
|
/>
|
||||||
) : eventsQuery.isError ? (
|
) : eventsQuery.isError ? (
|
||||||
<div className='p-8 border border-dashed rounded-xl bg-card'>
|
<div className='p-8 border border-dashed rounded-xl bg-card'>
|
||||||
<ErrorInline
|
<ErrorInline
|
||||||
@@ -325,10 +328,7 @@ export function EventsTab() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (eventsQuery.data ?? []).length === 0 ? (
|
) : (eventsQuery.data ?? []).length === 0 ? (
|
||||||
<EmptyStateWithBorder
|
<EmptyStateWithBorder icon={Layers} description={t('noEvents')} />
|
||||||
icon={Layers}
|
|
||||||
description={t('noEvents')}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div className='border border-dashed shadow-none rounded-lg overflow-hidden'>
|
<div className='border border-dashed shadow-none rounded-lg overflow-hidden'>
|
||||||
<Table className='w-full caption-bottom text-sm min-w-full'>
|
<Table className='w-full caption-bottom text-sm min-w-full'>
|
||||||
@@ -492,9 +492,7 @@ export function EventsTab() {
|
|||||||
<DialogContent className='sm:max-w-[550px] max-h-[85vh] overflow-y-auto'>
|
<DialogContent className='sm:max-w-[550px] max-h-[85vh] overflow-y-auto'>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('addEventDialogTitle')}</DialogTitle>
|
<DialogTitle>{t('addEventDialogTitle')}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>{t('addEventDialogDesc')}</DialogDescription>
|
||||||
{t('addEventDialogDesc')}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className='space-y-4 py-4'>
|
<div className='space-y-4 py-4'>
|
||||||
@@ -534,7 +532,9 @@ export function EventsTab() {
|
|||||||
|
|
||||||
{newEventType === 'builtin' ? (
|
{newEventType === 'builtin' ? (
|
||||||
<div className='space-y-1.5'>
|
<div className='space-y-1.5'>
|
||||||
<Label className='text-xs font-semibold'>{t('systemEvent')}</Label>
|
<Label className='text-xs font-semibold'>
|
||||||
|
{t('systemEvent')}
|
||||||
|
</Label>
|
||||||
{builtInEventsQuery.isLoading ? (
|
{builtInEventsQuery.isLoading ? (
|
||||||
<div className='flex items-center gap-2 text-xs text-muted-foreground'>
|
<div className='flex items-center gap-2 text-xs text-muted-foreground'>
|
||||||
<Loader2 className='size-3.5 animate-spin' />
|
<Loader2 className='size-3.5 animate-spin' />
|
||||||
@@ -724,7 +724,9 @@ export function EventsTab() {
|
|||||||
|
|
||||||
<div className='flex items-center justify-between p-3 border rounded-lg bg-muted/10'>
|
<div className='flex items-center justify-between p-3 border rounded-lg bg-muted/10'>
|
||||||
<div className='space-y-0.5'>
|
<div className='space-y-0.5'>
|
||||||
<Label className='text-xs font-semibold'>{t('enableStatus')}</Label>
|
<Label className='text-xs font-semibold'>
|
||||||
|
{t('enableStatus')}
|
||||||
|
</Label>
|
||||||
<div className='text-[10px] text-muted-foreground'>
|
<div className='text-[10px] text-muted-foreground'>
|
||||||
{t('enableStatusDesc')}
|
{t('enableStatusDesc')}
|
||||||
</div>
|
</div>
|
||||||
@@ -770,9 +772,7 @@ export function EventsTab() {
|
|||||||
<DialogContent className='sm:max-w-[550px] max-h-[85vh] overflow-y-auto'>
|
<DialogContent className='sm:max-w-[550px] max-h-[85vh] overflow-y-auto'>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('editEventDialogTitle')}</DialogTitle>
|
<DialogTitle>{t('editEventDialogTitle')}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>{t('editEventDialogDesc')}</DialogDescription>
|
||||||
{t('editEventDialogDesc')}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{selectedEvent && (
|
{selectedEvent && (
|
||||||
|
|||||||
@@ -271,9 +271,7 @@ export function HistoriesTab() {
|
|||||||
|
|
||||||
{historiesQuery.data && historiesQuery.data.total > 0 && (
|
{historiesQuery.data && historiesQuery.data.total > 0 && (
|
||||||
<div className='flex justify-between items-center text-xs text-muted-foreground'>
|
<div className='flex justify-between items-center text-xs text-muted-foreground'>
|
||||||
<span>
|
<span>{t('totalRecords', { total: historiesQuery.data.total })}</span>
|
||||||
{t('totalRecords', { total: historiesQuery.data.total })}
|
|
||||||
</span>
|
|
||||||
<div className='flex items-center gap-1.5'>
|
<div className='flex items-center gap-1.5'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
|
|||||||
@@ -335,9 +335,7 @@ export function SettingsTab() {
|
|||||||
className='size-8 mb-2 opacity-30 animate-spin'
|
className='size-8 mb-2 opacity-30 animate-spin'
|
||||||
style={{ animationDuration: '3s' }}
|
style={{ animationDuration: '3s' }}
|
||||||
/>
|
/>
|
||||||
<span className='text-xs font-medium'>
|
<span className='text-xs font-medium'>{t('noCustomChannels')}</span>
|
||||||
{t('noCustomChannels')}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='border border-dashed shadow-none rounded-lg overflow-hidden'>
|
<div className='border border-dashed shadow-none rounded-lg overflow-hidden'>
|
||||||
@@ -464,7 +462,9 @@ export function SettingsTab() {
|
|||||||
|
|
||||||
<div className='space-y-4 py-4'>
|
<div className='space-y-4 py-4'>
|
||||||
<div className='space-y-1.5'>
|
<div className='space-y-1.5'>
|
||||||
<Label className='text-xs font-semibold'>{t('channelName')}</Label>
|
<Label className='text-xs font-semibold'>
|
||||||
|
{t('channelName')}
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
type='text'
|
type='text'
|
||||||
placeholder={t('channelNamePlaceholder')}
|
placeholder={t('channelNamePlaceholder')}
|
||||||
@@ -479,7 +479,9 @@ export function SettingsTab() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='space-y-1.5'>
|
<div className='space-y-1.5'>
|
||||||
<Label className='text-xs font-semibold'>{t('channelRemark')}</Label>
|
<Label className='text-xs font-semibold'>
|
||||||
|
{t('channelRemark')}
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
type='text'
|
type='text'
|
||||||
placeholder={t('channelRemarkPlaceholder')}
|
placeholder={t('channelRemarkPlaceholder')}
|
||||||
@@ -490,7 +492,9 @@ export function SettingsTab() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='space-y-1.5'>
|
<div className='space-y-1.5'>
|
||||||
<Label className='text-xs font-semibold'>{t('channelType')}</Label>
|
<Label className='text-xs font-semibold'>
|
||||||
|
{t('channelType')}
|
||||||
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={channelType}
|
value={channelType}
|
||||||
onValueChange={handleChannelTypeChange}
|
onValueChange={handleChannelTypeChange}
|
||||||
|
|||||||
@@ -201,9 +201,7 @@ export function OpenFlareOpsSettings() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (optionsQuery.isLoading) {
|
if (optionsQuery.isLoading) {
|
||||||
return (
|
return <LoadingStateWithBorder icon={Server} description={t('loading')} />;
|
||||||
<LoadingStateWithBorder icon={Server} description={t('loading')} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (optionsQuery.isError) {
|
if (optionsQuery.isError) {
|
||||||
@@ -406,9 +404,7 @@ export function OpenFlareOpsSettings() {
|
|||||||
<Card className='border-dashed shadow-none'>
|
<Card className='border-dashed shadow-none'>
|
||||||
<CardHeader className='flex flex-row items-center justify-between gap-4'>
|
<CardHeader className='flex flex-row items-center justify-between gap-4'>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className='text-base'>
|
<CardTitle className='text-base'>{t('discovery.title')}</CardTitle>
|
||||||
{t('discovery.title')}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>{t('discovery.description')}</CardDescription>
|
<CardDescription>{t('discovery.description')}</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
|
|||||||
@@ -60,38 +60,41 @@ export function SystemStatusManager() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 获取状态数据
|
// 获取状态数据
|
||||||
const fetchStatus = useCallback(async (isSilent = false) => {
|
const fetchStatus = useCallback(
|
||||||
if (!isSilent) setWavelet(true);
|
async (isSilent = false) => {
|
||||||
try {
|
if (!isSilent) setWavelet(true);
|
||||||
const data = await services.adminStatus.getSystemStatus();
|
try {
|
||||||
|
const data = await services.adminStatus.getSystemStatus();
|
||||||
|
|
||||||
// 检测变化的字段用于微动画效果
|
// 检测变化的字段用于微动画效果
|
||||||
if (prevStatusRef.current) {
|
if (prevStatusRef.current) {
|
||||||
const changes: Record<string, boolean> = {};
|
const changes: Record<string, boolean> = {};
|
||||||
Object.keys(data).forEach((key) => {
|
Object.keys(data).forEach((key) => {
|
||||||
const k = key as keyof SystemStatus;
|
const k = key as keyof SystemStatus;
|
||||||
if (prevStatusRef.current && prevStatusRef.current[k] !== data[k]) {
|
if (prevStatusRef.current && prevStatusRef.current[k] !== data[k]) {
|
||||||
changes[k] = true;
|
changes[k] = true;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
setChangedFields(changes);
|
||||||
|
// 1秒后清除变化状态
|
||||||
|
setTimeout(() => {
|
||||||
|
setChangedFields({});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(data);
|
||||||
|
prevStatusRef.current = data;
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(t('loadFailed'), {
|
||||||
|
description: err instanceof Error ? err.message : t('unknownError'),
|
||||||
});
|
});
|
||||||
setChangedFields(changes);
|
} finally {
|
||||||
// 1秒后清除变化状态
|
setLoading(false);
|
||||||
setTimeout(() => {
|
setWavelet(false);
|
||||||
setChangedFields({});
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
setStatus(data);
|
[t],
|
||||||
prevStatusRef.current = data;
|
);
|
||||||
} catch (err) {
|
|
||||||
toast.error(t('loadFailed'), {
|
|
||||||
description: err instanceof Error ? err.message : t('unknownError'),
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
setWavelet(false);
|
|
||||||
}
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
// 轮询逻辑
|
// 轮询逻辑
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -256,7 +256,9 @@ export function TaskExecutionsManager() {
|
|||||||
<TableHead className='w-[110px]'>{t('colTrigger')}</TableHead>
|
<TableHead className='w-[110px]'>{t('colTrigger')}</TableHead>
|
||||||
<TableHead className='w-[120px]'>{t('colRetry')}</TableHead>
|
<TableHead className='w-[120px]'>{t('colRetry')}</TableHead>
|
||||||
<TableHead className='w-[120px]'>{t('colDuration')}</TableHead>
|
<TableHead className='w-[120px]'>{t('colDuration')}</TableHead>
|
||||||
<TableHead className='min-w-[220px]'>{t('colResult')}</TableHead>
|
<TableHead className='min-w-[220px]'>
|
||||||
|
{t('colResult')}
|
||||||
|
</TableHead>
|
||||||
<TableHead className='w-[170px]'>{t('colCreatedAt')}</TableHead>
|
<TableHead className='w-[170px]'>{t('colCreatedAt')}</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -376,9 +378,8 @@ export function TaskExecutionsManager() {
|
|||||||
{t('detailTrigger')}
|
{t('detailTrigger')}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-2 text-sm font-medium'>
|
<div className='mt-2 text-sm font-medium'>
|
||||||
{t(
|
{t(TRIGGER_LABELS_KEYS[selectedExecution.triggered_by]) ||
|
||||||
TRIGGER_LABELS_KEYS[selectedExecution.triggered_by],
|
selectedExecution.triggered_by}
|
||||||
) || selectedExecution.triggered_by}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='rounded-lg border p-3'>
|
<div className='rounded-lg border p-3'>
|
||||||
|
|||||||
@@ -165,7 +165,11 @@ function DatePickerWithTime({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CalendarIcon className='mr-1 size-3' />
|
<CalendarIcon className='mr-1 size-3' />
|
||||||
{date ? format(date, 'yyyy-MM-dd') : <span>{t('selectDate')}</span>}
|
{date ? (
|
||||||
|
format(date, 'yyyy-MM-dd')
|
||||||
|
) : (
|
||||||
|
<span>{t('selectDate')}</span>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className='w-auto p-0' align='start'>
|
<PopoverContent className='w-auto p-0' align='start'>
|
||||||
@@ -213,7 +217,9 @@ export function TaskManager() {
|
|||||||
const data = await services.adminTask.getTaskTypes();
|
const data = await services.adminTask.getTaskTypes();
|
||||||
setTaskTypes(data);
|
setTaskTypes(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err : new Error(t('loadTaskTypesFailed')));
|
setError(
|
||||||
|
err instanceof Error ? err : new Error(t('loadTaskTypesFailed')),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,9 @@ export function TaskSchedulesManager() {
|
|||||||
setSchedules(schedulesData || []);
|
setSchedules(schedulesData || []);
|
||||||
setTaskTypes(taskTypesData || []);
|
setTaskTypes(taskTypesData || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err : new Error(t('loadSchedulesFailed')));
|
setError(
|
||||||
|
err instanceof Error ? err : new Error(t('loadSchedulesFailed')),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -304,10 +306,7 @@ export function TaskSchedulesManager() {
|
|||||||
description={t('loadingSchedules')}
|
description={t('loadingSchedules')}
|
||||||
/>
|
/>
|
||||||
) : schedules.length === 0 ? (
|
) : schedules.length === 0 ? (
|
||||||
<EmptyStateWithBorder
|
<EmptyStateWithBorder icon={Clock} description={t('noSchedules')} />
|
||||||
icon={Clock}
|
|
||||||
description={t('noSchedules')}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div className='rounded-lg border bg-card'>
|
<div className='rounded-lg border bg-card'>
|
||||||
<Table className='min-w-[800px]'>
|
<Table className='min-w-[800px]'>
|
||||||
@@ -395,9 +394,7 @@ export function TaskSchedulesManager() {
|
|||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{editingSchedule ? t('editSchedule') : t('addSchedule')}
|
{editingSchedule ? t('editSchedule') : t('addSchedule')}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>{t('scheduleDialogDesc')}</DialogDescription>
|
||||||
{t('scheduleDialogDesc')}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className='grid gap-4 py-4'>
|
<div className='grid gap-4 py-4'>
|
||||||
@@ -574,9 +571,7 @@ export function TaskSchedulesManager() {
|
|||||||
<DialogContent className='sm:max-w-[400px]'>
|
<DialogContent className='sm:max-w-[400px]'>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('deleteScheduleTitle')}</DialogTitle>
|
<DialogTitle>{t('deleteScheduleTitle')}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>{t('deleteScheduleDesc')}</DialogDescription>
|
||||||
{t('deleteScheduleDesc')}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter className='gap-2 sm:gap-0'>
|
<DialogFooter className='gap-2 sm:gap-0'>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -218,7 +218,9 @@ export function EditUserModal({
|
|||||||
|
|
||||||
<div className='flex items-center justify-between rounded-lg border border-dashed p-3 bg-muted/10'>
|
<div className='flex items-center justify-between rounded-lg border border-dashed p-3 bg-muted/10'>
|
||||||
<div>
|
<div>
|
||||||
<div className='font-medium text-sm'>{t('adminPermission')}</div>
|
<div className='font-medium text-sm'>
|
||||||
|
{t('adminPermission')}
|
||||||
|
</div>
|
||||||
<div className='text-xs text-muted-foreground'>
|
<div className='text-xs text-muted-foreground'>
|
||||||
{isSelf
|
{isSelf
|
||||||
? t('cannotRevokeSelfAdmin')
|
? t('cannotRevokeSelfAdmin')
|
||||||
@@ -288,7 +290,9 @@ export function EditUserModal({
|
|||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel disabled={saving}>{t('cancel')}</AlertDialogCancel>
|
<AlertDialogCancel disabled={saving}>
|
||||||
|
{t('cancel')}
|
||||||
|
</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -172,7 +172,9 @@ export function UserDetailSheet({
|
|||||||
</h4>
|
</h4>
|
||||||
<div className='rounded-lg border divide-y bg-background/50'>
|
<div className='rounded-lg border divide-y bg-background/50'>
|
||||||
<div className='flex items-center justify-between p-3.5 text-sm'>
|
<div className='flex items-center justify-between p-3.5 text-sm'>
|
||||||
<span className='text-[10px]'>{t('accountStatus')}</span>
|
<span className='text-[10px]'>
|
||||||
|
{t('accountStatus')}
|
||||||
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
variant={
|
variant={
|
||||||
selectedUser.is_active ? 'secondary' : 'outline'
|
selectedUser.is_active ? 'secondary' : 'outline'
|
||||||
|
|||||||
@@ -174,8 +174,7 @@ export function ApplyLogsPageClient() {
|
|||||||
await fetchLogs();
|
await fetchLogs();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(t('clearFailed'), {
|
toast.error(t('clearFailed'), {
|
||||||
description:
|
description: err instanceof Error ? err.message : t('unknownError'),
|
||||||
err instanceof Error ? err.message : t('unknownError'),
|
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setCleaning(false);
|
setCleaning(false);
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ export default function CertificatesPage() {
|
|||||||
<div className='flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between'>
|
<div className='flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<FileKey className='size-5 text-primary' />
|
<FileKey className='size-5 text-primary' />
|
||||||
<h1 className='text-2xl font-semibold tracking-tight'>{t('title')}</h1>
|
<h1 className='text-2xl font-semibold tracking-tight'>
|
||||||
|
{t('title')}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
<Button
|
<Button
|
||||||
@@ -146,7 +148,9 @@ export default function CertificatesPage() {
|
|||||||
|
|
||||||
<Card className='border-dashed shadow-none'>
|
<Card className='border-dashed shadow-none'>
|
||||||
<CardHeader className='pb-3'>
|
<CardHeader className='pb-3'>
|
||||||
<CardTitle className='text-base font-semibold'>{t('listTitle')}</CardTitle>
|
<CardTitle className='text-base font-semibold'>
|
||||||
|
{t('listTitle')}
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>{t('listDesc')}</CardDescription>
|
<CardDescription>{t('listDesc')}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -167,10 +171,7 @@ export default function CertificatesPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : certificates.length === 0 ? (
|
) : certificates.length === 0 ? (
|
||||||
<EmptyStateWithBorder
|
<EmptyStateWithBorder icon={FileKey} description={t('emptyList')} />
|
||||||
icon={FileKey}
|
|
||||||
description={t('emptyList')}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div className='space-y-3'>
|
<div className='space-y-3'>
|
||||||
{certificates.map((certificate) => {
|
{certificates.map((certificate) => {
|
||||||
|
|||||||
@@ -69,9 +69,7 @@ export function GroupDialog({
|
|||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>{group ? t('editTitle') : t('createTitle')}</DialogTitle>
|
||||||
{group ? t('editTitle') : t('createTitle')}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>{t('description')}</DialogDescription>
|
<DialogDescription>{t('description')}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
|
|||||||
@@ -259,10 +259,7 @@ export function SyncTasksPanel() {
|
|||||||
onRetry={() => void executionsQuery.refetch()}
|
onRetry={() => void executionsQuery.refetch()}
|
||||||
/>
|
/>
|
||||||
) : loading && executions.length === 0 ? (
|
) : loading && executions.length === 0 ? (
|
||||||
<LoadingStateWithBorder
|
<LoadingStateWithBorder icon={Activity} description={t('loading')} />
|
||||||
icon={Activity}
|
|
||||||
description={t('loading')}
|
|
||||||
/>
|
|
||||||
) : executions.length === 0 ? (
|
) : executions.length === 0 ? (
|
||||||
<EmptyStateWithBorder
|
<EmptyStateWithBorder
|
||||||
icon={Activity}
|
icon={Activity}
|
||||||
|
|||||||
@@ -150,10 +150,7 @@ export function CloudflareGroupDetailPageClient() {
|
|||||||
if (detailQuery.isLoading)
|
if (detailQuery.isLoading)
|
||||||
return (
|
return (
|
||||||
<div className='w-full py-6 px-1'>
|
<div className='w-full py-6 px-1'>
|
||||||
<LoadingStateWithBorder
|
<LoadingStateWithBorder icon={Cloud} description={t('loadingDetail')} />
|
||||||
icon={Cloud}
|
|
||||||
description={t('loadingDetail')}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
if (detailQuery.isError || !detailQuery.data)
|
if (detailQuery.isError || !detailQuery.data)
|
||||||
|
|||||||
@@ -194,7 +194,9 @@ export default function CloudflarePage() {
|
|||||||
<p className='text-sm text-muted-foreground'>
|
<p className='text-sm text-muted-foreground'>
|
||||||
{t('memberSummary', {
|
{t('memberSummary', {
|
||||||
count: group.member_count,
|
count: group.member_count,
|
||||||
proxy: group.default_proxied ? t('proxyOn') : t('proxyOff'),
|
proxy: group.default_proxied
|
||||||
|
? t('proxyOn')
|
||||||
|
: t('proxyOff'),
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
|
|||||||
@@ -141,9 +141,7 @@ export default function CloudflareSettingsPage() {
|
|||||||
<Card className='border-dashed shadow-none'>
|
<Card className='border-dashed shadow-none'>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className='text-base'>{t('sourceTitle')}</CardTitle>
|
<CardTitle className='text-base'>{t('sourceTitle')}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>{t('sourceDesc')}</CardDescription>
|
||||||
{t('sourceDesc')}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
@@ -254,9 +252,7 @@ export default function CloudflareSettingsPage() {
|
|||||||
<ShieldCheck />
|
<ShieldCheck />
|
||||||
<AlertTitle>{t('statusTitle')}</AlertTitle>
|
<AlertTitle>{t('statusTitle')}</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{connectionQuery.data?.ready
|
{connectionQuery.data?.ready ? t('ready') : t('notReady')}
|
||||||
? t('ready')
|
|
||||||
: t('notReady')}
|
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ export function NodeHealthTable({ nodes }: { nodes: DashboardNodeHealth[] }) {
|
|||||||
<CardHeader className='flex flex-row items-start justify-between gap-4'>
|
<CardHeader className='flex flex-row items-start justify-between gap-4'>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className='text-sm font-semibold'>{t('title')}</CardTitle>
|
<CardTitle className='text-sm font-semibold'>{t('title')}</CardTitle>
|
||||||
<CardDescription className='text-xs'>{t('description')}</CardDescription>
|
<CardDescription className='text-xs'>
|
||||||
|
{t('description')}
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button variant='outline' size='sm' className='h-8 text-xs' asChild>
|
<Button variant='outline' size='sm' className='h-8 text-xs' asChild>
|
||||||
<Link href='/nodes'>{t('openNodes')}</Link>
|
<Link href='/nodes'>{t('openNodes')}</Link>
|
||||||
@@ -80,7 +82,9 @@ export function NodeHealthTable({ nodes }: { nodes: DashboardNodeHealth[] }) {
|
|||||||
<TableHead className='py-2 h-8'>{t('colCpuMem')}</TableHead>
|
<TableHead className='py-2 h-8'>{t('colCpuMem')}</TableHead>
|
||||||
<TableHead className='py-2 h-8'>{t('colReqErr')}</TableHead>
|
<TableHead className='py-2 h-8'>{t('colReqErr')}</TableHead>
|
||||||
<TableHead className='py-2 h-8'>{t('colEvents')}</TableHead>
|
<TableHead className='py-2 h-8'>{t('colEvents')}</TableHead>
|
||||||
<TableHead className='py-2 h-8'>{t('colHeartbeat')}</TableHead>
|
<TableHead className='py-2 h-8'>
|
||||||
|
{t('colHeartbeat')}
|
||||||
|
</TableHead>
|
||||||
<TableHead className='py-2 h-8 text-right'>
|
<TableHead className='py-2 h-8 text-right'>
|
||||||
{t('colActions')}
|
{t('colActions')}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ export function TrafficCapacityTrendChart({
|
|||||||
return (
|
return (
|
||||||
<Card className='border-dashed shadow-none'>
|
<Card className='border-dashed shadow-none'>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className='text-sm font-semibold'>
|
<CardTitle className='text-sm font-semibold'>{t('title')}</CardTitle>
|
||||||
{t('title')}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-6'>
|
<CardContent className='space-y-6'>
|
||||||
<TrendChart
|
<TrendChart
|
||||||
|
|||||||
@@ -75,9 +75,7 @@ export function CleanupDialog({
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
{...form.register('keepCount', { valueAsNumber: true })}
|
{...form.register('keepCount', { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<p className='text-xs text-muted-foreground'>
|
<p className='text-xs text-muted-foreground'>{t('keepHint')}</p>
|
||||||
{t('keepHint')}
|
|
||||||
</p>
|
|
||||||
{form.formState.errors.keepCount ? (
|
{form.formState.errors.keepCount ? (
|
||||||
<p className='text-xs text-destructive'>
|
<p className='text-xs text-destructive'>
|
||||||
{form.formState.errors.keepCount.message}
|
{form.formState.errors.keepCount.message}
|
||||||
@@ -86,7 +84,9 @@ export function CleanupDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel disabled={loading}>{tc('cancel')}</AlertDialogCancel>
|
<AlertDialogCancel disabled={loading}>
|
||||||
|
{tc('cancel')}
|
||||||
|
</AlertDialogCancel>
|
||||||
<Button
|
<Button
|
||||||
variant='destructive'
|
variant='destructive'
|
||||||
onClick={() => void handleConfirm()}
|
onClick={() => void handleConfirm()}
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ export function VersionSnapshotSheet({
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
toast.error(t('snapshotLoadFailed'), {
|
toast.error(t('snapshotLoadFailed'), {
|
||||||
description:
|
description: err instanceof Error ? err.message : t('unknownError'),
|
||||||
err instanceof Error ? err.message : t('unknownError'),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ export default function ConfigVersionsPage() {
|
|||||||
setPreview(previewData);
|
setPreview(previewData);
|
||||||
setDiff(diffData);
|
setDiff(diffData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : t('previewLoadFailed');
|
const message =
|
||||||
|
err instanceof Error ? err.message : t('previewLoadFailed');
|
||||||
setPreviewError(message);
|
setPreviewError(message);
|
||||||
setDiffError(message);
|
setDiffError(message);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ export default function DnsAccountsPage() {
|
|||||||
<div className='flex items-center justify-between gap-3'>
|
<div className='flex items-center justify-between gap-3'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Cloud className='size-5 text-primary' />
|
<Cloud className='size-5 text-primary' />
|
||||||
<h1 className='text-2xl font-semibold tracking-tight'>{t('title')}</h1>
|
<h1 className='text-2xl font-semibold tracking-tight'>
|
||||||
|
{t('title')}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Button
|
<Button
|
||||||
@@ -108,10 +110,7 @@ export default function DnsAccountsPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : accounts.length === 0 ? (
|
) : accounts.length === 0 ? (
|
||||||
<EmptyStateWithBorder
|
<EmptyStateWithBorder icon={Cloud} description={t('emptyList')} />
|
||||||
icon={Cloud}
|
|
||||||
description={t('emptyList')}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div className='space-y-3'>
|
<div className='space-y-3'>
|
||||||
{accounts.map((account) => (
|
{accounts.map((account) => (
|
||||||
|
|||||||
@@ -223,7 +223,9 @@ export default function WafIPGroupsPage() {
|
|||||||
<div className='flex items-center justify-between gap-3'>
|
<div className='flex items-center justify-between gap-3'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Network className='size-5 text-primary' />
|
<Network className='size-5 text-primary' />
|
||||||
<h1 className='text-2xl font-semibold tracking-tight'>{t('title')}</h1>
|
<h1 className='text-2xl font-semibold tracking-tight'>
|
||||||
|
{t('title')}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Button
|
<Button
|
||||||
@@ -265,23 +267,20 @@ export default function WafIPGroupsPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{groupsQuery.isLoading ? (
|
{groupsQuery.isLoading ? (
|
||||||
<LoadingStateWithBorder
|
<LoadingStateWithBorder icon={Network} description={t('loading')} />
|
||||||
icon={Network}
|
|
||||||
description={t('loading')}
|
|
||||||
/>
|
|
||||||
) : groupsQuery.isError ? (
|
) : groupsQuery.isError ? (
|
||||||
<div className='p-8 border border-dashed rounded-lg'>
|
<div className='p-8 border border-dashed rounded-lg'>
|
||||||
<ErrorInline
|
<ErrorInline
|
||||||
message={getErrorMessage(groupsQuery.error, t('operationFailed'))}
|
message={getErrorMessage(
|
||||||
|
groupsQuery.error,
|
||||||
|
t('operationFailed'),
|
||||||
|
)}
|
||||||
onRetry={handleRefresh}
|
onRetry={handleRefresh}
|
||||||
className='justify-center'
|
className='justify-center'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : groups.length === 0 ? (
|
) : groups.length === 0 ? (
|
||||||
<EmptyStateWithBorder
|
<EmptyStateWithBorder icon={Network} description={t('empty')} />
|
||||||
icon={Network}
|
|
||||||
description={t('empty')}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<IPGroupsTable
|
<IPGroupsTable
|
||||||
groups={groups}
|
groups={groups}
|
||||||
|
|||||||
@@ -144,9 +144,7 @@ export function InstallCommand({
|
|||||||
variant='secondary'
|
variant='secondary'
|
||||||
size='sm'
|
size='sm'
|
||||||
className='h-7 text-xs'
|
className='h-7 text-xs'
|
||||||
onClick={() =>
|
onClick={() => void handleCopy(scriptCommand, t('copiedScript'))}
|
||||||
void handleCopy(scriptCommand, t('copiedScript'))
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Copy className='size-3.5 mr-1' />
|
<Copy className='size-3.5 mr-1' />
|
||||||
{t('copyScript')}
|
{t('copyScript')}
|
||||||
@@ -157,9 +155,7 @@ export function InstallCommand({
|
|||||||
variant='secondary'
|
variant='secondary'
|
||||||
size='sm'
|
size='sm'
|
||||||
className='h-7 text-xs'
|
className='h-7 text-xs'
|
||||||
onClick={() =>
|
onClick={() => void handleCopy(dockerCommand, t('copiedDocker'))}
|
||||||
void handleCopy(dockerCommand, t('copiedDocker'))
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Copy className='size-3.5 mr-1' />
|
<Copy className='size-3.5 mr-1' />
|
||||||
{t('copyDocker')}
|
{t('copyDocker')}
|
||||||
@@ -182,28 +178,20 @@ export function InstallCommand({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<Label htmlFor={`server-url-${variant}`}>
|
<Label htmlFor={`server-url-${variant}`}>{t('serverUrl')}</Label>
|
||||||
{t('serverUrl')}
|
|
||||||
</Label>
|
|
||||||
<Input
|
<Input
|
||||||
id={`server-url-${variant}`}
|
id={`server-url-${variant}`}
|
||||||
value={serverUrl}
|
value={serverUrl}
|
||||||
onChange={(event) => setServerUrl(event.target.value)}
|
onChange={(event) => setServerUrl(event.target.value)}
|
||||||
placeholder='https://openflare.example.com'
|
placeholder='https://openflare.example.com'
|
||||||
/>
|
/>
|
||||||
<p className='text-xs text-muted-foreground'>
|
<p className='text-xs text-muted-foreground'>{t('serverUrlHint')}</p>
|
||||||
{t('serverUrlHint')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!node.access_token ? (
|
{!node.access_token ? (
|
||||||
<p className='text-sm text-muted-foreground'>
|
<p className='text-sm text-muted-foreground'>{t('noToken')}</p>
|
||||||
{t('noToken')}
|
|
||||||
</p>
|
|
||||||
) : !normalizedServerUrl ? (
|
) : !normalizedServerUrl ? (
|
||||||
<p className='text-sm text-muted-foreground'>
|
<p className='text-sm text-muted-foreground'>{t('needServerUrl')}</p>
|
||||||
{t('needServerUrl')}
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{scriptCommand && meta.scriptLabel ? (
|
{scriptCommand && meta.scriptLabel ? (
|
||||||
|
|||||||
@@ -207,7 +207,9 @@ export function NodeEditorDialog({
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value='edge_node'>{t('editor.typeEdge')}</SelectItem>
|
<SelectItem value='edge_node'>
|
||||||
|
{t('editor.typeEdge')}
|
||||||
|
</SelectItem>
|
||||||
<SelectItem value='tunnel_relay'>
|
<SelectItem value='tunnel_relay'>
|
||||||
{t('editor.typeRelay')}
|
{t('editor.typeRelay')}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
@@ -314,9 +314,7 @@ function HealthEventTimeline({
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className='text-sm text-muted-foreground'>
|
<p className='text-sm text-muted-foreground'>{t('obs.noEvents')}</p>
|
||||||
{t('obs.noEvents')}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -41,10 +41,7 @@ export function NodeDetailPageClient() {
|
|||||||
if (!nodeId) {
|
if (!nodeId) {
|
||||||
return (
|
return (
|
||||||
<div className='py-6 px-1'>
|
<div className='py-6 px-1'>
|
||||||
<EmptyStateWithBorder
|
<EmptyStateWithBorder icon={Server} description={t('missingId')} />
|
||||||
icon={Server}
|
|
||||||
description={t('missingId')}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,9 @@ export function NodesPageClient() {
|
|||||||
<div className='flex items-center justify-between gap-3'>
|
<div className='flex items-center justify-between gap-3'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Server className='size-5 text-primary' />
|
<Server className='size-5 text-primary' />
|
||||||
<h1 className='text-2xl font-semibold tracking-tight'>{t('title')}</h1>
|
<h1 className='text-2xl font-semibold tracking-tight'>
|
||||||
|
{t('title')}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Button variant='outline' size='sm' className='h-7 text-xs' asChild>
|
<Button variant='outline' size='sm' className='h-7 text-xs' asChild>
|
||||||
|
|||||||
@@ -82,7 +82,10 @@ export function OriginDetailPageClient() {
|
|||||||
if (originQuery.isLoading) {
|
if (originQuery.isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className='py-6 px-1'>
|
<div className='py-6 px-1'>
|
||||||
<LoadingStateWithBorder icon={MapPin} description={t('loadingDetail')} />
|
<LoadingStateWithBorder
|
||||||
|
icon={MapPin}
|
||||||
|
description={t('loadingDetail')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -200,11 +203,15 @@ export function OriginDetailPageClient() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader className='bg-muted/40'>
|
<TableHeader className='bg-muted/40'>
|
||||||
<TableRow className='border-dashed hover:bg-transparent'>
|
<TableRow className='border-dashed hover:bg-transparent'>
|
||||||
<TableHead className='text-xs font-semibold'>{t('domain')}</TableHead>
|
<TableHead className='text-xs font-semibold'>
|
||||||
|
{t('domain')}
|
||||||
|
</TableHead>
|
||||||
<TableHead className='text-xs font-semibold'>
|
<TableHead className='text-xs font-semibold'>
|
||||||
{t('address')}
|
{t('address')}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className='text-xs font-semibold'>{t('status')}</TableHead>
|
<TableHead className='text-xs font-semibold'>
|
||||||
|
{t('status')}
|
||||||
|
</TableHead>
|
||||||
<TableHead className='text-xs font-semibold'>
|
<TableHead className='text-xs font-semibold'>
|
||||||
{t('updatedAt')}
|
{t('updatedAt')}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ export default function OriginsPage() {
|
|||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<MapPin className='size-5 text-primary' />
|
<MapPin className='size-5 text-primary' />
|
||||||
<div>
|
<div>
|
||||||
<h1 className='text-2xl font-semibold tracking-tight'>{t('title')}</h1>
|
<h1 className='text-2xl font-semibold tracking-tight'>
|
||||||
|
{t('title')}
|
||||||
|
</h1>
|
||||||
<p className='text-sm text-muted-foreground'>{t('subtitle')}</p>
|
<p className='text-sm text-muted-foreground'>{t('subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,12 +39,16 @@ export default function OpenFlareDashboardPage() {
|
|||||||
<div className='flex flex-wrap items-center justify-between gap-3'>
|
<div className='flex flex-wrap items-center justify-between gap-3'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<LayoutDashboard className='size-5 text-primary' />
|
<LayoutDashboard className='size-5 text-primary' />
|
||||||
<h1 className='text-2xl font-semibold tracking-tight'>{t('title')}</h1>
|
<h1 className='text-2xl font-semibold tracking-tight'>
|
||||||
|
{t('title')}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2 text-xs text-muted-foreground'>
|
<div className='flex items-center gap-2 text-xs text-muted-foreground'>
|
||||||
{overview?.generated_at ? (
|
{overview?.generated_at ? (
|
||||||
<span>
|
<span>
|
||||||
{t('generatedAt', { time: formatDateTime(overview.generated_at) })}
|
{t('generatedAt', {
|
||||||
|
time: formatDateTime(overview.generated_at),
|
||||||
|
})}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -165,9 +165,7 @@ export function DeploymentUploadDialog({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<UploadCloud className='size-8 text-muted-foreground' />
|
<UploadCloud className='size-8 text-muted-foreground' />
|
||||||
<span className='text-sm font-medium'>
|
<span className='text-sm font-medium'>{t('dropHint')}</span>
|
||||||
{t('dropHint')}
|
|
||||||
</span>
|
|
||||||
<span className='text-xs text-muted-foreground'>
|
<span className='text-xs text-muted-foreground'>
|
||||||
zip、tar.gz、tar.xz、tar.bz2、tar、7z
|
zip、tar.gz、tar.xz、tar.bz2、tar、7z
|
||||||
</span>
|
</span>
|
||||||
@@ -199,9 +197,7 @@ export function DeploymentUploadDialog({
|
|||||||
<div className='rounded-md border bg-muted/20 px-3 py-2 font-mono text-sm'>
|
<div className='rounded-md border bg-muted/20 px-3 py-2 font-mono text-sm'>
|
||||||
{pagesEntryPath(rootDir, entryFile)}
|
{pagesEntryPath(rootDir, entryFile)}
|
||||||
</div>
|
</div>
|
||||||
<FieldDescription>
|
<FieldDescription>{t('entryHint')}</FieldDescription>
|
||||||
{t('entryHint')}
|
|
||||||
</FieldDescription>
|
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
{uploadProgress !== null ? (
|
{uploadProgress !== null ? (
|
||||||
@@ -211,7 +207,9 @@ export function DeploymentUploadDialog({
|
|||||||
{uploadProgress >= 100 ? t('processing') : t('progress')}
|
{uploadProgress >= 100 ? t('processing') : t('progress')}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{uploadProgress >= 100 ? t('pleaseWait') : `${uploadProgress}%`}
|
{uploadProgress >= 100
|
||||||
|
? t('pleaseWait')
|
||||||
|
: `${uploadProgress}%`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={Math.min(uploadProgress, 100)} />
|
<Progress value={Math.min(uploadProgress, 100)} />
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ function createPagesProjectSchema(t: (key: string) => string) {
|
|||||||
entry_file: z.string().trim().min(1, t('form.entryRequired')).max(512),
|
entry_file: z.string().trim().min(1, t('form.entryRequired')).max(512),
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
if (data.spa_fallback_enabled && !data.spa_fallback_path.startsWith('/')) {
|
if (
|
||||||
|
data.spa_fallback_enabled &&
|
||||||
|
!data.spa_fallback_path.startsWith('/')
|
||||||
|
) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
path: ['spa_fallback_path'],
|
path: ['spa_fallback_path'],
|
||||||
|
|||||||
@@ -282,7 +282,9 @@ export function DeploymentHistory({
|
|||||||
setPendingAction(null);
|
setPendingAction(null);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error instanceof Error ? error.message : t('history.activateFailed'));
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : t('history.activateFailed'),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -298,7 +300,9 @@ export function DeploymentHistory({
|
|||||||
setPendingAction(null);
|
setPendingAction(null);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error instanceof Error ? error.message : t('history.deleteFailed'));
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : t('history.deleteFailed'),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -398,9 +402,7 @@ export function DeploymentHistory({
|
|||||||
<Card className='border-dashed shadow-none'>
|
<Card className='border-dashed shadow-none'>
|
||||||
<CardHeader className='pb-3'>
|
<CardHeader className='pb-3'>
|
||||||
<CardTitle className='text-base'>Production</CardTitle>
|
<CardTitle className='text-base'>Production</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>{t('history.productionHint')}</CardDescription>
|
||||||
{t('history.productionHint')}
|
|
||||||
</CardDescription>
|
|
||||||
<CardAction>
|
<CardAction>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
@@ -439,9 +441,7 @@ export function DeploymentHistory({
|
|||||||
<Card className='border-dashed shadow-none'>
|
<Card className='border-dashed shadow-none'>
|
||||||
<CardHeader className='pb-3'>
|
<CardHeader className='pb-3'>
|
||||||
<CardTitle className='text-base'>All deployments</CardTitle>
|
<CardTitle className='text-base'>All deployments</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>{t('history.historyHint')}</CardDescription>
|
||||||
{t('history.historyHint')}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-3'>
|
<CardContent className='space-y-3'>
|
||||||
{allDeployments.length === 0 ? (
|
{allDeployments.length === 0 ? (
|
||||||
|
|||||||
@@ -267,7 +267,8 @@ export function PagesSourceCard({ projectId }: { projectId: number }) {
|
|||||||
toast.success(t('source.actionDone', { action: actionLabel }));
|
toast.success(t('source.actionDone', { action: actionLabel }));
|
||||||
} else {
|
} else {
|
||||||
toast.error(
|
toast.error(
|
||||||
execution.error_message || t('source.actionFailed', { action: actionLabel }),
|
execution.error_message ||
|
||||||
|
t('source.actionFailed', { action: actionLabel }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setActiveAction(null);
|
setActiveAction(null);
|
||||||
@@ -284,7 +285,9 @@ export function PagesSourceCard({ projectId }: { projectId: number }) {
|
|||||||
toast.success(t('source.checkQueued'));
|
toast.success(t('source.checkQueued'));
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error instanceof Error ? error.message : t('source.checkQueueFailed'));
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : t('source.checkQueueFailed'),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -300,7 +303,9 @@ export function PagesSourceCard({ projectId }: { projectId: number }) {
|
|||||||
toast.success(t('source.syncQueued'));
|
toast.success(t('source.syncQueued'));
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error instanceof Error ? error.message : t('source.syncQueueFailed'));
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : t('source.syncQueueFailed'),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -405,9 +410,7 @@ export function PagesSourceCard({ projectId }: { projectId: number }) {
|
|||||||
<div className='flex items-start justify-between gap-3'>
|
<div className='flex items-start justify-between gap-3'>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className='text-base'>{t('source.title')}</CardTitle>
|
<CardTitle className='text-base'>{t('source.title')}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>{t('source.configDesc')}</CardDescription>
|
||||||
{t('source.configDesc')}
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
</div>
|
||||||
{status ? (
|
{status ? (
|
||||||
<Badge variant={status.variant}>{t(status.labelKey)}</Badge>
|
<Badge variant={status.variant}>{t(status.labelKey)}</Badge>
|
||||||
|
|||||||
@@ -136,9 +136,7 @@ export function PagesSourceGitHubFields({
|
|||||||
<FieldLabel htmlFor='pages-github-auto-update'>
|
<FieldLabel htmlFor='pages-github-auto-update'>
|
||||||
{t('autoUpdate')}
|
{t('autoUpdate')}
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
<FieldDescription>
|
<FieldDescription>{t('autoUpdateHint')}</FieldDescription>
|
||||||
{t('autoUpdateHint')}
|
|
||||||
</FieldDescription>
|
|
||||||
</FieldContent>
|
</FieldContent>
|
||||||
<Switch
|
<Switch
|
||||||
id='pages-github-auto-update'
|
id='pages-github-auto-update'
|
||||||
@@ -182,9 +180,7 @@ export function PagesSourceGitHubFields({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Field data-invalid={Boolean(errors.assetName)}>
|
<Field data-invalid={Boolean(errors.assetName)}>
|
||||||
<FieldLabel htmlFor='pages-github-asset'>
|
<FieldLabel htmlFor='pages-github-asset'>{t('assetName')}</FieldLabel>
|
||||||
{t('assetName')}
|
|
||||||
</FieldLabel>
|
|
||||||
<Input
|
<Input
|
||||||
id='pages-github-asset'
|
id='pages-github-asset'
|
||||||
placeholder={defaultAssetName}
|
placeholder={defaultAssetName}
|
||||||
|
|||||||
@@ -67,10 +67,7 @@ export function getProxyRouteConfigSection(
|
|||||||
: 'domains';
|
: 'domains';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateDomain(
|
export function validateDomain(domain: string, t: TranslateFn): string | null {
|
||||||
domain: string,
|
|
||||||
t: TranslateFn,
|
|
||||||
): string | null {
|
|
||||||
const normalized = domain.trim().toLowerCase();
|
const normalized = domain.trim().toLowerCase();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return t('validation.enterDomain');
|
return t('validation.enterDomain');
|
||||||
|
|||||||
@@ -269,8 +269,7 @@ export function ProxyRouteCreateSheet({
|
|||||||
onCreated(route);
|
onCreated(route);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
form.setError('root', {
|
form.setError('root', {
|
||||||
message:
|
message: error instanceof Error ? error.message : t('createFailed'),
|
||||||
error instanceof Error ? error.message : t('createFailed'),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -400,7 +399,9 @@ export function ProxyRouteCreateSheet({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value='none'>{t('pleaseSelect')}</SelectItem>
|
<SelectItem value='none'>
|
||||||
|
{t('pleaseSelect')}
|
||||||
|
</SelectItem>
|
||||||
{tunnelClients.map((tunnel) => (
|
{tunnelClients.map((tunnel) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={tunnel.id}
|
key={tunnel.id}
|
||||||
@@ -415,7 +416,9 @@ export function ProxyRouteCreateSheet({
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormDescription>{t('tunnelForwardHint')}</FormDescription>
|
<FormDescription>
|
||||||
|
{t('tunnelForwardHint')}
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -455,7 +458,9 @@ export function ProxyRouteCreateSheet({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder='127.0.0.1:8080' {...field} />
|
<Input placeholder='127.0.0.1:8080' {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>{t('tunnelAddressHint')}</FormDescription>
|
<FormDescription>
|
||||||
|
{t('tunnelAddressHint')}
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -483,7 +488,9 @@ export function ProxyRouteCreateSheet({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value='none'>{t('pleaseSelect')}</SelectItem>
|
<SelectItem value='none'>
|
||||||
|
{t('pleaseSelect')}
|
||||||
|
</SelectItem>
|
||||||
{pagesProjects.map((project) => (
|
{pagesProjects.map((project) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={project.id}
|
key={project.id}
|
||||||
|
|||||||
@@ -119,22 +119,20 @@ export function ZoneDomainSelector({
|
|||||||
|
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<div className='rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground'>
|
<div className='rounded-lg border border-dashed px-4 py-8 text-center text-sm text-muted-foreground'>
|
||||||
{domains.length === 0 ? (
|
{domains.length === 0
|
||||||
t.rich('noZoneDomains', {
|
? t.rich('noZoneDomains', {
|
||||||
websites: (chunks) => (
|
websites: (chunks) => (
|
||||||
<Link
|
<Link
|
||||||
href='/websites'
|
href='/websites'
|
||||||
className='text-primary underline-offset-4 hover:underline'
|
className='text-primary underline-offset-4 hover:underline'
|
||||||
>
|
>
|
||||||
{chunks}
|
{chunks}
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
) : availableDomains.length === 0 ? (
|
: availableDomains.length === 0
|
||||||
t('noBindableDomains')
|
? t('noBindableDomains')
|
||||||
) : (
|
: t('noMatchingDomains')}
|
||||||
t('noMatchingDomains')
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='max-h-72 space-y-1 overflow-y-auto rounded-lg border p-2'>
|
<div className='max-h-72 space-y-1 overflow-y-auto rounded-lg border p-2'>
|
||||||
|
|||||||
@@ -162,9 +162,7 @@ export function AuthSection({
|
|||||||
<SelectItem value='basic'>Basic Auth</SelectItem>
|
<SelectItem value='basic'>Basic Auth</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormDescription>
|
<FormDescription>{t('powHint')}</FormDescription>
|
||||||
{t('powHint')}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -286,7 +286,9 @@ export function CacheSection({
|
|||||||
<SelectItem value='path_prefix'>
|
<SelectItem value='path_prefix'>
|
||||||
{t('policyPrefix')}
|
{t('policyPrefix')}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value='path_exact'>{t('policyExact')}</SelectItem>
|
<SelectItem value='path_exact'>
|
||||||
|
{t('policyExact')}
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -349,7 +349,9 @@ export function ProxySection({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value='none'>{t('pleaseSelect')}</SelectItem>
|
<SelectItem value='none'>
|
||||||
|
{t('pleaseSelect')}
|
||||||
|
</SelectItem>
|
||||||
{tunnelClients.map((tunnel) => (
|
{tunnelClients.map((tunnel) => (
|
||||||
<SelectItem key={tunnel.id} value={String(tunnel.id)}>
|
<SelectItem key={tunnel.id} value={String(tunnel.id)}>
|
||||||
{tunnel.name} (
|
{tunnel.name} (
|
||||||
@@ -426,7 +428,9 @@ export function ProxySection({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value='none'>{t('pleaseSelect')}</SelectItem>
|
<SelectItem value='none'>
|
||||||
|
{t('pleaseSelect')}
|
||||||
|
</SelectItem>
|
||||||
{pagesProjects.map((project) => (
|
{pagesProjects.map((project) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={project.id}
|
key={project.id}
|
||||||
@@ -454,9 +458,7 @@ export function ProxySection({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder='origin.example.internal' {...field} />
|
<Input placeholder='origin.example.internal' {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>{t('originHostHint')}</FormDescription>
|
||||||
{t('originHostHint')}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -153,7 +153,9 @@ export function RouteHeader({ route }: RouteHeaderProps) {
|
|||||||
<AlertDialogDescription>{t('publishDesc')}</AlertDialogDescription>
|
<AlertDialogDescription>{t('publishDesc')}</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel disabled={publishing}>{tc('cancel')}</AlertDialogCancel>
|
<AlertDialogCancel disabled={publishing}>
|
||||||
|
{tc('cancel')}
|
||||||
|
</AlertDialogCancel>
|
||||||
<Button onClick={() => void handlePublish()} disabled={publishing}>
|
<Button onClick={() => void handlePublish()} disabled={publishing}>
|
||||||
{publishing ? (
|
{publishing ? (
|
||||||
<Loader2 className='size-4 animate-spin' />
|
<Loader2 className='size-4 animate-spin' />
|
||||||
|
|||||||
@@ -116,7 +116,9 @@ export function ProxyRoutesPageClient() {
|
|||||||
<div className='flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between'>
|
<div className='flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Route className='size-5 text-primary' />
|
<Route className='size-5 text-primary' />
|
||||||
<h1 className='text-2xl font-semibold tracking-tight'>{t('title')}</h1>
|
<h1 className='text-2xl font-semibold tracking-tight'>
|
||||||
|
{t('title')}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex flex-wrap gap-2'>
|
||||||
<Button
|
<Button
|
||||||
@@ -143,7 +145,9 @@ export function ProxyRoutesPageClient() {
|
|||||||
<Card className='border-border/40 shadow-sm'>
|
<Card className='border-border/40 shadow-sm'>
|
||||||
<CardHeader className='pb-3'>
|
<CardHeader className='pb-3'>
|
||||||
<div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
|
<div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
|
||||||
<CardTitle className='text-sm font-semibold'>{t('listTitle')}</CardTitle>
|
<CardTitle className='text-sm font-semibold'>
|
||||||
|
{t('listTitle')}
|
||||||
|
</CardTitle>
|
||||||
<Input
|
<Input
|
||||||
value={keyword}
|
value={keyword}
|
||||||
onChange={(event) => setKeyword(event.target.value)}
|
onChange={(event) => setKeyword(event.target.value)}
|
||||||
@@ -261,7 +265,9 @@ export function ProxyRoutesPageClient() {
|
|||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel disabled={deleting}>{tc('cancel')}</AlertDialogCancel>
|
<AlertDialogCancel disabled={deleting}>
|
||||||
|
{tc('cancel')}
|
||||||
|
</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
className='bg-destructive text-white hover:bg-destructive/90'
|
className='bg-destructive text-white hover:bg-destructive/90'
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ function mapOptionsToFields(
|
|||||||
|
|
||||||
function validateFields(
|
function validateFields(
|
||||||
fields: RateLimitFields,
|
fields: RateLimitFields,
|
||||||
t: (key: 'config.invalidConn' | 'config.invalidRate' | 'config.invalidReq') => string,
|
t: (
|
||||||
|
key: 'config.invalidConn' | 'config.invalidRate' | 'config.invalidReq',
|
||||||
|
) => string,
|
||||||
) {
|
) {
|
||||||
for (const key of [KEY_CONN_PER_SERVER, KEY_CONN_PER_IP] as const) {
|
for (const key of [KEY_CONN_PER_SERVER, KEY_CONN_PER_IP] as const) {
|
||||||
const raw = fields[key].trim();
|
const raw = fields[key].trim();
|
||||||
@@ -155,7 +157,9 @@ export function ConfigTab() {
|
|||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
toast.error(error instanceof Error ? error.message : t('config.saveFailed'));
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : t('config.saveFailed'),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ function RateLimitsPageContent() {
|
|||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Gauge className='size-5 text-primary' />
|
<Gauge className='size-5 text-primary' />
|
||||||
<div>
|
<div>
|
||||||
<h1 className='text-2xl font-semibold tracking-tight'>{t('title')}</h1>
|
<h1 className='text-2xl font-semibold tracking-tight'>
|
||||||
|
{t('title')}
|
||||||
|
</h1>
|
||||||
<p className='text-sm text-muted-foreground'>{t('subtitle')}</p>
|
<p className='text-sm text-muted-foreground'>{t('subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,10 +118,7 @@ export default function RateLimitsPage() {
|
|||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<div className='py-6 px-1'>
|
<div className='py-6 px-1'>
|
||||||
<LoadingStateWithBorder
|
<LoadingStateWithBorder icon={Gauge} description={t('loadingPage')} />
|
||||||
icon={Gauge}
|
|
||||||
description={t('loadingPage')}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -116,7 +116,9 @@ export function ErrorPageTab({
|
|||||||
<CardContent className='space-y-0 divide-y'>
|
<CardContent className='space-y-0 divide-y'>
|
||||||
<div className='flex items-start justify-between gap-6 pb-5'>
|
<div className='flex items-start justify-between gap-6 pb-5'>
|
||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
<Label className='text-sm font-medium'>{t('enableErrorPage')}</Label>
|
<Label className='text-sm font-medium'>
|
||||||
|
{t('enableErrorPage')}
|
||||||
|
</Label>
|
||||||
<p className='text-sm text-muted-foreground'>
|
<p className='text-sm text-muted-foreground'>
|
||||||
{t('enableErrorPageDesc')}
|
{t('enableErrorPageDesc')}
|
||||||
</p>
|
</p>
|
||||||
@@ -134,7 +136,9 @@ export function ErrorPageTab({
|
|||||||
<div className='flex items-start justify-between gap-6 py-5'>
|
<div className='flex items-start justify-between gap-6 py-5'>
|
||||||
<div className='space-y-1'>
|
<div className='space-y-1'>
|
||||||
<Label className='text-sm font-medium'>{t('getOnly')}</Label>
|
<Label className='text-sm font-medium'>{t('getOnly')}</Label>
|
||||||
<p className='text-sm text-muted-foreground'>{t('getOnlyDesc')}</p>
|
<p className='text-sm text-muted-foreground'>
|
||||||
|
{t('getOnlyDesc')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={fields.getOnly}
|
checked={fields.getOnly}
|
||||||
|
|||||||
@@ -157,7 +157,9 @@ export function ScopeDomainDialog({
|
|||||||
<Badge variant='secondary' className='font-normal'>
|
<Badge variant='secondary' className='font-normal'>
|
||||||
{t('selectedCount', { count: selectedSet.size })}
|
{t('selectedCount', { count: selectedSet.size })}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span>{t('visibleCount', { count: visibleDomains.length })}</span>
|
<span>
|
||||||
|
{t('visibleCount', { count: visibleDomains.length })}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-1'>
|
<div className='flex items-center gap-1'>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -90,7 +90,9 @@ export default function ErrorPagePreviewPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<div className='min-w-0'>
|
<div className='min-w-0'>
|
||||||
<p className='text-sm font-semibold truncate'>{t('errorPreview')}</p>
|
<p className='text-sm font-semibold truncate'>
|
||||||
|
{t('errorPreview')}
|
||||||
|
</p>
|
||||||
<p className='text-[11px] text-muted-foreground font-mono truncate'>
|
<p className='text-[11px] text-muted-foreground font-mono truncate'>
|
||||||
{'{{status}}'}→502 · {'{{host}}'}→example.com · {t('fullscreen')}
|
{'{{status}}'}→502 · {'{{host}}'}→example.com · {t('fullscreen')}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -91,7 +91,9 @@ export default function OfflinePagePreviewPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<div className='min-w-0'>
|
<div className='min-w-0'>
|
||||||
<p className='text-sm font-semibold truncate'>{t('offlinePreview')}</p>
|
<p className='text-sm font-semibold truncate'>
|
||||||
|
{t('offlinePreview')}
|
||||||
|
</p>
|
||||||
<p className='text-[11px] text-muted-foreground font-mono truncate'>
|
<p className='text-[11px] text-muted-foreground font-mono truncate'>
|
||||||
{t('fullscreen')}
|
{t('fullscreen')}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -116,7 +116,9 @@ function ResponsesPageContent() {
|
|||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<MessageSquareText className='size-5 text-primary' />
|
<MessageSquareText className='size-5 text-primary' />
|
||||||
<div>
|
<div>
|
||||||
<h1 className='text-2xl font-semibold tracking-tight'>{t('title')}</h1>
|
<h1 className='text-2xl font-semibold tracking-tight'>
|
||||||
|
{t('title')}
|
||||||
|
</h1>
|
||||||
<p className='text-sm text-muted-foreground'>{t('subtitle')}</p>
|
<p className='text-sm text-muted-foreground'>{t('subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -89,7 +89,9 @@ export function formatIPGroupBanRemaining(
|
|||||||
(expireDate.getTime() - reference.getTime()) / (60 * 1000),
|
(expireDate.getTime() - reference.getTime()) / (60 * 1000),
|
||||||
);
|
);
|
||||||
if (diffMins < 60) {
|
if (diffMins < 60) {
|
||||||
return t ? t('ban.minutesLater', { count: diffMins }) : `${diffMins} 分钟后`;
|
return t
|
||||||
|
? t('ban.minutesLater', { count: diffMins })
|
||||||
|
: `${diffMins} 分钟后`;
|
||||||
}
|
}
|
||||||
const hours = Math.round(diffMins / 60);
|
const hours = Math.round(diffMins / 60);
|
||||||
return t ? t('ban.hoursLater', { count: hours }) : `${hours} 小时后`;
|
return t ? t('ban.hoursLater', { count: hours }) : `${hours} 小时后`;
|
||||||
@@ -105,7 +107,12 @@ export function getIPGroupViewEntries(
|
|||||||
return group.ext_ips.map((item) => ({
|
return group.ext_ips.map((item) => ({
|
||||||
ip: item.ip,
|
ip: item.ip,
|
||||||
capturedAt: item.captured_at,
|
capturedAt: item.captured_at,
|
||||||
banRemaining: formatIPGroupBanRemaining(item.captured_at, ttl, undefined, t),
|
banRemaining: formatIPGroupBanRemaining(
|
||||||
|
item.captured_at,
|
||||||
|
ttl,
|
||||||
|
undefined,
|
||||||
|
t,
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return (group.ip_list ?? []).map((ip) => ({ ip }));
|
return (group.ip_list ?? []).map((ip) => ({ ip }));
|
||||||
|
|||||||
@@ -205,7 +205,11 @@ export function IPGroupDialog({
|
|||||||
const tWaf = useTranslations('waf');
|
const tWaf = useTranslations('waf');
|
||||||
const tCommon = useTranslations('common');
|
const tCommon = useTranslations('common');
|
||||||
const ipGroupSchema = useMemo(
|
const ipGroupSchema = useMemo(
|
||||||
() => createIPGroupSchema((key) => t(key), (key) => tWaf(key)),
|
() =>
|
||||||
|
createIPGroupSchema(
|
||||||
|
(key) => t(key),
|
||||||
|
(key) => tWaf(key),
|
||||||
|
),
|
||||||
[t, tWaf],
|
[t, tWaf],
|
||||||
);
|
);
|
||||||
const form = useForm<IPGroupFormValues>({
|
const form = useForm<IPGroupFormValues>({
|
||||||
@@ -226,8 +230,7 @@ export function IPGroupDialog({
|
|||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
form.setError('root', {
|
form.setError('root', {
|
||||||
message:
|
message: error instanceof Error ? error.message : tWaf('saveFailed'),
|
||||||
error instanceof Error ? error.message : tWaf('saveFailed'),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -96,7 +96,9 @@ export default function WafPage() {
|
|||||||
<div className='flex items-center justify-between gap-3'>
|
<div className='flex items-center justify-between gap-3'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Shield className='size-5 text-primary' />
|
<Shield className='size-5 text-primary' />
|
||||||
<h1 className='text-2xl font-semibold tracking-tight'>{t('title')}</h1>
|
<h1 className='text-2xl font-semibold tracking-tight'>
|
||||||
|
{t('title')}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Button
|
<Button
|
||||||
@@ -137,10 +139,7 @@ export default function WafPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingStateWithBorder
|
<LoadingStateWithBorder icon={Shield} description={t('loading')} />
|
||||||
icon={Shield}
|
|
||||||
description={t('loading')}
|
|
||||||
/>
|
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className='p-8 border border-dashed rounded-lg'>
|
<div className='p-8 border border-dashed rounded-lg'>
|
||||||
<ErrorInline
|
<ErrorInline
|
||||||
@@ -150,10 +149,7 @@ export default function WafPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : groups.length === 0 ? (
|
) : groups.length === 0 ? (
|
||||||
<EmptyStateWithBorder
|
<EmptyStateWithBorder icon={Shield} description={t('empty')} />
|
||||||
icon={Shield}
|
|
||||||
description={t('empty')}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<RuleGroupsTable
|
<RuleGroupsTable
|
||||||
groups={groups}
|
groups={groups}
|
||||||
|
|||||||
@@ -305,9 +305,7 @@ function EditorContent() {
|
|||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>{t('leaveTitle')}</AlertDialogTitle>
|
<AlertDialogTitle>{t('leaveTitle')}</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>{t('leaveDesc')}</AlertDialogDescription>
|
||||||
{t('leaveDesc')}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>{tCommon('cancel')}</AlertDialogCancel>
|
<AlertDialogCancel>{tCommon('cancel')}</AlertDialogCancel>
|
||||||
|
|||||||
@@ -44,10 +44,7 @@ export function ZoneEditorDialog({
|
|||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1, t('rootRequired'))
|
.min(1, t('rootRequired'))
|
||||||
.refine(
|
.refine((value) => !/[*/?#@]|:\/\//.test(value), t('rootInvalid')),
|
||||||
(value) => !/[*/?#@]|:\/\//.test(value),
|
|
||||||
t('rootInvalid'),
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
const form = useForm<Values>({
|
const form = useForm<Values>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
|
|||||||
@@ -110,17 +110,13 @@ export function ZonePageClient() {
|
|||||||
await queryClient.invalidateQueries({ queryKey: zoneQueryKey });
|
await queryClient.invalidateQueries({ queryKey: zoneQueryKey });
|
||||||
window.location.assign('/websites');
|
window.location.assign('/websites');
|
||||||
},
|
},
|
||||||
onError: (error) =>
|
onError: (error) => toast.error(getErrorMessage(error, t('requestFailed'))),
|
||||||
toast.error(getErrorMessage(error, t('requestFailed'))),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return (
|
return (
|
||||||
<div className='py-6 px-1'>
|
<div className='py-6 px-1'>
|
||||||
<LoadingStateWithBorder
|
<LoadingStateWithBorder icon={Globe} description={t('loadingDetail')} />
|
||||||
icon={Globe}
|
|
||||||
description={t('loadingDetail')}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -128,10 +124,7 @@ export function ZonePageClient() {
|
|||||||
if (!Number.isInteger(zoneId) || zoneId <= 0) {
|
if (!Number.isInteger(zoneId) || zoneId <= 0) {
|
||||||
return (
|
return (
|
||||||
<div className='py-6 px-1'>
|
<div className='py-6 px-1'>
|
||||||
<EmptyStateWithBorder
|
<EmptyStateWithBorder icon={Globe} description={t('invalidId')} />
|
||||||
icon={Globe}
|
|
||||||
description={t('invalidId')}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -139,10 +132,7 @@ export function ZonePageClient() {
|
|||||||
if (overviewQuery.isLoading) {
|
if (overviewQuery.isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className='py-6 px-1'>
|
<div className='py-6 px-1'>
|
||||||
<LoadingStateWithBorder
|
<LoadingStateWithBorder icon={Globe} description={t('loadingDetail')} />
|
||||||
icon={Globe}
|
|
||||||
description={t('loadingDetail')}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -159,10 +149,7 @@ export function ZonePageClient() {
|
|||||||
<ArrowLeft className='size-3.5' />
|
<ArrowLeft className='size-3.5' />
|
||||||
{t('back')}
|
{t('back')}
|
||||||
</Button>
|
</Button>
|
||||||
<EmptyStateWithBorder
|
<EmptyStateWithBorder icon={Globe} description={t('notFound')} />
|
||||||
icon={Globe}
|
|
||||||
description={t('notFound')}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -195,7 +182,9 @@ export function ZonePageClient() {
|
|||||||
<h1 className='text-2xl font-semibold tracking-tight'>
|
<h1 className='text-2xl font-semibold tracking-tight'>
|
||||||
{overview.zone.domain}
|
{overview.zone.domain}
|
||||||
</h1>
|
</h1>
|
||||||
<p className='text-sm text-muted-foreground'>{t('detailSubtitle')}</p>
|
<p className='text-sm text-muted-foreground'>
|
||||||
|
{t('detailSubtitle')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -257,13 +246,17 @@ export function ZonePageClient() {
|
|||||||
</p>
|
</p>
|
||||||
<div className='mt-3 grid gap-2 text-sm sm:grid-cols-2'>
|
<div className='mt-3 grid gap-2 text-sm sm:grid-cols-2'>
|
||||||
<div>
|
<div>
|
||||||
<p className='text-xs text-muted-foreground'>{t('currentRoot')}</p>
|
<p className='text-xs text-muted-foreground'>
|
||||||
|
{t('currentRoot')}
|
||||||
|
</p>
|
||||||
<p className='mt-1 font-mono text-[13px] font-medium'>
|
<p className='mt-1 font-mono text-[13px] font-medium'>
|
||||||
{overview.zone.domain}
|
{overview.zone.domain}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className='text-xs text-muted-foreground'>{t('domainCount')}</p>
|
<p className='text-xs text-muted-foreground'>
|
||||||
|
{t('domainCount')}
|
||||||
|
</p>
|
||||||
<p className='mt-1 font-mono text-[13px] font-medium'>
|
<p className='mt-1 font-mono text-[13px] font-medium'>
|
||||||
{overview.domains.length}
|
{overview.domains.length}
|
||||||
</p>
|
</p>
|
||||||
@@ -281,7 +274,9 @@ export function ZonePageClient() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='rounded-lg border border-destructive/30 p-4'>
|
<div className='rounded-lg border border-destructive/30 p-4'>
|
||||||
<p className='text-sm font-semibold text-destructive'>{t('dangerZone')}</p>
|
<p className='text-sm font-semibold text-destructive'>
|
||||||
|
{t('dangerZone')}
|
||||||
|
</p>
|
||||||
<p className='mt-1 text-sm text-muted-foreground'>
|
<p className='mt-1 text-sm text-muted-foreground'>
|
||||||
{t('dangerZoneDesc')}
|
{t('dangerZoneDesc')}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -181,7 +181,10 @@ export function CertificateApplyDialog({
|
|||||||
<div className='grid gap-4 md:grid-cols-2'>
|
<div className='grid gap-4 md:grid-cols-2'>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<Label>{t('name')}</Label>
|
<Label>{t('name')}</Label>
|
||||||
<Input placeholder={t('namePlaceholder')} {...form.register('name')} />
|
<Input
|
||||||
|
placeholder={t('namePlaceholder')}
|
||||||
|
{...form.register('name')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<Label>{t('primaryDomain')}</Label>
|
<Label>{t('primaryDomain')}</Label>
|
||||||
|
|||||||
@@ -115,7 +115,10 @@ export function CertificateEditorDialog({
|
|||||||
<LoadingStateWithBorder description={t('loadingContent')} />
|
<LoadingStateWithBorder description={t('loadingContent')} />
|
||||||
) : certificateQuery.isError ? (
|
) : certificateQuery.isError ? (
|
||||||
<ErrorInline
|
<ErrorInline
|
||||||
message={getErrorMessage(certificateQuery.error, t('requestFailed'))}
|
message={getErrorMessage(
|
||||||
|
certificateQuery.error,
|
||||||
|
t('requestFailed'),
|
||||||
|
)}
|
||||||
className='justify-center'
|
className='justify-center'
|
||||||
/>
|
/>
|
||||||
) : !certificateQuery.data ? (
|
) : !certificateQuery.data ? (
|
||||||
|
|||||||
@@ -268,15 +268,9 @@ export function QuickCreateZoneDomainDialog({
|
|||||||
) : (
|
) : (
|
||||||
<p className='text-xs text-muted-foreground'>
|
<p className='text-xs text-muted-foreground'>
|
||||||
{t.rich('domainExample', {
|
{t.rich('domainExample', {
|
||||||
api: (chunks) => (
|
api: (chunks) => <code className='font-mono'>{chunks}</code>,
|
||||||
<code className='font-mono'>{chunks}</code>
|
fqdn: (chunks) => <code className='font-mono'>{chunks}</code>,
|
||||||
),
|
apex: (chunks) => <code className='font-mono'>{chunks}</code>,
|
||||||
fqdn: (chunks) => (
|
|
||||||
<code className='font-mono'>{chunks}</code>
|
|
||||||
),
|
|
||||||
apex: (chunks) => (
|
|
||||||
<code className='font-mono'>{chunks}</code>
|
|
||||||
),
|
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ export const defaultFileImportValues: FileImportFormValues = {
|
|||||||
export function createAcmeApplySchema(t: TranslateFn) {
|
export function createAcmeApplySchema(t: TranslateFn) {
|
||||||
return z.object({
|
return z.object({
|
||||||
name: z.string().trim().min(1, t('validation.nameRequired')).max(255),
|
name: z.string().trim().min(1, t('validation.nameRequired')).max(255),
|
||||||
primary_domain: z.string().trim().min(1, t('validation.primaryDomainRequired')),
|
primary_domain: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, t('validation.primaryDomainRequired')),
|
||||||
other_domains: z.string(),
|
other_domains: z.string(),
|
||||||
dns_account_id: z.number().min(1, t('validation.dnsAccountRequired')),
|
dns_account_id: z.number().min(1, t('validation.dnsAccountRequired')),
|
||||||
acme_account_id: z.number(),
|
acme_account_id: z.number(),
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ export default function WebsitesPage() {
|
|||||||
<div className='flex items-center justify-between gap-3 pb-2'>
|
<div className='flex items-center justify-between gap-3 pb-2'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Globe className='size-5 text-primary' />
|
<Globe className='size-5 text-primary' />
|
||||||
<h1 className='text-2xl font-semibold tracking-tight'>{t('title')}</h1>
|
<h1 className='text-2xl font-semibold tracking-tight'>
|
||||||
|
{t('title')}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant='secondary'
|
variant='secondary'
|
||||||
@@ -81,10 +83,7 @@ export default function WebsitesPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{zonesQuery.isLoading ? (
|
{zonesQuery.isLoading ? (
|
||||||
<LoadingStateWithBorder
|
<LoadingStateWithBorder icon={Globe} description={t('loadingList')} />
|
||||||
icon={Globe}
|
|
||||||
description={t('loadingList')}
|
|
||||||
/>
|
|
||||||
) : zonesQuery.isError ? (
|
) : zonesQuery.isError ? (
|
||||||
<div className='rounded-lg border border-dashed p-8'>
|
<div className='rounded-lg border border-dashed p-8'>
|
||||||
<ErrorInline
|
<ErrorInline
|
||||||
@@ -96,9 +95,7 @@ export default function WebsitesPage() {
|
|||||||
) : zones.length === 0 ? (
|
) : zones.length === 0 ? (
|
||||||
<EmptyStateWithBorder
|
<EmptyStateWithBorder
|
||||||
icon={Globe}
|
icon={Globe}
|
||||||
description={
|
description={search ? t('emptySearch') : t('emptyList')}
|
||||||
search ? t('emptySearch') : t('emptyList')
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className='overflow-hidden rounded-lg border border-dashed shadow-none'>
|
<div className='overflow-hidden rounded-lg border border-dashed shadow-none'>
|
||||||
|
|||||||
@@ -110,9 +110,7 @@ export function LoginPage() {
|
|||||||
}, 1500);
|
}, 1500);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('OAuth callback error:', error);
|
console.error('OAuth callback error:', error);
|
||||||
toast.error(
|
toast.error(error instanceof Error ? error.message : t('failed'));
|
||||||
error instanceof Error ? error.message : t('failed'),
|
|
||||||
);
|
|
||||||
setIsProcessingCallback(false);
|
setIsProcessingCallback(false);
|
||||||
router.replace('/login');
|
router.replace('/login');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,11 @@ function SidebarNavGroupMenuItem({
|
|||||||
<SidebarMenuSubItem key={item.url}>
|
<SidebarMenuSubItem key={item.url}>
|
||||||
<SidebarMenuSubButton
|
<SidebarMenuSubButton
|
||||||
asChild
|
asChild
|
||||||
isActive={matchesNavPath(pathname, item.url, item.childUrls)}
|
isActive={matchesNavPath(
|
||||||
|
pathname,
|
||||||
|
item.url,
|
||||||
|
item.childUrls,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Link href={item.url} onClick={onNavigate}>
|
<Link href={item.url} onClick={onNavigate}>
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ describe('AppIntlProvider (static export locale resolution)', () => {
|
|||||||
document.cookie = 'NEXT_LOCALE=en; Path=/';
|
document.cookie = 'NEXT_LOCALE=en; Path=/';
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<AppIntlProvider locale="zh-CN" messages={zhCNMessages}>
|
<AppIntlProvider locale='zh-CN' messages={zhCNMessages}>
|
||||||
<Probe />
|
<Probe />
|
||||||
</AppIntlProvider>,
|
</AppIntlProvider>,
|
||||||
);
|
);
|
||||||
@@ -52,7 +52,7 @@ describe('AppIntlProvider (static export locale resolution)', () => {
|
|||||||
document.cookie = 'NEXT_LOCALE=zh-CN; Path=/';
|
document.cookie = 'NEXT_LOCALE=zh-CN; Path=/';
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<AppIntlProvider locale="zh-CN" messages={zhCNMessages}>
|
<AppIntlProvider locale='zh-CN' messages={zhCNMessages}>
|
||||||
<Probe />
|
<Probe />
|
||||||
</AppIntlProvider>,
|
</AppIntlProvider>,
|
||||||
);
|
);
|
||||||
@@ -63,11 +63,11 @@ describe('AppIntlProvider (static export locale resolution)', () => {
|
|||||||
|
|
||||||
it('does not flip without a cookie preference', () => {
|
it('does not flip without a cookie preference', () => {
|
||||||
render(
|
render(
|
||||||
<AppIntlProvider locale="zh-CN" messages={zhCNMessages}>
|
<AppIntlProvider locale='zh-CN' messages={zhCNMessages}>
|
||||||
<Probe />
|
<Probe />
|
||||||
</AppIntlProvider>,
|
</AppIntlProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText(zhSave)).toBeInTheDocument();
|
expect(screen.getByText(zhSave)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,4 +52,4 @@ export function AppIntlProvider({
|
|||||||
{children}
|
{children}
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,12 @@ export const openflareSidebarNav: OpenFlareSidebarNavEntry[] = [
|
|||||||
url: '/config-versions',
|
url: '/config-versions',
|
||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
},
|
},
|
||||||
{ kind: 'item', titleKey: 'accessLogs', url: '/access-logs', icon: ScrollText },
|
{
|
||||||
|
kind: 'item',
|
||||||
|
titleKey: 'accessLogs',
|
||||||
|
url: '/access-logs',
|
||||||
|
icon: ScrollText,
|
||||||
|
},
|
||||||
{ kind: 'item', titleKey: 'performance', url: '/performance', icon: Gauge },
|
{ kind: 'item', titleKey: 'performance', url: '/performance', icon: Gauge },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,11 @@ try {
|
|||||||
fs.mkdirSync(outputDir, { recursive: true });
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(themes, null, 2) + '\n', 'utf-8');
|
fs.writeFileSync(
|
||||||
|
OUTPUT_FILE,
|
||||||
|
JSON.stringify(themes, null, 2) + '\n',
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
console.log(`Successfully generated themes.json at ${OUTPUT_FILE}`);
|
console.log(`Successfully generated themes.json at ${OUTPUT_FILE}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to generate themes.json:', error);
|
console.error('Failed to generate themes.json:', error);
|
||||||
|
|||||||
@@ -29,9 +29,7 @@ function mergeLocale(locale) {
|
|||||||
const suffix = `.${locale}.json`;
|
const suffix = `.${locale}.json`;
|
||||||
for (const name of readdirSync(fragmentsDir).sort()) {
|
for (const name of readdirSync(fragmentsDir).sort()) {
|
||||||
if (!name.endsWith(suffix)) continue;
|
if (!name.endsWith(suffix)) continue;
|
||||||
const frag = JSON.parse(
|
const frag = JSON.parse(readFileSync(resolve(fragmentsDir, name), 'utf8'));
|
||||||
readFileSync(resolve(fragmentsDir, name), 'utf8'),
|
|
||||||
);
|
|
||||||
deepMerge(data, frag);
|
deepMerge(data, frag);
|
||||||
console.log(`merged ${name}`);
|
console.log(`merged ${name}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,13 @@ vi.mock('next/navigation', () => ({
|
|||||||
useSearchParams: () => new URLSearchParams(''),
|
useSearchParams: () => new URLSearchParams(''),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { getUserInfoMock, getPublicConfigMock, getAuthSourcesMock } =
|
const { getUserInfoMock, getPublicConfigMock, getAuthSourcesMock } = vi.hoisted(
|
||||||
vi.hoisted(() => ({
|
() => ({
|
||||||
getUserInfoMock: vi.fn(),
|
getUserInfoMock: vi.fn(),
|
||||||
getPublicConfigMock: vi.fn(),
|
getPublicConfigMock: vi.fn(),
|
||||||
getAuthSourcesMock: vi.fn(),
|
getAuthSourcesMock: vi.fn(),
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
vi.mock('@/lib/services/auth', async (importOriginal) => {
|
vi.mock('@/lib/services/auth', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('@/lib/services/auth')>();
|
const actual = await importOriginal<typeof import('@/lib/services/auth')>();
|
||||||
@@ -127,7 +128,11 @@ describe('a11y(axe-core 结构性规则)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('人机验证小部件(手动模式)无 axe 违规', async () => {
|
it('人机验证小部件(手动模式)无 axe 违规', async () => {
|
||||||
renderWithProviders(<main><CapWidget autoStart={false} onToken={() => {}} /></main>);
|
renderWithProviders(
|
||||||
|
<main>
|
||||||
|
<CapWidget autoStart={false} onToken={() => {}} />
|
||||||
|
</main>,
|
||||||
|
);
|
||||||
|
|
||||||
await screen.findByRole('button');
|
await screen.findByRole('button');
|
||||||
expect(await runAxe(document.body)).toEqual([]);
|
expect(await runAxe(document.body)).toEqual([]);
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ function renderPage() {
|
|||||||
defaultOptions: { queries: { retry: false, gcTime: 0 } },
|
defaultOptions: { queries: { retry: false, gcTime: 0 } },
|
||||||
});
|
});
|
||||||
render(
|
render(
|
||||||
<NextIntlClientProvider locale='zh-CN' messages={zhCN} timeZone='Asia/Shanghai'>
|
<NextIntlClientProvider
|
||||||
|
locale='zh-CN'
|
||||||
|
messages={zhCN}
|
||||||
|
timeZone='Asia/Shanghai'
|
||||||
|
>
|
||||||
<QueryClientProvider client={client}>
|
<QueryClientProvider client={client}>
|
||||||
<CloudflareGroupDetailPageClient />
|
<CloudflareGroupDetailPageClient />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -43,7 +43,11 @@ function renderPage() {
|
|||||||
defaultOptions: { queries: { retry: false, gcTime: 0 } },
|
defaultOptions: { queries: { retry: false, gcTime: 0 } },
|
||||||
});
|
});
|
||||||
render(
|
render(
|
||||||
<NextIntlClientProvider locale='zh-CN' messages={zhCN} timeZone='Asia/Shanghai'>
|
<NextIntlClientProvider
|
||||||
|
locale='zh-CN'
|
||||||
|
messages={zhCN}
|
||||||
|
timeZone='Asia/Shanghai'
|
||||||
|
>
|
||||||
<QueryClientProvider client={client}>
|
<QueryClientProvider client={client}>
|
||||||
<CloudflarePage />
|
<CloudflarePage />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -37,17 +37,21 @@ describe('MemberAddDialog advanced domain picker', () => {
|
|||||||
it('groups domains under top-level zone roots and supports batch select', () => {
|
it('groups domains under top-level zone roots and supports batch select', () => {
|
||||||
const onSubmit = vi.fn();
|
const onSubmit = vi.fn();
|
||||||
render(
|
render(
|
||||||
<NextIntlClientProvider locale='zh-CN' messages={zhCN} timeZone='Asia/Shanghai'>
|
<NextIntlClientProvider
|
||||||
<MemberAddDialog
|
locale='zh-CN'
|
||||||
open
|
messages={zhCN}
|
||||||
onOpenChange={vi.fn()}
|
timeZone='Asia/Shanghai'
|
||||||
domains={domains}
|
>
|
||||||
defaultProxied
|
<MemberAddDialog
|
||||||
pending={false}
|
open
|
||||||
onSubmit={onSubmit}
|
onOpenChange={vi.fn()}
|
||||||
/>
|
domains={domains}
|
||||||
</NextIntlClientProvider>,
|
defaultProxied
|
||||||
);
|
pending={false}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
|
</NextIntlClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
expect(screen.getAllByText('example.com').length).toBeGreaterThan(0);
|
expect(screen.getAllByText('example.com').length).toBeGreaterThan(0);
|
||||||
expect(screen.getAllByText('other.io').length).toBeGreaterThan(0);
|
expect(screen.getAllByText('other.io').length).toBeGreaterThan(0);
|
||||||
@@ -65,17 +69,21 @@ describe('MemberAddDialog advanced domain picker', () => {
|
|||||||
|
|
||||||
it('filters by keyword across domain and zone root', () => {
|
it('filters by keyword across domain and zone root', () => {
|
||||||
render(
|
render(
|
||||||
<NextIntlClientProvider locale='zh-CN' messages={zhCN} timeZone='Asia/Shanghai'>
|
<NextIntlClientProvider
|
||||||
<MemberAddDialog
|
locale='zh-CN'
|
||||||
open
|
messages={zhCN}
|
||||||
onOpenChange={vi.fn()}
|
timeZone='Asia/Shanghai'
|
||||||
domains={domains}
|
>
|
||||||
defaultProxied={false}
|
<MemberAddDialog
|
||||||
pending={false}
|
open
|
||||||
onSubmit={vi.fn()}
|
onOpenChange={vi.fn()}
|
||||||
/>
|
domains={domains}
|
||||||
</NextIntlClientProvider>,
|
defaultProxied={false}
|
||||||
);
|
pending={false}
|
||||||
|
onSubmit={vi.fn()}
|
||||||
|
/>
|
||||||
|
</NextIntlClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
fireEvent.change(screen.getByPlaceholderText('搜索域名或顶级域…'), {
|
fireEvent.change(screen.getByPlaceholderText('搜索域名或顶级域…'), {
|
||||||
target: { value: 'other' },
|
target: { value: 'other' },
|
||||||
@@ -89,17 +97,21 @@ describe('MemberAddDialog advanced domain picker', () => {
|
|||||||
it('selects an entire top-level domain group', () => {
|
it('selects an entire top-level domain group', () => {
|
||||||
const onSubmit = vi.fn();
|
const onSubmit = vi.fn();
|
||||||
render(
|
render(
|
||||||
<NextIntlClientProvider locale='zh-CN' messages={zhCN} timeZone='Asia/Shanghai'>
|
<NextIntlClientProvider
|
||||||
<MemberAddDialog
|
locale='zh-CN'
|
||||||
open
|
messages={zhCN}
|
||||||
onOpenChange={vi.fn()}
|
timeZone='Asia/Shanghai'
|
||||||
domains={domains}
|
>
|
||||||
defaultProxied
|
<MemberAddDialog
|
||||||
pending={false}
|
open
|
||||||
onSubmit={onSubmit}
|
onOpenChange={vi.fn()}
|
||||||
/>
|
domains={domains}
|
||||||
</NextIntlClientProvider>,
|
defaultProxied
|
||||||
);
|
pending={false}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
|
</NextIntlClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
screen.getByRole('checkbox', { name: '选择顶级域 example.com' }),
|
screen.getByRole('checkbox', { name: '选择顶级域 example.com' }),
|
||||||
@@ -115,17 +127,21 @@ describe('MemberAddDialog advanced domain picker', () => {
|
|||||||
it('toggles individual domains within a group', () => {
|
it('toggles individual domains within a group', () => {
|
||||||
const onSubmit = vi.fn();
|
const onSubmit = vi.fn();
|
||||||
render(
|
render(
|
||||||
<NextIntlClientProvider locale='zh-CN' messages={zhCN} timeZone='Asia/Shanghai'>
|
<NextIntlClientProvider
|
||||||
<MemberAddDialog
|
locale='zh-CN'
|
||||||
open
|
messages={zhCN}
|
||||||
onOpenChange={vi.fn()}
|
timeZone='Asia/Shanghai'
|
||||||
domains={domains}
|
>
|
||||||
defaultProxied={false}
|
<MemberAddDialog
|
||||||
pending={false}
|
open
|
||||||
onSubmit={onSubmit}
|
onOpenChange={vi.fn()}
|
||||||
/>
|
domains={domains}
|
||||||
</NextIntlClientProvider>,
|
defaultProxied={false}
|
||||||
);
|
pending={false}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
|
</NextIntlClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
const wwwLabel = screen.getByText('www.example.com').closest('label');
|
const wwwLabel = screen.getByText('www.example.com').closest('label');
|
||||||
expect(wwwLabel).toBeTruthy();
|
expect(wwwLabel).toBeTruthy();
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ function renderPanel() {
|
|||||||
defaultOptions: { queries: { retry: false, gcTime: 0 } },
|
defaultOptions: { queries: { retry: false, gcTime: 0 } },
|
||||||
});
|
});
|
||||||
render(
|
render(
|
||||||
<NextIntlClientProvider locale='zh-CN' messages={zhCN} timeZone='Asia/Shanghai'>
|
<NextIntlClientProvider
|
||||||
|
locale='zh-CN'
|
||||||
|
messages={zhCN}
|
||||||
|
timeZone='Asia/Shanghai'
|
||||||
|
>
|
||||||
<QueryClientProvider client={client}>
|
<QueryClientProvider client={client}>
|
||||||
<SyncTasksPanel />
|
<SyncTasksPanel />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ function renderWithQuery(ui: React.ReactNode) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const rendered = render(
|
const rendered = render(
|
||||||
<NextIntlClientProvider locale='zh-CN' messages={zhCN} timeZone='Asia/Shanghai'>
|
<NextIntlClientProvider
|
||||||
|
locale='zh-CN'
|
||||||
|
messages={zhCN}
|
||||||
|
timeZone='Asia/Shanghai'
|
||||||
|
>
|
||||||
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
|
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
|
||||||
</NextIntlClientProvider>,
|
</NextIntlClientProvider>,
|
||||||
);
|
);
|
||||||
@@ -59,8 +63,14 @@ function renderWithQuery(ui: React.ReactNode) {
|
|||||||
queryClient,
|
queryClient,
|
||||||
rerenderWithQuery: (nextUI: React.ReactNode) =>
|
rerenderWithQuery: (nextUI: React.ReactNode) =>
|
||||||
rendered.rerender(
|
rendered.rerender(
|
||||||
<NextIntlClientProvider locale='zh-CN' messages={zhCN} timeZone='Asia/Shanghai'>
|
<NextIntlClientProvider
|
||||||
<QueryClientProvider client={queryClient}>{nextUI}</QueryClientProvider>
|
locale='zh-CN'
|
||||||
|
messages={zhCN}
|
||||||
|
timeZone='Asia/Shanghai'
|
||||||
|
>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
{nextUI}
|
||||||
|
</QueryClientProvider>
|
||||||
</NextIntlClientProvider>,
|
</NextIntlClientProvider>,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -58,7 +58,11 @@ function renderWithQuery(ui: React.ReactNode) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
return render(
|
return render(
|
||||||
<NextIntlClientProvider locale='zh-CN' messages={zhCN} timeZone='Asia/Shanghai'>
|
<NextIntlClientProvider
|
||||||
|
locale='zh-CN'
|
||||||
|
messages={zhCN}
|
||||||
|
timeZone='Asia/Shanghai'
|
||||||
|
>
|
||||||
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
|
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
|
||||||
</NextIntlClientProvider>,
|
</NextIntlClientProvider>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ class ResizeObserverMock {
|
|||||||
disconnect() {}
|
disconnect() {}
|
||||||
}
|
}
|
||||||
if (typeof globalThis.ResizeObserver === 'undefined') {
|
if (typeof globalThis.ResizeObserver === 'undefined') {
|
||||||
globalThis.ResizeObserver = ResizeObserverMock as unknown as typeof ResizeObserver;
|
globalThis.ResizeObserver =
|
||||||
|
ResizeObserverMock as unknown as typeof ResizeObserver;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,11 @@ describe('WAF route binding order', () => {
|
|||||||
defaultOptions: { queries: { retry: false } },
|
defaultOptions: { queries: { retry: false } },
|
||||||
});
|
});
|
||||||
render(
|
render(
|
||||||
<NextIntlClientProvider locale='zh-CN' messages={zhCN} timeZone='Asia/Shanghai'>
|
<NextIntlClientProvider
|
||||||
|
locale='zh-CN'
|
||||||
|
messages={zhCN}
|
||||||
|
timeZone='Asia/Shanghai'
|
||||||
|
>
|
||||||
<QueryClientProvider client={client}>
|
<QueryClientProvider client={client}>
|
||||||
<WafSection route={{ id: 9 } as ProxyRouteItem} />
|
<WafSection route={{ id: 9 } as ProxyRouteItem} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -157,7 +157,11 @@ describe('WAF rule creation flow', () => {
|
|||||||
defaultOptions: { queries: { retry: false, gcTime: 0 } },
|
defaultOptions: { queries: { retry: false, gcTime: 0 } },
|
||||||
});
|
});
|
||||||
render(
|
render(
|
||||||
<NextIntlClientProvider locale='zh-CN' messages={zhCN} timeZone='Asia/Shanghai'>
|
<NextIntlClientProvider
|
||||||
|
locale='zh-CN'
|
||||||
|
messages={zhCN}
|
||||||
|
timeZone='Asia/Shanghai'
|
||||||
|
>
|
||||||
<QueryClientProvider client={client}>
|
<QueryClientProvider client={client}>
|
||||||
<WafPage />
|
<WafPage />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -32,7 +32,11 @@ function renderPage() {
|
|||||||
defaultOptions: { queries: { retry: false } },
|
defaultOptions: { queries: { retry: false } },
|
||||||
});
|
});
|
||||||
return render(
|
return render(
|
||||||
<NextIntlClientProvider locale='zh-CN' messages={zhCN} timeZone='Asia/Shanghai'>
|
<NextIntlClientProvider
|
||||||
|
locale='zh-CN'
|
||||||
|
messages={zhCN}
|
||||||
|
timeZone='Asia/Shanghai'
|
||||||
|
>
|
||||||
<QueryClientProvider client={client}>
|
<QueryClientProvider client={client}>
|
||||||
<WebsitesPage />
|
<WebsitesPage />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -48,7 +48,11 @@ function renderSelector(ui: React.ReactElement) {
|
|||||||
defaultOptions: { queries: { retry: false } },
|
defaultOptions: { queries: { retry: false } },
|
||||||
});
|
});
|
||||||
return render(
|
return render(
|
||||||
<NextIntlClientProvider locale='zh-CN' messages={zhCN} timeZone='Asia/Shanghai'>
|
<NextIntlClientProvider
|
||||||
|
locale='zh-CN'
|
||||||
|
messages={zhCN}
|
||||||
|
timeZone='Asia/Shanghai'
|
||||||
|
>
|
||||||
<QueryClientProvider client={client}>{ui}</QueryClientProvider>
|
<QueryClientProvider client={client}>{ui}</QueryClientProvider>
|
||||||
</NextIntlClientProvider>,
|
</NextIntlClientProvider>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -70,7 +70,11 @@ function renderPage(zoneId: number, paramZoneId = zoneId) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
return render(
|
return render(
|
||||||
<NextIntlClientProvider locale='zh-CN' messages={zhCN} timeZone='Asia/Shanghai'>
|
<NextIntlClientProvider
|
||||||
|
locale='zh-CN'
|
||||||
|
messages={zhCN}
|
||||||
|
timeZone='Asia/Shanghai'
|
||||||
|
>
|
||||||
<QueryClientProvider client={client}>
|
<QueryClientProvider client={client}>
|
||||||
<ZonePageClient />
|
<ZonePageClient />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
|
|
||||||
package push
|
package push
|
||||||
|
|
||||||
import "slices"
|
import (
|
||||||
|
"slices"
|
||||||
import "sync"
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// KeyURL represents the URL field key
|
// KeyURL represents the URL field key
|
||||||
|
|||||||
Reference in New Issue
Block a user