feat(approval): Approval Workflow v2 Phase 4 monitoring + pending-review badges
Approval Workflow v2 — Phase 4: monitoring + UX visibility.
Backend metrics (Micrometer → Prometheus):
- Gauge nsi_draft_pending_total — global queue depth (polled на scrape).
- Counter nsi_draft_submit_total{dictionary,operation,outcome=ok|already_pending|error}
— submit attempts с разбивкой по outcome. Phase 3 lessons: error outcome
выявит schema bugs которые ранее маскировались.
- Counter nsi_draft_decision_total{dictionary,operation,outcome=approved|rejected|withdrawn}
— counts decisions, разбит по типу.
- Timer nsi_draft_review_duration_seconds — time-to-decide histogram (от
submit до approve/reject/withdraw). publishPercentileHistogram() — для
Prometheus histogram_quantile p95/p99.
DraftService:
- Optional<MeterRegistry> — null-safe в test ctor'ах.
- @PostConstruct registerGauges() — Gauge.builder polled от
RecordDraftRepository.countPending().
- recordDecision() helper в approve/reject/withdraw — записывает Timer +
Counter одной точкой.
- 4-arg ctor compat для Phase 1 unit tests.
UI (admin):
- useDictPendingDrafts — per-dict pending list, refetch 30s, gated на
detailQuery.data.approvalRequired (no-op для не-approval dicts).
- "На review" badge в businessKey cell records table — моментальная
видимость что есть pending draft. Open /reviews для act'а.
- i18n RU/EN: dict.pendingReview.label + tooltip.
Domain:
- RecordDraftRepository.countPending() — для Gauge polled count.
Tests: ordinis-rest-api unit 119/119, ordinis-app e2e 28/28, vitest 89/89.
This commit is contained in:
@@ -23,7 +23,13 @@ import {
|
||||
TextInput,
|
||||
} from '@nstart/ui'
|
||||
import { PlusIcon, PencilSimpleIcon, XCircleIcon, GearIcon, ClockCounterClockwiseIcon, MapTrifoldIcon, TrashIcon, CheckCircleIcon, WarningIcon, XIcon, DownloadSimpleIcon, CaretLeftIcon, CaretRightIcon } from '@phosphor-icons/react'
|
||||
import { useDictionaryDetail, useRecordRaw, useRecords, type RecordsFilter } from '@/api/queries'
|
||||
import {
|
||||
useDictionaryDetail,
|
||||
useDictPendingDrafts,
|
||||
useRecordRaw,
|
||||
useRecords,
|
||||
type RecordsFilter,
|
||||
} from '@/api/queries'
|
||||
import { useBulkCloseRecords, useBulkExportRecords, useCreateRecord, useUpdateRecord, useCloseRecord } from '@/api/mutations'
|
||||
import type { BulkCloseResponse, CreateRecordRequest, DataScope, FlattenedRecord } from '@/api/client'
|
||||
import { SchemaDrivenForm } from '@/components/form/SchemaDrivenForm'
|
||||
@@ -117,6 +123,19 @@ function DictionaryDetail() {
|
||||
}
|
||||
: undefined
|
||||
const recordsResult = useRecords(name, 'PUBLIC,INTERNAL,RESTRICTED', filter)
|
||||
/**
|
||||
* Approval Workflow v2 Phase 4: pending drafts на этот dict — для бейджа
|
||||
* "На review" в каждой строке records table. Polled 30s. Bypass'нем если
|
||||
* dict не approval-required (нет смысла грузить).
|
||||
*/
|
||||
const pendingDraftsQuery = useDictPendingDrafts(
|
||||
detailQuery.data?.approvalRequired ? name : undefined,
|
||||
)
|
||||
const pendingByKey = useMemo(() => {
|
||||
const set = new Set<string>()
|
||||
for (const d of pendingDraftsQuery.data ?? []) set.add(d.businessKey)
|
||||
return set
|
||||
}, [pendingDraftsQuery.data])
|
||||
const createMut = useCreateRecord(name)
|
||||
const updateMut = useUpdateRecord(name)
|
||||
const closeMut = useCloseRecord(name)
|
||||
@@ -672,7 +691,14 @@ function DictionaryDetail() {
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="font-mono text-2xs">{r.businessKey}</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-mono text-2xs">{r.businessKey}</span>
|
||||
{pendingByKey.has(r.businessKey) ? (
|
||||
<span title={t('dict.pendingReview.tooltip')}>
|
||||
<Badge variant="warning">{t('dict.pendingReview.label')}</Badge>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{recordDisplayName(
|
||||
|
||||
Reference in New Issue
Block a user