chore: format

This commit is contained in:
ryan
2026-08-16 21:39:25 +08:00
parent e5d2e4b6d5
commit 55c1fcd5f9
93 changed files with 544 additions and 439 deletions
@@ -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);
+7 -6
View File
@@ -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)
+3 -1
View File
@@ -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'),
}); });
} }
}) })
+2 -1
View File
@@ -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 {
+4 -5
View File
@@ -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) => (
+9 -10
View File
@@ -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>
); );
} }
+3 -1
View File
@@ -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>
+3 -1
View File
@@ -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>
+6 -2
View File
@@ -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'),
);
}, },
}); });
+4 -5
View File
@@ -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>
+3 -1
View File
@@ -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'),
}); });
} }
}); });
+5 -9
View File
@@ -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(),
+5 -8
View File
@@ -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'>
+1 -3
View File
@@ -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>
); );
} }
+6 -1
View File
@@ -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 },
]; ];
+5 -1
View File
@@ -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);
+1 -3
View File
@@ -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}`);
} }
+9 -4
View File
@@ -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>,
); );
+2 -1
View File
@@ -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>
+5 -1
View File
@@ -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>,
); );
+5 -1
View File
@@ -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