feat(design-preview): журнал изменений — лента, таблица и ротация без дублирующих фильтров
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5de88e18c8
commit
1a46aaa6a5
1 file changed
+34
-50
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { DownloadIcon, Trash2Icon, HistoryIcon, UserCogIcon, ShieldAlertIcon, NetworkIcon } from 'lucide-react'
|
import { DownloadIcon, Trash2Icon, HistoryIcon, UserCogIcon, ShieldAlertIcon, NetworkIcon } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import type { AuditLogEntry, AuditSeverity } from '@authportal/shared'
|
import type { AuditLogEntry, AuditSeverity } from '@authportal/shared'
|
||||||
@@ -7,7 +7,7 @@ import { AuditRetentionPanel } from '@/components/reui-kit/audit-retention-panel
|
|||||||
import { KpiStatGrid } from '@/components/reui-kit/kpi-stat-grid'
|
import { KpiStatGrid } from '@/components/reui-kit/kpi-stat-grid'
|
||||||
import { Button } from '@authportal/ui/components/button'
|
import { Button } from '@authportal/ui/components/button'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@authportal/ui/components/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@authportal/ui/components/tabs'
|
||||||
import { Segmented, CodeBlock, KvList, PageHead, PreviewSheet, StatusBadge, TypedConfirm } from '../bits'
|
import { CodeBlock, KvList, PageHead, PreviewSheet, StatusBadge, TypedConfirm } from '../bits'
|
||||||
import { kpi } from '../kpi'
|
import { kpi } from '../kpi'
|
||||||
import { MOCK_AUDIT, APP_SHORT, type Tone } from '../mock'
|
import { MOCK_AUDIT, APP_SHORT, type Tone } from '../mock'
|
||||||
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid'
|
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid'
|
||||||
@@ -34,38 +34,16 @@ const columns: GridColumn<AuditLogEntry>[] = [
|
|||||||
|
|
||||||
const VIEWS = [
|
const VIEWS = [
|
||||||
{ label: 'Все', match: () => true },
|
{ label: 'Все', match: () => true },
|
||||||
{ label: 'Warning и выше', match: (r: AuditLogEntry) => r.severity !== 'info' },
|
{ label: 'Auth', match: (r: AuditLogEntry) => r.action.startsWith('auth.') },
|
||||||
{ label: 'Critical', match: (r: AuditLogEntry) => r.severity === 'critical' },
|
{ label: 'Пользователи', match: (r: AuditLogEntry) => r.action.startsWith('user.') },
|
||||||
|
{ label: 'Настройки', match: (r: AuditLogEntry) => /^(oidc|app_switcher|audit)\./.test(r.action) },
|
||||||
|
{ label: 'Критичные', match: (r: AuditLogEntry) => r.severity === 'critical' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export function AuditPage() {
|
export function AuditPage() {
|
||||||
const [entry, setEntry] = useState<AuditLogEntry | null>(null)
|
const [entry, setEntry] = useState<AuditLogEntry | null>(null)
|
||||||
const [purgeOpen, setPurgeOpen] = useState(false)
|
const [purgeOpen, setPurgeOpen] = useState(false)
|
||||||
const [retention, setRetention] = useState(90)
|
const [retention, setRetention] = useState(90)
|
||||||
const [period, setPeriod] = useState(1)
|
|
||||||
|
|
||||||
const auth = useMemo(() => MOCK_AUDIT.filter((e) => e.action.startsWith('auth.')), [])
|
|
||||||
const users = useMemo(() => MOCK_AUDIT.filter((e) => e.action.startsWith('user.')), [])
|
|
||||||
const settings = useMemo(
|
|
||||||
() => MOCK_AUDIT.filter((e) => e.action.startsWith('oidc.') || e.action.startsWith('app_switcher.') || e.action.startsWith('audit.')),
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
function grid(title: string, description: string, rows: AuditLogEntry[]) {
|
|
||||||
return (
|
|
||||||
<PreviewSectionGrid
|
|
||||||
title={title}
|
|
||||||
description={description}
|
|
||||||
rows={rows}
|
|
||||||
columns={columns}
|
|
||||||
getRowId={(r) => r.id}
|
|
||||||
searchText={(r) => `${r.action} ${r.summary} ${r.actor_email ?? ''} ${r.ip ?? ''}`}
|
|
||||||
searchPlaceholder="Событие, пользователь, IP"
|
|
||||||
views={VIEWS}
|
|
||||||
onRowClick={setEntry}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -73,19 +51,12 @@ export function AuditPage() {
|
|||||||
title="Изменения"
|
title="Изменения"
|
||||||
sub="Журнал действий администраторов и приложений · портал, EvoFirewall, EvoBGP, MikrotikManager и другие через ingest"
|
sub="Журнал действий администраторов и приложений · портал, EvoFirewall, EvoBGP, MikrotikManager и другие через ingest"
|
||||||
actions={
|
actions={
|
||||||
<>
|
<Button variant="outline" onClick={() => setPurgeOpen(true)}>
|
||||||
<Button variant="outline" onClick={() => toast.success('CSV сформирован (макет)')}>
|
<Trash2Icon aria-hidden />
|
||||||
<DownloadIcon aria-hidden />
|
Очистить старые записи…
|
||||||
Экспорт CSV
|
</Button>
|
||||||
</Button>
|
|
||||||
<Button variant="outline" onClick={() => setPurgeOpen(true)}>
|
|
||||||
<Trash2Icon aria-hidden />
|
|
||||||
Очистить старые записи…
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Segmented label="Период" options={['24 ч', '7 д', '30 д', '90 д', 'Весь период']} value={period} onChange={setPeriod} />
|
|
||||||
<KpiStatGrid
|
<KpiStatGrid
|
||||||
aria-label="Показатели"
|
aria-label="Показатели"
|
||||||
items={[
|
items={[
|
||||||
@@ -95,21 +66,34 @@ export function AuditPage() {
|
|||||||
kpi('src', 'Источников', '6', { icon: NetworkIcon, tone: 'in', badge: 'ingest', sub: 'портал + 5 приложений' }),
|
kpi('src', 'Источников', '6', { icon: NetworkIcon, tone: 'in', badge: 'ingest', sub: 'портал + 5 приложений' }),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Tabs defaultValue="all" className="min-w-0">
|
<Tabs defaultValue="feed" className="min-w-0">
|
||||||
<TabsList className="max-w-full justify-start overflow-x-auto">
|
<TabsList>
|
||||||
<TabsTrigger value="all">Все · 148</TabsTrigger>
|
<TabsTrigger value="feed">Лента</TabsTrigger>
|
||||||
<TabsTrigger value="auth">Auth · 31</TabsTrigger>
|
<TabsTrigger value="table">Таблица · {MOCK_AUDIT.length}</TabsTrigger>
|
||||||
<TabsTrigger value="users">Пользователи · 74</TabsTrigger>
|
|
||||||
<TabsTrigger value="settings">Настройки · 43</TabsTrigger>
|
|
||||||
<TabsTrigger value="retention">Ротация</TabsTrigger>
|
<TabsTrigger value="retention">Ротация</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="all" className="flex min-w-0 flex-col gap-4">
|
<TabsContent value="feed" className="min-w-0">
|
||||||
<AuditLogTimeline entries={MOCK_AUDIT} totalCount={MOCK_AUDIT.length} />
|
<AuditLogTimeline entries={MOCK_AUDIT} totalCount={MOCK_AUDIT.length} />
|
||||||
{grid('Журнал изменений', 'Поиск по событию, пользователю и IP · фильтры: источник, важность, объект', MOCK_AUDIT)}
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="auth">{grid('События аутентификации', 'Входы, выходы и SSO · подробности — в разделе «Входы»', auth)}</TabsContent>
|
<TabsContent value="table" className="min-w-0">
|
||||||
<TabsContent value="users">{grid('Изменения пользователей', 'Создание, правки, права и passkeys', users)}</TabsContent>
|
<PreviewSectionGrid
|
||||||
<TabsContent value="settings">{grid('Изменения настроек', 'Переключатель приложений, OIDC, ротация журнала', settings)}</TabsContent>
|
title="Журнал изменений"
|
||||||
|
description="Поиск по событию, пользователю и IP"
|
||||||
|
rows={MOCK_AUDIT}
|
||||||
|
columns={columns}
|
||||||
|
getRowId={(r) => r.id}
|
||||||
|
searchText={(r) => `${r.action} ${r.summary} ${r.actor_email ?? ''} ${r.ip ?? ''}`}
|
||||||
|
searchPlaceholder="Событие, пользователь, IP"
|
||||||
|
views={VIEWS}
|
||||||
|
onRowClick={setEntry}
|
||||||
|
toolbar={
|
||||||
|
<Button variant="outline" size="sm" onClick={() => toast.success('CSV сформирован (макет)')}>
|
||||||
|
<DownloadIcon aria-hidden />
|
||||||
|
Экспорт CSV
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
<TabsContent value="retention">
|
<TabsContent value="retention">
|
||||||
<AuditRetentionPanel
|
<AuditRetentionPanel
|
||||||
retentionDays={retention}
|
retentionDays={retention}
|
||||||
|
|||||||
Reference in new issue
Block a user