Webhooks: Send test on a saved webhook #54
No files matched your search
@@ -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 ? (
|
||||
<DynamicForm viewName={viewName} objectId={id} />
|
||||
<DynamicForm
|
||||
viewName={viewName}
|
||||
objectId={id}
|
||||
intro={resolved.objectName === 'x:WebHook' ? <WebhookTestCard webhookId={id} /> : undefined}
|
||||
/>
|
||||
) : (
|
||||
<DynamicViewPage viewName={viewName} objectId={id} />
|
||||
);
|
||||
|
||||
@@ -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<WebhookTestAnswer | null>(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 (
|
||||
<div className="mx-auto max-w-4xl space-y-3 rounded-xl border bg-card p-4">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Send className="h-5 w-5 text-primary" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">{t('webhookTest.title', 'Send a test event')}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{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.',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button type="button" variant="outline" onClick={() => void send()} disabled={busy}>
|
||||
{busy ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Send className="mr-2 h-4 w-4" />}
|
||||
{t('webhookTest.send', 'Send test')}
|
||||
</Button>
|
||||
</div>
|
||||
{answer && (
|
||||
<p
|
||||
role="status"
|
||||
className={`flex items-start gap-2 text-sm ${answer.sent ? 'text-green-600 dark:text-green-400' : 'text-destructive'}`}
|
||||
>
|
||||
{answer.sent ? (
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
) : (
|
||||
<XCircle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
)}
|
||||
{describeTestAnswer(answer)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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'}`;
|
||||
}
|
||||
Reference in new issue
Block a user