refactor(web): вынести общие блоки макета в components/portal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
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,
|
||||||
+1
-1
@@ -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 = {
|
||||||
+3
-3
@@ -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.
File renamed without changes.
+3
-3
@@ -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,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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in new issue
Block a user