Merge branch 'fix/schema-review-queue-page-total-field' into 'main'
fix(sidebar,audit): badges fix + outbox/webhooks counts + clarify audit scope label See merge request 2-6/2-6-4/terravault/ordinis!159
This commit is contained in:
@@ -659,12 +659,20 @@ export type PublishSchemaDraftResult = {
|
|||||||
publishedAt: string
|
publishedAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pool queue для /admin/schema-reviews/pending. */
|
/** Pool queue для /admin/schema-reviews/pending.
|
||||||
|
*
|
||||||
|
* <p>Backend сериализует Spring {@code Page<SchemaDraft>} как:
|
||||||
|
* {@code {items, page, size, totalElements, totalPages}}. Раньше type
|
||||||
|
* объявлял {@code total} что было mismatched — consumers (`reviews.tsx`,
|
||||||
|
* `useReviewsBadgeCount`) читали {@code data.total} который === undefined,
|
||||||
|
* fallback'или на {@code items.length} (или 0 для badge — отсюда баг
|
||||||
|
* "бэйджа нет"). Type совпадает с фактическим контрактом backend'а. */
|
||||||
export type SchemaReviewQueuePage = {
|
export type SchemaReviewQueuePage = {
|
||||||
items: SchemaDraft[]
|
items: SchemaDraft[]
|
||||||
page: number
|
page: number
|
||||||
size: number
|
size: number
|
||||||
total: number
|
totalElements: number
|
||||||
|
totalPages: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChangelogDiff = {
|
export type ChangelogDiff = {
|
||||||
|
|||||||
@@ -647,7 +647,7 @@ export const useReviewsBadgeCount = (enabled = true) => {
|
|||||||
refetchInterval: 60_000,
|
refetchInterval: 60_000,
|
||||||
})
|
})
|
||||||
const recordCount = recordQ.data?.totalElements ?? 0
|
const recordCount = recordQ.data?.totalElements ?? 0
|
||||||
const schemaCount = schemaQ.data?.total ?? 0
|
const schemaCount = schemaQ.data?.totalElements ?? 0
|
||||||
return {
|
return {
|
||||||
total: recordCount + schemaCount,
|
total: recordCount + schemaCount,
|
||||||
recordCount,
|
recordCount,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function ReviewsPage() {
|
|||||||
const queue = useReviewQueue(0, 50)
|
const queue = useReviewQueue(0, 50)
|
||||||
const schemaQueue = useSchemaReviewQueue(0, 50)
|
const schemaQueue = useSchemaReviewQueue(0, 50)
|
||||||
const recordsCount = queue.data?.totalElements ?? 0
|
const recordsCount = queue.data?.totalElements ?? 0
|
||||||
const schemasCount = schemaQueue.data?.total ?? 0
|
const schemasCount = schemaQueue.data?.totalElements ?? 0
|
||||||
// Tab дефолтит на records (старее, чаще). Если schema queue не пустой
|
// Tab дефолтит на records (старее, чаще). Если schema queue не пустой
|
||||||
// а record queue пустой — переключаемся на schemas чтобы reviewer'у не
|
// а record queue пустой — переключаемся на schemas чтобы reviewer'у не
|
||||||
// пришлось кликать дважды.
|
// пришлось кликать дважды.
|
||||||
@@ -590,8 +590,8 @@ function SchemaReviewQueuePanel() {
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-cell text-mute mt-0.5">
|
<p className="text-cell text-mute mt-0.5">
|
||||||
{t('reviews.schema.queueTotal', {
|
{t('reviews.schema.queueTotal', {
|
||||||
count: queue.data?.total ?? items.length,
|
count: queue.data?.totalElements ?? items.length,
|
||||||
defaultValue: `${queue.data?.total ?? items.length} draft(s) ждут ревью`,
|
defaultValue: `${queue.data?.totalElements ?? items.length} draft(s) ждут ревью`,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user