711f218622
Comprehensive pre-production sweep across the Aurora redesign — drives svelte-check to 0 errors / 0 warnings (was 61) without changing visual intent. Highlights: - Mobile: hero title shrinks at 480px, signal-list stacks timestamp under sentence below 640px, sidebar icon buttons bumped to 40x40 - Light theme: muted-foreground darkened to #3a3560 to clear WCAG AA on glass surfaces and the modal close button - Perf: topbar backdrop-filter 28→14px, mobile-more sheet 28→12px to cut concurrent blur layers on mid-tier mobile - a11y: prefers-reduced-motion mute for aurora drift / pulses / shimmer / stagger; aria-label on every icon-only button; aria-describedby on Hint; combobox/listbox/aria-activedescendant on SearchPalette; modal dialog tabindex; 47 label-without-control warnings across 14 form pages cleaned up via for=/id= or label→div - Dashboard derived state split into topology- vs status-bound layers so polling no longer re-runs the full provider/wires computation - Mobile bottom nav derived from baseNavEntries by key lookup so adding a top-level nav entry keeps the two trees in sync - Bug: template-configs page now respects the global provider filter for both the count meter and the type pill (was reading the unfiltered cache) - Misc: portal EventChart tooltip and switch its swatches to Aurora tokens; CollapsibleSlot warning state uses warning-fg/-bg tokens instead of #d97706; Hint z-index 99999→9999; element refs across Modal/EntitySelect/MultiEntitySelect/SearchPalette/IconGridSelect/ Hint/targets converted to \$state for reactivity; 4 dead .topbar-cta selectors removed
137 lines
5.6 KiB
Svelte
137 lines
5.6 KiB
Svelte
<script lang="ts">
|
|
import { api } from '$lib/api';
|
|
import { t } from '$lib/i18n';
|
|
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
|
import Card from '$lib/components/Card.svelte';
|
|
import IconPicker from '$lib/components/IconPicker.svelte';
|
|
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
|
|
import { goto } from '$app/navigation';
|
|
import { providerTypeItems, webhookAuthModeItems } from '$lib/grid-items';
|
|
|
|
const gridItemSources: Record<string, () => any[]> = { webhookAuthModeItems };
|
|
import { getDescriptor, buildProviderFormDefaults } from '$lib/providers';
|
|
import Button from '$lib/components/Button.svelte';
|
|
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
|
|
|
|
let form = $state(buildProviderFormDefaults());
|
|
let error = $state('');
|
|
let testing = $state(false);
|
|
let saving = $state(false);
|
|
let descriptor = $derived(getDescriptor(form.type));
|
|
|
|
async function testAndSave() {
|
|
const desc = descriptor;
|
|
if (!desc) { error = t('providers.selectType'); return; }
|
|
const { config, error: buildError } = desc.buildConfig(form, false);
|
|
if (buildError) { error = t(buildError); snackError(error); return; }
|
|
|
|
testing = true; error = '';
|
|
let createdId: number | null = null;
|
|
try {
|
|
const provider = await api('/providers', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ type: form.type, name: form.name || desc.defaultName, icon: form.icon, config }),
|
|
});
|
|
createdId = provider.id;
|
|
const result = await api(`/providers/${provider.id}/test`, { method: 'POST' });
|
|
if (!result.ok) {
|
|
await api(`/providers/${provider.id}`, { method: 'DELETE' }).catch(() => {});
|
|
createdId = null;
|
|
error = result.message || t('providers.testFailed');
|
|
snackError(error);
|
|
} else {
|
|
snackSuccess(t('snack.providerSaved'));
|
|
goto('/providers');
|
|
}
|
|
} catch (e: any) {
|
|
if (createdId) await api(`/providers/${createdId}`, { method: 'DELETE' }).catch(() => {});
|
|
error = e.message || t('providers.testFailed'); snackError(error);
|
|
}
|
|
finally { testing = false; }
|
|
}
|
|
|
|
async function saveWithoutTest() {
|
|
const desc = descriptor;
|
|
if (!desc) { error = t('providers.selectType'); return; }
|
|
const { config, error: buildError } = desc.buildConfig(form, false);
|
|
if (buildError) { error = t(buildError); snackError(error); return; }
|
|
|
|
saving = true; error = '';
|
|
try {
|
|
await api('/providers', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ type: form.type, name: form.name || desc.defaultName, icon: form.icon, config }),
|
|
});
|
|
snackSuccess(t('snack.providerSaved'));
|
|
goto('/providers');
|
|
} catch (e: any) { error = e.message || t('common.saveFailed'); snackError(error); }
|
|
finally { saving = false; }
|
|
}
|
|
</script>
|
|
|
|
<div class="mb-4">
|
|
<a href="/providers" class="text-sm text-[var(--color-muted-foreground)] hover:underline">← {t('providers.title')}</a>
|
|
</div>
|
|
|
|
<h2 class="text-xl font-semibold mb-8">{t('providers.addProvider')}</h2>
|
|
|
|
<Card>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<div class="block text-sm font-medium mb-1">{t('providers.type')}</div>
|
|
<IconGridSelect items={providerTypeItems()} bind:value={form.type} columns={2} />
|
|
</div>
|
|
<div>
|
|
<label for="prv-name" class="block text-sm font-medium mb-1">{t('providers.name')}</label>
|
|
<div class="flex gap-2">
|
|
<IconPicker value={form.icon} onselect={(v: string) => form.icon = v} />
|
|
<input id="prv-name" bind:value={form.name} placeholder={descriptor?.defaultName || ''} class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
|
</div>
|
|
</div>
|
|
|
|
{#if descriptor?.hasUrl}
|
|
<div>
|
|
<label for="prv-url" class="block text-sm font-medium mb-1">{t('providers.url')}</label>
|
|
<input id="prv-url" type="url" bind:value={form.url} required placeholder={descriptor.urlPlaceholder || t('providers.urlPlaceholder')} class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
|
</div>
|
|
{/if}
|
|
|
|
{#each descriptor?.configFields ?? [] as field (field.key)}
|
|
<div>
|
|
<label for="prv-{field.key}" class="block text-sm font-medium mb-1">
|
|
{t(field.label)}
|
|
{#if field.optional}<span class="text-xs text-[var(--color-muted-foreground)]">({t('providers.optional')})</span>{/if}
|
|
</label>
|
|
{#if field.type === 'grid-select' && field.gridItems}
|
|
<IconGridSelect items={gridItemSources[field.gridItems]()} bind:value={form[field.key]} columns={field.gridColumns ?? 2} compact />
|
|
{:else if field.type === 'number'}
|
|
<input id="prv-{field.key}" type="number" bind:value={form[field.key]} min={field.min} max={field.max}
|
|
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
|
{:else}
|
|
<input id="prv-{field.key}" type={field.type} bind:value={form[field.key]}
|
|
required={field.required === true || field.required === 'create-only'}
|
|
placeholder={field.placeholder || ''} autocomplete="off"
|
|
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
|
{/if}
|
|
{#if field.hint}
|
|
<p class="text-xs text-[var(--color-muted-foreground)] mt-1">{t(field.hint)}</p>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
|
|
<ErrorBanner message={error} />
|
|
|
|
<div class="flex gap-3 pt-2">
|
|
<Button onclick={testAndSave} disabled={testing || saving}>
|
|
{testing ? t('providers.connecting') : t('providers.testAndSave')}
|
|
</Button>
|
|
<Button variant="secondary" onclick={saveWithoutTest} disabled={testing || saving}>
|
|
{saving ? t('common.loading') : t('providers.saveWithoutTest')}
|
|
</Button>
|
|
<Button variant="secondary" href="/providers">
|
|
{t('common.cancel')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|