import { createSignal, createEffect, Show, For, Index } from 'solid-js'; import { NotificationsAPI, Webhook } from '@/api/notifications'; import { formField, labelClass, controlClass, formHelpText, formCheckbox, } from '@/components/shared/Form'; interface WebhookTemplate { service: string; name: string; urlPattern: string; method: string; headers: Record; payloadTemplate: string; instructions: string; } interface WebhookConfigProps { webhooks: Webhook[]; onAdd: (webhook: Omit) => void; onUpdate: (webhook: Webhook) => void; onDelete: (id: string) => void; onTest: (id: string, webhookData?: Omit) => void; testing?: string | null; } type HeaderInput = { id: string; key: string; value: string }; type CustomFieldPreset = { key: string; label: string; placeholder?: string; required?: boolean; }; type CustomFieldInput = HeaderInput & { label?: string; placeholder?: string; required?: boolean; }; const customFieldPresets: Record = { pushover: [ { key: 'app_token', label: 'Application Token', placeholder: 'Your Pushover application token', required: true, }, { key: 'user_token', label: 'User Key', placeholder: 'Primary user key or group key', required: true, }, ], }; const buildMapFromInputs = (inputs: Array<{ key: string; value: string }>): Record => { const map: Record = {}; inputs.forEach(({ key, value }) => { if (key) { map[key] = value; } }); return map; }; const createCustomFieldInputs = ( service: string, existing: Record = {}, ): CustomFieldInput[] => { const presets = customFieldPresets[service]; const timestamp = Date.now(); if (!presets) { return Object.entries(existing).map(([key, value], index) => ({ id: `custom-${key}-${timestamp}-${index}`, key, value, })); } const inputs: CustomFieldInput[] = presets.map((preset, index) => ({ id: `custom-${preset.key}-${timestamp}-${index}`, key: preset.key, value: existing[preset.key] ?? '', label: preset.label, placeholder: preset.placeholder, required: preset.required, })); Object.entries(existing) .filter(([key]) => !presets.some((preset) => preset.key === key)) .forEach(([key, value], index) => { inputs.push({ id: `custom-${key}-${timestamp}-${presets.length + index}`, key, value, }); }); return inputs; }; export function WebhookConfig(props: WebhookConfigProps) { const [adding, setAdding] = createSignal(false); const [editingId, setEditingId] = createSignal(null); const [formData, setFormData] = createSignal< Omit & { service: string; payloadTemplate?: string } >({ name: '', url: '', method: 'POST', service: 'generic', headers: { 'Content-Type': 'application/json' }, enabled: true, payloadTemplate: '', customFields: {}, }); const [templates, setTemplates] = createSignal([]); const [showServiceDropdown, setShowServiceDropdown] = createSignal(false); // Track header inputs separately to avoid focus loss const [headerInputs, setHeaderInputs] = createSignal([]); const [customFieldInputs, _setCustomFieldInputs] = createSignal([]); const setCustomFieldInputs = (inputs: CustomFieldInput[]) => { _setCustomFieldInputs(inputs); setFormData((prev) => ({ ...prev, customFields: buildMapFromInputs(inputs), })); }; const updateCustomFieldInputs = ( updater: (inputs: CustomFieldInput[]) => CustomFieldInput[], ) => { _setCustomFieldInputs((prev) => { const next = updater(prev); setFormData((prevForm) => ({ ...prevForm, customFields: buildMapFromInputs(next), })); return next; }); }; const ensurePresetCustomFields = (service: string) => { if (!customFieldPresets[service]) { return; } const existing = formData().customFields || {}; const inputs = createCustomFieldInputs(service, existing); setCustomFieldInputs(inputs); }; // Load webhook templates createEffect(async () => { try { const data = await NotificationsAPI.getWebhookTemplates(); setTemplates(data); } catch (err) { console.error('Failed to load webhook templates:', err); } }); const saveWebhook = () => { const data = formData(); if (!data.name || !data.url) return; // Build headers from headerInputs const headers: Record = {}; headerInputs().forEach((input) => { if (input.key) { headers[input.key] = input.value; } }); const customFields = buildMapFromInputs(customFieldInputs()); if (editingId()) { props.onUpdate({ ...data, id: editingId()!, headers, service: data.service, template: data.payloadTemplate, customFields, }); setEditingId(null); setAdding(false); setHeaderInputs([]); setCustomFieldInputs([]); } else { // onAdd expects a webhook without id, but with service const newWebhook: Omit = { name: data.name, url: data.url, method: data.method, headers, enabled: data.enabled, service: data.service, template: data.payloadTemplate, customFields, }; props.onAdd(newWebhook); // Reset form and close the adding panel setFormData({ name: '', url: '', method: 'POST', service: 'generic', headers: { 'Content-Type': 'application/json' }, enabled: true, payloadTemplate: '', customFields: {}, }); setHeaderInputs([]); setCustomFieldInputs([]); setAdding(false); } }; const cancelForm = () => { setAdding(false); setEditingId(null); setFormData({ name: '', url: '', method: 'POST', service: 'generic', headers: { 'Content-Type': 'application/json' }, enabled: true, payloadTemplate: '', customFields: {}, }); setHeaderInputs([]); setCustomFieldInputs([]); }; const editWebhook = (webhook: Webhook) => { if (webhook.id) { setEditingId(webhook.id); } setFormData({ ...webhook, service: webhook.service || 'generic', payloadTemplate: webhook.template || '', customFields: webhook.customFields || {}, }); // Set up header inputs for editing const headers = webhook.headers || {}; setHeaderInputs( Object.entries(headers).map(([key, value], index) => ({ id: `header-${Date.now()}-${index}`, key, value, })), ); const service = webhook.service || 'generic'; const existingCustomFields = webhook.customFields || {}; if (customFieldPresets[service] || Object.keys(existingCustomFields).length > 0) { setCustomFieldInputs(createCustomFieldInputs(service, existingCustomFields)); } else { setCustomFieldInputs([]); } setAdding(true); }; const selectService = (service: string) => { const template = templates().find((t) => t.service === service); if (template) { setFormData({ ...formData(), service: template.service, method: template.method, headers: { ...template.headers }, name: formData().name || template.name, // Clear the payload template when switching services // Only generic service should have custom payloads payloadTemplate: service === 'generic' ? formData().payloadTemplate : '', }); // Update header inputs when switching services const headers = template.headers || {}; setHeaderInputs( Object.entries(headers).map(([key, value], index) => ({ id: `header-${Date.now()}-${index}`, key, value, })), ); } else { setFormData({ ...formData(), service, }); } ensurePresetCustomFields(service); setShowServiceDropdown(false); }; const currentTemplate = () => templates().find((t) => t.service === formData().service); const serviceName = (service: string) => { const names: Record = { generic: 'Generic', discord: 'Discord', slack: 'Slack', telegram: 'Telegram', teams: 'Microsoft Teams', 'teams-adaptive': 'Teams (Adaptive)', pagerduty: 'PagerDuty', pushover: 'Pushover', gotify: 'Gotify', ntfy: 'ntfy', }; return names[service] || service; }; const toggleAllWebhooks = (enabled: boolean) => { props.webhooks.forEach((webhook) => { props.onUpdate({ ...webhook, enabled }); }); }; const allEnabled = () => props.webhooks.every((w) => w.enabled); const someEnabled = () => props.webhooks.some((w) => w.enabled); return (
{/* Existing Webhooks List */} 0}>
{/* Quick Actions Bar */}
{props.webhooks.filter((w) => w.enabled).length} of {props.webhooks.length} webhooks enabled
{(webhook) => (
{webhook.name}
{serviceName(webhook.service || 'generic')} {webhook.method} ID: {webhook.id || '—'}

{webhook.url}

)}
{/* Add/Edit Form */}
{/* Service Selection */}
{(service) => ( )}

Setup Instructions

{currentTemplate()!.instructions}
{/* Basic Configuration */}
setFormData({ ...formData(), name: e.currentTarget.value })} placeholder={currentTemplate()?.name || 'My Webhook'} class={controlClass('px-2 py-1.5')} />
setFormData({ ...formData(), url: e.currentTarget.value })} placeholder={currentTemplate()?.urlPattern || 'https://example.com/webhook'} class={controlClass('px-2 py-1.5 font-mono')} />
{/* Custom Payload Template - only show for generic service */}