mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-03 23:06:36 +08:00
feat(access-logs): 日志明细支持按状态码筛选并折叠次要搜索项,修复首页来源分布无数据
- 修复 PostgreSQL/SQLite 日志库下首页「来源分布」卡片无数据:RegionCounts 对空 节点 ID 误拼 node_id = '' 恒空条件,改为空节点 ID 表示全节点聚合(对齐 CH 语义), 并过滤空白归属地 - /access-logs?tab=list 新增状态码筛选:状态码下拉含常用 2xx/3xx/4xx/5xx 选项, 校验 100-599,非法值返回 400;ClickHouse 与 PostgreSQL/SQLite 日志库均支持 - 搜索框折叠:默认仅显示来源 IP 与状态码,节点 ID/访问域名/请求路径折叠进 「更多筛选」
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { Search } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { ChevronDown, Search } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
@@ -12,7 +18,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import type { SearchDraft } from './access-log-utils';
|
||||
import { PAGE_SIZE_OPTIONS } from './access-log-utils';
|
||||
import { PAGE_SIZE_OPTIONS, STATUS_CODE_OPTIONS } from './access-log-utils';
|
||||
|
||||
interface AccessLogFiltersProps {
|
||||
draft: SearchDraft;
|
||||
@@ -23,6 +29,21 @@ interface AccessLogFiltersProps {
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
function FilterField({
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className='space-y-1.5'>
|
||||
<p className='text-xs font-medium text-muted-foreground'>{label}</p>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AccessLogFilters({
|
||||
draft,
|
||||
pageSize,
|
||||
@@ -31,28 +52,12 @@ export function AccessLogFilters({
|
||||
onSearch,
|
||||
onReset,
|
||||
}: AccessLogFiltersProps) {
|
||||
const [moreOpen, setMoreOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className='space-y-3'>
|
||||
<div className='grid gap-3 md:grid-cols-2 xl:grid-cols-4'>
|
||||
<div className='space-y-1.5'>
|
||||
<p className='text-xs font-medium text-muted-foreground'>节点 ID</p>
|
||||
<div className='relative'>
|
||||
<Search className='absolute left-2.5 top-2.5 size-3.5 text-muted-foreground' />
|
||||
<Input
|
||||
value={draft.nodeId}
|
||||
onChange={(e) =>
|
||||
onDraftChange({ ...draft, nodeId: e.target.value })
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') onSearch();
|
||||
}}
|
||||
placeholder='按 node_id 搜索'
|
||||
className='pl-8 h-9 text-xs'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='space-y-1.5'>
|
||||
<p className='text-xs font-medium text-muted-foreground'>来源 IP</p>
|
||||
<FilterField label='来源 IP'>
|
||||
<Input
|
||||
value={draft.remoteAddr}
|
||||
onChange={(e) =>
|
||||
@@ -64,33 +69,91 @@ export function AccessLogFilters({
|
||||
placeholder='按 IP 搜索'
|
||||
className='h-9 text-xs'
|
||||
/>
|
||||
</div>
|
||||
<div className='space-y-1.5'>
|
||||
<p className='text-xs font-medium text-muted-foreground'>访问域名</p>
|
||||
<Input
|
||||
value={draft.host}
|
||||
onChange={(e) => onDraftChange({ ...draft, host: e.target.value })}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') onSearch();
|
||||
}}
|
||||
placeholder='按域名搜索'
|
||||
className='h-9 text-xs'
|
||||
/>
|
||||
</div>
|
||||
<div className='space-y-1.5'>
|
||||
<p className='text-xs font-medium text-muted-foreground'>请求路径</p>
|
||||
<Input
|
||||
value={draft.path}
|
||||
onChange={(e) => onDraftChange({ ...draft, path: e.target.value })}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') onSearch();
|
||||
}}
|
||||
placeholder='按路径搜索'
|
||||
className='h-9 text-xs'
|
||||
/>
|
||||
</div>
|
||||
</FilterField>
|
||||
<FilterField label='状态码'>
|
||||
<Select
|
||||
value={draft.statusCode}
|
||||
onValueChange={(value) =>
|
||||
onDraftChange({ ...draft, statusCode: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className='h-9 text-xs'>
|
||||
<SelectValue placeholder='全部状态码' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{STATUS_CODE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FilterField>
|
||||
</div>
|
||||
|
||||
<Collapsible open={moreOpen} onOpenChange={setMoreOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='-ml-1 h-8 gap-1 px-1 text-xs text-muted-foreground'
|
||||
>
|
||||
更多筛选
|
||||
<ChevronDown
|
||||
className={`size-3.5 transition-transform ${
|
||||
moreOpen ? 'rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className='grid gap-3 pt-3 md:grid-cols-2 xl:grid-cols-3'>
|
||||
<FilterField label='节点 ID'>
|
||||
<div className='relative'>
|
||||
<Search className='absolute left-2.5 top-2.5 size-3.5 text-muted-foreground' />
|
||||
<Input
|
||||
value={draft.nodeId}
|
||||
onChange={(e) =>
|
||||
onDraftChange({ ...draft, nodeId: e.target.value })
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') onSearch();
|
||||
}}
|
||||
placeholder='按 node_id 搜索'
|
||||
className='pl-8 h-9 text-xs'
|
||||
/>
|
||||
</div>
|
||||
</FilterField>
|
||||
<FilterField label='访问域名'>
|
||||
<Input
|
||||
value={draft.host}
|
||||
onChange={(e) =>
|
||||
onDraftChange({ ...draft, host: e.target.value })
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') onSearch();
|
||||
}}
|
||||
placeholder='按域名搜索'
|
||||
className='h-9 text-xs'
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label='请求路径'>
|
||||
<Input
|
||||
value={draft.path}
|
||||
onChange={(e) =>
|
||||
onDraftChange({ ...draft, path: e.target.value })
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') onSearch();
|
||||
}}
|
||||
placeholder='按路径搜索'
|
||||
className='h-9 text-xs'
|
||||
/>
|
||||
</FilterField>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
<div className='flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between'>
|
||||
<div className='space-y-1.5 w-full sm:max-w-[180px]'>
|
||||
<p className='text-xs font-medium text-muted-foreground'>每页条数</p>
|
||||
|
||||
@@ -5,6 +5,7 @@ export type SearchDraft = {
|
||||
remoteAddr: string;
|
||||
host: string;
|
||||
path: string;
|
||||
statusCode: string;
|
||||
};
|
||||
|
||||
export type OverviewRangeHours = 24 | 168 | 360 | 720;
|
||||
@@ -14,6 +15,23 @@ export type RateLimitRangeHours = 24 | 72;
|
||||
|
||||
export const PAGE_SIZE_OPTIONS = [20, 50, 100, 200];
|
||||
|
||||
/** 状态码快捷筛选项(空值表示全部,由 Select placeholder 兜底)。 */
|
||||
export const STATUS_CODE_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: '200', label: '200 OK' },
|
||||
{ value: '301', label: '301 永久重定向' },
|
||||
{ value: '302', label: '302 临时重定向' },
|
||||
{ value: '304', label: '304 未修改' },
|
||||
{ value: '400', label: '400 请求错误' },
|
||||
{ value: '401', label: '401 未授权' },
|
||||
{ value: '403', label: '403 禁止访问' },
|
||||
{ value: '404', label: '404 未找到' },
|
||||
{ value: '429', label: '429 请求过多' },
|
||||
{ value: '500', label: '500 服务器错误' },
|
||||
{ value: '502', label: '502 网关错误' },
|
||||
{ value: '503', label: '503 服务不可用' },
|
||||
{ value: '504', label: '504 网关超时' },
|
||||
];
|
||||
|
||||
export const OVERVIEW_RANGE_OPTIONS: {
|
||||
value: OverviewRangeHours;
|
||||
label: string;
|
||||
|
||||
@@ -33,6 +33,7 @@ const emptyDraft: SearchDraft = {
|
||||
remoteAddr: '',
|
||||
host: '',
|
||||
path: '',
|
||||
statusCode: '',
|
||||
};
|
||||
|
||||
function resolveTab(value: string | null): AccessLogTab {
|
||||
@@ -111,6 +112,9 @@ function AccessLogsPageContent() {
|
||||
remote_addr: filters.remoteAddr || undefined,
|
||||
host: filters.host || undefined,
|
||||
path: filters.path || undefined,
|
||||
status_code: filters.statusCode
|
||||
? Number.parseInt(filters.statusCode, 10)
|
||||
: undefined,
|
||||
p: page,
|
||||
page_size: pageSize,
|
||||
sort_by: detailSortState.sortBy,
|
||||
@@ -161,6 +165,7 @@ function AccessLogsPageContent() {
|
||||
remoteAddr: draft.remoteAddr.trim(),
|
||||
host: draft.host.trim(),
|
||||
path: draft.path.trim(),
|
||||
statusCode: draft.statusCode.trim(),
|
||||
});
|
||||
setPage(0);
|
||||
}, [draft]);
|
||||
|
||||
@@ -630,6 +630,7 @@ export interface AccessLogFilters {
|
||||
remote_addr?: string;
|
||||
host?: string;
|
||||
path?: string;
|
||||
status_code?: number;
|
||||
p?: number;
|
||||
page_size?: number;
|
||||
sort_by?: string;
|
||||
|
||||
Reference in New Issue
Block a user