feat(webhook): Phase 4 — admin-ui /webhooks page

Routes:
- /webhooks (layout) → /webhooks/index (list) + /webhooks/$id (detail)
- nav link добавлен в __root.tsx между Outbox и Language switch

List page (webhooks.index.tsx):
- Table: name | URL | scope filter (colored dots) | dictionaries | status
- ScopeChips reuse SCOPE_DOT из lib/scope-style.ts (consistency с
  dictionaries scope coloring)
- Create dialog: name, URL, scope filter (MultiSelect), dictionary &
  event type (CSV inputs), description. Client-side validation
  (URL scheme, name required) дублирует server-side @Pattern.
- Secret reveal modal после create: warning + plaintext + copy button.
  Single-time view — после закрытия secret не получить, только rotate.

Detail page (webhooks.$id.tsx):
- Subscription metadata grid (URL, status, filters, masked secret,
  createdBy + timestamp)
- Action buttons: rotate-secret + delete (с confirmation)
- Delivery history table: outboxEventId | status badge (delivered/
  retrying/pending/dlq) | attempts | last attempt | HTTP code | error
- Pagination 50/page
- Status badges: delivered=success, retrying=warning, pending=neutral,
  dlq=error

API client (queries + mutations):
- 4 queries: subscriptions list, subscription by id, deliveries page,
  DLQ page (last unused в UI пока, но готов для admin DLQ tab)
- 4 mutations: create, update, delete, rotateSecret. Все invalidate
  правильные query keys.

i18n: 50 ключей × 2 локали (ru-RU + en-US) под webhooks.*
This commit is contained in:
Zimin A.N.
2026-05-06 15:22:28 +03:00
parent e9a7278709
commit 96bfac174d
9 changed files with 1078 additions and 1 deletions
+77 -1
View File
@@ -9,13 +9,21 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as WebhooksRouteImport } from './routes/webhooks'
import { Route as OutboxRouteImport } from './routes/outbox'
import { Route as DictionariesRouteImport } from './routes/dictionaries'
import { Route as AuditRouteImport } from './routes/audit'
import { Route as IndexRouteImport } from './routes/index'
import { Route as WebhooksIndexRouteImport } from './routes/webhooks.index'
import { Route as DictionariesIndexRouteImport } from './routes/dictionaries.index'
import { Route as WebhooksIdRouteImport } from './routes/webhooks.$id'
import { Route as DictionariesNameRouteImport } from './routes/dictionaries.$name'
const WebhooksRoute = WebhooksRouteImport.update({
id: '/webhooks',
path: '/webhooks',
getParentRoute: () => rootRouteImport,
} as any)
const OutboxRoute = OutboxRouteImport.update({
id: '/outbox',
path: '/outbox',
@@ -36,11 +44,21 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const WebhooksIndexRoute = WebhooksIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => WebhooksRoute,
} as any)
const DictionariesIndexRoute = DictionariesIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => DictionariesRoute,
} as any)
const WebhooksIdRoute = WebhooksIdRouteImport.update({
id: '/$id',
path: '/$id',
getParentRoute: () => WebhooksRoute,
} as any)
const DictionariesNameRoute = DictionariesNameRouteImport.update({
id: '/$name',
path: '/$name',
@@ -52,15 +70,20 @@ export interface FileRoutesByFullPath {
'/audit': typeof AuditRoute
'/dictionaries': typeof DictionariesRouteWithChildren
'/outbox': typeof OutboxRoute
'/webhooks': typeof WebhooksRouteWithChildren
'/dictionaries/$name': typeof DictionariesNameRoute
'/webhooks/$id': typeof WebhooksIdRoute
'/dictionaries/': typeof DictionariesIndexRoute
'/webhooks/': typeof WebhooksIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/audit': typeof AuditRoute
'/outbox': typeof OutboxRoute
'/dictionaries/$name': typeof DictionariesNameRoute
'/webhooks/$id': typeof WebhooksIdRoute
'/dictionaries': typeof DictionariesIndexRoute
'/webhooks': typeof WebhooksIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
@@ -68,8 +91,11 @@ export interface FileRoutesById {
'/audit': typeof AuditRoute
'/dictionaries': typeof DictionariesRouteWithChildren
'/outbox': typeof OutboxRoute
'/webhooks': typeof WebhooksRouteWithChildren
'/dictionaries/$name': typeof DictionariesNameRoute
'/webhooks/$id': typeof WebhooksIdRoute
'/dictionaries/': typeof DictionariesIndexRoute
'/webhooks/': typeof WebhooksIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
@@ -78,18 +104,31 @@ export interface FileRouteTypes {
| '/audit'
| '/dictionaries'
| '/outbox'
| '/webhooks'
| '/dictionaries/$name'
| '/webhooks/$id'
| '/dictionaries/'
| '/webhooks/'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/audit' | '/outbox' | '/dictionaries/$name' | '/dictionaries'
to:
| '/'
| '/audit'
| '/outbox'
| '/dictionaries/$name'
| '/webhooks/$id'
| '/dictionaries'
| '/webhooks'
id:
| '__root__'
| '/'
| '/audit'
| '/dictionaries'
| '/outbox'
| '/webhooks'
| '/dictionaries/$name'
| '/webhooks/$id'
| '/dictionaries/'
| '/webhooks/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
@@ -97,10 +136,18 @@ export interface RootRouteChildren {
AuditRoute: typeof AuditRoute
DictionariesRoute: typeof DictionariesRouteWithChildren
OutboxRoute: typeof OutboxRoute
WebhooksRoute: typeof WebhooksRouteWithChildren
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/webhooks': {
id: '/webhooks'
path: '/webhooks'
fullPath: '/webhooks'
preLoaderRoute: typeof WebhooksRouteImport
parentRoute: typeof rootRouteImport
}
'/outbox': {
id: '/outbox'
path: '/outbox'
@@ -129,6 +176,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/webhooks/': {
id: '/webhooks/'
path: '/'
fullPath: '/webhooks/'
preLoaderRoute: typeof WebhooksIndexRouteImport
parentRoute: typeof WebhooksRoute
}
'/dictionaries/': {
id: '/dictionaries/'
path: '/'
@@ -136,6 +190,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DictionariesIndexRouteImport
parentRoute: typeof DictionariesRoute
}
'/webhooks/$id': {
id: '/webhooks/$id'
path: '/$id'
fullPath: '/webhooks/$id'
preLoaderRoute: typeof WebhooksIdRouteImport
parentRoute: typeof WebhooksRoute
}
'/dictionaries/$name': {
id: '/dictionaries/$name'
path: '/$name'
@@ -160,11 +221,26 @@ const DictionariesRouteWithChildren = DictionariesRoute._addFileChildren(
DictionariesRouteChildren,
)
interface WebhooksRouteChildren {
WebhooksIdRoute: typeof WebhooksIdRoute
WebhooksIndexRoute: typeof WebhooksIndexRoute
}
const WebhooksRouteChildren: WebhooksRouteChildren = {
WebhooksIdRoute: WebhooksIdRoute,
WebhooksIndexRoute: WebhooksIndexRoute,
}
const WebhooksRouteWithChildren = WebhooksRoute._addFileChildren(
WebhooksRouteChildren,
)
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AuditRoute: AuditRoute,
DictionariesRoute: DictionariesRouteWithChildren,
OutboxRoute: OutboxRoute,
WebhooksRoute: WebhooksRouteWithChildren,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)