Replace KpiTrendCard with KpiStatCard for incident metrics display, enhancing visual consistency and user experience. Update empty state messages in the data table and adjust layout properties for improved responsiveness.
Publish telemt-api gateway Docker image / test (push) Successful in 30s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 2m28s

This commit is contained in:
Denozordec
2026-04-10 01:02:05 +07:00
parent 8ad627cb9b
commit 2373ed388b
+34 -45
View File
@@ -10,11 +10,10 @@
type IncidentsData
} from '$lib/api/client.js';
import * as Card from '$lib/components/ui/card/index.js';
import KpiTrendCard from '$lib/components/kpi-trend-card.svelte';
import KpiStatCard from '$lib/components/kpi-stat-card.svelte';
import DataTableCard from '$lib/components/data-table/data-table-card.svelte';
import DataTableToolbar from '$lib/components/data-table/data-table-toolbar.svelte';
import SortableTh from '$lib/components/data-table/sortable-th.svelte';
import TableEmpty from '$lib/components/data-table/table-empty.svelte';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import SearchIcon from '@lucide/svelte/icons/search';
import { nextSortDir, compareStrings, compareNum, type SortDir } from '$lib/table-sort.js';
@@ -31,6 +30,9 @@
import FleetRefreshInput from '$lib/components/fleet/fleet-refresh-input.svelte';
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
import InfoIcon from '@lucide/svelte/icons/info';
import SirenIcon from '@lucide/svelte/icons/siren';
import TriangleAlertIcon from '@lucide/svelte/icons/triangle-alert';
import InfoIconBadge from '@lucide/svelte/icons/info';
type TriageState = {
ack?: boolean;
@@ -56,9 +58,6 @@
let staleTimer: ReturnType<typeof setInterval> | null = null;
let triageById = $state<Record<string, TriageState>>({});
type IncKpiPrev = { critical: number; warning: number; info: number };
let kpiPrev = $state<IncKpiPrev | null>(null);
let incSearch = $state('');
let incSortKey = $state<'sev' | 'title' | null>(null);
let incSortDir = $state<SortDir | null>(null);
@@ -153,18 +152,9 @@
err = null;
try {
const activeAlias = aliasOverride ?? aliasFilter;
const prev =
data != null
? {
critical: data.critical_total ?? 0,
warning: data.warning_total ?? 0,
info: data.info_total ?? 0
}
: null;
const env = await fetchAggIncidents({
aliases: activeAlias === 'all' ? undefined : activeAlias
});
kpiPrev = prev;
data = env.data ?? null;
partial = !!env.partial;
generatedAt = env.generated_at;
@@ -336,29 +326,24 @@
{/snippet}
{#snippet children()}
<div class="mb-4 grid gap-4 sm:grid-cols-3">
<KpiTrendCard
label="Критичные"
valueDisplay={String(data?.critical_total ?? 0)}
prevValue={kpiPrev?.critical ?? null}
currentValue={data != null ? (data.critical_total ?? 0) : null}
invertTrend={true}
insight="Открытые критичные; меньше — лучше. К прошлому опросу."
/>
<KpiTrendCard
label="Предупреждения"
valueDisplay={String(data?.warning_total ?? 0)}
prevValue={kpiPrev?.warning ?? null}
currentValue={data != null ? (data.warning_total ?? 0) : null}
invertTrend={true}
insight="Открытые предупреждения; меньше — лучше."
/>
<KpiTrendCard
label="Инфо"
valueDisplay={String(data?.info_total ?? 0)}
prevValue={kpiPrev?.info ?? null}
currentValue={data != null ? (data.info_total ?? 0) : null}
insight="Открытые инфо. К прошлому опросу."
/>
<KpiStatCard accent="danger" label="Критичные" footer="Открытые инциденты" valueClass="text-xl">
{#snippet icon()}
<SirenIcon class="size-5" aria-hidden="true" />
{/snippet}
{data?.critical_total ?? 0}
</KpiStatCard>
<KpiStatCard accent="warning" label="Предупреждения" footer="Открытые инциденты" valueClass="text-xl">
{#snippet icon()}
<TriangleAlertIcon class="size-5" aria-hidden="true" />
{/snippet}
{data?.warning_total ?? 0}
</KpiStatCard>
<KpiStatCard accent="info" label="Инфо" footer="Открытые инциденты" valueClass="text-xl">
{#snippet icon()}
<InfoIconBadge class="size-5" aria-hidden="true" />
{/snippet}
{data?.info_total ?? 0}
</KpiStatCard>
</div>
<DataTableCard title="Инциденты" description="Список и разбор">
@@ -382,7 +367,7 @@
>Сброс</Button>
</DataTableToolbar>
{/snippet}
<ScrollArea class="max-h-[min(75vh,640px)] w-full" orientation="both">
<ScrollArea class="max-h-[min(70vh,560px)] w-full" orientation="both">
<Table.Root>
<Table.Header class="sticky top-0 z-10 bg-muted">
<Table.Row class="border-b border-border/80 hover:bg-transparent">
@@ -419,14 +404,14 @@
<Table.Body>
{#if (data?.items.length ?? 0) === 0}
<Table.Row>
<Table.Cell colspan={5} class="p-0">
<TableEmpty title="Инцидентов нет" description="На снимке пусто." />
<Table.Cell colspan={5} class="p-4 text-center text-muted-foreground">
Инцидентов нет
</Table.Cell>
</Table.Row>
{:else if filteredIncidents.length === 0}
<Table.Row>
<Table.Cell colspan={5} class="p-0">
<TableEmpty title="Нет по фильтру" description="Измените поиск." />
<Table.Cell colspan={5} class="p-4 text-center text-muted-foreground">
Нет строк по фильтру
</Table.Cell>
</Table.Row>
{:else}
@@ -456,22 +441,26 @@
{/each}
</div>
</Table.Cell>
<Table.Cell class="min-w-[280px]">
<Table.Cell class="min-w-[240px]">
{@const triage = triageFor(item.id)}
<div class="mb-2 flex flex-wrap gap-1">
<Button
size="xs"
class="h-7 px-2 text-xs"
variant={triage.ack ? 'default' : 'outline'}
onclick={() => toggleAck(item.id)}
title={triage.ack ? 'Снять подтверждение' : 'Подтвердить'}
>
{triage.ack ? 'Снять подтверждение' : 'Подтвердить'}
{triage.ack ? 'Снять' : 'Подтв.'}
</Button>
<Button
size="xs"
class="h-7 px-2 text-xs"
variant={triage.resolved ? 'default' : 'outline'}
onclick={() => toggleResolved(item.id)}
title={triage.resolved ? 'Вернуть в работу' : 'Решить'}
>
{triage.resolved ? 'Вернуть в работу' : 'Решить'}
{triage.resolved ? 'В работу' : 'Решить'}
</Button>
</div>
<div class="mb-1">