Implement alias filtering across multiple pages
- Introduced an `AliasFilterSelect` component to streamline alias selection in various routes, enhancing user experience. - Refactored alias handling logic to use a single filter mechanism, improving consistency and reducing code duplication. - Updated API calls to utilize the new alias filtering logic, ensuring accurate data retrieval based on user-selected aliases. - Enhanced the loading of available aliases dynamically from aggregated summary data, keeping the interface up-to-date.
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import * as Select from "$lib/components/ui/select/index.js";
|
||||
|
||||
let {
|
||||
availableAliases = [],
|
||||
value = $bindable("all"),
|
||||
label = "Alias",
|
||||
allLabel = "All nodes"
|
||||
}: {
|
||||
availableAliases: string[];
|
||||
value?: string;
|
||||
label?: string;
|
||||
allLabel?: string;
|
||||
} = $props();
|
||||
|
||||
let selectedLabel = $derived(value === "all" ? allLabel : value);
|
||||
</script>
|
||||
|
||||
<label class="text-xs text-muted-foreground">
|
||||
{label}
|
||||
<Select.Root type="single" bind:value>
|
||||
<Select.Trigger class="mt-1 w-[220px]">
|
||||
{selectedLabel}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="all">{allLabel}</Select.Item>
|
||||
{#each availableAliases as alias (alias)}
|
||||
<Select.Item value={alias}>{alias}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</label>
|
||||
+29
-19
@@ -17,6 +17,7 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import AliasFilterSelect from '$lib/components/alias-filter-select.svelte';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
import AlertTriangleIcon from '@lucide/svelte/icons/triangle-alert';
|
||||
@@ -75,8 +76,8 @@
|
||||
let uniqueIps = $state<components['schemas']['UniqueIPsRow'][] | null>(null);
|
||||
let generatedAt = $state<string | null>(null);
|
||||
let partial = $state(false);
|
||||
let aliases = $state('');
|
||||
let aliasesInput = $state('');
|
||||
let aliasFilter = $state('all');
|
||||
let availableAliases = $state<string[]>([]);
|
||||
let refreshSeconds = $state(30);
|
||||
let refreshInput = $state('30');
|
||||
let lastSuccessAtMs = $state<number | null>(null);
|
||||
@@ -90,20 +91,39 @@
|
||||
>
|
||||
>({});
|
||||
|
||||
function parseAliasFilter(raw: string | null): string {
|
||||
const value = raw?.trim() ?? '';
|
||||
return !value || value.includes(',') ? 'all' : value;
|
||||
}
|
||||
|
||||
function collectAliases(...lists: Array<Array<string | null | undefined> | null | undefined>): string[] {
|
||||
const out = new Set<string>();
|
||||
for (const list of lists) {
|
||||
for (const item of list ?? []) {
|
||||
if (typeof item === 'string' && item.trim()) out.add(item);
|
||||
}
|
||||
}
|
||||
return Array.from(out).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
err = null;
|
||||
try {
|
||||
const [s, f, u] = await Promise.all([
|
||||
fetchAggSummary({ top_n: 15, aliases: aliases || undefined }),
|
||||
fetchAggFleetStatus({ aliases: aliases || undefined }),
|
||||
fetchAggUniqueIps({ aliases: aliases || undefined, geo: true })
|
||||
fetchAggSummary({ top_n: 15, aliases: aliasFilter === 'all' ? undefined : aliasFilter }),
|
||||
fetchAggFleetStatus({ aliases: aliasFilter === 'all' ? undefined : aliasFilter }),
|
||||
fetchAggUniqueIps({ aliases: aliasFilter === 'all' ? undefined : aliasFilter, geo: true })
|
||||
]);
|
||||
generatedAt = s.generated_at;
|
||||
partial = !!(s.partial || f.partial || u.partial);
|
||||
summary = s.data;
|
||||
fleet = f.data ?? null;
|
||||
uniqueIps = u.data ?? null;
|
||||
availableAliases = collectAliases(
|
||||
(fleet?.servers ?? []).map((x) => x.alias),
|
||||
(summary?.servers ?? []).map((x) => x.alias)
|
||||
);
|
||||
lastSuccessAtMs = Date.now();
|
||||
|
||||
// Как бейдж «OK» в таблице: только health + system/info (без x.ok — в JSON поле ok опционально).
|
||||
@@ -163,10 +183,9 @@
|
||||
|
||||
async function applyControls() {
|
||||
const q = new URLSearchParams(page.url.searchParams);
|
||||
const normalizedAliases = aliasesInput.trim();
|
||||
const parsedRefresh = parseRefresh(refreshInput);
|
||||
if (normalizedAliases) q.set('aliases', normalizedAliases);
|
||||
else q.delete('aliases');
|
||||
if (aliasFilter === 'all') q.delete('aliases');
|
||||
else q.set('aliases', aliasFilter);
|
||||
if (parsedRefresh === 30) q.delete('refresh');
|
||||
else q.set('refresh', String(parsedRefresh));
|
||||
const qs = q.toString();
|
||||
@@ -181,9 +200,8 @@
|
||||
$effect(() => {
|
||||
queryKey;
|
||||
if (typeof window === 'undefined') return;
|
||||
aliases = page.url.searchParams.get('aliases')?.trim() ?? '';
|
||||
aliasFilter = parseAliasFilter(page.url.searchParams.get('aliases'));
|
||||
refreshSeconds = parseRefresh(page.url.searchParams.get('refresh'));
|
||||
aliasesInput = aliases;
|
||||
refreshInput = String(refreshSeconds);
|
||||
restartPolling();
|
||||
void load();
|
||||
@@ -370,15 +388,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-4 flex flex-wrap items-end gap-2">
|
||||
<label class="text-xs text-muted-foreground">
|
||||
Aliases
|
||||
<input
|
||||
type="text"
|
||||
class="mt-1 h-9 w-56 rounded-md border bg-background px-2 text-sm"
|
||||
bind:value={aliasesInput}
|
||||
placeholder="node-a,node-b"
|
||||
/>
|
||||
</label>
|
||||
<AliasFilterSelect availableAliases={availableAliases} bind:value={aliasFilter} />
|
||||
<label class="text-xs text-muted-foreground">
|
||||
Refresh (sec)
|
||||
<input
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import AliasFilterSelect from '$lib/components/alias-filter-select.svelte';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
|
||||
type TriageState = {
|
||||
@@ -31,7 +32,7 @@
|
||||
let generatedAt = $state<string | null>(null);
|
||||
let data = $state<IncidentsData | null>(null);
|
||||
let availableAliases = $state<string[]>([]);
|
||||
let selectedAliases = $state<string[]>([]);
|
||||
let aliasFilter = $state('all');
|
||||
let refreshSeconds = $state(30);
|
||||
let refreshInput = $state('30');
|
||||
let lastSuccessAtMs = $state<number | null>(null);
|
||||
@@ -48,25 +49,9 @@
|
||||
return Math.max(10, Math.min(300, Math.floor(n)));
|
||||
}
|
||||
|
||||
function parseAliases(raw: string | null): string[] {
|
||||
if (!raw) return [];
|
||||
const out = raw
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
return Array.from(new Set(out)).sort();
|
||||
}
|
||||
|
||||
function aliasesLabel(): string {
|
||||
if (selectedAliases.length === 0) return 'Все ноды';
|
||||
if (selectedAliases.length <= 3) return selectedAliases.join(', ');
|
||||
return `${selectedAliases.slice(0, 3).join(', ')} +${selectedAliases.length - 3}`;
|
||||
}
|
||||
|
||||
function toggleAlias(a: string) {
|
||||
if (!a) return;
|
||||
const has = selectedAliases.includes(a);
|
||||
selectedAliases = has ? selectedAliases.filter((x) => x !== a) : [...selectedAliases, a].sort();
|
||||
function parseAliasFilter(raw: string | null): string {
|
||||
const value = raw?.trim() ?? '';
|
||||
return !value || value.includes(',') ? 'all' : value;
|
||||
}
|
||||
|
||||
function triageFor(id: string): TriageState {
|
||||
@@ -133,8 +118,9 @@
|
||||
loading = true;
|
||||
err = null;
|
||||
try {
|
||||
const aliasesParam = selectedAliases.length ? selectedAliases.join(',') : undefined;
|
||||
const env = await fetchAggIncidents({ aliases: aliasesParam });
|
||||
const env = await fetchAggIncidents({
|
||||
aliases: aliasFilter === 'all' ? undefined : aliasFilter
|
||||
});
|
||||
data = env.data ?? null;
|
||||
partial = !!env.partial;
|
||||
generatedAt = env.generated_at;
|
||||
@@ -149,8 +135,8 @@
|
||||
async function applyControls() {
|
||||
const q = new URLSearchParams(page.url.searchParams);
|
||||
const parsedRefresh = parseRefresh(refreshInput);
|
||||
if (selectedAliases.length > 0) q.set('aliases', selectedAliases.join(','));
|
||||
else q.delete('aliases');
|
||||
if (aliasFilter === 'all') q.delete('aliases');
|
||||
else q.set('aliases', aliasFilter);
|
||||
if (parsedRefresh === 30) q.delete('refresh');
|
||||
else q.set('refresh', String(parsedRefresh));
|
||||
const qs = q.toString();
|
||||
@@ -165,7 +151,7 @@
|
||||
$effect(() => {
|
||||
queryKey;
|
||||
if (typeof window === 'undefined') return;
|
||||
selectedAliases = parseAliases(page.url.searchParams.get('aliases'));
|
||||
aliasFilter = parseAliasFilter(page.url.searchParams.get('aliases'));
|
||||
refreshSeconds = parseRefresh(page.url.searchParams.get('refresh'));
|
||||
refreshInput = String(refreshSeconds);
|
||||
restartPolling();
|
||||
@@ -187,9 +173,9 @@
|
||||
const aliases = (env.data.servers ?? [])
|
||||
.map((s) => s.alias)
|
||||
.filter((a): a is string => typeof a === 'string' && a.length > 0)
|
||||
.sort();
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
availableAliases = aliases;
|
||||
selectedAliases = selectedAliases.filter((a) => aliases.includes(a));
|
||||
if (aliasFilter !== 'all' && !aliases.includes(aliasFilter)) aliasFilter = 'all';
|
||||
} catch {
|
||||
availableAliases = [];
|
||||
}
|
||||
@@ -224,31 +210,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-4 flex flex-wrap items-end gap-2">
|
||||
<details class="rounded-md border bg-background p-2">
|
||||
<summary class="cursor-pointer select-none text-sm">
|
||||
<span class="text-xs text-muted-foreground">Aliases</span>
|
||||
<span class="ml-2 font-medium">{aliasesLabel()}</span>
|
||||
</summary>
|
||||
<div class="mt-2 max-h-48 overflow-auto pr-1">
|
||||
{#if availableAliases.length === 0}
|
||||
<div class="px-2 py-1 text-xs text-muted-foreground">Список нод не загружен</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each availableAliases as a (a)}
|
||||
<label class="flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-accent/50">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="rounded"
|
||||
checked={selectedAliases.includes(a)}
|
||||
onchange={() => toggleAlias(a)}
|
||||
/>
|
||||
<span class="text-sm">{a}</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</details>
|
||||
<AliasFilterSelect availableAliases={availableAliases} bind:value={aliasFilter} />
|
||||
<label class="text-xs text-muted-foreground">
|
||||
Refresh (sec)
|
||||
<input
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { fetchAggUniqueIps, ApiError } from '$lib/api/client.js';
|
||||
import { fetchAggUniqueIps, fetchAggSummary, ApiError } from '$lib/api/client.js';
|
||||
import type { components } from '$lib/api/aggregate.gen.js';
|
||||
import type * as Leaflet from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
@@ -13,14 +13,15 @@
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
import AliasFilterSelect from '$lib/components/alias-filter-select.svelte';
|
||||
|
||||
let loading = $state(true);
|
||||
let err = $state<string | null>(null);
|
||||
let rows = $state<components['schemas']['UniqueIPsRow'][]>([]);
|
||||
let partial = $state(false);
|
||||
let geo = $state(true);
|
||||
let aliases = $state('');
|
||||
let aliasesInput = $state('');
|
||||
let aliasFilter = $state('all');
|
||||
let availableAliases = $state<string[]>([]);
|
||||
let refreshSeconds = $state(30);
|
||||
let refreshInput = $state('30');
|
||||
let lastSuccessAtMs = $state<number | null>(null);
|
||||
@@ -36,6 +37,11 @@
|
||||
let mapStatus = $state<string | null>(null);
|
||||
let renderAttempts = $state(0);
|
||||
|
||||
function parseAliasFilter(raw: string | null): string {
|
||||
const value = raw?.trim() ?? '';
|
||||
return !value || value.includes(',') ? 'all' : value;
|
||||
}
|
||||
|
||||
type IpCategory = 'vpn' | 'hoster' | 'isp' | 'unknown';
|
||||
|
||||
function classifyByAsOrg(asOrg: string | null | undefined): IpCategory {
|
||||
@@ -283,7 +289,10 @@
|
||||
// Ждём, пока bind:this завершится и контейнеру карты будет доступен размер.
|
||||
await tick();
|
||||
initMap();
|
||||
const env = await fetchAggUniqueIps({ aliases: aliases || undefined, geo });
|
||||
const env = await fetchAggUniqueIps({
|
||||
aliases: aliasFilter === 'all' ? undefined : aliasFilter,
|
||||
geo
|
||||
});
|
||||
partial = !!env.partial;
|
||||
rows = env.data ?? [];
|
||||
lastSuccessAtMs = Date.now();
|
||||
@@ -326,10 +335,9 @@
|
||||
|
||||
async function applyControls() {
|
||||
const q = new URLSearchParams(page.url.searchParams);
|
||||
const normalizedAliases = aliasesInput.trim();
|
||||
const parsedRefresh = parseRefresh(refreshInput);
|
||||
if (normalizedAliases) q.set('aliases', normalizedAliases);
|
||||
else q.delete('aliases');
|
||||
if (aliasFilter === 'all') q.delete('aliases');
|
||||
else q.set('aliases', aliasFilter);
|
||||
if (parsedRefresh === 30) q.delete('refresh');
|
||||
else q.set('refresh', String(parsedRefresh));
|
||||
q.set('geo', geo ? '1' : '0');
|
||||
@@ -345,10 +353,9 @@
|
||||
$effect(() => {
|
||||
queryKey;
|
||||
if (typeof window === 'undefined') return;
|
||||
aliases = page.url.searchParams.get('aliases')?.trim() ?? '';
|
||||
aliasFilter = parseAliasFilter(page.url.searchParams.get('aliases'));
|
||||
refreshSeconds = parseRefresh(page.url.searchParams.get('refresh'));
|
||||
geo = parse01(page.url.searchParams.get('geo'), true);
|
||||
aliasesInput = aliases;
|
||||
refreshInput = String(refreshSeconds);
|
||||
restartPolling();
|
||||
void load();
|
||||
@@ -362,6 +369,19 @@
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
const env = await fetchAggSummary({ top_n: 10 });
|
||||
const aliases = (env.data.servers ?? [])
|
||||
.map((s) => s.alias)
|
||||
.filter((a): a is string => typeof a === 'string' && a.length > 0)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
availableAliases = aliases;
|
||||
if (aliasFilter !== 'all' && !aliases.includes(aliasFilter)) aliasFilter = 'all';
|
||||
} catch {
|
||||
availableAliases = [];
|
||||
}
|
||||
})();
|
||||
staleTimer = setInterval(() => {
|
||||
nowMs = Date.now();
|
||||
}, 1000);
|
||||
@@ -398,15 +418,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-4 flex flex-wrap items-end gap-2">
|
||||
<label class="text-xs text-muted-foreground">
|
||||
Aliases
|
||||
<input
|
||||
type="text"
|
||||
class="mt-1 h-9 w-56 rounded-md border bg-background px-2 text-sm"
|
||||
bind:value={aliasesInput}
|
||||
placeholder="node-a,node-b"
|
||||
/>
|
||||
</label>
|
||||
<AliasFilterSelect availableAliases={availableAliases} bind:value={aliasFilter} />
|
||||
<label class="text-xs text-muted-foreground">
|
||||
Refresh (sec)
|
||||
<input
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
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 AliasFilterSelect from '$lib/components/alias-filter-select.svelte';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
|
||||
type LiveStatus = 'healthy' | 'degraded' | 'critical';
|
||||
@@ -34,7 +35,7 @@
|
||||
};
|
||||
|
||||
let availableAliases = $state<string[]>([]);
|
||||
let selectedAliases = $state<string[]>([]);
|
||||
let aliasFilter = $state('all');
|
||||
let err = $state<string | null>(null);
|
||||
let connected = $state(false);
|
||||
let reconnectInSec = $state<number | null>(null);
|
||||
@@ -58,25 +59,9 @@
|
||||
return 'default';
|
||||
}
|
||||
|
||||
function parseAliases(raw: string | null): string[] {
|
||||
if (!raw) return [];
|
||||
const out = raw
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
return Array.from(new Set(out)).sort();
|
||||
}
|
||||
|
||||
function aliasesLabel(): string {
|
||||
if (selectedAliases.length === 0) return 'Все ноды';
|
||||
if (selectedAliases.length <= 3) return selectedAliases.join(', ');
|
||||
return `${selectedAliases.slice(0, 3).join(', ')} +${selectedAliases.length - 3}`;
|
||||
}
|
||||
|
||||
function toggleAlias(a: string) {
|
||||
if (!a) return;
|
||||
const has = selectedAliases.includes(a);
|
||||
selectedAliases = has ? selectedAliases.filter((x) => x !== a) : [...selectedAliases, a].sort();
|
||||
function parseAliasFilter(raw: string | null): string {
|
||||
const value = raw?.trim() ?? '';
|
||||
return !value || value.includes(',') ? 'all' : value;
|
||||
}
|
||||
|
||||
function normIncident(v: unknown): IncidentItem | null {
|
||||
@@ -170,7 +155,7 @@
|
||||
closeStream();
|
||||
clearReconnectTimer();
|
||||
err = null;
|
||||
const aliasesParam = selectedAliases.length ? selectedAliases.join(',') : undefined;
|
||||
const aliasesParam = aliasFilter === 'all' ? undefined : aliasFilter;
|
||||
const url = liveEventsUrl({ aliases: aliasesParam });
|
||||
const next = new EventSource(url);
|
||||
next.onopen = () => {
|
||||
@@ -215,8 +200,8 @@
|
||||
|
||||
async function applyControls() {
|
||||
const q = new URLSearchParams(page.url.searchParams);
|
||||
if (selectedAliases.length > 0) q.set('aliases', selectedAliases.join(','));
|
||||
else q.delete('aliases');
|
||||
if (aliasFilter === 'all') q.delete('aliases');
|
||||
else q.set('aliases', aliasFilter);
|
||||
const qs = q.toString();
|
||||
await goto(`${page.url.pathname}${qs ? `?${qs}` : ''}`, {
|
||||
replaceState: true,
|
||||
@@ -229,7 +214,7 @@
|
||||
$effect(() => {
|
||||
queryKey;
|
||||
if (typeof window === 'undefined') return;
|
||||
selectedAliases = parseAliases(page.url.searchParams.get('aliases'));
|
||||
aliasFilter = parseAliasFilter(page.url.searchParams.get('aliases'));
|
||||
connectStream();
|
||||
});
|
||||
|
||||
@@ -245,9 +230,9 @@
|
||||
const aliases = (env.data.servers ?? [])
|
||||
.map((s) => s.alias)
|
||||
.filter((a): a is string => typeof a === 'string' && a.length > 0)
|
||||
.sort();
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
availableAliases = aliases;
|
||||
selectedAliases = selectedAliases.filter((a) => aliases.includes(a));
|
||||
if (aliasFilter !== 'all' && !aliases.includes(aliasFilter)) aliasFilter = 'all';
|
||||
} catch {
|
||||
availableAliases = [];
|
||||
}
|
||||
@@ -283,31 +268,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-4 flex flex-wrap items-end gap-2">
|
||||
<details class="rounded-md border bg-background p-2">
|
||||
<summary class="cursor-pointer select-none text-sm">
|
||||
<span class="text-xs text-muted-foreground">Aliases</span>
|
||||
<span class="ml-2 font-medium">{aliasesLabel()}</span>
|
||||
</summary>
|
||||
<div class="mt-2 max-h-48 overflow-auto pr-1">
|
||||
{#if availableAliases.length === 0}
|
||||
<div class="px-2 py-1 text-xs text-muted-foreground">Список нод не загружен</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each availableAliases as a (a)}
|
||||
<label class="flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-accent/50">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="rounded"
|
||||
checked={selectedAliases.includes(a)}
|
||||
onchange={() => toggleAlias(a)}
|
||||
/>
|
||||
<span class="text-sm">{a}</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</details>
|
||||
<AliasFilterSelect availableAliases={availableAliases} bind:value={aliasFilter} />
|
||||
<Button variant="outline" size="sm" onclick={() => void applyControls()}>
|
||||
Apply
|
||||
</Button>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { fetchAggUsers, ApiError } from '$lib/api/client.js';
|
||||
import { fetchAggUsers, fetchAggSummary, ApiError } from '$lib/api/client.js';
|
||||
import type { components } from '$lib/api/aggregate.gen.js';
|
||||
import { formatMiB } from '$lib/format.js';
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
@@ -10,6 +10,7 @@
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
|
||||
import AliasFilterSelect from '$lib/components/alias-filter-select.svelte';
|
||||
import CopyIcon from '@lucide/svelte/icons/copy';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
|
||||
@@ -18,8 +19,8 @@
|
||||
let rows = $state<components['schemas']['UsersRow'][]>([]);
|
||||
let partial = $state(false);
|
||||
let includeLinks = $state(false);
|
||||
let aliases = $state('');
|
||||
let aliasesInput = $state('');
|
||||
let aliasFilter = $state('all');
|
||||
let availableAliases = $state<string[]>([]);
|
||||
let refreshSeconds = $state(30);
|
||||
let refreshInput = $state('30');
|
||||
let lastSuccessAtMs = $state<number | null>(null);
|
||||
@@ -27,6 +28,11 @@
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let staleTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function parseAliasFilter(raw: string | null): string {
|
||||
const value = raw?.trim() ?? '';
|
||||
return !value || value.includes(',') ? 'all' : value;
|
||||
}
|
||||
|
||||
function parseRefresh(raw: string | null): number {
|
||||
if (raw == null || raw.trim() === '') return 30;
|
||||
const n = Number(raw);
|
||||
@@ -45,7 +51,7 @@
|
||||
err = null;
|
||||
try {
|
||||
const usersEnv = await fetchAggUsers({
|
||||
aliases: aliases || undefined,
|
||||
aliases: aliasFilter === 'all' ? undefined : aliasFilter,
|
||||
include_links: includeLinks
|
||||
});
|
||||
partial = !!usersEnv.partial;
|
||||
@@ -72,10 +78,9 @@
|
||||
|
||||
async function applyControls() {
|
||||
const q = new URLSearchParams(page.url.searchParams);
|
||||
const normalizedAliases = aliasesInput.trim();
|
||||
const parsedRefresh = parseRefresh(refreshInput);
|
||||
if (normalizedAliases) q.set('aliases', normalizedAliases);
|
||||
else q.delete('aliases');
|
||||
if (aliasFilter === 'all') q.delete('aliases');
|
||||
else q.set('aliases', aliasFilter);
|
||||
if (parsedRefresh === 30) q.delete('refresh');
|
||||
else q.set('refresh', String(parsedRefresh));
|
||||
q.set('include_links', includeLinks ? '1' : '0');
|
||||
@@ -91,10 +96,9 @@
|
||||
$effect(() => {
|
||||
queryKey;
|
||||
if (typeof window === 'undefined') return;
|
||||
aliases = page.url.searchParams.get('aliases')?.trim() ?? '';
|
||||
aliasFilter = parseAliasFilter(page.url.searchParams.get('aliases'));
|
||||
refreshSeconds = parseRefresh(page.url.searchParams.get('refresh'));
|
||||
includeLinks = parse01(page.url.searchParams.get('include_links'), false);
|
||||
aliasesInput = aliases;
|
||||
refreshInput = String(refreshSeconds);
|
||||
restartPolling();
|
||||
void load();
|
||||
@@ -108,6 +112,19 @@
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
const env = await fetchAggSummary({ top_n: 10 });
|
||||
const aliases = (env.data.servers ?? [])
|
||||
.map((s) => s.alias)
|
||||
.filter((a): a is string => typeof a === 'string' && a.length > 0)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
availableAliases = aliases;
|
||||
if (aliasFilter !== 'all' && !aliases.includes(aliasFilter)) aliasFilter = 'all';
|
||||
} catch {
|
||||
availableAliases = [];
|
||||
}
|
||||
})();
|
||||
staleTimer = setInterval(() => {
|
||||
nowMs = Date.now();
|
||||
}, 1000);
|
||||
@@ -180,15 +197,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-4 flex flex-wrap items-end gap-2">
|
||||
<label class="text-xs text-muted-foreground">
|
||||
Aliases
|
||||
<input
|
||||
type="text"
|
||||
class="mt-1 h-9 w-56 rounded-md border bg-background px-2 text-sm"
|
||||
bind:value={aliasesInput}
|
||||
placeholder="node-a,node-b"
|
||||
/>
|
||||
</label>
|
||||
<AliasFilterSelect availableAliases={availableAliases} bind:value={aliasFilter} />
|
||||
<label class="text-xs text-muted-foreground">
|
||||
Refresh (sec)
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user