Enhance IP display and server presence in Svelte components
- Added `IpServerPresence` component to streamline the display of active and recent servers for IPs, improving code readability and maintainability. - Introduced tooltips for the "Servers" column in tables to provide additional context on active and recent server lists, enhancing user experience. - Updated text descriptions for clarity and consistency across the IP and user pages, ensuring better understanding of the displayed data.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
<script lang="ts">
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
import { Separator } from '$lib/components/ui/separator/index.js';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import InfoIcon from '@lucide/svelte/icons/info';
|
||||
import StarIcon from '@lucide/svelte/icons/star';
|
||||
|
||||
let {
|
||||
active_on_servers,
|
||||
recent_on_servers,
|
||||
primary_server,
|
||||
linkToServer = true,
|
||||
class: className
|
||||
}: {
|
||||
active_on_servers?: string[] | null;
|
||||
recent_on_servers?: string[] | null;
|
||||
primary_server?: string | null;
|
||||
/** Ссылка на `/servers/{alias}` у бейджей */
|
||||
linkToServer?: boolean;
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
const activeServers = $derived(((active_on_servers ?? []) as string[]).filter(Boolean));
|
||||
const recentServers = $derived(((recent_on_servers ?? []) as string[]).filter(Boolean));
|
||||
const recentOnly = $derived(recentServers.filter((s) => !activeServers.includes(s)));
|
||||
|
||||
const soleActiveTip =
|
||||
'Единственный сервер в активных на этом снимке (не приоритет, а однозначность).';
|
||||
|
||||
function serverHref(alias: string): string | undefined {
|
||||
if (!linkToServer) return undefined;
|
||||
return `/servers/${encodeURIComponent(alias)}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={cn('space-y-2 text-xs', className)}>
|
||||
{#if activeServers.length > 0}
|
||||
<div class="space-y-1">
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<span class="text-muted-foreground">Активные</span>
|
||||
<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="top" class="max-w-xs">
|
||||
<p class="text-xs">
|
||||
Серверы, где этот IP есть в списке активных уникальных адресов на момент снимка
|
||||
шлюза.
|
||||
</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
{#each activeServers as srv (srv)}
|
||||
{#if primary_server && srv === primary_server && activeServers.length === 1}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger class="inline-flex">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="font-normal px-2 py-0.5 gap-1"
|
||||
href={serverHref(srv)}
|
||||
>
|
||||
<StarIcon class="size-3 shrink-0 opacity-90" aria-hidden="true" />
|
||||
{srv}
|
||||
</Badge>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="bottom" class="max-w-xs">
|
||||
<p class="text-xs">{soleActiveTip}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{:else}
|
||||
<Badge variant="secondary" class="font-normal px-2 py-0.5" href={serverHref(srv)}>
|
||||
{srv}
|
||||
</Badge>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-muted-foreground">
|
||||
{recentOnly.length > 0 ? 'Нет в активных' : '—'}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if recentOnly.length > 0}
|
||||
{#if activeServers.length > 0}
|
||||
<Separator class="my-1" />
|
||||
{/if}
|
||||
<div class="space-y-1">
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<span class="text-muted-foreground">Только недавние</span>
|
||||
<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="top" class="max-w-xs">
|
||||
<p class="text-xs">
|
||||
Серверы, где IP есть в недавнем списке, но на этом снимке не в активных — например
|
||||
недавнее подключение с другого шлюза.
|
||||
</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
{#each recentOnly as srv (srv)}
|
||||
<Badge variant="outline" class="font-normal px-2 py-0.5" href={serverHref(srv)}>
|
||||
{srv}
|
||||
</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -12,10 +12,13 @@
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
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 { Skeleton } from '$lib/components/ui/skeleton/index.js';
|
||||
import { Switch } from '$lib/components/ui/switch/index.js';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
import InfoIcon from '@lucide/svelte/icons/info';
|
||||
import AliasFilterSelect from '$lib/components/alias-filter-select.svelte';
|
||||
import DataQueryState from '$lib/components/fleet/data-query-state.svelte';
|
||||
import FleetLiveStaleBadge from '$lib/components/fleet/fleet-live-stale-badge.svelte';
|
||||
@@ -428,7 +431,9 @@
|
||||
<div class="mb-6 flex flex-wrap items-end justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Уникальные IP</h1>
|
||||
<p class="text-sm text-muted-foreground">Снимок active/recent с шлюза; GeoIP из конфига шлюза.</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Снимок активных и недавних списков с шлюза; GeoIP из конфига шлюза.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<FleetLiveStaleBadge {isStale} {staleSeconds} refreshSeconds={refreshSeconds} />
|
||||
@@ -500,7 +505,7 @@
|
||||
<Card.Root class="mb-6">
|
||||
<Card.Header>
|
||||
<Card.Title>Карта подключений</Card.Title>
|
||||
<Card.Description>GeoIP координаты из снимка `unique-ips`</Card.Description>
|
||||
<Card.Description>GeoIP координаты из снимка unique-ips</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="p-0">
|
||||
<div class="h-[420px] w-full rounded-md" bind:this={mapEl}></div>
|
||||
@@ -531,7 +536,24 @@
|
||||
<Table.Head>Пользователь</Table.Head>
|
||||
<Table.Head>IP</Table.Head>
|
||||
<Table.Head>Geo</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.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
@@ -554,37 +576,12 @@
|
||||
<Badge variant="outline" class="ml-1">AS{ip.asn}</Badge>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="max-w-[240px] text-xs">
|
||||
{@const activeServers = (ip.active_on_servers ?? []).filter(Boolean)}
|
||||
{@const recentServers = (ip.recent_on_servers ?? []).filter(Boolean)}
|
||||
{@const primaryServer = ip.primary_server ?? null}
|
||||
{@const recentOnly = recentServers.filter((s) => !activeServers.includes(s))}
|
||||
|
||||
{#if activeServers.length === 0}
|
||||
<span class="text-muted-foreground">—</span>
|
||||
{:else}
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
{#each activeServers as srv (srv)}
|
||||
<Badge variant="secondary" class="font-normal px-2 py-0.5">
|
||||
{srv}
|
||||
</Badge>
|
||||
{#if primaryServer && srv === primaryServer}
|
||||
<Badge class="font-normal px-2 py-0.5">primary</Badge>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if recentOnly.length > 0}
|
||||
<div class="mt-1 flex flex-wrap items-center gap-1">
|
||||
<Badge variant="outline" class="font-normal px-2 py-0.5">recent</Badge>
|
||||
{#each recentOnly as srv (srv)}
|
||||
<Badge variant="outline" class="font-normal px-2 py-0.5">
|
||||
{srv}
|
||||
</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<Table.Cell class="max-w-[280px] align-top text-xs">
|
||||
<IpServerPresence
|
||||
active_on_servers={ip.active_on_servers}
|
||||
recent_on_servers={ip.recent_on_servers}
|
||||
primary_server={ip.primary_server ?? null}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
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 CopyIcon from '@lucide/svelte/icons/copy';
|
||||
import InfoIcon from '@lucide/svelte/icons/info';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let loading = $state(true);
|
||||
@@ -187,7 +190,9 @@
|
||||
<Card.Root class="mt-4">
|
||||
<Card.Header>
|
||||
<Card.Title>IP и подключения</Card.Title>
|
||||
<Card.Description>active/recent с серверами и Geo/ASN (если доступно)</Card.Description>
|
||||
<Card.Description>
|
||||
Снимок шлюза: активные и недавние списки по серверам; Geo/ASN при доступности данных.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="p-0">
|
||||
{#if ipErr}
|
||||
@@ -202,7 +207,25 @@
|
||||
<Table.Header>
|
||||
<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">
|
||||
<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 class="text-xs sm:text-sm">Geo / ASN</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
@@ -224,16 +247,12 @@
|
||||
>
|
||||
{ip.ip}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-xs sm:text-sm text-muted-foreground whitespace-normal">
|
||||
active: {((ip.active_on_servers ?? []).filter(Boolean) as string[]).join(', ') || '—'}
|
||||
{#if (ip.recent_on_servers?.length ?? 0) > 0}
|
||||
<br />
|
||||
recent: {(ip.recent_on_servers ?? []).filter(Boolean).join(', ')}
|
||||
{/if}
|
||||
{#if ip.primary_server}
|
||||
<br />
|
||||
primary: {ip.primary_server}
|
||||
{/if}
|
||||
<Table.Cell class="text-xs sm:text-sm whitespace-normal align-top min-w-[12rem]">
|
||||
<IpServerPresence
|
||||
active_on_servers={ip.active_on_servers}
|
||||
recent_on_servers={ip.recent_on_servers}
|
||||
primary_server={ip.primary_server ?? null}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-xs sm:text-sm whitespace-normal">
|
||||
<div class="space-y-1">
|
||||
|
||||
Reference in New Issue
Block a user