mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
chore: format
This commit is contained in:
@@ -298,9 +298,7 @@ function AddToIPGroupPanel({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user