From 72f9597edee903674ac13d38258c5ace4ef05362 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Tue, 21 Jul 2026 18:10:43 +0700 Subject: [PATCH] =?UTF-8?q?feat(audit):=20=D0=BF=D0=BE=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=D1=8B=D0=B2=D0=B0=D1=82=D1=8C=20=D1=87=D0=B5=D0=BB=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D0=BA=D0=BE=D1=87=D0=B8=D1=82=D0=B0=D0=B5=D0=BC=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=B0=20VPS=20=D0=B2=20=D0=B6?= =?UTF-8?q?=D1=83=D1=80=D0=BD=D0=B0=D0=BB=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вместо сырого ID — dns/ip (и name для хостера/аккаунта/проекта); id остаётся в title и деталях. Co-authored-by: Cursor --- .../web/src/components/domain/audit-labels.ts | 41 +++++++++++++ .../src/components/domain/audit-timeline.tsx | 44 +++++++++++--- apps/web/src/routes/_auth/audit.tsx | 59 ++++++++++++++----- 3 files changed, 122 insertions(+), 22 deletions(-) diff --git a/apps/web/src/components/domain/audit-labels.ts b/apps/web/src/components/domain/audit-labels.ts index f2cc3b0..f12b293 100644 --- a/apps/web/src/components/domain/audit-labels.ts +++ b/apps/web/src/components/domain/audit-labels.ts @@ -63,3 +63,44 @@ export function diffPreview(diff: Record | null | undefined, ma const rest = entries.length - maxKeys return rest > 0 ? `${head} (+${rest})` : head } + +export type AuditEntityLookup = { + vps?: Map + provider?: Map + providerAccount?: Map + serverProject?: Map +} + +function strFromDiff(diff: Record | null | undefined, key: string): string { + const v = diff?.[key] + return typeof v === 'string' && v.trim() ? v.trim() : '' +} + +/** Человекочитаемое имя сущности: dns/ip для VPS, name для хостера/аккаунта/проекта. */ +export function auditEntityTitle( + entity: string, + entityId: string, + diff?: Record | null, + lookup?: AuditEntityLookup, +): string { + if (entity === 'vps') { + const row = lookup?.vps?.get(entityId) + const fromSnap = (row?.dns || row?.ip || '').trim() + if (fromSnap) return fromSnap + const fromDiff = strFromDiff(diff, 'dns') || strFromDiff(diff, 'ip') + if (fromDiff) return fromDiff + } + if (entity === 'provider') { + const name = (lookup?.provider?.get(entityId)?.name || strFromDiff(diff, 'name')).trim() + if (name) return name + } + if (entity === 'providerAccount') { + const name = (lookup?.providerAccount?.get(entityId)?.name || strFromDiff(diff, 'name')).trim() + if (name) return name + } + if (entity === 'serverProject') { + const name = (lookup?.serverProject?.get(entityId)?.name || strFromDiff(diff, 'name')).trim() + if (name) return name + } + return entityId +} diff --git a/apps/web/src/components/domain/audit-timeline.tsx b/apps/web/src/components/domain/audit-timeline.tsx index d504dd0..41fc3b7 100644 --- a/apps/web/src/components/domain/audit-timeline.tsx +++ b/apps/web/src/components/domain/audit-timeline.tsx @@ -25,7 +25,9 @@ import { auditActionBadgeVariant, auditActionLabel, auditEntityLabel, + auditEntityTitle, diffFieldEntries, + type AuditEntityLookup, } from '@/components/domain/audit-labels' import { cn } from '@cfdm/ui/lib/utils' import { Button } from '@cfdm/ui/components/button' @@ -47,6 +49,7 @@ export interface AuditRow { interface AuditTimelineProps { rows: AuditRow[] + entityLookup?: AuditEntityLookup } function actionIcon(action: string) { @@ -79,19 +82,32 @@ function timeLabel(iso: string): string { return d.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) } -function EntityIdLink({ entity, entityId }: { entity: string; entityId: string }) { +function EntityTargetLink({ + entity, + entityId, + title, +}: { + entity: string + entityId: string + title: string +}) { if (entity === 'vps') { return ( ) } - return {entityId} + return ( + + {title} + + ) } function EventRow({ @@ -99,15 +115,18 @@ function EventRow({ step, isLast, defaultOpen, + entityLookup, }: { row: AuditRow step: number isLast: boolean defaultOpen: boolean + entityLookup?: AuditEntityLookup }) { const [open, setOpen] = useState(defaultOpen) const fieldCount = diffFieldEntries(row.diff).length const actor = row.actorUserId?.trim() || 'система' + const title = auditEntityTitle(row.entity, row.entityId, row.diff, entityLookup) return ( @@ -136,7 +155,7 @@ function EventRow({ {actor} · - + {fieldCount > 0 ? ( {fieldCount} {fieldCount === 1 ? 'поле' : 'полей'} @@ -157,11 +176,19 @@ function EventRow({
{auditEntityLabel(row.entity)}
-
ID
+
Объект
- +
+ {title !== row.entityId ? ( +
+
ID
+
+ {row.entityId} +
+
+ ) : null}
Актор
{actor}
@@ -187,7 +214,7 @@ function EventRow({ } /** Day-grouped audit timeline — DNA solution-users-6. */ -export function AuditTimeline({ rows }: AuditTimelineProps) { +export function AuditTimeline({ rows, entityLookup }: AuditTimelineProps) { const days = useMemo(() => { const map = new Map() for (const row of rows) { @@ -217,6 +244,7 @@ export function AuditTimeline({ rows }: AuditTimelineProps) { step={index + 1} isLast={index === day.events.length - 1} defaultOpen={dayIndex === 0 && index < 2} + entityLookup={entityLookup} /> ))} diff --git a/apps/web/src/routes/_auth/audit.tsx b/apps/web/src/routes/_auth/audit.tsx index c8e570e..91802ff 100644 --- a/apps/web/src/routes/_auth/audit.tsx +++ b/apps/web/src/routes/_auth/audit.tsx @@ -5,6 +5,7 @@ import { HistoryIcon, UserRoundIcon } from 'lucide-react' import { z } from 'zod' import { api } from '@/lib/api-client' +import { snapshotQueryOptions } from '@/queries/snapshot' import { PageShell } from '@/components/page-shell' import { PageHeader } from '@/components/page-header' import { QueryState } from '@/components/query-state' @@ -17,7 +18,9 @@ import { auditActionBadgeVariant, auditActionLabel, auditEntityLabel, + auditEntityTitle, diffPreview, + type AuditEntityLookup, } from '@/components/domain/audit-labels' import { Button } from '@cfdm/ui/components/button' import { Badge } from '@/components/reui/badge' @@ -40,6 +43,8 @@ const ACTION_FILTERS: { value: AuditActionFilter; label: string }[] = [ export const Route = createFileRoute('/_auth/audit')({ validateSearch: (search) => auditSearchSchema.parse(search), + loader: ({ context: { queryClient } }) => + queryClient.ensureQueryData(snapshotQueryOptions()), component: AuditPage, }) @@ -53,6 +58,23 @@ function AuditPage() { queryKey: ['audit'], queryFn: () => api.fetchAuditLog(200) as Promise, }) + const { data: snapshot } = useQuery(snapshotQueryOptions()) + + const entityLookup = useMemo(() => { + const vps = new Map( + (snapshot?.vps ?? []).map((v) => [v.id, { dns: v.dns, ip: v.ip }] as const), + ) + const provider = new Map( + (snapshot?.providers ?? []).map((p) => [p.id, { name: p.name }] as const), + ) + const providerAccount = new Map( + (snapshot?.providerAccounts ?? []).map((a) => [a.id, { name: a.name }] as const), + ) + const serverProject = new Map( + (snapshot?.serverProjects ?? []).map((p) => [p.id, { name: p.name }] as const), + ) + return { vps, provider, providerAccount, serverProject } + }, [snapshot]) const filtered = useMemo(() => { const rows = data ?? [] @@ -123,19 +145,28 @@ function AuditPage() { }, { key: 'entityId', - header: 'ID', - cell: (r) => - r.entity === 'vps' ? ( - - ) : ( - {r.entityId} - ), + header: 'Объект', + sortValue: (r) => auditEntityTitle(r.entity, r.entityId, r.diff, entityLookup), + cell: (r) => { + const title = auditEntityTitle(r.entity, r.entityId, r.diff, entityLookup) + if (r.entity === 'vps') { + return ( + + ) + } + return ( + + {title} + + ) + }, }, { key: 'actor', @@ -218,7 +249,7 @@ function AuditPage() { ) } - return + return }}