feat(changelog): wire up History tab to /changelog endpoint + diff modal

This commit is contained in:
Александр Зимин
2026-05-12 10:49:53 +00:00
parent ba04c2199e
commit 57be0d88d3
6 changed files with 614 additions and 38 deletions
+72
View File
@@ -482,3 +482,75 @@ export type WebhookTestPingResult = {
latencyMs?: number | null
message?: string | null
}
// === Changelog (v2.4.0 + full timeline + diff) ===
/**
* Frontend-friendly event kind. Backend mapping см.
* ChangelogService.mapEventTypeToKind. Используем для icon/color/label
* выбора в History tab. Versioning events
* (definition_create / definition_update / draft_publish) показываются
* с version label; promotional draft events идут без version.
*/
export type ChangelogKind =
| 'definition_create'
| 'definition_update'
| 'draft_create'
| 'draft_review'
| 'draft_approve'
| 'draft_reject'
| 'draft_changes_requested'
| 'draft_publish'
| 'draft_withdraw'
export type ChangelogPublisher = {
/** JWT sub claim (Keycloak user id). */
sub: string
/** Display name. Backend пока эхает sub; готовим место под user-resolve. */
name: string
}
export type ChangelogDiffSummary = {
added: string[]
removed: string[]
changed: string[]
}
export type ChangelogEntry = {
/** audit_log.id — deep-link на diff endpoint. */
id: number
/** Semver. null для promotional draft events. */
version: string | null
publishedAt: string
publishedBy: ChangelogPublisher
/** Header summary text для card UI. */
headers: string
diff: ChangelogDiffSummary
recordsAffected: number
isCurrent: boolean
kind: ChangelogKind
}
export type ChangelogResponse = {
dictionary: string
currentVersion: string
entries: ChangelogEntry[]
}
/**
* Full before/after schemas для side-by-side render. before=null для
* definition_create. Для draft_* (не publish) — after=proposed_schema,
* before=live schema на момент event'а (то что предлагалось vs реальность).
*/
export type ChangelogDiff = {
id: number
dictionaryName: string
/** Raw audit event_type (SCHEMA_UPDATED / SCHEMA_DRAFT_PUBLISHED / ...). */
eventType: string
kind: ChangelogKind
occurredAt: string
publishedBy: ChangelogPublisher
before: unknown | null
after: unknown | null
summary: ChangelogDiffSummary
}
+54
View File
@@ -4,6 +4,8 @@ import {
type AuditFilters,
type AuditPage,
type CascadePlan,
type ChangelogDiff,
type ChangelogResponse,
type DictionaryDefinition,
type DictionaryDetail,
type DlqPage,
@@ -138,6 +140,58 @@ export const useRecordHistory = (
enabled: Boolean(businessKey),
})
// === Changelog (schema versions timeline) ============================
/**
* Per-dict changelog. Backend default limit=50; покрывает типичные ~5-30
* schema versions без pagination. Cursor можно добавить позже без
* breaking change.
*/
export const changelogQuery = (dictionaryName: string, limit = 50) =>
queryOptions({
queryKey: ['changelog', dictionaryName, limit] as const,
queryFn: async (): Promise<ChangelogResponse> => {
const { data } = await apiClient.get<ChangelogResponse>(
`/dictionaries/${dictionaryName}/changelog`,
{ params: { limit } },
)
return data
},
})
export const useChangelog = (dictionaryName: string | undefined, limit = 50) =>
useQuery({
...changelogQuery(dictionaryName ?? '', limit),
enabled: Boolean(dictionaryName),
})
/**
* Full before/after diff payload для одного changelog entry'я. Lazy —
* fetch только когда юзер раскрывает Compare modal. Кешируем по
* (dict, entryId) — diff immutable, можно держать stale infinitely
* (audit_log row не меняется).
*/
export const changelogDiffQuery = (dictionaryName: string, entryId: number) =>
queryOptions({
queryKey: ['changelog-diff', dictionaryName, entryId] as const,
queryFn: async (): Promise<ChangelogDiff> => {
const { data } = await apiClient.get<ChangelogDiff>(
`/dictionaries/${dictionaryName}/changelog/${entryId}/diff`,
)
return data
},
staleTime: Infinity,
})
export const useChangelogDiff = (
dictionaryName: string | undefined,
entryId: number | undefined,
) =>
useQuery({
...changelogDiffQuery(dictionaryName ?? '', entryId ?? 0),
enabled: Boolean(dictionaryName) && Boolean(entryId),
})
export const recordRawQuery = (dictionaryName: string, businessKey: string) =>
queryOptions({
queryKey: ['record-raw', dictionaryName, businessKey] as const,