This commit is contained in:
ryan
2026-07-13 15:08:40 +08:00
parent 08e8eea932
commit 1eff7878a1
441 changed files with 26491 additions and 18405 deletions
@@ -1,21 +1,21 @@
"use client"
'use client';
import {Search} from "lucide-react"
import {Search} from 'lucide-react';
import {Button} from "@/components/ui/button"
import {Input} from "@/components/ui/input"
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select"
import type {AccessLogTab, SearchDraft} from "./access-log-utils"
import {PAGE_SIZE_OPTIONS} from "./access-log-utils"
import {Button} from '@/components/ui/button';
import {Input} from '@/components/ui/input';
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';
import type {AccessLogTab, SearchDraft} from './access-log-utils';
import {PAGE_SIZE_OPTIONS} from './access-log-utils';
interface AccessLogFiltersProps {
tab: AccessLogTab
draft: SearchDraft
pageSize: number
onDraftChange: (draft: SearchDraft) => void
onPageSizeChange: (pageSize: number) => void
onSearch: () => void
onReset: () => void
tab: AccessLogTab;
draft: SearchDraft;
pageSize: number;
onDraftChange: (draft: SearchDraft) => void;
onPageSizeChange: (pageSize: number) => void;
onSearch: () => void;
onReset: () => void;
}
export function AccessLogFilters({
@@ -27,78 +27,84 @@ export function AccessLogFilters({
onSearch,
onReset,
}: AccessLogFiltersProps) {
const showPath = tab === "list" || tab === "folds"
const showPath = tab === 'list' || tab === 'folds';
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" />
<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()
if (e.key === 'Enter') onSearch();
}}
placeholder="按 node_id 搜索"
className="pl-8 h-9 text-xs"
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>
<div className='space-y-1.5'>
<p className='text-xs font-medium text-muted-foreground'>来源 IP</p>
<Input
value={draft.remoteAddr}
onChange={(e) =>
onDraftChange({ ...draft, remoteAddr: e.target.value })
}
onKeyDown={(e) => {
if (e.key === "Enter") onSearch()
if (e.key === 'Enter') onSearch();
}}
placeholder="按 IP 搜索"
className="h-9 text-xs"
placeholder='按 IP 搜索'
className='h-9 text-xs'
/>
</div>
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground">访问域名</p>
<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()
if (e.key === 'Enter') onSearch();
}}
placeholder="按域名搜索"
className="h-9 text-xs"
placeholder='按域名搜索'
className='h-9 text-xs'
/>
</div>
{showPath ? (
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground">请求路径</p>
<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 })}
onChange={(e) =>
onDraftChange({ ...draft, path: e.target.value })
}
onKeyDown={(e) => {
if (e.key === "Enter") onSearch()
if (e.key === 'Enter') onSearch();
}}
placeholder="按路径搜索"
className="h-9 text-xs"
placeholder='按路径搜索'
className='h-9 text-xs'
/>
</div>
) : null}
</div>
<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>
<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>
<Select
value={String(pageSize)}
onValueChange={(value) => onPageSizeChange(Number.parseInt(value, 10))}
onValueChange={(value) =>
onPageSizeChange(Number.parseInt(value, 10))
}
>
<SelectTrigger className="h-9 text-xs">
<SelectTrigger className='h-9 text-xs'>
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -110,15 +116,15 @@ export function AccessLogFilters({
</SelectContent>
</Select>
</div>
<div className="flex gap-2">
<Button size="sm" onClick={onSearch}>
<div className='flex gap-2'>
<Button size='sm' onClick={onSearch}>
筛选
</Button>
<Button variant="outline" size="sm" onClick={onReset}>
<Button variant='outline' size='sm' onClick={onReset}>
清空
</Button>
</div>
</div>
</div>
)
);
}
@@ -1,48 +1,48 @@
export type AccessLogTab = "list" | "folds" | "ip-summary" | "ip-trend"
export type AccessLogTab = 'list' | 'folds' | 'ip-summary' | 'ip-trend';
export type SearchDraft = {
nodeId: string
remoteAddr: string
host: string
path: string
}
nodeId: string;
remoteAddr: string;
host: string;
path: string;
};
export const PAGE_SIZE_OPTIONS = [20, 50, 100, 200]
export const PAGE_SIZE_OPTIONS = [20, 50, 100, 200];
export const DETAIL_SORT_OPTIONS = [
{ value: "logged_at:desc", label: "时间从新到旧" },
{ value: "logged_at:asc", label: "时间从旧到新" },
{ value: "status_code:desc", label: "状态码从高到低" },
{ value: "status_code:asc", label: "状态码从低到高" },
{ value: "remote_addr:asc", label: "IP 正序" },
{ value: "remote_addr:desc", label: "IP 倒序" },
]
{ value: 'logged_at:desc', label: '时间从新到旧' },
{ value: 'logged_at:asc', label: '时间从旧到新' },
{ value: 'status_code:desc', label: '状态码从高到低' },
{ value: 'status_code:asc', label: '状态码从低到高' },
{ value: 'remote_addr:asc', label: 'IP 正序' },
{ value: 'remote_addr:desc', label: 'IP 倒序' },
];
export const FOLD_SORT_OPTIONS = [
{ value: "bucket_started_at:desc", label: "时间桶从新到旧" },
{ value: "bucket_started_at:asc", label: "时间桶从旧到新" },
{ value: "request_count:desc", label: "访问次数从高到低" },
{ value: "request_count:asc", label: "访问次数从低到高" },
]
{ value: 'bucket_started_at:desc', label: '时间桶从新到旧' },
{ value: 'bucket_started_at:asc', label: '时间桶从旧到新' },
{ value: 'request_count:desc', label: '访问次数从高到低' },
{ value: 'request_count:asc', label: '访问次数从低到高' },
];
export const IP_SORT_OPTIONS = [
{ value: "total_requests:desc", label: "总访问次数从高到低" },
{ value: "total_requests:asc", label: "总访问次数从低到高" },
{ value: "recent_requests:desc", label: "3 小时访问次数从高到低" },
{ value: "last_seen_at:desc", label: "最后访问时间从新到旧" },
]
{ value: 'total_requests:desc', label: '总访问次数从高到低' },
{ value: 'total_requests:asc', label: '总访问次数从低到高' },
{ value: 'recent_requests:desc', label: '3 小时访问次数从高到低' },
{ value: 'last_seen_at:desc', label: '最后访问时间从新到旧' },
];
export function parseSortValue(value: string) {
const [sortBy = "logged_at", sortOrder = "desc"] = value.split(":")
const [sortBy = 'logged_at', sortOrder = 'desc'] = value.split(':');
return {
sortBy,
sortOrder: sortOrder === "asc" ? ("asc" as const) : ("desc" as const),
}
sortOrder: sortOrder === 'asc' ? ('asc' as const) : ('desc' as const),
};
}
export function formatCompactNumber(value: number) {
return new Intl.NumberFormat("zh-CN", {
notation: value >= 10000 ? "compact" : "standard",
return new Intl.NumberFormat('zh-CN', {
notation: value >= 10000 ? 'compact' : 'standard',
maximumFractionDigits: 1,
}).format(value)
}
}).format(value);
}
@@ -1,7 +1,7 @@
"use client"
'use client';
import {useState} from "react"
import {Loader2} from "lucide-react"
import {useState} from 'react';
import {Loader2} from 'lucide-react';
import {
AlertDialog,
@@ -11,19 +11,19 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import {Button} from "@/components/ui/button"
import {Input} from "@/components/ui/input"
import {Label} from "@/components/ui/label"
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select"
} from '@/components/ui/alert-dialog';
import {Button} from '@/components/ui/button';
import {Input} from '@/components/ui/input';
import {Label} from '@/components/ui/label';
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';
const CLEANUP_PRESETS = [3, 7, 30]
const CLEANUP_PRESETS = [3, 7, 30];
interface CleanupDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onConfirm: (retentionDays: number) => void
loading: boolean
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: (retentionDays: number) => void;
loading: boolean;
}
export function CleanupDialog({
@@ -32,24 +32,24 @@ export function CleanupDialog({
onConfirm,
loading,
}: CleanupDialogProps) {
const [mode, setMode] = useState<string>("7")
const [customDays, setCustomDays] = useState("14")
const [error, setError] = useState<string | null>(null)
const [mode, setMode] = useState<string>('7');
const [customDays, setCustomDays] = useState('14');
const [error, setError] = useState<string | null>(null);
const handleConfirm = () => {
const retentionDays =
mode === "custom"
mode === 'custom'
? Number.parseInt(customDays, 10)
: Number.parseInt(mode, 10)
: Number.parseInt(mode, 10);
if (!Number.isFinite(retentionDays) || retentionDays < 1) {
setError("保留天数必须大于 0")
return
setError('保留天数必须大于 0');
return;
}
setError(null)
onConfirm(retentionDays)
}
setError(null);
onConfirm(retentionDays);
};
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
@@ -61,8 +61,8 @@ export function CleanupDialog({
</AlertDialogDescription>
</AlertDialogHeader>
<div className="space-y-3">
<div className="space-y-1.5">
<div className='space-y-3'>
<div className='space-y-1.5'>
<Label>保留策略</Label>
<Select value={mode} onValueChange={setMode}>
<SelectTrigger>
@@ -74,17 +74,17 @@ export function CleanupDialog({
保留最近 {days} 天
</SelectItem>
))}
<SelectItem value="custom">自定义天数</SelectItem>
<SelectItem value='custom'>自定义天数</SelectItem>
</SelectContent>
</Select>
</div>
{mode === "custom" ? (
<div className="space-y-1.5">
<Label htmlFor="customDays">自定义保留天数</Label>
{mode === 'custom' ? (
<div className='space-y-1.5'>
<Label htmlFor='customDays'>自定义保留天数</Label>
<Input
id="customDays"
type="number"
id='customDays'
type='number'
min={1}
value={customDays}
onChange={(e) => setCustomDays(e.target.value)}
@@ -93,23 +93,27 @@ export function CleanupDialog({
</div>
) : null}
{error ? <p className="text-xs text-destructive">{error}</p> : null}
{error ? <p className='text-xs text-destructive'>{error}</p> : null}
</div>
<AlertDialogFooter>
<AlertDialogCancel disabled={loading}>取消</AlertDialogCancel>
<Button variant="destructive" onClick={handleConfirm} disabled={loading}>
<Button
variant='destructive'
onClick={handleConfirm}
disabled={loading}
>
{loading ? (
<>
<Loader2 className="size-4 animate-spin mr-1" />
<Loader2 className='size-4 animate-spin mr-1' />
清理中...
</>
) : (
"确认清理"
'确认清理'
)}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
);
}