feat(design-preview): шапка как в канвасе — заголовок, инфо-кнопка и монитор «Внимание»
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
a31a964228
commit
f486b72884
2 files changed
+54
-20
No files matched your search
@@ -99,6 +99,14 @@ export const MONITOR_ITEMS: Array<{
|
|||||||
age: '3 дн',
|
age: '3 дн',
|
||||||
to: '/design-preview/oidc',
|
to: '/design-preview/oidc',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'nopasskey',
|
||||||
|
tone: 'wn',
|
||||||
|
title: '3 пользователя без passkey',
|
||||||
|
text: 'ops, pavel, dns-bot · вход только по паролю',
|
||||||
|
age: '1 дн',
|
||||||
|
to: '/design-preview/users',
|
||||||
|
},
|
||||||
{ id: 'api', tone: 'ok', title: 'API портала', text: '/ready · ответ 18 мс', age: 'сейчас', to: '/design-preview/apps' },
|
{ id: 'api', tone: 'ok', title: 'API портала', text: '/ready · ответ 18 мс', age: 'сейчас', to: '/design-preview/apps' },
|
||||||
{ id: 'jwks', tone: 'ok', title: 'JWKS и discovery', text: 'Подпись RS256 · ключ активен', age: 'сейчас', to: '/design-preview/oidc' },
|
{ id: 'jwks', tone: 'ok', title: 'JWKS и discovery', text: 'Подпись RS256 · ключ активен', age: 'сейчас', to: '/design-preview/oidc' },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
ContrastIcon,
|
ContrastIcon,
|
||||||
FingerprintIcon,
|
FingerprintIcon,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
|
InfoIcon,
|
||||||
KeyRoundIcon,
|
KeyRoundIcon,
|
||||||
LayoutDashboardIcon,
|
LayoutDashboardIcon,
|
||||||
LogOutIcon,
|
LogOutIcon,
|
||||||
@@ -32,7 +33,6 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
|
||||||
} from '@authportal/ui/components/breadcrumb'
|
} from '@authportal/ui/components/breadcrumb'
|
||||||
import { Button } from '@authportal/ui/components/button'
|
import { Button } from '@authportal/ui/components/button'
|
||||||
import {
|
import {
|
||||||
@@ -387,27 +387,37 @@ export function PreviewSidebar() {
|
|||||||
|
|
||||||
function MonitorPopover() {
|
function MonitorPopover() {
|
||||||
const bad = MONITOR_ITEMS.filter((m) => m.tone !== 'ok')
|
const bad = MONITOR_ITEMS.filter((m) => m.tone !== 'ok')
|
||||||
|
const errors = bad.filter((m) => m.tone === 'er').length
|
||||||
|
const warnings = bad.length - errors
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
render={
|
render={
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Монитор Auth Portal: внимание, 1 ошибка, 1 предупреждение"
|
aria-label={`Монитор Auth Portal: внимание, ${errors} ошибка, ${warnings} предупреждения`}
|
||||||
className="border-border hover:bg-accent focus-visible:ring-ring inline-flex h-8 items-center gap-1.5 rounded-md border px-2 outline-none focus-visible:ring-2"
|
className="border-warning/40 bg-warning/10 text-warning hover:bg-warning/15 focus-visible:ring-ring inline-flex h-9 items-center gap-2 rounded-lg border px-3 text-sm font-medium outline-none focus-visible:ring-2"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ActivityIcon aria-hidden className="text-destructive size-3.5" />
|
<ActivityIcon aria-hidden className="size-4" />
|
||||||
<span className="text-foreground hidden text-xs font-medium @2xl/header:inline">Система</span>
|
<span className="hidden @md/header:inline">Внимание</span>
|
||||||
<Badge variant="destructive-light" size="xs" className="h-4 px-1.5 text-[10px]">
|
<span className="bg-destructive inline-flex size-5 items-center justify-center rounded-full text-xs font-semibold text-white tabular-nums">
|
||||||
{bad.length}
|
{bad.length}
|
||||||
</Badge>
|
</span>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent align="end" sideOffset={8} className="flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-0! p-0!">
|
<PopoverContent align="end" sideOffset={8} className="flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-0! p-0!">
|
||||||
<div className="border-border flex items-center justify-between border-b px-3 py-2.5">
|
<div className="border-border flex items-center justify-between border-b px-3 py-2.5">
|
||||||
<span className="text-xs font-medium">Монитор Auth Portal</span>
|
<div className="flex flex-col">
|
||||||
<Badge variant="warning-light" size="xs">Внимание</Badge>
|
<span className="text-xs font-medium">Монитор Auth Portal</span>
|
||||||
|
<span className="text-muted-foreground text-[11px]">обновлено 14:02</span>
|
||||||
|
</div>
|
||||||
|
<Badge variant="warning-light" size="sm">Внимание</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground flex gap-3 border-b px-3 py-2 text-xs">
|
||||||
|
<span><b className="text-destructive font-semibold">{errors}</b> ошибка</span>
|
||||||
|
<span><b className="text-warning font-semibold">{warnings}</b> предупреждения</span>
|
||||||
|
<span><b className="text-success font-semibold">{MONITOR_ITEMS.length - bad.length}</b> в норме</span>
|
||||||
</div>
|
</div>
|
||||||
<ul className="flex flex-col">
|
<ul className="flex flex-col">
|
||||||
{MONITOR_ITEMS.map((m) => (
|
{MONITOR_ITEMS.map((m) => (
|
||||||
@@ -434,10 +444,35 @@ function MonitorPopover() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FreshnessPopover() {
|
||||||
|
const [auto, setAuto] = useState(true)
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger
|
||||||
|
render={
|
||||||
|
<Button type="button" variant="ghost" size="icon-sm" aria-label="Источники и свежесть данных" title="Источники и свежесть данных" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<InfoIcon aria-hidden />
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent align="end" sideOffset={8} className="flex w-72 max-w-[calc(100vw-2rem)] flex-col gap-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="bg-success size-2 rounded-full" aria-hidden />
|
||||||
|
<span className="text-sm font-medium">Обновлено 14:02:11 · Live</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground text-xs">Данные портала: GET /api/v1/admin/* · сессия и права из базы портала. В макете запросы не отправляются.</p>
|
||||||
|
<label className="flex items-center justify-between gap-2 text-sm">
|
||||||
|
Автообновление
|
||||||
|
<Switch checked={auto} onCheckedChange={setAuto} aria-label="Автообновление" />
|
||||||
|
</label>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function PreviewHeader() {
|
export function PreviewHeader() {
|
||||||
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
||||||
const { setPaletteOpen } = usePreviewStore()
|
const { setPaletteOpen } = usePreviewStore()
|
||||||
const [auto, setAuto] = useState(true)
|
|
||||||
const current = NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`))
|
const current = NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -446,22 +481,13 @@ export function PreviewHeader() {
|
|||||||
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
||||||
<Breadcrumb className="min-w-0">
|
<Breadcrumb className="min-w-0">
|
||||||
<BreadcrumbList className="flex-nowrap">
|
<BreadcrumbList className="flex-nowrap">
|
||||||
<BreadcrumbItem className="hidden @lg/header:inline-flex">Auth Portal</BreadcrumbItem>
|
|
||||||
<BreadcrumbSeparator className="hidden @lg/header:block" />
|
|
||||||
<BreadcrumbItem className="hidden @md/header:inline-flex">{current?.group ?? 'Портал'}</BreadcrumbItem>
|
|
||||||
<BreadcrumbSeparator className="hidden @md/header:block" />
|
|
||||||
<BreadcrumbItem className="min-w-0">
|
<BreadcrumbItem className="min-w-0">
|
||||||
<BreadcrumbPage className="truncate">{current?.label ?? 'Макет'}</BreadcrumbPage>
|
<BreadcrumbPage className="truncate">{current?.label ?? 'Макет'}</BreadcrumbPage>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
<div className="ml-auto flex shrink-0 items-center gap-2">
|
<div className="ml-auto flex shrink-0 items-center gap-2">
|
||||||
<span role="status" className="text-muted-foreground hidden items-center gap-1.5 text-xs @3xl/header:flex">
|
<FreshnessPopover />
|
||||||
<span className="bg-success size-1.5 rounded-full" aria-hidden />
|
|
||||||
Обновлено 14:02:11 · Live
|
|
||||||
<Switch size="sm" checked={auto} onCheckedChange={setAuto} aria-label="Автообновление" />
|
|
||||||
<span>авто</span>
|
|
||||||
</span>
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
Reference in new issue
Block a user