feat(service-integrations): phase 2 — integration widget & app form UI

- Add 6 renderer components: StatCard, Gauge, List, Progress, AlertBanner, Chart
- Add IntegrationWidget container with auto-refresh, loading, error states
- Add IntegrationAlertOverlay for layout-level critical alerts
- Add IntegrationConfigFields for dynamic form generation from Zod schemas
- Register integration type in WidgetRenderer
- Extend WidgetCreationForm with integration app/endpoint pickers
- Extend AppForm with integration config section and test connection button
- Add /api/integrations/alerts endpoint
This commit is contained in:
2026-03-25 22:07:51 +03:00
parent 114dee57a8
commit 50e8519220
25 changed files with 1360 additions and 1 deletions
@@ -0,0 +1,57 @@
<script lang="ts">
import type { IntegrationFieldDescriptor } from '$lib/server/integrations/types.js';
interface Props {
fields: IntegrationFieldDescriptor[];
values: Record<string, unknown>;
onchange: (name: string, value: unknown) => void;
idPrefix?: string;
}
let { fields, values, onchange, idPrefix = 'int' }: Props = $props();
const inputClass = 'w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30';
</script>
<div class="space-y-3">
{#each fields as field (field.name)}
<div>
<label for="{idPrefix}-{field.name}" class="mb-1 block text-sm font-medium text-card-foreground">
{field.label}
{#if field.required}
<span class="text-destructive">*</span>
{/if}
</label>
{#if field.type === 'boolean'}
<label class="flex items-center gap-2">
<input
id="{idPrefix}-{field.name}"
type="checkbox"
checked={!!values[field.name]}
onchange={(e) => onchange(field.name, e.currentTarget.checked)}
class="h-4 w-4 rounded border-input accent-primary"
/>
<span class="text-sm text-muted-foreground">{field.description ?? ''}</span>
</label>
{:else if field.type === 'number'}
<input
id="{idPrefix}-{field.name}"
type="number"
value={values[field.name] ?? ''}
oninput={(e) => onchange(field.name, parseInt(e.currentTarget.value) || 0)}
class={inputClass}
placeholder={field.description ?? ''}
/>
{:else}
<input
id="{idPrefix}-{field.name}"
type={field.name.toLowerCase().includes('password') || field.name.toLowerCase().includes('secret') ? 'password' : 'text'}
value={values[field.name] ?? ''}
oninput={(e) => onchange(field.name, e.currentTarget.value)}
class={inputClass}
placeholder={field.description ?? field.label}
/>
{/if}
</div>
{/each}
</div>