Refactor IP server presence display and enhance tooltip functionality
Publish telemt-api gateway Docker image / test (push) Successful in 35s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 2m48s

- Updated the `IpServerPresence` component to improve the display of active and recent servers for IPs, enhancing code clarity.
- Added tooltips to the "Servers" column in various tables, providing users with additional context about the displayed server lists.
- Refactored server-related data handling to ensure consistency and better user experience across multiple routes.
This commit is contained in:
Denozordec
2026-03-31 00:06:32 +07:00
parent 46d080e70a
commit 992eebd842
6 changed files with 139 additions and 36 deletions
@@ -64,7 +64,10 @@
class="font-normal px-2 py-0.5 gap-1"
href={serverHref(srv)}
>
<StarIcon class="size-3 shrink-0 opacity-90" aria-hidden="true" />
<StarIcon
class="size-3 shrink-0 fill-amber-400 stroke-amber-400 text-amber-400 drop-shadow-[0_0_5px_rgba(251,191,36,0.55)]"
aria-hidden="true"
/>
{srv}
</Badge>
</Tooltip.Trigger>
@@ -0,0 +1,35 @@
<script lang="ts">
import { Badge, type BadgeVariant } from '$lib/components/ui/badge/index.js';
import { cn } from '$lib/utils.js';
let {
aliases,
variant = 'outline',
linkToServer = true,
class: className
}: {
aliases: string[] | null | undefined;
variant?: BadgeVariant;
linkToServer?: boolean;
class?: string;
} = $props();
const list = $derived(((aliases ?? []) as string[]).filter(Boolean));
function href(alias: string): string | undefined {
if (!linkToServer) return undefined;
return `/servers/${encodeURIComponent(alias)}`;
}
</script>
{#if list.length === 0}
<span class="text-muted-foreground"></span>
{:else}
<div class={cn('flex flex-wrap items-center gap-1', className)}>
{#each list as alias (alias)}
<Badge variant={variant} class="font-normal px-2 py-0.5" href={href(alias)}>
{alias}
</Badge>
{/each}
</div>
{/if}
+56 -20
View File
@@ -13,6 +13,8 @@
import type { components } from '$lib/api/aggregate.gen.js';
import { formatDurationSeconds, formatMiB, flagEmoji } from '$lib/format.js';
import { Badge } from '$lib/components/ui/badge/index.js';
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
import IpServerPresence from '$lib/components/ip-server-presence.svelte';
import * as Card from '$lib/components/ui/card/index.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
import * as Table from '$lib/components/ui/table/index.js';
@@ -31,6 +33,7 @@
import GlobeIcon from '@lucide/svelte/icons/globe';
import ActivityIcon from '@lucide/svelte/icons/activity';
import LockKeyholeIcon from '@lucide/svelte/icons/lock-keyhole';
import InfoIcon from '@lucide/svelte/icons/info';
/** Семантика дашборда: граница + иконка + при необходимости цвет числа (WCAG: акцент не только цветом — иконка + подпись). */
const kpiAccent = {
@@ -331,14 +334,23 @@
username: string;
ip: string;
country_code: string | null | undefined;
servers: string[];
activeLabel: string;
active_on_servers: string[];
recent_on_servers: string[];
primary_server: string | null;
};
let activeIpServerRows = $derived.by((): ActiveIpServerRow[] => {
const map = new Map<
string,
{ row: Omit<ActiveIpServerRow, 'servers'>; serversSet: Set<string> }
{
row: Omit<
ActiveIpServerRow,
'active_on_servers' | 'recent_on_servers' | 'primary_server'
>;
activeSet: Set<string>;
recentSet: Set<string>;
}
>();
const maxUniqueIpRows = 80;
const maxOutRows = 2000;
@@ -351,8 +363,8 @@
const ip = ipa.ip ?? '';
if (!ip) continue;
const servers = (ipa.active_on_servers ?? []).filter(Boolean);
if (servers.length === 0) continue;
const active = (ipa.active_on_servers ?? []).filter(Boolean);
if (active.length === 0) continue;
const key = `${username}|${ip}`;
let item = map.get(key);
@@ -365,23 +377,33 @@
country_code: ipa.country_code,
activeLabel: chipLabel(ipa)
},
serversSet: new Set<string>()
activeSet: new Set<string>(),
recentSet: new Set<string>()
};
map.set(key, item);
}
for (const server of servers) {
for (const server of active) {
if (!server) continue;
item.serversSet.add(server);
item.activeSet.add(server);
}
for (const server of (ipa.recent_on_servers ?? []).filter(Boolean)) {
if (!server) continue;
item.recentSet.add(server);
}
}
}
const out: ActiveIpServerRow[] = [];
for (const { row, serversSet } of map.values()) {
for (const { row, activeSet, recentSet } of map.values()) {
const active_on_servers = [...activeSet].sort((a, b) => a.localeCompare(b));
const recent_on_servers = [...recentSet].sort((a, b) => a.localeCompare(b));
const primary_server = active_on_servers.length === 1 ? active_on_servers[0] : null;
out.push({
...row,
servers: [...serversSet].sort((a, b) => a.localeCompare(b))
active_on_servers,
recent_on_servers,
primary_server
});
}
@@ -594,7 +616,25 @@
<Table.Row>
<Table.Head class="text-xs sm:text-sm">IP со страной</Table.Head>
<Table.Head class="text-xs sm:text-sm">Клиент</Table.Head>
<Table.Head class="text-xs sm:text-sm">Серверы</Table.Head>
<Table.Head class="text-xs sm:text-sm">
<div class="flex items-center gap-1">
Серверы
<Tooltip.Root>
<Tooltip.Trigger
class="inline-flex size-3.5 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:text-foreground"
aria-label="Пояснение колонки"
>
<InfoIcon class="size-3.5" />
</Tooltip.Trigger>
<Tooltip.Content side="bottom" class="max-w-xs">
<p class="text-xs">
Активные и недавние — списки на момент снимка шлюза; в ячейке пояснены
группы.
</p>
</Tooltip.Content>
</Tooltip.Root>
</div>
</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
@@ -618,16 +658,12 @@
{r.username}
</a>
</Table.Cell>
<Table.Cell class="text-xs sm:text-sm whitespace-normal">
<div class="flex flex-wrap items-center gap-1">
{#each r.servers as srv (srv)}
<a href="/servers/{encodeURIComponent(srv)}" class="inline-flex">
<Badge variant="secondary" class="font-normal px-2 py-0.5">
{srv}
</Badge>
</a>
{/each}
</div>
<Table.Cell class="max-w-[280px] align-top text-xs sm:text-sm whitespace-normal">
<IpServerPresence
active_on_servers={r.active_on_servers}
recent_on_servers={r.recent_on_servers}
primary_server={r.primary_server}
/>
</Table.Cell>
</Table.Row>
{/each}
+21 -7
View File
@@ -12,6 +12,8 @@
import * as Card from '$lib/components/ui/card/index.js';
import * as Table from '$lib/components/ui/table/index.js';
import { Badge } from '$lib/components/ui/badge/index.js';
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
import ServerAliasBadges from '$lib/components/server-alias-badges.svelte';
import { Button } from '$lib/components/ui/button/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Skeleton } from '$lib/components/ui/skeleton/index.js';
@@ -20,6 +22,7 @@
import FleetLiveStaleBadge from '$lib/components/fleet/fleet-live-stale-badge.svelte';
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';
type TriageState = {
ack?: boolean;
@@ -295,7 +298,22 @@
<Table.Row>
<Table.Head>Severity</Table.Head>
<Table.Head>Инцидент</Table.Head>
<Table.Head>Серверы</Table.Head>
<Table.Head>
<div class="flex items-center gap-1">
Серверы
<Tooltip.Root>
<Tooltip.Trigger
class="inline-flex size-3.5 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:text-foreground"
aria-label="Пояснение колонки"
>
<InfoIcon class="size-3.5" />
</Tooltip.Trigger>
<Tooltip.Content side="bottom" class="max-w-xs">
<p class="text-xs">Ноды, к которым относится инцидент (алиасы шлюзов).</p>
</Tooltip.Content>
</Tooltip.Root>
</div>
</Table.Head>
<Table.Head>Runbook</Table.Head>
<Table.Head>Triage</Table.Head>
</Table.Row>
@@ -322,12 +340,8 @@
</div>
{/if}
</Table.Cell>
<Table.Cell>
<div class="flex flex-wrap gap-1">
{#each item.affected_aliases ?? [] as alias (alias)}
<Badge variant="outline">{alias}</Badge>
{/each}
</div>
<Table.Cell class="max-w-[240px] align-top text-xs">
<ServerAliasBadges aliases={item.affected_aliases ?? []} variant="outline" />
</Table.Cell>
<Table.Cell class="max-w-[240px]">
<div class="flex flex-wrap gap-1">
+21 -7
View File
@@ -12,11 +12,14 @@
import * as Card from '$lib/components/ui/card/index.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
import { Badge } from '$lib/components/ui/badge/index.js';
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
import ServerAliasBadges from '$lib/components/server-alias-badges.svelte';
import { Button } from '$lib/components/ui/button/index.js';
import * as Table from '$lib/components/ui/table/index.js';
import AliasFilterSelect from '$lib/components/alias-filter-select.svelte';
import FleetLiveStaleBadge from '$lib/components/fleet/fleet-live-stale-badge.svelte';
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
import InfoIcon from '@lucide/svelte/icons/info';
type LiveStatus = 'healthy' | 'degraded' | 'critical';
@@ -337,7 +340,22 @@
<Table.Head>Severity</Table.Head>
<Table.Head>Title</Table.Head>
<Table.Head>Summary</Table.Head>
<Table.Head>Aliases</Table.Head>
<Table.Head>
<div class="flex items-center gap-1">
Серверы
<Tooltip.Root>
<Tooltip.Trigger
class="inline-flex size-3.5 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:text-foreground"
aria-label="Пояснение колонки"
>
<InfoIcon class="size-3.5" />
</Tooltip.Trigger>
<Tooltip.Content side="bottom" class="max-w-xs">
<p class="text-xs">Ноды, к которым относится инцидент (алиасы шлюзов).</p>
</Tooltip.Content>
</Tooltip.Root>
</div>
</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
@@ -355,12 +373,8 @@
</Table.Cell>
<Table.Cell class="font-medium">{item.title}</Table.Cell>
<Table.Cell class="max-w-[380px] text-sm text-muted-foreground">{item.summary}</Table.Cell>
<Table.Cell>
<div class="flex flex-wrap gap-1">
{#each item.affected_aliases ?? [] as alias (alias)}
<Badge variant="outline">{alias}</Badge>
{/each}
</div>
<Table.Cell class="max-w-[240px] align-top text-xs">
<ServerAliasBadges aliases={item.affected_aliases ?? []} variant="outline" />
</Table.Cell>
</Table.Row>
{/each}
+2 -1
View File
@@ -11,6 +11,7 @@
import * as Table from '$lib/components/ui/table/index.js';
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
import IpServerPresence from '$lib/components/ip-server-presence.svelte';
import ServerAliasBadges from '$lib/components/server-alias-badges.svelte';
import CopyIcon from '@lucide/svelte/icons/copy';
import InfoIcon from '@lucide/svelte/icons/info';
import { toast } from 'svelte-sonner';
@@ -159,7 +160,7 @@
<Card.Content class="space-y-2">
{#each Object.entries(row.by_server ?? {}) as [alias, st] (alias)}
<div class="flex flex-wrap items-center justify-between gap-2 rounded-md border border-border px-3 py-2 text-sm">
<Badge variant="secondary">{alias}</Badge>
<ServerAliasBadges aliases={[alias]} variant="secondary" />
<span class="text-muted-foreground tabular-nums">
{formatMiB(st.total_megabytes ?? 0)} · conn {st.current_connections ?? 0}
</span>