Refactor IP server presence display and enhance tooltip functionality
- 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:
@@ -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
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user