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.
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user