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