From f2cf34e2fc96ca8477f1209160af4a2ac9bc9c04 Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Wed, 6 May 2026 21:34:04 +0300 Subject: [PATCH] =?UTF-8?q?feat(webhook):=20delivery=20list=20=E2=80=94=20?= =?UTF-8?q?server-side=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=20=D0=BF?= =?UTF-8?q?=D0=BE=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раньше при page=50 admin не мог найти редкий dlq среди delivered'ов: client-side filter показал бы 1 совпадение из 50, а на page=1 могли быть ещё. Делаем server-side через ?status=. Backend: - WebhookDeliveryRepository.findBySubscriptionIdAndStatusOrderByCreatedAtDesc — Spring Data derived query, JPA генерирует SQL. - GET /admin/webhooks/subscriptions/{id}/deliveries?status=dlq — опциональный param. Invalid status → 400 OrdinisException. Allowed: pending, retrying, delivered, dlq. UI: - 4 chip'а pending/retrying/delivered/dlq над таблицей deliveries (одинаковый паттерн со scope-чипами в dictionaries). - Click toggle + reset page=0. 'Сбросить' появляется когда фильтр активен. - Status в URL пока не персистится — для одной session-страницы не критично. --- ordinis-admin-ui/src/api/queries.ts | 22 ++++++--- ordinis-admin-ui/src/i18n.ts | 4 ++ ordinis-admin-ui/src/routes/webhooks.$id.tsx | 45 ++++++++++++++++++- .../webhook/WebhookDeliveryRepository.java | 4 ++ .../WebhookSubscriptionController.java | 24 ++++++++-- 5 files changed, 89 insertions(+), 10 deletions(-) diff --git a/ordinis-admin-ui/src/api/queries.ts b/ordinis-admin-ui/src/api/queries.ts index 835897a..fe049a9 100644 --- a/ordinis-admin-ui/src/api/queries.ts +++ b/ordinis-admin-ui/src/api/queries.ts @@ -220,21 +220,33 @@ export const useWebhookSubscription = (id: string) => enabled: Boolean(id), }) -export const webhookDeliveriesQuery = (id: string, page: number, size: number) => +export const webhookDeliveriesQuery = ( + id: string, + page: number, + size: number, + statusFilter?: string, +) => queryOptions({ - queryKey: ['webhooks', 'deliveries', id, page, size] as const, + queryKey: ['webhooks', 'deliveries', id, page, size, statusFilter ?? null] as const, queryFn: async (): Promise => { + const params: Record = { page, size } + if (statusFilter) params.status = statusFilter const { data } = await apiClient.get( `/admin/webhooks/subscriptions/${encodeURIComponent(id)}/deliveries`, - { params: { page, size } }, + { params }, ) return data }, }) -export const useWebhookDeliveries = (id: string, page: number, size: number) => +export const useWebhookDeliveries = ( + id: string, + page: number, + size: number, + statusFilter?: string, +) => useQuery({ - ...webhookDeliveriesQuery(id, page, size), + ...webhookDeliveriesQuery(id, page, size, statusFilter), enabled: Boolean(id), }) diff --git a/ordinis-admin-ui/src/i18n.ts b/ordinis-admin-ui/src/i18n.ts index 5c0a8fc..4b99b7e 100644 --- a/ordinis-admin-ui/src/i18n.ts +++ b/ordinis-admin-ui/src/i18n.ts @@ -71,6 +71,8 @@ i18n 'webhooks.deliveries.col.lastError': 'Последняя ошибка', 'webhooks.deliveries.action.retry': 'Повторить доставку', 'webhooks.deliveries.confirmRetry': 'Сбросить attemptCount=0 и поставить в очередь повтор? Receiver получит payload снова.', + 'webhooks.deliveries.filter.label': 'Фильтр по статусу', + 'webhooks.deliveries.filter.clear': 'Сбросить', 'audit.title': 'Журнал аудита', 'audit.subtitle': 'Юридически значимый лог изменений НСИ', 'audit.filter.dictionary': 'Справочник', @@ -327,6 +329,8 @@ i18n 'webhooks.deliveries.col.lastError': 'Last error', 'webhooks.deliveries.action.retry': 'Retry delivery', 'webhooks.deliveries.confirmRetry': 'Reset attemptCount=0 and queue a retry? The receiver will get the payload again.', + 'webhooks.deliveries.filter.label': 'Status filter', + 'webhooks.deliveries.filter.clear': 'Clear', 'audit.title': 'Audit log', 'audit.subtitle': 'Legally significant log of NSI changes', 'audit.filter.dictionary': 'Dictionary', diff --git a/ordinis-admin-ui/src/routes/webhooks.$id.tsx b/ordinis-admin-ui/src/routes/webhooks.$id.tsx index 7355126..2280f38 100644 --- a/ordinis-admin-ui/src/routes/webhooks.$id.tsx +++ b/ordinis-admin-ui/src/routes/webhooks.$id.tsx @@ -53,7 +53,12 @@ function WebhookDetailPage() { const { t } = useTranslation() const sub = useWebhookSubscription(id) const [page, setPage] = useState(0) - const deliveries = useWebhookDeliveries(id, page, 50) + const [statusFilter, setStatusFilter] = useState(undefined) + const deliveries = useWebhookDeliveries(id, page, 50, statusFilter) + const handleStatusToggle = (s: string) => { + setPage(0) + setStatusFilter((curr) => (curr === s ? undefined : s)) + } const rotateMut = useRotateWebhookSecret() const deleteMut = useDeleteWebhook() const retryMut = useRetryWebhookDelivery() @@ -213,6 +218,44 @@ function WebhookDetailPage() { /> +
+ {(['pending', 'retrying', 'delivered', 'dlq'] as const).map((s) => { + const active = statusFilter === s + return ( + + ) + })} + {statusFilter && ( + + )} +
+ {deliveries.isLoading ? ( ) : !deliveries.data || deliveries.data.content.length === 0 ? ( diff --git a/ordinis-domain/src/main/java/cloud/nstart/terravault/ordinis/domain/webhook/WebhookDeliveryRepository.java b/ordinis-domain/src/main/java/cloud/nstart/terravault/ordinis/domain/webhook/WebhookDeliveryRepository.java index 4db3d02..84f626a 100644 --- a/ordinis-domain/src/main/java/cloud/nstart/terravault/ordinis/domain/webhook/WebhookDeliveryRepository.java +++ b/ordinis-domain/src/main/java/cloud/nstart/terravault/ordinis/domain/webhook/WebhookDeliveryRepository.java @@ -54,4 +54,8 @@ public interface WebhookDeliveryRepository extends JpaRepository findBySubscriptionIdOrderByCreatedAtDesc( UUID subscriptionId, Pageable pageable); + + /** История доставок одной подписки фильтрованная по status. */ + Page findBySubscriptionIdAndStatusOrderByCreatedAtDesc( + UUID subscriptionId, WebhookDelivery.Status status, Pageable pageable); } diff --git a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/web/webhook/WebhookSubscriptionController.java b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/web/webhook/WebhookSubscriptionController.java index b2c9a27..47bbfb7 100644 --- a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/web/webhook/WebhookSubscriptionController.java +++ b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/web/webhook/WebhookSubscriptionController.java @@ -127,11 +127,27 @@ public class WebhookSubscriptionController { public Page deliveries( @PathVariable UUID id, @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "50") int size) { + @RequestParam(defaultValue = "50") int size, + @RequestParam(value = "status", required = false) String statusFilter) { requireInternal(); - Page deliveries = deliveryRepository - .findBySubscriptionIdOrderByCreatedAtDesc( - id, PageRequest.of(page, Math.min(size, 200))); + PageRequest pr = PageRequest.of(page, Math.min(size, 200)); + Page deliveries; + if (statusFilter != null && !statusFilter.isBlank()) { + WebhookDelivery.Status s; + try { + s = WebhookDelivery.Status.valueOf(statusFilter.toLowerCase()); + } catch (IllegalArgumentException e) { + throw OrdinisException.badRequest( + "webhook_status_invalid", + "Unknown delivery status: " + statusFilter + + ". Allowed: pending, retrying, delivered, dlq."); + } + deliveries = deliveryRepository + .findBySubscriptionIdAndStatusOrderByCreatedAtDesc(id, s, pr); + } else { + deliveries = deliveryRepository + .findBySubscriptionIdOrderByCreatedAtDesc(id, pr); + } return deliveries.map(WebhookDeliveryResponse::from); }