fix(frontend): logs/analysis P0 — 职责拆分与布局收口
砍 analysis 明细可视化改「去日志查询」;无数据源 EmptyState;日图标固定近7天。 logs 去掉负 margin、页头常显、筛选/查询分层;level 色阶;Label 保持默认收起。
This commit is contained in:
1 parent
4f75ef8578
commit
f993d5b420
5 files changed
+431
-110
No files matched your search
@@ -4,29 +4,30 @@ import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import type { LogEntry } from '@/api/logs'
|
||||
import { parseIngressAccessLog } from '../ingress-access-log'
|
||||
import { parseIngressAccessLog } from '@/lib/ingress-access-log'
|
||||
import { applyLabelFilter } from '../query-utils'
|
||||
import { HighlightedLogLine } from './HighlightedLogLine'
|
||||
import { LogResultViews, type ResultView } from './LogResultViews'
|
||||
import { QueryEmptyState } from './QueryEmptyState'
|
||||
|
||||
type Bucket = { timestamp: string; count: number }
|
||||
const viewIds: ResultView[] = ['raw', 'table', 'line', 'stream', 'pie']
|
||||
const viewLabelKeys: Record<ResultView, string> = {
|
||||
raw: 'results.viewRaw',
|
||||
table: 'results.viewTable',
|
||||
line: 'results.viewLine',
|
||||
stream: 'results.viewStream',
|
||||
pie: 'results.viewPie',
|
||||
function levelBadgeClass(level: string) {
|
||||
const value = (level || '').toLowerCase()
|
||||
if (value.includes('error') || value === 'err' || value === 'fatal' || value === 'critical') {
|
||||
return 'rounded bg-destructive/15 px-1 text-destructive'
|
||||
}
|
||||
if (value.includes('warn')) return 'rounded bg-warning/15 px-1 text-warning'
|
||||
if (value.includes('info')) return 'rounded bg-info/15 px-1 text-info'
|
||||
if (value.includes('debug') || value.includes('trace')) return 'rounded bg-muted px-1 text-muted-foreground'
|
||||
return 'rounded bg-muted px-1 text-muted-foreground'
|
||||
}
|
||||
|
||||
export function LogResultsPanel({ entries, buckets, start, end, searched, hasSource, pageIndex, hasNextPage, loading, loadingStartedAt, query, terms, onQueryChange, onApplyDomain, onShowContext, onSearch, onNextPage, onPreviousPage }: {
|
||||
entries: LogEntry[]; buckets: Bucket[]; start: string; end: string; searched: boolean; hasSource: boolean
|
||||
// 只保留原始日志视图;表格/折线/流图/饼图等可视化已迁至「日志分析」页(LogDetailSection)
|
||||
|
||||
export function LogResultsPanel({ entries, searched, hasSource, pageIndex, hasNextPage, loading, loadingStartedAt, query, terms, onQueryChange, onApplyDomain, onShowContext, onSearch, onNextPage, onPreviousPage }: {
|
||||
entries: LogEntry[]; searched: boolean; hasSource: boolean
|
||||
pageIndex: number; hasNextPage: boolean; loading: boolean; loadingStartedAt: number | null; query: string; terms: string[]
|
||||
onQueryChange: (query: string) => void; onApplyDomain: (domain: string) => void; onShowContext: (row: LogEntry) => void; onSearch: () => void; onNextPage: () => void; onPreviousPage: () => void
|
||||
}) {
|
||||
const { t } = useTranslation('logs')
|
||||
const [view, setView] = useState<ResultView>('raw')
|
||||
const [domainFilter, setDomainFilter] = useState('')
|
||||
const [clock, setClock] = useState(0)
|
||||
useEffect(() => {
|
||||
@@ -34,10 +35,6 @@ export function LogResultsPanel({ entries, buckets, start, end, searched, hasSou
|
||||
const timer = window.setInterval(() => setClock(Date.now()), 1000)
|
||||
return () => window.clearInterval(timer)
|
||||
}, [loading])
|
||||
const views = useMemo(
|
||||
() => viewIds.map(id => ({ id, label: t(viewLabelKeys[id]) })),
|
||||
[t],
|
||||
)
|
||||
const domains = useMemo(() => [...new Set(entries.map(entry => parseIngressAccessLog(entry.message)?.domain).filter((domain): domain is string => Boolean(domain)))].sort(), [entries])
|
||||
const visibleEntries = useMemo(() => {
|
||||
const needle = domainFilter.trim().toLowerCase()
|
||||
@@ -46,9 +43,6 @@ export function LogResultsPanel({ entries, buckets, start, end, searched, hasSou
|
||||
return <section className="rounded-xl border bg-card p-4" aria-label={t('results.aria')}>
|
||||
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 className="font-semibold">{t('results.title')} <span className="text-sm font-normal text-muted-foreground">{hasNextPage ? t('results.summaryMore', { page: pageIndex + 1, count: entries.length }) : t('results.summary', { page: pageIndex + 1, count: entries.length })}</span></h2>
|
||||
<div role="tablist" aria-label={t('results.viewAria')} className="flex flex-wrap gap-1 rounded-lg bg-muted/60 p-1">
|
||||
{views.map(item => <button key={item.id} role="tab" aria-selected={view === item.id} type="button" className={`rounded px-2.5 py-1.5 text-xs transition-colors ${view === item.id ? 'bg-card font-medium text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`} onClick={() => setView(item.id)}>{item.label}</button>)}
|
||||
</div>
|
||||
</div>
|
||||
{loading && <div role="status" aria-live="polite" className="mb-3 flex items-center gap-2 rounded-md border border-info/30 bg-info/5 px-3 py-2 text-sm text-foreground">
|
||||
<Loader2 className="size-4 shrink-0 animate-spin text-info" />
|
||||
@@ -62,17 +56,17 @@ export function LogResultsPanel({ entries, buckets, start, end, searched, hasSou
|
||||
<Button type="button" size="sm" variant="outline" disabled={!domainFilter.trim() || loading} onClick={() => onApplyDomain(domainFilter.trim())}>{t('results.domainQuery')}</Button>
|
||||
<span className="text-muted-foreground">{t('results.domainHint', { shown: visibleEntries.length, total: entries.length })}</span>
|
||||
</div>}
|
||||
{view === 'raw' ? entries.length === 0 ? <QueryEmptyState searched={searched} hasSource={hasSource} onRetry={onSearch} /> : visibleEntries.length === 0 ? <p className="py-10 text-center text-sm text-muted-foreground">{t('results.domainNoMatch')}</p> : <div className="divide-y">
|
||||
{entries.length === 0 ? <QueryEmptyState searched={searched} hasSource={hasSource} onRetry={onSearch} /> : visibleEntries.length === 0 ? <p className="py-10 text-center text-sm text-muted-foreground">{t('results.domainNoMatch')}</p> : <div className="divide-y">
|
||||
{visibleEntries.map((row, index) => <div key={`${row.timestampNs}-${index}`} className="py-2 text-sm">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<time className="font-mono tabular-nums">{new Date(row.timestamp).toLocaleString()}</time>
|
||||
<span className="rounded bg-muted px-1">{row.level}</span>
|
||||
<span className={levelBadgeClass(row.level)}>{row.level}</span>
|
||||
{Object.entries(row.labels).filter(([key]) => ['namespace', 'app', 'pod'].includes(key)).map(([key, value]) => <button key={key} title={t('results.filterLabel')} className="hover:text-primary" onClick={() => onQueryChange(applyLabelFilter(query, key, value))}>{key}={value}</button>)}
|
||||
<button className="text-primary hover:underline" onClick={() => onShowContext(row)}>{t('results.showContext')}</button>
|
||||
</div>
|
||||
<HighlightedLogLine message={row.message} terms={terms} className="mt-1 whitespace-pre-wrap break-all font-mono" />
|
||||
</div>)}
|
||||
</div> : <LogResultViews view={view} entries={visibleEntries} buckets={buckets} start={start} end={end} domainFiltered={Boolean(domainFilter.trim())} />}
|
||||
</div>}
|
||||
{searched && entries.length > 0 && <div className="mt-3 flex items-center gap-2">
|
||||
<Button variant="outline" disabled={loading || pageIndex === 0} onClick={onPreviousPage}>{t('results.prevPage')}</Button>
|
||||
<span className="text-xs tabular-nums text-muted-foreground">{t('results.pageOf', { page: pageIndex + 1 })}</span>
|
||||
|
||||
Reference in new issue
Block a user