From a07cb0e55a87435fe04c98d05e7aef4e8557c237 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 18 Oct 2026 16:54:54 +0700 Subject: [PATCH] =?UTF-8?q?feat(design-preview):=20=D0=B8=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D0=BA=D0=B8=20=D0=B2=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE?= =?UTF-8?q?=D1=87=D0=BA=D0=B0=D1=85=20KPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- apps/web/src/design-preview/kpi-strip.tsx | 39 +++++++++++++------ apps/web/src/design-preview/kpi.tsx | 10 +++-- .../design-preview/pages/app-links-page.tsx | 17 +++----- .../src/design-preview/pages/apps-page.tsx | 16 +++----- .../src/design-preview/pages/audit-page.tsx | 9 +++-- .../src/design-preview/pages/logins-page.tsx | 9 +++-- .../src/design-preview/pages/oidc-page.tsx | 10 ++--- .../design-preview/pages/security-page.tsx | 10 ++--- .../src/design-preview/pages/users-page.tsx | 9 +++-- 9 files changed, 70 insertions(+), 59 deletions(-) diff --git a/apps/web/src/design-preview/kpi-strip.tsx b/apps/web/src/design-preview/kpi-strip.tsx index 056e4e2..c75b4db 100644 --- a/apps/web/src/design-preview/kpi-strip.tsx +++ b/apps/web/src/design-preview/kpi-strip.tsx @@ -1,4 +1,5 @@ import { Badge } from '@/components/reui/badge' +import { IconTile } from '@/components/reui/icon-tile' import { Frame, FramePanel } from '@/components/reui/frame' import { cn } from '@authportal/ui/lib/utils' import type { KpiItem, KpiTone } from './kpi' @@ -19,6 +20,14 @@ const COLS: Record = { 4: 'grid-cols-1 min-[640px]:grid-cols-2 min-[1240px]:grid-cols-4', } +const TILE: Record = { + neutral: 'text-muted-foreground', + ok: 'text-success', + in: 'text-info', + wn: 'text-warning', + er: 'text-destructive', +} + const GLYPH: Record = { neutral: '', ok: '●', in: '', wn: '▲', er: '✕' } const BADGE = { @@ -38,18 +47,26 @@ export function KpiStrip({ items, 'aria-label': ariaLabel }: { items: KpiItem[];
- {items.map((k) => ( -
-
{k.label}
-
- {k.value} - - {GLYPH[k.tone] ? {GLYPH[k.tone]} : null} - {k.hint} - + {items.map((k) => { + const Icon = k.icon + return ( +
+ + + +
+
{k.label}
+
+ {k.value} + + {GLYPH[k.tone] ? {GLYPH[k.tone]} : null} + {k.hint} + +
+
-
- ))} + ) + })}
diff --git a/apps/web/src/design-preview/kpi.tsx b/apps/web/src/design-preview/kpi.tsx index 86d0b6b..d9b4abb 100644 --- a/apps/web/src/design-preview/kpi.tsx +++ b/apps/web/src/design-preview/kpi.tsx @@ -1,3 +1,5 @@ +import type { LucideIcon } from 'lucide-react' + export type KpiTone = 'neutral' | 'ok' | 'wn' | 'er' | 'in' export type KpiItem = { @@ -7,14 +9,16 @@ export type KpiItem = { /** Плашка-подсказка рядом со значением. */ hint: string tone: KpiTone + /** Иконка показателя: плитка слева от подписи. */ + icon: LucideIcon } -/** Плитка KPI доски: подпись, значение (цветом при wn/er) и плашка-подсказка. */ +/** Плитка KPI: иконка, подпись, значение (цветом при wn/er) и плашка-подсказка. */ export function kpi( id: string, label: string, value: string, - opts: { icon?: unknown; tone?: KpiTone; badge: string; sub?: string }, + opts: { icon: LucideIcon; tone?: KpiTone; badge: string }, ): KpiItem { - return { id, label, value, hint: opts.badge, tone: opts.tone ?? 'neutral' } + return { id, label, value, hint: opts.badge, tone: opts.tone ?? 'neutral', icon: opts.icon } } 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 663253e..e1a6e74 100644 --- a/apps/web/src/design-preview/pages/app-links-page.tsx +++ b/apps/web/src/design-preview/pages/app-links-page.tsx @@ -1,12 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { - ChartColumnIcon, - CloudIcon, - LayoutDashboardIcon, - NetworkIcon, - ServerIcon, - type LucideIcon -} from 'lucide-react' +import { ChartColumnIcon, CloudIcon, EyeIcon, KeyRoundIcon, LayoutDashboardIcon, LayoutGridIcon, LinkIcon, NetworkIcon, ServerIcon, type LucideIcon } from 'lucide-react' import { toast } from 'sonner' import type { AppId, AppSwitcherIconName } from '@authportal/shared' import { Button } from '@authportal/ui/components/button' @@ -175,10 +168,10 @@ export function AppLinksPage() { r.mode === 'JWT').length), { badge: 'fragment-токен' }), - kpi('oidc', 'OIDC', String(rows.filter((r) => r.mode === 'OIDC').length), { badge: 'Technitium DNS' }), + kpi('n', 'Приложений', String(rows.length), { icon: LayoutGridIcon, badge: 'в каталоге' }), + kpi('sw', 'В переключателе', String(shown.length), { icon: EyeIcon, tone: 'ok', badge: 'видны всем' }), + kpi('jwt', 'JWT SSO', String(rows.filter((r) => r.mode === 'JWT').length), { icon: KeyRoundIcon, badge: 'fragment-токен' }), + kpi('oidc', 'OIDC', String(rows.filter((r) => r.mode === 'OIDC').length), { icon: LinkIcon, badge: 'Technitium DNS' }), ]} /> diff --git a/apps/web/src/design-preview/pages/apps-page.tsx b/apps/web/src/design-preview/pages/apps-page.tsx index 1684253..d63dc51 100644 --- a/apps/web/src/design-preview/pages/apps-page.tsx +++ b/apps/web/src/design-preview/pages/apps-page.tsx @@ -1,11 +1,5 @@ import { Link } from '@tanstack/react-router' -import { - CloudIcon, - GlobeIcon, - LayoutDashboardIcon, - ServerIcon, - ShieldIcon -} from 'lucide-react' +import { BadgeCheckIcon, CloudIcon, GlobeIcon, KeyRoundIcon, LayoutDashboardIcon, LayoutGridIcon, ServerIcon, ShieldIcon } from 'lucide-react' import { toast } from 'sonner' import { APPS, type AppId } from '@authportal/shared' import { KpiStrip } from '../kpi-strip' @@ -63,10 +57,10 @@ export function AppsPage() { a.authMode === 'jwt').length), { badge: 'fragment-токен' }), - kpi('oidc', 'OIDC', String(allowed.filter((a) => a.authMode === 'oidc').length), { badge: 'Technitium DNS' }), - kpi('role', 'Роль', 'Админ', { badge: 'is_admin' }), + kpi('avail', 'Доступно приложений', String(allowed.length), { icon: LayoutGridIcon, tone: 'ok', badge: `из ${APPS.length}` }), + kpi('jwt', 'JWT SSO', String(allowed.filter((a) => a.authMode === 'jwt').length), { icon: KeyRoundIcon, badge: 'fragment-токен' }), + kpi('oidc', 'OIDC', String(allowed.filter((a) => a.authMode === 'oidc').length), { icon: BadgeCheckIcon, badge: 'Technitium DNS' }), + kpi('role', 'Роль', 'Админ', { icon: ShieldIcon, badge: 'is_admin' }), ]} /> diff --git a/apps/web/src/design-preview/pages/audit-page.tsx b/apps/web/src/design-preview/pages/audit-page.tsx index 543be88..0b27549 100644 --- a/apps/web/src/design-preview/pages/audit-page.tsx +++ b/apps/web/src/design-preview/pages/audit-page.tsx @@ -1,3 +1,4 @@ +import { HistoryIcon, NetworkIcon, OctagonAlertIcon, UserCogIcon } from 'lucide-react' import { useState } from 'react' import { toast } from 'sonner' import { KpiStrip } from '../kpi-strip' @@ -48,10 +49,10 @@ export function AuditPage() { diff --git a/apps/web/src/design-preview/pages/logins-page.tsx b/apps/web/src/design-preview/pages/logins-page.tsx index 99ec923..23ee267 100644 --- a/apps/web/src/design-preview/pages/logins-page.tsx +++ b/apps/web/src/design-preview/pages/logins-page.tsx @@ -1,3 +1,4 @@ +import { CircleCheckIcon, GlobeIcon, MonitorSmartphoneIcon, ShieldAlertIcon } from 'lucide-react' import { useMemo, useState } from 'react' import { useNavigate } from '@tanstack/react-router' import { toast } from 'sonner' @@ -63,10 +64,10 @@ export function LoginsPage() { diff --git a/apps/web/src/design-preview/pages/oidc-page.tsx b/apps/web/src/design-preview/pages/oidc-page.tsx index fbe9e08..1d509ef 100644 --- a/apps/web/src/design-preview/pages/oidc-page.tsx +++ b/apps/web/src/design-preview/pages/oidc-page.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react' -import { CopyIcon } from 'lucide-react' +import { CircleCheckIcon, CopyIcon, KeyRoundIcon, LockKeyholeIcon, TicketIcon } from 'lucide-react' import { toast } from 'sonner' import type { CreateOidcClientValues } from '@/components/reui-kit/create-oidc-client-sheet' import { CreateOidcPreviewSheet } from '../create-sheets' @@ -99,10 +99,10 @@ export function OidcPage() { c.enabled).length), { tone: 'ok', badge: 'принимают вход' }), - kpi('tok', 'Токенов за 24 ч', '41', { badge: 'authorization_code' }), - kpi('key', 'Ключ подписи', 'RS256', { badge: 'ротация вручную' }), + kpi('n', 'Клиентов', String(clients.length), { icon: KeyRoundIcon, badge: 'зарегистрировано' }), + kpi('on', 'Включено', String(clients.filter((c) => c.enabled).length), { icon: CircleCheckIcon, tone: 'ok', badge: 'принимают вход' }), + kpi('tok', 'Токенов за 24 ч', '41', { icon: TicketIcon, badge: 'authorization_code' }), + kpi('key', 'Ключ подписи', 'RS256', { icon: LockKeyholeIcon, badge: 'ротация вручную' }), ]} /> diff --git a/apps/web/src/design-preview/pages/security-page.tsx b/apps/web/src/design-preview/pages/security-page.tsx index 3037c38..ac53505 100644 --- a/apps/web/src/design-preview/pages/security-page.tsx +++ b/apps/web/src/design-preview/pages/security-page.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react' -import { KeyRoundIcon, TrashIcon } from 'lucide-react' +import { ClockIcon, FingerprintIcon, KeyRoundIcon, LockIcon, MonitorSmartphoneIcon, TrashIcon } from 'lucide-react' import { toast } from 'sonner' import { KpiStrip } from '../kpi-strip' import { Button } from '@authportal/ui/components/button' @@ -59,10 +59,10 @@ export function SecurityPage() { diff --git a/apps/web/src/design-preview/pages/users-page.tsx b/apps/web/src/design-preview/pages/users-page.tsx index 725ca32..28e446c 100644 --- a/apps/web/src/design-preview/pages/users-page.tsx +++ b/apps/web/src/design-preview/pages/users-page.tsx @@ -1,3 +1,4 @@ +import { FingerprintIcon, ShieldIcon, UserXIcon, UsersIcon } from 'lucide-react' import { useCallback, useEffect, useRef, useState } from 'react' import type { AdminUser, CreateUserRequest } from '@authportal/shared' import { CreateUserPreviewSheet } from '../create-sheets' @@ -111,10 +112,10 @@ export function UsersPage() {