Implement alias selection and summary fetching in incident and live pages
- Added functionality to manage and display available aliases for incidents and live updates, enhancing user interaction. - Refactored alias handling to use checkboxes for selection, improving usability and clarity in the UI. - Integrated fetching of aggregated summary data to populate available aliases dynamically, ensuring up-to-date information. - Updated the logic for applying selected aliases in API requests, streamlining data retrieval based on user preferences.
This commit is contained in:
@@ -356,6 +356,13 @@ func (s *statusWriter) WriteHeader(code int) {
|
|||||||
s.ResponseWriter.WriteHeader(code)
|
s.ResponseWriter.WriteHeader(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flush keeps streaming compatibility (SSE/EventSource) through middleware wrappers.
|
||||||
|
func (s *statusWriter) Flush() {
|
||||||
|
if f, ok := s.ResponseWriter.(http.Flusher); ok {
|
||||||
|
f.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Shutdown idle connections on the shared transport.
|
// Shutdown idle connections on the shared transport.
|
||||||
func (g *Gateway) Shutdown(ctx context.Context) error {
|
func (g *Gateway) Shutdown(ctx context.Context) error {
|
||||||
g.transport.CloseIdleConnections()
|
g.transport.CloseIdleConnections()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import {
|
import {
|
||||||
ApiError,
|
ApiError,
|
||||||
fetchAggIncidents,
|
fetchAggIncidents,
|
||||||
|
fetchAggSummary,
|
||||||
type IncidentItem,
|
type IncidentItem,
|
||||||
type IncidentsData
|
type IncidentsData
|
||||||
} from '$lib/api/client.js';
|
} from '$lib/api/client.js';
|
||||||
@@ -29,8 +30,8 @@
|
|||||||
let partial = $state(false);
|
let partial = $state(false);
|
||||||
let generatedAt = $state<string | null>(null);
|
let generatedAt = $state<string | null>(null);
|
||||||
let data = $state<IncidentsData | null>(null);
|
let data = $state<IncidentsData | null>(null);
|
||||||
let aliases = $state('');
|
let availableAliases = $state<string[]>([]);
|
||||||
let aliasesInput = $state('');
|
let selectedAliases = $state<string[]>([]);
|
||||||
let refreshSeconds = $state(30);
|
let refreshSeconds = $state(30);
|
||||||
let refreshInput = $state('30');
|
let refreshInput = $state('30');
|
||||||
let lastSuccessAtMs = $state<number | null>(null);
|
let lastSuccessAtMs = $state<number | null>(null);
|
||||||
@@ -47,6 +48,27 @@
|
|||||||
return Math.max(10, Math.min(300, Math.floor(n)));
|
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 triageFor(id: string): TriageState {
|
function triageFor(id: string): TriageState {
|
||||||
return triageById[id] ?? {};
|
return triageById[id] ?? {};
|
||||||
}
|
}
|
||||||
@@ -111,7 +133,8 @@
|
|||||||
loading = true;
|
loading = true;
|
||||||
err = null;
|
err = null;
|
||||||
try {
|
try {
|
||||||
const env = await fetchAggIncidents({ aliases: aliases || undefined });
|
const aliasesParam = selectedAliases.length ? selectedAliases.join(',') : undefined;
|
||||||
|
const env = await fetchAggIncidents({ aliases: aliasesParam });
|
||||||
data = env.data ?? null;
|
data = env.data ?? null;
|
||||||
partial = !!env.partial;
|
partial = !!env.partial;
|
||||||
generatedAt = env.generated_at;
|
generatedAt = env.generated_at;
|
||||||
@@ -125,9 +148,8 @@
|
|||||||
|
|
||||||
async function applyControls() {
|
async function applyControls() {
|
||||||
const q = new URLSearchParams(page.url.searchParams);
|
const q = new URLSearchParams(page.url.searchParams);
|
||||||
const normalizedAliases = aliasesInput.trim();
|
|
||||||
const parsedRefresh = parseRefresh(refreshInput);
|
const parsedRefresh = parseRefresh(refreshInput);
|
||||||
if (normalizedAliases) q.set('aliases', normalizedAliases);
|
if (selectedAliases.length > 0) q.set('aliases', selectedAliases.join(','));
|
||||||
else q.delete('aliases');
|
else q.delete('aliases');
|
||||||
if (parsedRefresh === 30) q.delete('refresh');
|
if (parsedRefresh === 30) q.delete('refresh');
|
||||||
else q.set('refresh', String(parsedRefresh));
|
else q.set('refresh', String(parsedRefresh));
|
||||||
@@ -143,9 +165,8 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
queryKey;
|
queryKey;
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
aliases = page.url.searchParams.get('aliases')?.trim() ?? '';
|
selectedAliases = parseAliases(page.url.searchParams.get('aliases'));
|
||||||
refreshSeconds = parseRefresh(page.url.searchParams.get('refresh'));
|
refreshSeconds = parseRefresh(page.url.searchParams.get('refresh'));
|
||||||
aliasesInput = aliases;
|
|
||||||
refreshInput = String(refreshSeconds);
|
refreshInput = String(refreshSeconds);
|
||||||
restartPolling();
|
restartPolling();
|
||||||
void load();
|
void load();
|
||||||
@@ -160,6 +181,19 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadTriage();
|
loadTriage();
|
||||||
|
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();
|
||||||
|
availableAliases = aliases;
|
||||||
|
selectedAliases = selectedAliases.filter((a) => aliases.includes(a));
|
||||||
|
} catch {
|
||||||
|
availableAliases = [];
|
||||||
|
}
|
||||||
|
})();
|
||||||
staleTimer = setInterval(() => {
|
staleTimer = setInterval(() => {
|
||||||
nowMs = Date.now();
|
nowMs = Date.now();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
@@ -190,15 +224,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4 flex flex-wrap items-end gap-2">
|
<div class="mb-4 flex flex-wrap items-end gap-2">
|
||||||
<label class="text-xs text-muted-foreground">
|
<details class="rounded-md border bg-background p-2">
|
||||||
Aliases
|
<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
|
<input
|
||||||
type="text"
|
type="checkbox"
|
||||||
class="mt-1 h-9 w-56 rounded-md border bg-background px-2 text-sm"
|
class="rounded"
|
||||||
bind:value={aliasesInput}
|
checked={selectedAliases.includes(a)}
|
||||||
placeholder="node-a,node-b"
|
onchange={() => toggleAlias(a)}
|
||||||
/>
|
/>
|
||||||
|
<span class="text-sm">{a}</span>
|
||||||
</label>
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
<label class="text-xs text-muted-foreground">
|
<label class="text-xs text-muted-foreground">
|
||||||
Refresh (sec)
|
Refresh (sec)
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import {
|
import {
|
||||||
liveEventsUrl,
|
liveEventsUrl,
|
||||||
|
fetchAggSummary,
|
||||||
type IncidentItem,
|
type IncidentItem,
|
||||||
type IncidentSeverity,
|
type IncidentSeverity,
|
||||||
type IncidentStatus
|
type IncidentStatus
|
||||||
@@ -32,8 +33,8 @@
|
|||||||
aliases_used?: string[];
|
aliases_used?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
let aliases = $state('');
|
let availableAliases = $state<string[]>([]);
|
||||||
let aliasesInput = $state('');
|
let selectedAliases = $state<string[]>([]);
|
||||||
let err = $state<string | null>(null);
|
let err = $state<string | null>(null);
|
||||||
let connected = $state(false);
|
let connected = $state(false);
|
||||||
let reconnectInSec = $state<number | null>(null);
|
let reconnectInSec = $state<number | null>(null);
|
||||||
@@ -57,6 +58,27 @@
|
|||||||
return 'default';
|
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 normIncident(v: unknown): IncidentItem | null {
|
function normIncident(v: unknown): IncidentItem | null {
|
||||||
if (!v || typeof v !== 'object') return null;
|
if (!v || typeof v !== 'object') return null;
|
||||||
const x = v as Record<string, unknown>;
|
const x = v as Record<string, unknown>;
|
||||||
@@ -148,7 +170,8 @@
|
|||||||
closeStream();
|
closeStream();
|
||||||
clearReconnectTimer();
|
clearReconnectTimer();
|
||||||
err = null;
|
err = null;
|
||||||
const url = liveEventsUrl({ aliases: aliases || undefined });
|
const aliasesParam = selectedAliases.length ? selectedAliases.join(',') : undefined;
|
||||||
|
const url = liveEventsUrl({ aliases: aliasesParam });
|
||||||
const next = new EventSource(url);
|
const next = new EventSource(url);
|
||||||
next.onopen = () => {
|
next.onopen = () => {
|
||||||
connected = true;
|
connected = true;
|
||||||
@@ -192,8 +215,7 @@
|
|||||||
|
|
||||||
async function applyControls() {
|
async function applyControls() {
|
||||||
const q = new URLSearchParams(page.url.searchParams);
|
const q = new URLSearchParams(page.url.searchParams);
|
||||||
const normalizedAliases = aliasesInput.trim();
|
if (selectedAliases.length > 0) q.set('aliases', selectedAliases.join(','));
|
||||||
if (normalizedAliases) q.set('aliases', normalizedAliases);
|
|
||||||
else q.delete('aliases');
|
else q.delete('aliases');
|
||||||
const qs = q.toString();
|
const qs = q.toString();
|
||||||
await goto(`${page.url.pathname}${qs ? `?${qs}` : ''}`, {
|
await goto(`${page.url.pathname}${qs ? `?${qs}` : ''}`, {
|
||||||
@@ -207,8 +229,7 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
queryKey;
|
queryKey;
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
aliases = page.url.searchParams.get('aliases')?.trim() ?? '';
|
selectedAliases = parseAliases(page.url.searchParams.get('aliases'));
|
||||||
aliasesInput = aliases;
|
|
||||||
connectStream();
|
connectStream();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -218,6 +239,19 @@
|
|||||||
let isStale = $derived.by(() => staleSeconds == null || staleSeconds > 12);
|
let isStale = $derived.by(() => staleSeconds == null || staleSeconds > 12);
|
||||||
|
|
||||||
onMount(() => {
|
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();
|
||||||
|
availableAliases = aliases;
|
||||||
|
selectedAliases = selectedAliases.filter((a) => aliases.includes(a));
|
||||||
|
} catch {
|
||||||
|
availableAliases = [];
|
||||||
|
}
|
||||||
|
})();
|
||||||
staleTimer = setInterval(() => {
|
staleTimer = setInterval(() => {
|
||||||
nowMs = Date.now();
|
nowMs = Date.now();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
@@ -249,15 +283,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4 flex flex-wrap items-end gap-2">
|
<div class="mb-4 flex flex-wrap items-end gap-2">
|
||||||
<label class="text-xs text-muted-foreground">
|
<details class="rounded-md border bg-background p-2">
|
||||||
Aliases
|
<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
|
<input
|
||||||
type="text"
|
type="checkbox"
|
||||||
class="mt-1 h-9 w-56 rounded-md border bg-background px-2 text-sm"
|
class="rounded"
|
||||||
bind:value={aliasesInput}
|
checked={selectedAliases.includes(a)}
|
||||||
placeholder="node-a,node-b"
|
onchange={() => toggleAlias(a)}
|
||||||
/>
|
/>
|
||||||
|
<span class="text-sm">{a}</span>
|
||||||
</label>
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
<Button variant="outline" size="sm" onclick={() => void applyControls()}>
|
<Button variant="outline" size="sm" onclick={() => void applyControls()}>
|
||||||
Apply
|
Apply
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user