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