diff --git a/src/components/layout/MainContent.tsx b/src/components/layout/MainContent.tsx index 02243f2..90a0955 100644 --- a/src/components/layout/MainContent.tsx +++ b/src/components/layout/MainContent.tsx @@ -18,6 +18,7 @@ import { DynamicViewPage } from '@/components/views/DynamicViewPage'; import { LoadingFallback } from '@/components/common/LoadingFallback'; import { LegacyProtocolsBanner } from '@/features/hardening/LegacyProtocolsBanner'; import { SpamSettingsPage } from '@/features/spamLevel/SpamSettingsPage'; +import { WebhookTestCard } from '@/features/events/WebhookTestCard'; import { WebhooksPage } from '@/features/events/WebhooksPage'; import { ListenersPage } from '@/features/ports/ListenersPage'; import { MetricsPage } from '@/features/metricsConnect/MetricsPage'; @@ -311,7 +312,11 @@ function renderView(schema: Schema | null, viewName?: string, id?: string, secti } const canUpdate = useAccountStore.getState().hasObjectPermission(resolved.permissionPrefix, 'Update'); const page = canUpdate ? ( - + : undefined} + /> ) : ( ); diff --git a/src/features/events/WebhookTestCard.tsx b/src/features/events/WebhookTestCard.tsx new file mode 100644 index 0000000..c4223f3 --- /dev/null +++ b/src/features/events/WebhookTestCard.tsx @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2026 Coffey Labs + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * inbuxa: "Send test" on a saved webhook (settings-reorg, Webhooks). The + * server sends one sample event, of type webhook.test, the way it sends a + * real batch, and says what came back (POST /api/webhook/test). + */ + +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { CheckCircle2, Loader2, Send, XCircle } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { apiFetch } from '@/services/api'; +import { describeTestAnswer, type WebhookTestAnswer } from './webhookTest'; + +export function WebhookTestCard({ webhookId }: { webhookId: string }) { + const { t } = useTranslation(); + const [busy, setBusy] = useState(false); + const [answer, setAnswer] = useState(null); + + const send = async () => { + setBusy(true); + setAnswer(null); + try { + const res = await apiFetch('/api/webhook/test', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ webhookId }), + }); + if (!res.ok) { + const problem = (await res.json().catch(() => ({}))) as { detail?: string; title?: string }; + throw new Error(problem.detail ?? problem.title ?? `The test failed (${res.status}).`); + } + setAnswer((await res.json()) as WebhookTestAnswer); + } catch (e) { + setAnswer({ sent: false, error: e instanceof Error ? e.message : String(e) }); + } finally { + setBusy(false); + } + }; + + return ( +
+
+ +
+

{t('webhookTest.title', 'Send a test event')}

+

+ {t( + 'webhookTest.body', + 'One sample event of type webhook.test, sent with the saved settings, even while the webhook is off. Save changes first to test them.', + )} +

+
+ +
+ {answer && ( +

+ {answer.sent ? ( + + ) : ( + + )} + {describeTestAnswer(answer)} +

+ )} +
+ ); +} diff --git a/src/features/events/webhookTest.test.ts b/src/features/events/webhookTest.test.ts new file mode 100644 index 0000000..982d78b --- /dev/null +++ b/src/features/events/webhookTest.test.ts @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2026 Coffey Labs + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, it, expect } from 'vitest'; +import { describeTestAnswer } from './webhookTest'; + +describe('describeTestAnswer', () => { + it('says what came back', () => { + expect(describeTestAnswer({ sent: true, status: 204, ms: 84 })).toBe( + 'Delivered: the receiver answered 204 in 84 ms.', + ); + expect(describeTestAnswer({ sent: false, status: 403 })).toBe( + 'Not accepted: the receiver answered 403. Check the sign-in details or signature key.', + ); + expect(describeTestAnswer({ sent: false, error: 'connection refused' })).toBe('Not delivered: connection refused'); + }); +}); diff --git a/src/features/events/webhookTest.ts b/src/features/events/webhookTest.ts new file mode 100644 index 0000000..5a2a93e --- /dev/null +++ b/src/features/events/webhookTest.ts @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2026 Coffey Labs + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** inbuxa: what POST /api/webhook/test answers, and that answer in words. */ + +export interface WebhookTestAnswer { + sent: boolean; + status?: number; + ms?: number; + error?: string; +} + +/** The answer as a sentence. */ +export function describeTestAnswer(a: WebhookTestAnswer): string { + if (a.sent) return `Delivered: the receiver answered ${a.status}${a.ms !== undefined ? ` in ${a.ms} ms` : ''}.`; + if (a.status !== undefined) { + const hint = + a.status === 401 || a.status === 403 + ? ' Check the sign-in details or signature key.' + : a.status === 404 + ? ' Check the URL.' + : ''; + return `Not accepted: the receiver answered ${a.status}.${hint}`; + } + return `Not delivered: ${a.error ?? 'no answer'}`; +}