fix(sidebar,audit): badges для Outbox + Webhooks, clarify audit scope label
Sidebar:
- Outbox: бэйдж = outbox.pending (события queued, не отосланы). DLQ
отдельно — alarming но не actionable из бэйджа, видно на /outbox.
- Webhooks: бэйдж = count активных subscriptions (w.active === true).
User feedback: 'вебхуки активные есть а цифры с бэйджем нет'.
Audit log column header:
- 'Scope' → 'Доступ актора' (ru) / 'Actor scope' (en)
- Backend audit_log хранит userScope (claim из JWT актора), НЕ scope
изменённого ресурса. User confusion: 'правки в public были а тут
RESTRICTED' — потому что у пользователя scope-привилегия RESTRICTED.
Уточнённая label убирает путаницу.
- TODO: backend feature — добавить dictionaryScope в AuditEntry чтобы
показать обе ('Актор: RESTRICTED → Ресурс: PUBLIC' — двусторонний
audit trail для compliance).
This commit is contained in:
@@ -14,7 +14,13 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useAuth } from 'react-oidc-context'
|
import { useAuth } from 'react-oidc-context'
|
||||||
import { useCanMutate } from '@/auth/useCanMutate'
|
import { useCanMutate } from '@/auth/useCanMutate'
|
||||||
import { useDictionaries, useMyDrafts, useReviewsBadgeCount } from '@/api/queries'
|
import {
|
||||||
|
useDictionaries,
|
||||||
|
useMyDrafts,
|
||||||
|
useOutboxStats,
|
||||||
|
useReviewsBadgeCount,
|
||||||
|
useWebhookSubscriptions,
|
||||||
|
} from '@/api/queries'
|
||||||
import { Sheet, SheetContent } from '@/ui'
|
import { Sheet, SheetContent } from '@/ui'
|
||||||
import { ThemeSwitch } from '@/components/layout/ThemeSwitch'
|
import { ThemeSwitch } from '@/components/layout/ThemeSwitch'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -56,13 +62,22 @@ export function Sidebar() {
|
|||||||
// Loaded just for сидбар counter — light request, кэшируется.
|
// Loaded just for сидбар counter — light request, кэшируется.
|
||||||
const myDrafts = useMyDrafts(0, 1)
|
const myDrafts = useMyDrafts(0, 1)
|
||||||
// Reviews badge — суммирует record-reviews + schema-reviews pending queues
|
// Reviews badge — суммирует record-reviews + schema-reviews pending queues
|
||||||
// через size=1 paged запросы (cheap: одна запись + metadata). Gated на
|
// через size=1 paged запросы. Gated на authenticated.
|
||||||
// authenticated, чтобы anonymous не пытался admin endpoint'ы.
|
|
||||||
const reviews = useReviewsBadgeCount(auth.isAuthenticated)
|
const reviews = useReviewsBadgeCount(auth.isAuthenticated)
|
||||||
|
// Outbox/Webhooks badges — light periodic poll. Backend гейтит, поэтому
|
||||||
|
// anonymous safe (просто 401 проигнорируется).
|
||||||
|
const outboxStats = useOutboxStats()
|
||||||
|
const webhooks = useWebhookSubscriptions()
|
||||||
|
|
||||||
const dictsCount = dictsQuery.data?.length ?? 0
|
const dictsCount = dictsQuery.data?.length ?? 0
|
||||||
const myDraftsCount = myDrafts.data?.totalElements ?? 0
|
const myDraftsCount = myDrafts.data?.totalElements ?? 0
|
||||||
const reviewsCount = reviews.total
|
const reviewsCount = reviews.total
|
||||||
|
// Pending events ждут dispatch. dlq отдельно — alarming, но не блокирует
|
||||||
|
// нормальную работу, можно увидеть на /outbox. Badge показывает только
|
||||||
|
// pending (actionable: 'надо разобраться почему queue растёт').
|
||||||
|
const outboxPending = outboxStats.data?.pending ?? 0
|
||||||
|
const webhooksActive =
|
||||||
|
webhooks.data?.filter((w) => w.active).length ?? 0
|
||||||
|
|
||||||
// Flat list per redesign/compact.html — no section labels, /graph removed
|
// Flat list per redesign/compact.html — no section labels, /graph removed
|
||||||
// (now reachable via catalog toolbar "Граф ⇄" button). Order: Главная →
|
// (now reachable via catalog toolbar "Граф ⇄" button). Order: Главная →
|
||||||
@@ -91,8 +106,20 @@ export function Sidebar() {
|
|||||||
badge: reviewsCount > 0 ? reviewsCount : undefined,
|
badge: reviewsCount > 0 ? reviewsCount : undefined,
|
||||||
authRequired: true,
|
authRequired: true,
|
||||||
},
|
},
|
||||||
{ to: '/outbox', label: t('nav.outbox'), icon: Inbox, authRequired: true },
|
{
|
||||||
{ to: '/webhooks', label: t('nav.webhooks'), icon: Webhook, authRequired: true },
|
to: '/outbox',
|
||||||
|
label: t('nav.outbox'),
|
||||||
|
icon: Inbox,
|
||||||
|
badge: outboxPending > 0 ? outboxPending : undefined,
|
||||||
|
authRequired: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/webhooks',
|
||||||
|
label: t('nav.webhooks'),
|
||||||
|
icon: Webhook,
|
||||||
|
badge: webhooksActive > 0 ? webhooksActive : undefined,
|
||||||
|
authRequired: true,
|
||||||
|
},
|
||||||
{ to: '/audit', label: t('nav.audit'), icon: ScrollText, authRequired: true },
|
{ to: '/audit', label: t('nav.audit'), icon: ScrollText, authRequired: true },
|
||||||
{ to: '/search', label: t('nav.search'), icon: Search },
|
{ to: '/search', label: t('nav.search'), icon: Search },
|
||||||
],
|
],
|
||||||
@@ -394,10 +421,15 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () =>
|
|||||||
const dictsQuery = useDictionaries()
|
const dictsQuery = useDictionaries()
|
||||||
const myDrafts = useMyDrafts(0, 1)
|
const myDrafts = useMyDrafts(0, 1)
|
||||||
const reviews = useReviewsBadgeCount(auth.isAuthenticated)
|
const reviews = useReviewsBadgeCount(auth.isAuthenticated)
|
||||||
|
const outboxStats = useOutboxStats()
|
||||||
|
const webhooks = useWebhookSubscriptions()
|
||||||
|
|
||||||
const dictsCount = dictsQuery.data?.length ?? 0
|
const dictsCount = dictsQuery.data?.length ?? 0
|
||||||
const myDraftsCount = myDrafts.data?.totalElements ?? 0
|
const myDraftsCount = myDrafts.data?.totalElements ?? 0
|
||||||
const reviewsCount = reviews.total
|
const reviewsCount = reviews.total
|
||||||
|
const outboxPending = outboxStats.data?.pending ?? 0
|
||||||
|
const webhooksActive =
|
||||||
|
webhooks.data?.filter((w) => w.active).length ?? 0
|
||||||
|
|
||||||
// Flat list per redesign — same as desktop Sidebar above.
|
// Flat list per redesign — same as desktop Sidebar above.
|
||||||
const sections: NavSection[] = [
|
const sections: NavSection[] = [
|
||||||
@@ -424,8 +456,20 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () =>
|
|||||||
badge: reviewsCount > 0 ? reviewsCount : undefined,
|
badge: reviewsCount > 0 ? reviewsCount : undefined,
|
||||||
authRequired: true,
|
authRequired: true,
|
||||||
},
|
},
|
||||||
{ to: '/outbox', label: t('nav.outbox'), icon: Inbox, authRequired: true },
|
{
|
||||||
{ to: '/webhooks', label: t('nav.webhooks'), icon: Webhook, authRequired: true },
|
to: '/outbox',
|
||||||
|
label: t('nav.outbox'),
|
||||||
|
icon: Inbox,
|
||||||
|
badge: outboxPending > 0 ? outboxPending : undefined,
|
||||||
|
authRequired: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/webhooks',
|
||||||
|
label: t('nav.webhooks'),
|
||||||
|
icon: Webhook,
|
||||||
|
badge: webhooksActive > 0 ? webhooksActive : undefined,
|
||||||
|
authRequired: true,
|
||||||
|
},
|
||||||
{ to: '/audit', label: t('nav.audit'), icon: ScrollText, authRequired: true },
|
{ to: '/audit', label: t('nav.audit'), icon: ScrollText, authRequired: true },
|
||||||
{ to: '/search', label: t('nav.search'), icon: Search },
|
{ to: '/search', label: t('nav.search'), icon: Search },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -268,7 +268,12 @@ i18n
|
|||||||
'audit.col.dictionary': 'Справочник',
|
'audit.col.dictionary': 'Справочник',
|
||||||
'audit.col.businessKey': 'Бизнес-ключ',
|
'audit.col.businessKey': 'Бизнес-ключ',
|
||||||
'audit.col.user': 'Пользователь',
|
'audit.col.user': 'Пользователь',
|
||||||
'audit.col.scope': 'Scope',
|
// 'Scope' колонка показывает scope актора (claim из JWT), а НЕ
|
||||||
|
// scope ресурса. Backend audit log не хранит scope dictionary'я.
|
||||||
|
// User confusion: 'правки в public были, а тут RESTRICTED' —
|
||||||
|
// потому что у пользователя scope=RESTRICTED привилегия. Label
|
||||||
|
// 'Доступ актора' разъясняет это.
|
||||||
|
'audit.col.scope': 'Доступ актора',
|
||||||
'audit.col.trace': 'Trace',
|
'audit.col.trace': 'Trace',
|
||||||
'audit.col.diff': 'Изменения',
|
'audit.col.diff': 'Изменения',
|
||||||
'audit.action.expand': 'Раскрыть',
|
'audit.action.expand': 'Раскрыть',
|
||||||
@@ -972,7 +977,7 @@ i18n
|
|||||||
'audit.col.dictionary': 'Dictionary',
|
'audit.col.dictionary': 'Dictionary',
|
||||||
'audit.col.businessKey': 'Business key',
|
'audit.col.businessKey': 'Business key',
|
||||||
'audit.col.user': 'User',
|
'audit.col.user': 'User',
|
||||||
'audit.col.scope': 'Scope',
|
'audit.col.scope': 'Actor scope',
|
||||||
'audit.col.trace': 'Trace',
|
'audit.col.trace': 'Trace',
|
||||||
'audit.col.diff': 'Diff',
|
'audit.col.diff': 'Diff',
|
||||||
'audit.action.expand': 'Expand',
|
'audit.action.expand': 'Expand',
|
||||||
|
|||||||
Reference in New Issue
Block a user