refactor(web): вынести общие блоки макета в components/portal

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Sonnet 5.5 committed 2026-10-23 13:53:59 +07:00
1 parent b89e72ec1b
commit 6898d2255d
23 files changed
+92 -86

No files matched your search

@@ -33,7 +33,7 @@ import {
SheetHeader, SheetHeader,
SheetTitle, SheetTitle,
} from '@authportal/ui/components/sheet' } from '@authportal/ui/components/sheet'
import type { Tone } from './mock' import type { Tone } from './tone'
import { TONE_BADGE } from './tone' import { TONE_BADGE } from './tone'
const TONE_GLYPH: Record<Tone, string> = { ok: '●', wn: '▲', er: '✕', mu: '○', in: '' } const TONE_GLYPH: Record<Tone, string> = { ok: '●', wn: '▲', er: '✕', mu: '○', in: '' }
@@ -104,7 +104,7 @@ export function SignalAlert({
} }
/** Боковое окно (как CreateUserSheet): заголовок, прокручиваемое тело, подвал. */ /** Боковое окно (как CreateUserSheet): заголовок, прокручиваемое тело, подвал. */
export function PreviewSheet({ export function PortalSheet({
open, open,
onOpenChange, onOpenChange,
title, title,
@@ -3,7 +3,7 @@ import { Badge } from '@/components/reui/badge'
import { Frame, FrameDescription, FrameHeader, FramePanel, FrameTitle } from '@/components/reui/frame' import { Frame, FrameDescription, FrameHeader, FramePanel, FrameTitle } from '@/components/reui/frame'
import { IconTile } from '@/components/reui/icon-tile' import { IconTile } from '@/components/reui/icon-tile'
import { cn } from '@authportal/ui/lib/utils' import { cn } from '@authportal/ui/lib/utils'
import type { Tone } from './mock' import type { Tone } from './tone'
import { StatusBadge } from './bits' import { StatusBadge } from './bits'
export type AppCardItem = { export type AppCardItem = {
@@ -2,7 +2,7 @@ import { useState, type ReactNode } from 'react'
import { useNavigate } from '@tanstack/react-router' import { useNavigate } from '@tanstack/react-router'
import { Button } from '@authportal/ui/components/button' import { Button } from '@authportal/ui/components/button'
import { cn } from '@authportal/ui/lib/utils' import { cn } from '@authportal/ui/lib/utils'
import { CodeBlock, PreviewSheet, SignalAlert, StatusBadge } from './bits' import { CodeBlock, PortalSheet, SignalAlert, StatusBadge } from './bits'
/** /**
* Баннер доски: заголовок и пояснение, справа «Объяснить» — открывает панель * Баннер доски: заголовок и пояснение, справа «Объяснить» — открывает панель
@@ -38,7 +38,7 @@ export function ExplainBanner({
Объяснить Объяснить
</Button> </Button>
</div> </div>
<PreviewSheet <PortalSheet
open={open} open={open}
onOpenChange={setOpen} onOpenChange={setOpen}
title="Ассистент · разбор" title="Ассистент · разбор"
@@ -77,7 +77,7 @@ export function ExplainBanner({
<SignalAlert tone="wn" title="Ассистент ничего не меняет сам"> <SignalAlert tone="wn" title="Ассистент ничего не меняет сам">
Блокировка и сброс passkeys выполняются вручную с подтверждением и попадают в журнал «Изменения». Блокировка и сброс passkeys выполняются вручную с подтверждением и попадают в журнал «Изменения».
</SignalAlert> </SignalAlert>
</PreviewSheet> </PortalSheet>
</> </>
) )
} }
File renamed without changes.
@@ -32,8 +32,8 @@ import { Switch } from '@authportal/ui/components/switch'
import { Tabs, TabsList, TabsTrigger } from '@authportal/ui/components/tabs' import { Tabs, TabsList, TabsTrigger } from '@authportal/ui/components/tabs'
import { cn } from '@authportal/ui/lib/utils' import { cn } from '@authportal/ui/lib/utils'
import { StatusBadge } from './bits' import { StatusBadge } from './bits'
import type { Tone } from './mock' import type { Tone } from './tone'
import type { RowMark } from './preview-store' import type { RowMark } from './tone'
import { TONE_DOT } from './tone' import { TONE_DOT } from './tone'
export type GridColumn<T> = { export type GridColumn<T> = {
@@ -83,7 +83,7 @@ const ACTIONS_COLUMN_SIZE = 120
* сохранённые виды линией со счётчиками, панель массовых действий, таблица со статусом и меню строки, * сохранённые виды линией со счётчиками, панель массовых действий, таблица со статусом и меню строки,
* подвал «Показано N из M». Ширина таблицы не меньше суммы колонок — прокрутка внутри контейнера. * подвал «Показано N из M». Ширина таблицы не меньше суммы колонок — прокрутка внутри контейнера.
*/ */
export function PreviewSectionGrid<T extends object>({ export function SectionGrid<T extends object>({
title, title,
description, description,
rows, rows,
@@ -1,4 +1,8 @@
import type { Tone } from './mock' export type Tone = 'ok' | 'wn' | 'er' | 'mu' | 'in'
/** Пометка строки с накопленной правкой. */
export type RowMark = 'new' | 'dirty' | 'deleted'
type BadgeVariant = type BadgeVariant =
| 'success-light' | 'success-light'
@@ -10,7 +10,7 @@ import {
} from '@authportal/ui/components/select' } from '@authportal/ui/components/select'
import { Switch } from '@authportal/ui/components/switch' import { Switch } from '@authportal/ui/components/switch'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@authportal/ui/components/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@authportal/ui/components/tabs'
import { CodeBlock, SignalAlert } from './bits' import { CodeBlock, SignalAlert } from '@/components/portal/bits'
import { withLevel, levelOf, type AccessValue, type Level } from './access-model' import { withLevel, levelOf, type AccessValue, type Level } from './access-model'
import { APP_SHORT, USER_JOURNAL } from './mock' import { APP_SHORT, USER_JOURNAL } from './mock'
@@ -1,6 +1,6 @@
import { StatusBadge } from './bits' import { StatusBadge } from '@/components/portal/bits'
import type { Tone } from './mock' import type { Tone } from './mock'
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from './section-grid' import { SectionGrid, TwoLineCell, type GridColumn } from '@/components/portal/section-grid'
type MatrixRow = { type MatrixRow = {
id: string id: string
@@ -39,7 +39,7 @@ const COLUMNS: GridColumn<MatrixRow>[] = [
/** Матрица доступа по доске: сколько пользователей имеет доступ к каждому приложению и с каким уровнем. */ /** Матрица доступа по доске: сколько пользователей имеет доступ к каждому приложению и с каким уровнем. */
export function AccessMatrix() { export function AccessMatrix() {
return ( return (
<PreviewSectionGrid <SectionGrid
title="Матрица доступа" title="Матрица доступа"
description="Сколько пользователей имеет доступ к каждому приложению и с каким уровнем" description="Сколько пользователей имеет доступ к каждому приложению и с каким уровнем"
rows={ROWS} rows={ROWS}
+3 -3
View File
@@ -3,7 +3,7 @@ import type { AdminUser } from '@authportal/shared'
import { Button } from '@authportal/ui/components/button' import { Button } from '@authportal/ui/components/button'
import { AccessEditor } from './access-editor' import { AccessEditor } from './access-editor'
import { diffAccess, type AccessValue } from './access-model' import { diffAccess, type AccessValue } from './access-model'
import { PreviewSheet } from './bits' import { PortalSheet } from '@/components/portal/bits'
/** Окно «Права доступа»: широкая правая панель, чтобы таблица оставалась в контексте. */ /** Окно «Права доступа»: широкая правая панель, чтобы таблица оставалась в контексте. */
export function AccessSheet({ export function AccessSheet({
@@ -19,7 +19,7 @@ export function AccessSheet({
const changes = useMemo(() => diffAccess({ apps: user.apps, permissions: user.permissions }, value).length, [user, value]) const changes = useMemo(() => diffAccess({ apps: user.apps, permissions: user.permissions }, value).length, [user, value])
return ( return (
<PreviewSheet <PortalSheet
open open
onOpenChange={(o) => !o && onClose()} onOpenChange={(o) => !o && onClose()}
size="lg" size="lg"
@@ -47,6 +47,6 @@ export function AccessSheet({
} }
> >
<AccessEditor user={user} value={value} onChange={setValue} /> <AccessEditor user={user} value={value} onChange={setValue} />
</PreviewSheet> </PortalSheet>
) )
} }
@@ -6,10 +6,10 @@ import { Field, FieldDescription, FieldError, FieldLabel } from '@authportal/ui/
import { Input } from '@authportal/ui/components/input' import { Input } from '@authportal/ui/components/input'
import { Switch } from '@authportal/ui/components/switch' import { Switch } from '@authportal/ui/components/switch'
import { Textarea } from '@authportal/ui/components/textarea' import { Textarea } from '@authportal/ui/components/textarea'
import { PreviewSheet, SignalAlert } from './bits' import { PortalSheet, SignalAlert } from '@/components/portal/bits'
/** Окно «Создать пользователя» по доске: email, имя, временный пароль, администратор и заметка про журнал. */ /** Окно «Создать пользователя» по доске: email, имя, временный пароль, администратор и заметка про журнал. */
export function CreateUserPreviewSheet({ export function CreateUserPortalSheet({
open, open,
onOpenChange, onOpenChange,
onSubmit, onSubmit,
@@ -37,7 +37,7 @@ export function CreateUserPreviewSheet({
} }
return ( return (
<PreviewSheet <PortalSheet
open={open} open={open}
onOpenChange={(o) => { onOpenChange={(o) => {
if (!o) reset() if (!o) reset()
@@ -93,12 +93,12 @@ export function CreateUserPreviewSheet({
<SignalAlert tone="wn" title="Журнал"> <SignalAlert tone="wn" title="Журнал">
Создание записывается в журнал «Изменения» с вашим именем и IP. Создание записывается в журнал «Изменения» с вашим именем и IP.
</SignalAlert> </SignalAlert>
</PreviewSheet> </PortalSheet>
) )
} }
/** Окно «Создать OIDC-клиент» по доске: название, redirect URI, включён. */ /** Окно «Создать OIDC-клиент» по доске: название, redirect URI, включён. */
export function CreateOidcPreviewSheet({ export function CreateOidcPortalSheet({
open, open,
onOpenChange, onOpenChange,
onSubmit, onSubmit,
@@ -128,7 +128,7 @@ export function CreateOidcPreviewSheet({
} }
return ( return (
<PreviewSheet <PortalSheet
open={open} open={open}
onOpenChange={(o) => { onOpenChange={(o) => {
if (!o) reset() if (!o) reset()
@@ -189,6 +189,6 @@ export function CreateOidcPreviewSheet({
<FieldDescription>Выключенный клиент не может получить токены</FieldDescription> <FieldDescription>Выключенный клиент не может получить токены</FieldDescription>
</div> </div>
</Field> </Field>
</PreviewSheet> </PortalSheet>
) )
} }
+2 -1
View File
@@ -21,7 +21,8 @@ import {
type AuditLogEntry, type AuditLogEntry,
} from '@authportal/shared' } from '@authportal/shared'
export type Tone = 'ok' | 'wn' | 'er' | 'mu' | 'in' import type { Tone } from '@/components/portal/tone'
export type { Tone }
export type NavItem = { export type NavItem = {
id: string id: string
@@ -15,13 +15,13 @@ import {
} from '@authportal/ui/components/select' } from '@authportal/ui/components/select'
import { Switch } from '@authportal/ui/components/switch' import { Switch } from '@authportal/ui/components/switch'
import { Tabs, TabsContent } from '@authportal/ui/components/tabs' import { Tabs, TabsContent } from '@authportal/ui/components/tabs'
import { KpiStrip } from '../kpi-strip' import { KpiStrip } from '@/components/portal/kpi-strip'
import { PageHead, PreviewSheet, StatusBadge, PageTab, PageTabsList, FormFrame, SettingRow } from '../bits' import { PageHead, PortalSheet, StatusBadge, PageTab, PageTabsList, FormFrame, SettingRow } from '@/components/portal/bits'
import { AppCards } from '../canvas-blocks' import { AppCards } from '@/components/portal/canvas-blocks'
import { kpi } from '../kpi' import { kpi } from '@/components/portal/kpi'
import { MOCK_SWITCHER } from '../mock' import { MOCK_SWITCHER } from '../mock'
import { usePreviewStore } from '../preview-store' import { usePreviewStore } from '../preview-store'
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid' import { SectionGrid, TwoLineCell, type GridColumn } from '@/components/portal/section-grid'
type LinkRow = { type LinkRow = {
id: AppId id: AppId
@@ -180,7 +180,7 @@ export function AppLinksPage() {
<PageTab value="menu" label="Меню" /> <PageTab value="menu" label="Меню" />
</PageTabsList> </PageTabsList>
<TabsContent value="list" className="min-w-0"> <TabsContent value="list" className="min-w-0">
<PreviewSectionGrid <SectionGrid
title="Ссылки сервисов" title="Ссылки сервисов"
description="Порядок строк = порядок в меню · переключатель управляет видимостью · изменения применяются после сохранения" description="Порядок строк = порядок в меню · переключатель управляет видимостью · изменения применяются после сохранения"
rows={rows} rows={rows}
@@ -247,7 +247,7 @@ export function AppLinksPage() {
</TabsContent> </TabsContent>
</Tabs> </Tabs>
<PreviewSheet <PortalSheet
open={editing !== null} open={editing !== null}
onOpenChange={(o) => !o && setEditing(null)} onOpenChange={(o) => !o && setEditing(null)}
title="Приложение" title="Приложение"
@@ -323,7 +323,7 @@ export function AppLinksPage() {
</Field> </Field>
</> </>
) : null} ) : null}
</PreviewSheet> </PortalSheet>
</> </>
) )
} }
@@ -2,12 +2,12 @@ import { Link } from '@tanstack/react-router'
import { BadgeCheckIcon, CloudIcon, GlobeIcon, KeyRoundIcon, LayoutDashboardIcon, LayoutGridIcon, ServerIcon, ShieldIcon } from 'lucide-react' import { BadgeCheckIcon, CloudIcon, GlobeIcon, KeyRoundIcon, LayoutDashboardIcon, LayoutGridIcon, ServerIcon, ShieldIcon } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { APPS, type AppId } from '@authportal/shared' import { APPS, type AppId } from '@authportal/shared'
import { KpiStrip } from '../kpi-strip' import { KpiStrip } from '@/components/portal/kpi-strip'
import { Button } from '@authportal/ui/components/button' import { Button } from '@authportal/ui/components/button'
import { Tabs, TabsContent } from '@authportal/ui/components/tabs' import { Tabs, TabsContent } from '@authportal/ui/components/tabs'
import { PageHead, PageTab, PageTabsList } from '../bits' import { PageHead, PageTab, PageTabsList } from '@/components/portal/bits'
import { AppCards, type AppCardItem } from '../canvas-blocks' import { AppCards, type AppCardItem } from '@/components/portal/canvas-blocks'
import { kpi } from '../kpi' import { kpi } from '@/components/portal/kpi'
const ICONS: Record<AppId, typeof CloudIcon> = { const ICONS: Record<AppId, typeof CloudIcon> = {
cfdm: CloudIcon, cfdm: CloudIcon,
@@ -1,16 +1,16 @@
import { HistoryIcon, NetworkIcon, OctagonAlertIcon, UserCogIcon } from 'lucide-react' import { HistoryIcon, NetworkIcon, OctagonAlertIcon, UserCogIcon } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { KpiStrip } from '../kpi-strip' import { KpiStrip } from '@/components/portal/kpi-strip'
import { Button } from '@authportal/ui/components/button' import { Button } from '@authportal/ui/components/button'
import { Input } from '@authportal/ui/components/input' import { Input } from '@authportal/ui/components/input'
import { Switch } from '@authportal/ui/components/switch' import { Switch } from '@authportal/ui/components/switch'
import { Tabs, TabsContent } from '@authportal/ui/components/tabs' import { Tabs, TabsContent } from '@authportal/ui/components/tabs'
import { CodeBlock, FormFrame, KvList, PageHead, PageTab, PageTabsList, PreviewSheet, SettingRow, StatusBadge, TypedConfirm } from '../bits' import { CodeBlock, FormFrame, KvList, PageHead, PageTab, PageTabsList, PortalSheet, SettingRow, StatusBadge, TypedConfirm } from '@/components/portal/bits'
import { ActivityFeed } from '../canvas-blocks' import { ActivityFeed } from '@/components/portal/canvas-blocks'
import { kpi } from '../kpi' import { kpi } from '@/components/portal/kpi'
import { AUDIT_FEED, AUDIT_ROWS, type AuditRow } from '../mock' import { AUDIT_FEED, AUDIT_ROWS, type AuditRow } from '../mock'
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid' import { SectionGrid, TwoLineCell, type GridColumn } from '@/components/portal/section-grid'
const mono = (v: string) => <span className="font-mono text-[12.5px] whitespace-nowrap">{v}</span> const mono = (v: string) => <span className="font-mono text-[12.5px] whitespace-nowrap">{v}</span>
@@ -74,7 +74,7 @@ export function AuditPage() {
/> />
</TabsContent> </TabsContent>
<TabsContent value="table" className="min-w-0"> <TabsContent value="table" className="min-w-0">
<PreviewSectionGrid <SectionGrid
title="Журнал изменений" title="Журнал изменений"
description="Поиск по событию, пользователю и IP · виды по важности" description="Поиск по событию, пользователю и IP · виды по важности"
rows={AUDIT_ROWS} rows={AUDIT_ROWS}
@@ -113,7 +113,7 @@ export function AuditPage() {
</TabsContent> </TabsContent>
</Tabs> </Tabs>
<PreviewSheet <PortalSheet
open={entry !== null} open={entry !== null}
onOpenChange={(o) => !o && setEntry(null)} onOpenChange={(o) => !o && setEntry(null)}
title="Событие журнала" title="Событие журнала"
@@ -137,7 +137,7 @@ export function AuditPage() {
</CodeBlock> </CodeBlock>
</> </>
) : null} ) : null}
</PreviewSheet> </PortalSheet>
<TypedConfirm <TypedConfirm
open={purgeOpen} open={purgeOpen}
@@ -9,7 +9,7 @@ import { Button } from '@authportal/ui/components/button'
import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from '@authportal/ui/components/field' import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from '@authportal/ui/components/field'
import { Input } from '@authportal/ui/components/input' import { Input } from '@authportal/ui/components/input'
import { Separator } from '@authportal/ui/components/separator' import { Separator } from '@authportal/ui/components/separator'
import { SignalAlert } from '../bits' import { SignalAlert } from '@/components/portal/bits'
const PERKS = [ const PERKS = [
{ icon: FingerprintIcon, title: 'Passkey вместо пароля', text: 'Отпечаток, Face ID или ключ безопасности' }, { icon: FingerprintIcon, title: 'Passkey вместо пароля', text: 'Отпечаток, Face ID или ключ безопасности' },
@@ -2,14 +2,14 @@ import { CircleCheckIcon, GlobeIcon, MonitorSmartphoneIcon, ShieldAlertIcon } fr
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { useNavigate } from '@tanstack/react-router' import { useNavigate } from '@tanstack/react-router'
import { toast } from 'sonner' import { toast } from 'sonner'
import { KpiStrip } from '../kpi-strip' import { KpiStrip } from '@/components/portal/kpi-strip'
import { Button } from '@authportal/ui/components/button' import { Button } from '@authportal/ui/components/button'
import { Tabs, TabsContent } from '@authportal/ui/components/tabs' import { Tabs, TabsContent } from '@authportal/ui/components/tabs'
import { ExplainBanner } from '../explain-banner' import { ExplainBanner } from '@/components/portal/explain-banner'
import { Segmented, CodeBlock, KvList, PageHead, PreviewSheet, StatusBadge, PageTab, PageTabsList } from '../bits' import { Segmented, CodeBlock, KvList, PageHead, PortalSheet, StatusBadge, PageTab, PageTabsList } from '@/components/portal/bits'
import { kpi } from '../kpi' import { kpi } from '@/components/portal/kpi'
import { MOCK_LOGINS, MOCK_SESSIONS, type LoginRow, type SessionRow } from '../mock' import { MOCK_LOGINS, MOCK_SESSIONS, type LoginRow, type SessionRow } from '../mock'
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid' import { SectionGrid, TwoLineCell, type GridColumn } from '@/components/portal/section-grid'
const PERIODS = ['24 ч', '7 д', '30 д', '90 д'] const PERIODS = ['24 ч', '7 д', '30 д', '90 д']
@@ -76,7 +76,7 @@ export function LoginsPage() {
<PageTab value="sess" label="Активные сессии" count={sessions.length} /> <PageTab value="sess" label="Активные сессии" count={sessions.length} />
</PageTabsList> </PageTabsList>
<TabsContent value="hist" className="flex min-w-0 flex-col gap-4"> <TabsContent value="hist" className="flex min-w-0 flex-col gap-4">
<PreviewSectionGrid <SectionGrid
title="Журнал входов" title="Журнал входов"
description="Поиск по логину и IP · фильтры: результат, приложение, пользователь · период выбирается выше" description="Поиск по логину и IP · фильтры: результат, приложение, пользователь · период выбирается выше"
rows={MOCK_LOGINS} rows={MOCK_LOGINS}
@@ -94,7 +94,7 @@ export function LoginsPage() {
/> />
</TabsContent> </TabsContent>
<TabsContent value="sess" className="flex min-w-0 flex-col gap-4"> <TabsContent value="sess" className="flex min-w-0 flex-col gap-4">
<PreviewSectionGrid <SectionGrid
title="Активные сессии" title="Активные сессии"
description="Токены, действующие сейчас · завершение сессии выкидывает пользователя из портала и приложений" description="Токены, действующие сейчас · завершение сессии выкидывает пользователя из портала и приложений"
rows={sessions} rows={sessions}
@@ -112,7 +112,7 @@ export function LoginsPage() {
</TabsContent> </TabsContent>
</Tabs> </Tabs>
<PreviewSheet <PortalSheet
open={event !== null} open={event !== null}
onOpenChange={(o) => !o && setEvent(null)} onOpenChange={(o) => !o && setEvent(null)}
title="Событие входа" title="Событие входа"
@@ -148,7 +148,7 @@ export function LoginsPage() {
<CodeBlock label="Событие">{`action: ${event.tone === 'er' ? 'auth.login_failed' : 'auth.login'}\nsource_app: portal\nseverity: ${event.tone === 'er' ? 'warning' : 'info'}\nip: ${event.ip}`}</CodeBlock> <CodeBlock label="Событие">{`action: ${event.tone === 'er' ? 'auth.login_failed' : 'auth.login'}\nsource_app: portal\nseverity: ${event.tone === 'er' ? 'warning' : 'info'}\nip: ${event.ip}`}</CodeBlock>
</> </>
) : null} ) : null}
</PreviewSheet> </PortalSheet>
</> </>
) )
} }
+10 -10
View File
@@ -2,8 +2,8 @@ import { useMemo, useState } from 'react'
import { CircleCheckIcon, CopyIcon, KeyRoundIcon, LockKeyholeIcon, TicketIcon } from 'lucide-react' import { CircleCheckIcon, CopyIcon, KeyRoundIcon, LockKeyholeIcon, TicketIcon } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import type { CreateOidcClientValues } from '@/components/reui-kit/create-oidc-client-sheet' import type { CreateOidcClientValues } from '@/components/reui-kit/create-oidc-client-sheet'
import { CreateOidcPreviewSheet } from '../create-sheets' import { CreateOidcPortalSheet } from '../create-sheets'
import { KpiStrip } from '../kpi-strip' import { KpiStrip } from '@/components/portal/kpi-strip'
import { Button } from '@authportal/ui/components/button' import { Button } from '@authportal/ui/components/button'
import { import {
AlertDialog, AlertDialog,
@@ -18,9 +18,9 @@ import { Input } from '@authportal/ui/components/input'
import { Switch } from '@authportal/ui/components/switch' import { Switch } from '@authportal/ui/components/switch'
import { Tabs, TabsContent } from '@authportal/ui/components/tabs' import { Tabs, TabsContent } from '@authportal/ui/components/tabs'
import { Textarea } from '@authportal/ui/components/textarea' import { Textarea } from '@authportal/ui/components/textarea'
import { ExplainBanner } from '../explain-banner' import { ExplainBanner } from '@/components/portal/explain-banner'
import { CopyList, KvFrame, PageHead, PreviewSheet, StatusBadge, TypedConfirm, PageTab, PageTabsList } from '../bits' import { CopyList, KvFrame, PageHead, PortalSheet, StatusBadge, TypedConfirm, PageTab, PageTabsList } from '@/components/portal/bits'
import { kpi } from '../kpi' import { kpi } from '@/components/portal/kpi'
import { import {
MOCK_CLIENT_SECRET, MOCK_CLIENT_SECRET,
MOCK_OIDC_CLIENTS, MOCK_OIDC_CLIENTS,
@@ -29,7 +29,7 @@ import {
type OidcClientRow, type OidcClientRow,
type Tone type Tone
} from '../mock' } from '../mock'
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid' import { SectionGrid, TwoLineCell, type GridColumn } from '@/components/portal/section-grid'
function tone(c: OidcClientRow): Tone { function tone(c: OidcClientRow): Tone {
return !c.enabled ? 'mu' : c.stale ? 'wn' : 'ok' return !c.enabled ? 'mu' : c.stale ? 'wn' : 'ok'
@@ -111,7 +111,7 @@ export function OidcPage() {
<PageTab value="meta" label="Параметры issuer" /> <PageTab value="meta" label="Параметры issuer" />
</PageTabsList> </PageTabsList>
<TabsContent value="clients" className="flex min-w-0 flex-col gap-4"> <TabsContent value="clients" className="flex min-w-0 flex-col gap-4">
<PreviewSectionGrid <SectionGrid
title="Зарегистрированные клиенты" title="Зарегистрированные клиенты"
description="Каждый клиент — внешнее приложение (RP) · секрет показывается один раз при создании и обновлении" description="Каждый клиент — внешнее приложение (RP) · секрет показывается один раз при создании и обновлении"
rows={clients} rows={clients}
@@ -148,9 +148,9 @@ export function OidcPage() {
</TabsContent> </TabsContent>
</Tabs> </Tabs>
<CreateOidcPreviewSheet open={createOpen} onOpenChange={setCreateOpen} onSubmit={onCreate} /> <CreateOidcPortalSheet open={createOpen} onOpenChange={setCreateOpen} onSubmit={onCreate} />
<PreviewSheet <PortalSheet
open={editing !== null} open={editing !== null}
onOpenChange={(o) => !o && setEditing(null)} onOpenChange={(o) => !o && setEditing(null)}
title="OIDC-клиент" title="OIDC-клиент"
@@ -193,7 +193,7 @@ export function OidcPage() {
</Field> </Field>
</> </>
) : null} ) : null}
</PreviewSheet> </PortalSheet>
<TypedConfirm <TypedConfirm
open={rotating !== null} open={rotating !== null}
@@ -1,17 +1,17 @@
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { ClockIcon, FingerprintIcon, KeyRoundIcon, LockIcon, MonitorSmartphoneIcon, TrashIcon } from 'lucide-react' import { ClockIcon, FingerprintIcon, KeyRoundIcon, LockIcon, MonitorSmartphoneIcon, TrashIcon } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { KpiStrip } from '../kpi-strip' import { KpiStrip } from '@/components/portal/kpi-strip'
import { Button } from '@authportal/ui/components/button' import { Button } from '@authportal/ui/components/button'
import { Field, FieldDescription, FieldLabel } from '@authportal/ui/components/field' import { Field, FieldDescription, FieldLabel } from '@authportal/ui/components/field'
import { Input } from '@authportal/ui/components/input' import { Input } from '@authportal/ui/components/input'
import { Switch } from '@authportal/ui/components/switch' import { Switch } from '@authportal/ui/components/switch'
import { Tabs, TabsContent } from '@authportal/ui/components/tabs' import { Tabs, TabsContent } from '@authportal/ui/components/tabs'
import { ExplainBanner } from '../explain-banner' import { ExplainBanner } from '@/components/portal/explain-banner'
import { KvFrame, KvList, PageHead, PreviewSheet, SignalAlert, StatusBadge, TypedConfirm, PageTab, PageTabsList, FormFrame, SettingRow } from '../bits' import { KvFrame, KvList, PageHead, PortalSheet, SignalAlert, StatusBadge, TypedConfirm, PageTab, PageTabsList, FormFrame, SettingRow } from '@/components/portal/bits'
import { kpi } from '../kpi' import { kpi } from '@/components/portal/kpi'
import { MOCK_PASSKEYS, type PasskeyRow } from '../mock' import { MOCK_PASSKEYS, type PasskeyRow } from '../mock'
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid' import { SectionGrid, TwoLineCell, type GridColumn } from '@/components/portal/section-grid'
export function SecurityPage() { export function SecurityPage() {
const [keys, setKeys] = useState<PasskeyRow[]>(MOCK_PASSKEYS) const [keys, setKeys] = useState<PasskeyRow[]>(MOCK_PASSKEYS)
@@ -72,7 +72,7 @@ export function SecurityPage() {
<PageTab value="acc" label="Учётная запись" /> <PageTab value="acc" label="Учётная запись" />
</PageTabsList> </PageTabsList>
<TabsContent value="keys" className="flex min-w-0 flex-col gap-4"> <TabsContent value="keys" className="flex min-w-0 flex-col gap-4">
<PreviewSectionGrid <SectionGrid
title="Мои passkeys" title="Мои passkeys"
description="Вход без пароля: отпечаток, Face ID, PIN или ключ безопасности · удаление необратимо" description="Вход без пароля: отпечаток, Face ID, PIN или ключ безопасности · удаление необратимо"
rows={view} rows={view}
@@ -137,7 +137,7 @@ export function SecurityPage() {
</TabsContent> </TabsContent>
</Tabs> </Tabs>
<PreviewSheet <PortalSheet
open={addOpen} open={addOpen}
onOpenChange={setAddOpen} onOpenChange={setAddOpen}
title="Добавить passkey" title="Добавить passkey"
@@ -169,9 +169,9 @@ export function SecurityPage() {
<SignalAlert tone="in" title="Что произойдёт дальше"> <SignalAlert tone="in" title="Что произойдёт дальше">
Откроется системный диалог: подтвердите отпечатком, Face ID, PIN или ключом безопасности. Закрытый ключ не покидает устройство. Откроется системный диалог: подтвердите отпечатком, Face ID, PIN или ключом безопасности. Закрытый ключ не покидает устройство.
</SignalAlert> </SignalAlert>
</PreviewSheet> </PortalSheet>
<PreviewSheet <PortalSheet
open={selected !== null} open={selected !== null}
onOpenChange={(o) => !o && setSelected(null)} onOpenChange={(o) => !o && setSelected(null)}
title="Passkey" title="Passkey"
@@ -213,7 +213,7 @@ export function SecurityPage() {
['Последний вход', `${selected?.lastUsed ?? ''} · 203.0.113.10`], ['Последний вход', `${selected?.lastUsed ?? ''} · 203.0.113.10`],
]} ]}
/> />
</PreviewSheet> </PortalSheet>
<TypedConfirm <TypedConfirm
open={deleting !== null} open={deleting !== null}
@@ -1,16 +1,16 @@
import { FingerprintIcon, ShieldIcon, UserXIcon, UsersIcon } from 'lucide-react' import { FingerprintIcon, ShieldIcon, UserXIcon, UsersIcon } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import type { AdminUser, CreateUserRequest } from '@authportal/shared' import type { AdminUser, CreateUserRequest } from '@authportal/shared'
import { CreateUserPreviewSheet } from '../create-sheets' import { CreateUserPortalSheet } from '../create-sheets'
import { KpiStrip } from '../kpi-strip' import { KpiStrip } from '@/components/portal/kpi-strip'
import { Button } from '@authportal/ui/components/button' import { Button } from '@authportal/ui/components/button'
import { Tabs, TabsContent } from '@authportal/ui/components/tabs' import { Tabs, TabsContent } from '@authportal/ui/components/tabs'
import { AccessSheet } from '../access-sheet' import { AccessSheet } from '../access-sheet'
import { diffAccess, type AccessValue } from '../access-model' import { diffAccess, type AccessValue } from '../access-model'
import { PageHead, TypedConfirm, PageTab, PageTabsList } from '../bits' import { PageHead, TypedConfirm, PageTab, PageTabsList } from '@/components/portal/bits'
import { kpi } from '../kpi' import { kpi } from '@/components/portal/kpi'
import { ALL_APP_IDS } from '../mock' import { ALL_APP_IDS } from '../mock'
import { ExplainBanner } from '../explain-banner' import { ExplainBanner } from '@/components/portal/explain-banner'
import { AccessMatrix } from '../access-matrix' import { AccessMatrix } from '../access-matrix'
import { UsersGrid, type UsersGridHandlers } from '../users-grid' import { UsersGrid, type UsersGridHandlers } from '../users-grid'
import { usePreviewStore } from '../preview-store' import { usePreviewStore } from '../preview-store'
@@ -131,7 +131,7 @@ export function UsersPage() {
</TabsContent> </TabsContent>
</Tabs> </Tabs>
<CreateUserPreviewSheet open={createOpen} onOpenChange={setCreateOpen} onSubmit={onCreate} /> <CreateUserPortalSheet open={createOpen} onOpenChange={setCreateOpen} onSubmit={onCreate} />
{accessFor ? ( {accessFor ? (
<AccessSheet <AccessSheet
@@ -67,7 +67,7 @@ import {
useSidebar, useSidebar,
} from '@authportal/ui/components/sidebar' } from '@authportal/ui/components/sidebar'
import { cn } from '@authportal/ui/lib/utils' import { cn } from '@authportal/ui/lib/utils'
import { Segmented } from './bits' import { Segmented } from '@/components/portal/bits'
import { FRESH_BY_PAGE, MONITOR_ITEMS, NAV_GROUPS, NAV_ITEMS, type NavItem } from './mock' import { FRESH_BY_PAGE, MONITOR_ITEMS, NAV_GROUPS, NAV_ITEMS, type NavItem } from './mock'
import { usePreviewStore } from './preview-store' import { usePreviewStore } from './preview-store'
+2 -1
View File
@@ -3,7 +3,8 @@ import type { AdminUser } from '@authportal/shared'
export type ChangeKind = 'add' | 'chg' | 'del' export type ChangeKind = 'add' | 'chg' | 'del'
export type RowMark = 'new' | 'dirty' | 'deleted' import type { RowMark } from '@/components/portal/tone'
export type { RowMark }
export type PendingChange = { export type PendingChange = {
id: string id: string
+3 -3
View File
@@ -2,10 +2,10 @@ import { useMemo } from 'react'
import type { AdminUser } from '@authportal/shared' import type { AdminUser } from '@authportal/shared'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Button } from '@authportal/ui/components/button' import { Button } from '@authportal/ui/components/button'
import { PendingChip, StatusBadge } from './bits' import { PendingChip, StatusBadge } from '@/components/portal/bits'
import type { RowMark } from './preview-store' import type { RowMark } from './preview-store'
import { ALL_APP_IDS, APP_SHORT, USER_META, type Tone } from './mock' import { ALL_APP_IDS, APP_SHORT, USER_META, type Tone } from './mock'
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from './section-grid' import { SectionGrid, TwoLineCell, type GridColumn } from '@/components/portal/section-grid'
function userStatus(u: AdminUser): { tone: Tone; label: string } { function userStatus(u: AdminUser): { tone: Tone; label: string } {
if (u.disabled) return { tone: 'mu', label: 'Отключён' } if (u.disabled) return { tone: 'mu', label: 'Отключён' }
@@ -65,7 +65,7 @@ export function UsersGrid({ users, handlers, marks }: { users: AdminUser[]; hand
) )
return ( return (
<PreviewSectionGrid <SectionGrid
title="Пользователи портала" title="Пользователи портала"
description="Поиск по email и имени · фильтры: роль, статус, приложения, passkey · колонки настраиваются, вид сохраняется" description="Поиск по email и имени · фильтры: роль, статус, приложения, passkey · колонки настраиваются, вид сохраняется"
rows={users} rows={users}