mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-12 18:26:36 +08:00
e49078ac3b
- Agent 访问日志上报 user_agent,OpenResty log_format 输出 http_user_agent - of_node_access_logs 新增 user_agent 列并写入 ClickHouse - 访问日志概览新增:设备类型饼图、状态码饼图、浏览器/OS/User-Agent 排行 - 日志明细列表增加 User-Agent 列 - 扩展 UA 解析工具(browser/os/device)并支持 CLI 识别
172 lines
4.9 KiB
TypeScript
172 lines
4.9 KiB
TypeScript
'use client';
|
|
|
|
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 type { AccessLogList } from '@/lib/services/openflare';
|
|
import { formatDateTime } from '@/lib/utils';
|
|
|
|
import { DETAIL_SORT_OPTIONS } from './access-log-utils';
|
|
|
|
function PaginationBar({
|
|
page,
|
|
hasMore,
|
|
loading,
|
|
onPrev,
|
|
onNext,
|
|
}: {
|
|
page: number;
|
|
hasMore: boolean;
|
|
loading: boolean;
|
|
onPrev: () => void;
|
|
onNext: () => void;
|
|
}) {
|
|
return (
|
|
<div className='flex items-center justify-between px-4 py-3 border-t border-dashed'>
|
|
<p className='text-xs text-muted-foreground'>当前第 {page + 1} 页</p>
|
|
<div className='flex gap-2'>
|
|
<Button
|
|
variant='outline'
|
|
size='sm'
|
|
disabled={loading || page <= 0}
|
|
onClick={onPrev}
|
|
>
|
|
上一页
|
|
</Button>
|
|
<Button
|
|
variant='outline'
|
|
size='sm'
|
|
disabled={loading || !hasMore}
|
|
onClick={onNext}
|
|
>
|
|
下一页
|
|
</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;
|
|
}) {
|
|
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'>日志明细</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}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
{error ? (
|
|
<div className='p-4'>
|
|
<ErrorInline
|
|
message={error.message || '加载失败'}
|
|
onRetry={onRetry}
|
|
/>
|
|
</div>
|
|
) : loading ? (
|
|
<LoadingStateWithBorder />
|
|
) : (data?.items ?? []).length === 0 ? (
|
|
<EmptyStateWithBorder title='暂无访问日志' />
|
|
) : (
|
|
<Table>
|
|
<TableHeader className='bg-muted/40'>
|
|
<TableRow className='border-dashed hover:bg-transparent'>
|
|
<TableHead className='text-xs'>时间</TableHead>
|
|
<TableHead className='text-xs'>节点</TableHead>
|
|
<TableHead className='text-xs'>IP</TableHead>
|
|
<TableHead className='text-xs'>域名</TableHead>
|
|
<TableHead className='text-xs'>路径</TableHead>
|
|
<TableHead className='text-xs'>User-Agent</TableHead>
|
|
<TableHead className='text-xs'>状态码</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{(data?.items ?? []).map((item) => (
|
|
<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 font-mono'>
|
|
{item.remote_addr}
|
|
</TableCell>
|
|
<TableCell className='text-xs'>{item.host}</TableCell>
|
|
<TableCell className='text-xs max-w-40 truncate'>
|
|
{item.path}
|
|
</TableCell>
|
|
<TableCell
|
|
className='text-xs max-w-56 truncate'
|
|
title={item.user_agent || undefined}
|
|
>
|
|
{item.user_agent || '—'}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant='outline' className='text-[10px]'>
|
|
{item.status_code}
|
|
</Badge>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
<PaginationBar
|
|
page={page}
|
|
hasMore={data?.has_more ?? false}
|
|
loading={isFetching}
|
|
onPrev={onPrevPage}
|
|
onNext={onNextPage}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|