mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 22:26:38 +08:00
5a8722ff07
接入无 URL 前缀的 zh-CN/en,顶栏与外观设置可切换语言;选择写入 cookie 后刷新生效。
249 lines
7.7 KiB
TypeScript
249 lines
7.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Eye } from 'lucide-react';
|
|
import { useTranslations } from 'next-intl';
|
|
|
|
import { EmptyStateWithBorder } from '@/components/layout/empty';
|
|
import { ErrorInline } from '@/components/layout/error';
|
|
import { LoadingStateWithBorder } from '@/components/layout/loading';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from '@/components/ui/tooltip';
|
|
import type { AccessLogItem, AccessLogList } from '@/lib/services/openflare';
|
|
import { formatDateTime } from '@/lib/utils';
|
|
|
|
import { AccessLogDetailDialog } from './access-log-detail-dialog';
|
|
import {
|
|
cacheOutcomeLabel,
|
|
DETAIL_SORT_OPTIONS,
|
|
resolveCacheOutcome,
|
|
type CacheOutcome,
|
|
} from './access-log-utils';
|
|
|
|
function cacheOutcomeVariant(
|
|
outcome: CacheOutcome,
|
|
): 'default' | 'secondary' | 'outline' | 'destructive' {
|
|
switch (outcome) {
|
|
case 'hit':
|
|
return 'default';
|
|
case 'origin':
|
|
return 'secondary';
|
|
default:
|
|
return 'outline';
|
|
}
|
|
}
|
|
|
|
function PaginationBar({
|
|
page,
|
|
hasMore,
|
|
loading,
|
|
onPrev,
|
|
onNext,
|
|
}: {
|
|
page: number;
|
|
hasMore: boolean;
|
|
loading: boolean;
|
|
onPrev: () => void;
|
|
onNext: () => void;
|
|
}) {
|
|
const t = useTranslations('accessLogs.detail');
|
|
return (
|
|
<div className='flex items-center justify-between px-4 py-3 border-t border-dashed'>
|
|
<p className='text-xs text-muted-foreground'>
|
|
{t('page', { page: page + 1 })}
|
|
</p>
|
|
<div className='flex gap-2'>
|
|
<Button
|
|
variant='outline'
|
|
size='sm'
|
|
disabled={loading || page <= 0}
|
|
onClick={onPrev}
|
|
>
|
|
{t('prev')}
|
|
</Button>
|
|
<Button
|
|
variant='outline'
|
|
size='sm'
|
|
disabled={loading || !hasMore}
|
|
onClick={onNext}
|
|
>
|
|
{t('next')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function DetailTab({
|
|
data,
|
|
loading,
|
|
error,
|
|
page,
|
|
detailSort,
|
|
onDetailSortChange,
|
|
onRetry,
|
|
onPrevPage,
|
|
onNextPage,
|
|
isFetching,
|
|
}: {
|
|
data?: AccessLogList;
|
|
loading: boolean;
|
|
error: Error | null;
|
|
page: number;
|
|
detailSort: string;
|
|
onDetailSortChange: (value: string) => void;
|
|
onRetry: () => void;
|
|
onPrevPage: () => void;
|
|
onNextPage: () => void;
|
|
isFetching: boolean;
|
|
}) {
|
|
const t = useTranslations('accessLogs');
|
|
const [selected, setSelected] = useState<AccessLogItem | null>(null);
|
|
const [detailOpen, setDetailOpen] = useState(false);
|
|
|
|
return (
|
|
<>
|
|
<div className='rounded-lg border border-dashed overflow-hidden bg-background'>
|
|
<div className='flex items-center justify-between px-4 py-3 border-b border-dashed'>
|
|
<p className='text-sm font-medium'>{t('detail.listTitle')}</p>
|
|
<Select value={detailSort} onValueChange={onDetailSortChange}>
|
|
<SelectTrigger className='h-8 w-44 text-xs'>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{DETAIL_SORT_OPTIONS.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{t(option.labelKey)}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
{error ? (
|
|
<div className='p-4'>
|
|
<ErrorInline
|
|
message={error.message || t('loadFailed')}
|
|
onRetry={onRetry}
|
|
/>
|
|
</div>
|
|
) : loading ? (
|
|
<LoadingStateWithBorder />
|
|
) : (data?.items ?? []).length === 0 ? (
|
|
<EmptyStateWithBorder title={t('detail.empty')} />
|
|
) : (
|
|
<Table>
|
|
<TableHeader className='bg-muted/40'>
|
|
<TableRow className='border-dashed hover:bg-transparent'>
|
|
<TableHead className='text-xs'>{t('detail.time')}</TableHead>
|
|
<TableHead className='text-xs'>{t('detail.node')}</TableHead>
|
|
<TableHead className='text-xs'>IP</TableHead>
|
|
<TableHead className='text-xs'>{t('detail.host')}</TableHead>
|
|
<TableHead className='text-xs'>{t('detail.path')}</TableHead>
|
|
<TableHead className='text-xs'>{t('detail.cache')}</TableHead>
|
|
<TableHead className='text-xs'>{t('detail.status')}</TableHead>
|
|
<TableHead className='w-12 text-center text-xs' />
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{(data?.items ?? []).map((item) => {
|
|
const outcome = resolveCacheOutcome(item.cache_status);
|
|
return (
|
|
<TableRow key={item.id} className='border-dashed'>
|
|
<TableCell className='text-xs'>
|
|
{formatDateTime(item.logged_at)}
|
|
</TableCell>
|
|
<TableCell className='text-xs'>
|
|
{item.node_name || item.node_id}
|
|
</TableCell>
|
|
<TableCell className='text-xs'>
|
|
<div className='flex flex-col gap-0.5'>
|
|
<span className='font-mono'>{item.remote_addr}</span>
|
|
{item.region ? (
|
|
<span className='text-[10px] text-muted-foreground'>
|
|
{item.region}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className='text-xs'>{item.host}</TableCell>
|
|
<TableCell className='text-xs max-w-48 truncate'>
|
|
{item.path}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge
|
|
variant={cacheOutcomeVariant(outcome)}
|
|
className='text-[10px]'
|
|
title={item.cache_status || undefined}
|
|
>
|
|
{cacheOutcomeLabel(outcome, (key) => t(key))}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant='outline' className='text-[10px]'>
|
|
{item.status_code}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className='text-center'>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant='ghost'
|
|
size='icon'
|
|
className='h-6 w-6 text-muted-foreground hover:text-foreground'
|
|
onClick={() => {
|
|
setSelected(item);
|
|
setDetailOpen(true);
|
|
}}
|
|
>
|
|
<Eye className='size-3' />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side='top' className='text-xs'>
|
|
{t('detail.view')}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
<PaginationBar
|
|
page={page}
|
|
hasMore={data?.has_more ?? false}
|
|
loading={isFetching}
|
|
onPrev={onPrevPage}
|
|
onNext={onNextPage}
|
|
/>
|
|
</div>
|
|
|
|
<AccessLogDetailDialog
|
|
open={detailOpen}
|
|
item={selected}
|
|
onOpenChange={setDetailOpen}
|
|
/>
|
|
</>
|
|
);
|
|
}
|