From 709dd935481a73a61220404422d11e779a85bd4e Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 18 Oct 2026 03:18:06 +0700 Subject: [PATCH] =?UTF-8?q?fix(design-preview):=20=C2=AB=D0=9F=D1=80=D0=B8?= =?UTF-8?q?=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=C2=BB=20=D0=B8=20?= =?UTF-8?q?=C2=AB=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=C2=BB=20=D1=82=D0=BE=D1=87=D0=BD=D0=BE=20=D0=BA=D0=B0=D0=BA=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B4=D0=BE=D1=81=D0=BA=D0=B0=D1=85=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=BD=D0=B2=D0=B0=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Плитки приложений и лента событий собраны по разметке доски: Frame с шапкой, сетка auto-fit от 260px, иконка, плашка, описание и адрес; лента — время, точка с линией, плашка и подпись. На «Приложениях» возвращена полоса KPI, «Весь журнал» открывает таблицу. Co-Authored-By: Claude Sonnet 5.5 --- apps/web/src/design-preview/canvas-blocks.tsx | 156 ++++++++++++++++++ .../design-preview/pages/app-links-page.tsx | 12 +- .../src/design-preview/pages/apps-page.tsx | 84 ++++++---- .../src/design-preview/pages/audit-page.tsx | 16 +- 4 files changed, 224 insertions(+), 44 deletions(-) create mode 100644 apps/web/src/design-preview/canvas-blocks.tsx diff --git a/apps/web/src/design-preview/canvas-blocks.tsx b/apps/web/src/design-preview/canvas-blocks.tsx new file mode 100644 index 0000000..63c2adb --- /dev/null +++ b/apps/web/src/design-preview/canvas-blocks.tsx @@ -0,0 +1,156 @@ +import type { ReactNode } from 'react' +import type { AuditLogEntry, AuditSeverity } from '@authportal/shared' +import { Badge } from '@/components/reui/badge' +import { Frame, FrameDescription, FrameHeader, FramePanel, FrameTitle } from '@/components/reui/frame' +import { IconTile } from '@/components/reui/icon-tile' +import { cn } from '@authportal/ui/lib/utils' +import { APP_SHORT } from './mock' +import { StatusBadge } from './bits' + +export type AppCardItem = { + id: string + title: string + desc: string + badge: string + badgeVariant?: 'info-light' | 'warning-light' | 'outline' + meta: string + icon: ReactNode + onSelect?: () => void +} + +/** + * Плитки приложений как на доске «Приложения»: Frame с шапкой, внутри сетка auto-fit от 260px, + * в плитке иконка, название, плашка действия, описание и адрес моноширинным шрифтом. + */ +export function AppCards({ + title, + description, + items, + action, +}: { + title: string + description: string + items: AppCardItem[] + action?: ReactNode +}) { + return ( + + +
+ {title} + {description} +
+ {action} +
+ +
    + {items.map((c) => { + const body = ( + <> +
    + + + {c.icon} + + {c.title} + + + {c.badge} + +
    +

    {c.desc}

    +

    {c.meta}

    + + ) + const cls = 'bg-muted/25 w-full rounded-lg border p-3.5 text-left' + return ( +
  • + {c.onSelect ? ( + + ) : ( +
    {body}
    + )} +
  • + ) + })} +
+
+ + ) +} + +const TONE: Record = { + info: { color: 'bg-success', label: 'Info', tone: 'ok' }, + warning: { color: 'bg-warning', label: 'Warning', tone: 'wn' }, + critical: { color: 'bg-destructive', label: 'Critical', tone: 'er' }, +} + +function when(iso: string) { + const d = new Date(iso) + const ageH = (Date.now() - d.getTime()) / 3_600_000 + if (ageH < 24) return new Intl.DateTimeFormat('ru-RU', { hour: '2-digit', minute: '2-digit' }).format(d) + if (ageH < 48) return 'вчера' + return new Intl.DateTimeFormat('ru-RU', { day: '2-digit', month: '2-digit' }).format(d) +} + +/** + * Лента событий как на доске «Изменения»: время, точка важности с линией, плашка, заголовок + * и строка «кто · источник · IP». + */ +export function ActivityFeed({ + title, + description, + entries, + action, +}: { + title: string + description: string + entries: AuditLogEntry[] + action?: ReactNode +}) { + return ( + + +
+ {title} + {description} +
+ {action} +
+ +
    + {entries.map((e) => { + const t = TONE[e.severity] + const source = e.source_app === 'portal' ? 'Auth Portal' : APP_SHORT[e.source_app] + return ( +
  1. + {when(e.created_at)} + + + + + + + {t.label} + + {e.summary} + + {e.actor_name ?? 'system'} · {source} + {e.ip ? ` · ${e.ip}` : ''} + + +
  2. + ) + })} +
+
+ + ) +} diff --git a/apps/web/src/design-preview/pages/app-links-page.tsx b/apps/web/src/design-preview/pages/app-links-page.tsx index f1da6e6..9f77fa5 100644 --- a/apps/web/src/design-preview/pages/app-links-page.tsx +++ b/apps/web/src/design-preview/pages/app-links-page.tsx @@ -17,7 +17,6 @@ import { } from 'lucide-react' import { toast } from 'sonner' import type { AppId, AppSwitcherIconName } from '@authportal/shared' -import { QuickActionGrid } from '@/components/reui-kit/quick-action-grid' import { Frame, FrameDescription, FrameHeader, FramePanel, FrameTitle } from '@/components/reui/frame' import { IconTile } from '@/components/reui/icon-tile' import { Button } from '@authportal/ui/components/button' @@ -42,6 +41,7 @@ import { Switch } from '@authportal/ui/components/switch' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@authportal/ui/components/tabs' import { KpiStatGrid } from '@/components/reui-kit/kpi-stat-grid' import { PageHead, PreviewSheet, StatusBadge } from '../bits' +import { AppCards } from '../canvas-blocks' import { kpi } from '../kpi' import { MOCK_SWITCHER } from '../mock' import { usePreviewStore } from '../preview-store' @@ -341,18 +341,18 @@ export function AppLinksPage() { - { + items={shown.slice(0, 4).map((r) => { const { Icon } = ICONS[r.icon] return { id: r.id, title: r.name, - description: r.url.replace('https://', ''), - badgeLabel: r.mode, + desc: r.url.replace('https://', ''), + badge: r.mode, badgeVariant: r.mode === 'OIDC' ? ('warning-light' as const) : ('info-light' as const), + meta: r.id, icon: , } })} diff --git a/apps/web/src/design-preview/pages/apps-page.tsx b/apps/web/src/design-preview/pages/apps-page.tsx index 6e8278a..129677a 100644 --- a/apps/web/src/design-preview/pages/apps-page.tsx +++ b/apps/web/src/design-preview/pages/apps-page.tsx @@ -1,11 +1,23 @@ import { Link } from '@tanstack/react-router' -import { CloudIcon, GlobeIcon, LayoutDashboardIcon, ServerIcon, ShieldIcon } from 'lucide-react' +import { + CloudIcon, + FingerprintIcon, + GlobeIcon, + KeyRoundIcon, + LayoutDashboardIcon, + LayoutGridIcon, + ServerIcon, + ShieldCheckIcon, + ShieldIcon, +} from 'lucide-react' import { toast } from 'sonner' import { APPS, type AppId } from '@authportal/shared' -import { QuickActionGrid } from '@/components/reui-kit/quick-action-grid' +import { KpiStatGrid } from '@/components/reui-kit/kpi-stat-grid' import { Button } from '@authportal/ui/components/button' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@authportal/ui/components/tabs' import { PageHead } from '../bits' +import { AppCards, type AppCardItem } from '../canvas-blocks' +import { kpi } from '../kpi' const ICONS: Record = { cfdm: CloudIcon, @@ -23,59 +35,61 @@ export function AppsPage() { const allowed = APPS.filter((a) => !NO_ACCESS.includes(a.id)) const denied = APPS.filter((a) => NO_ACCESS.includes(a.id)) + const toCard = (app: (typeof APPS)[number], open: boolean): AppCardItem => { + const Icon = ICONS[app.id] + const meta = `${app.url.replace('https://', '')} · ${app.authMode === 'oidc' ? 'OIDC' : 'JWT'}` + return { + id: app.id, + title: app.title, + desc: app.description, + badge: open ? 'Открыть' : 'Нет доступа', + badgeVariant: open ? 'info-light' : 'outline', + meta, + icon: , + onSelect: () => + open + ? toast(`Открыть ${app.title}`, { description: `${meta} (макет)` }) + : toast.warning(`Нет доступа к ${app.title}`, { description: 'Запросите права у администратора.' }), + } + } + return ( <> } nativeButton={false}> + } /> + a.authMode === 'jwt').length), { icon: KeyRoundIcon, tone: 'in', badge: 'fragment-токен', sub: 'вход без пароля' }), + kpi('oidc', 'OIDC', String(allowed.filter((a) => a.authMode === 'oidc').length), { icon: FingerprintIcon, tone: 'in', badge: 'Technitium DNS', sub: 'внешний RP' }), + kpi('role', 'Роль', 'Админ', { icon: ShieldCheckIcon, tone: 'wn', badge: 'is_admin', sub: 'полный доступ' }), + ]} + /> Доступные · {allowed.length} Без доступа · {denied.length} - - + { - const Icon = ICONS[app.id] - return { - id: app.id, - title: app.title, - description: app.description, - badgeLabel: 'Открыть', - badgeVariant: 'info-light' as const, - meta: `${app.url.replace('https://', '')} · ${app.authMode === 'oidc' ? 'OIDC' : 'JWT'}`, - icon: , - onSelect: () => toast(`Открыть ${app.title}`, { description: `${app.url.replace('https://', '')} · ${app.authMode.toUpperCase()} (макет)` }), - } - })} + description="Клик открывает приложение в новой вкладке · портал заново выпускает токен с актуальными правами" + items={allowed.map((a) => toCard(a, true))} /> - - + { - const Icon = ICONS[app.id] - return { - id: app.id, - title: app.title, - description: app.description, - badgeLabel: 'Нет доступа', - meta: `${app.url.replace('https://', '')} · ${app.authMode === 'oidc' ? 'OIDC' : 'JWT'}`, - icon: , - onSelect: () => toast.warning(`Нет доступа к ${app.title}`, { description: 'Запросите права у администратора.' }), - } - })} + items={denied.map((a) => toCard(a, false))} /> diff --git a/apps/web/src/design-preview/pages/audit-page.tsx b/apps/web/src/design-preview/pages/audit-page.tsx index 6632502..adb2002 100644 --- a/apps/web/src/design-preview/pages/audit-page.tsx +++ b/apps/web/src/design-preview/pages/audit-page.tsx @@ -2,8 +2,8 @@ import { useState } from 'react' import { DownloadIcon, Trash2Icon, HistoryIcon, UserCogIcon, ShieldAlertIcon, NetworkIcon } from 'lucide-react' import { toast } from 'sonner' import type { AuditLogEntry, AuditSeverity } from '@authportal/shared' -import { AuditLogTimeline } from '@/components/reui-kit/audit-log-timeline' import { AuditRetentionPanel } from '@/components/reui-kit/audit-retention-panel' +import { ActivityFeed } from '../canvas-blocks' import { KpiStatGrid } from '@/components/reui-kit/kpi-stat-grid' import { Button } from '@authportal/ui/components/button' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@authportal/ui/components/tabs' @@ -43,6 +43,7 @@ const VIEWS = [ export function AuditPage() { const [entry, setEntry] = useState(null) const [purgeOpen, setPurgeOpen] = useState(false) + const [tab, setTab] = useState('feed') const [retention, setRetention] = useState(90) return ( @@ -66,14 +67,23 @@ export function AuditPage() { kpi('src', 'Источников', '6', { icon: NetworkIcon, tone: 'in', badge: 'ingest', sub: 'портал + 5 приложений' }), ]} /> - + Лента Таблица · {MOCK_AUDIT.length} Ротация - + b.created_at.localeCompare(a.created_at)).slice(0, 6)} + action={ + + } + />