feat(observability): phases 4-7 - complete frontend UI (big bang)
Add all frontend pages for observability & proxy management: - Proxy Viewer: /proxies with grouped view, filtering, health indicators - Proxy Creation: form with live validation, diagnostic hints, edit/delete - Stale Containers: /containers/stale with dashboard widget, cleanup actions - Event Log: /events with filters, pagination, real-time SSE streaming - Navigation: proxies and events links in sidebar - i18n: full EN/RU translations for all new features - Settings: stale threshold configuration
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
<script lang="ts">
|
||||
import type { StaleContainer } from '$lib/types';
|
||||
import * as api from '$lib/api';
|
||||
import StaleContainerCard from '$lib/components/StaleContainerCard.svelte';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import EmptyState from '$lib/components/EmptyState.svelte';
|
||||
import SkeletonCard from '$lib/components/SkeletonCard.svelte';
|
||||
import { IconTrash, IconLoader } from '$lib/components/icons';
|
||||
import { toasts } from '$lib/stores/toast';
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
let containers = $state<StaleContainer[]>([]);
|
||||
let loading = $state(true);
|
||||
let error = $state('');
|
||||
|
||||
let confirmSingleId = $state('');
|
||||
let confirmBulk = $state(false);
|
||||
let cleaningIds = $state<Set<string>>(new Set());
|
||||
let bulkCleaning = $state(false);
|
||||
|
||||
async function loadStale() {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
containers = await api.fetchStaleContainers();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : $t('stale.loadFailed');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function requestCleanup(id: string) {
|
||||
confirmSingleId = id;
|
||||
}
|
||||
|
||||
async function handleConfirmCleanup() {
|
||||
const id = confirmSingleId;
|
||||
confirmSingleId = '';
|
||||
cleaningIds = new Set([...cleaningIds, id]);
|
||||
try {
|
||||
await api.cleanupStaleContainer(id);
|
||||
containers = containers.filter((c) => c.id !== id);
|
||||
toasts.success($t('stale.cleanedUp'));
|
||||
} catch (e) {
|
||||
toasts.error(e instanceof Error ? e.message : $t('stale.cleanupFailed'));
|
||||
} finally {
|
||||
const next = new Set(cleaningIds);
|
||||
next.delete(id);
|
||||
cleaningIds = next;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfirmBulkCleanup() {
|
||||
confirmBulk = false;
|
||||
bulkCleaning = true;
|
||||
try {
|
||||
const result = await api.bulkCleanupStaleContainers();
|
||||
containers = [];
|
||||
toasts.success($t('stale.bulkCleanedUp', { count: String(result.deleted) }));
|
||||
} catch (e) {
|
||||
toasts.error(e instanceof Error ? e.message : $t('stale.cleanupFailed'));
|
||||
} finally {
|
||||
bulkCleaning = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
loadStale();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t('stale.title')} - {$t('app.name')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold text-[var(--text-primary)]">{$t('stale.title')}</h1>
|
||||
{#if containers.length > 0}
|
||||
<button
|
||||
type="button"
|
||||
disabled={bulkCleaning}
|
||||
onclick={() => { confirmBulk = true; }}
|
||||
class="inline-flex items-center gap-2 rounded-lg border border-[var(--color-danger)] px-4 py-2.5 text-sm font-medium text-[var(--color-danger)] transition-colors hover:bg-[var(--color-danger-light)] disabled:opacity-50 active:animate-press"
|
||||
>
|
||||
{#if bulkCleaning}<IconLoader size={16} />{/if}
|
||||
<IconTrash size={16} />
|
||||
{$t('stale.cleanupAll')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
{#if loading}
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each Array(3) as _}
|
||||
<SkeletonCard />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="rounded-xl border border-[var(--color-danger-light)] bg-[var(--color-danger-light)] p-4">
|
||||
<p class="text-sm text-[var(--color-danger)]">{error}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-2 text-sm font-medium text-[var(--color-danger)] underline hover:no-underline"
|
||||
onclick={loadStale}
|
||||
>
|
||||
{$t('common.retry')}
|
||||
</button>
|
||||
</div>
|
||||
{:else if containers.length === 0}
|
||||
<EmptyState
|
||||
title={$t('stale.noStale')}
|
||||
description={$t('stale.noStaleDesc')}
|
||||
icon="instances"
|
||||
/>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each containers as container (container.id)}
|
||||
<StaleContainerCard
|
||||
{container}
|
||||
cleaning={cleaningIds.has(container.id)}
|
||||
oncleanup={requestCleanup}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Single cleanup confirm -->
|
||||
<ConfirmDialog
|
||||
open={confirmSingleId !== ''}
|
||||
title={$t('stale.cleanup')}
|
||||
message={$t('stale.confirmCleanup')}
|
||||
confirmLabel={$t('stale.cleanup')}
|
||||
confirmVariant="danger"
|
||||
onconfirm={handleConfirmCleanup}
|
||||
oncancel={() => { confirmSingleId = ''; }}
|
||||
/>
|
||||
|
||||
<!-- Bulk cleanup confirm -->
|
||||
<ConfirmDialog
|
||||
open={confirmBulk}
|
||||
title={$t('stale.cleanupAll')}
|
||||
message={$t('stale.confirmBulkCleanup')}
|
||||
confirmLabel={$t('stale.cleanupAll')}
|
||||
confirmVariant="danger"
|
||||
onconfirm={handleConfirmBulkCleanup}
|
||||
oncancel={() => { confirmBulk = false; }}
|
||||
/>
|
||||
@@ -0,0 +1,2 @@
|
||||
// Client-side only — data is fetched in the component.
|
||||
export const ssr = false;
|
||||
Reference in New Issue
Block a user