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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (g *Gateway) Shutdown(ctx context.Context) error {
|
||||
g.transport.CloseIdleConnections()
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import {
|
||||
ApiError,
|
||||
fetchAggIncidents,
|
||||
fetchAggSummary,
|
||||
type IncidentItem,
|
||||
type IncidentsData
|
||||
} from '$lib/api/client.js';
|
||||
@@ -29,8 +30,8 @@
|
||||
let partial = $state(false);
|
||||
let generatedAt = $state<string | null>(null);
|
||||
let data = $state<IncidentsData | null>(null);
|
||||
let aliases = $state('');
|
||||
let aliasesInput = $state('');
|
||||
let availableAliases = $state<string[]>([]);
|
||||
let selectedAliases = $state<string[]>([]);
|
||||
let refreshSeconds = $state(30);
|
||||
let refreshInput = $state('30');
|
||||
let lastSuccessAtMs = $state<number | null>(null);
|
||||
@@ -47,6 +48,27 @@
|
||||
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 {
|
||||
return triageById[id] ?? {};
|
||||
}
|
||||
@@ -111,7 +133,8 @@
|
||||
loading = true;
|
||||
err = null;
|
||||
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;
|
||||
partial = !!env.partial;
|
||||
generatedAt = env.generated_at;
|
||||
@@ -125,9 +148,8 @@
|
||||
|
||||
async function applyControls() {
|
||||
const q = new URLSearchParams(page.url.searchParams);
|
||||
const normalizedAliases = aliasesInput.trim();
|
||||
const parsedRefresh = parseRefresh(refreshInput);
|
||||
if (normalizedAliases) q.set('aliases', normalizedAliases);
|
||||
if (selectedAliases.length > 0) q.set('aliases', selectedAliases.join(','));
|
||||
else q.delete('aliases');
|
||||
if (parsedRefresh === 30) q.delete('refresh');
|
||||
else q.set('refresh', String(parsedRefresh));
|
||||
@@ -143,9 +165,8 @@
|
||||
$effect(() => {
|
||||
queryKey;
|
||||
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'));
|
||||
aliasesInput = aliases;
|
||||
refreshInput = String(refreshSeconds);
|
||||
restartPolling();
|
||||
void load();
|
||||
@@ -160,6 +181,19 @@
|
||||
|
||||
onMount(() => {
|
||||
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(() => {
|
||||
nowMs = Date.now();
|
||||
}, 1000);
|
||||
@@ -190,15 +224,31 @@
|
||||
</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>
|
||||
<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>
|
||||
<label class="text-xs text-muted-foreground">
|
||||
Refresh (sec)
|
||||
<input
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
liveEventsUrl,
|
||||
fetchAggSummary,
|
||||
type IncidentItem,
|
||||
type IncidentSeverity,
|
||||
type IncidentStatus
|
||||
@@ -32,8 +33,8 @@
|
||||
aliases_used?: string[];
|
||||
};
|
||||
|
||||
let aliases = $state('');
|
||||
let aliasesInput = $state('');
|
||||
let availableAliases = $state<string[]>([]);
|
||||
let selectedAliases = $state<string[]>([]);
|
||||
let err = $state<string | null>(null);
|
||||
let connected = $state(false);
|
||||
let reconnectInSec = $state<number | null>(null);
|
||||
@@ -57,6 +58,27 @@
|
||||
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 {
|
||||
if (!v || typeof v !== 'object') return null;
|
||||
const x = v as Record<string, unknown>;
|
||||
@@ -148,7 +170,8 @@
|
||||
closeStream();
|
||||
clearReconnectTimer();
|
||||
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);
|
||||
next.onopen = () => {
|
||||
connected = true;
|
||||
@@ -192,8 +215,7 @@
|
||||
|
||||
async function applyControls() {
|
||||
const q = new URLSearchParams(page.url.searchParams);
|
||||
const normalizedAliases = aliasesInput.trim();
|
||||
if (normalizedAliases) q.set('aliases', normalizedAliases);
|
||||
if (selectedAliases.length > 0) q.set('aliases', selectedAliases.join(','));
|
||||
else q.delete('aliases');
|
||||
const qs = q.toString();
|
||||
await goto(`${page.url.pathname}${qs ? `?${qs}` : ''}`, {
|
||||
@@ -207,8 +229,7 @@
|
||||
$effect(() => {
|
||||
queryKey;
|
||||
if (typeof window === 'undefined') return;
|
||||
aliases = page.url.searchParams.get('aliases')?.trim() ?? '';
|
||||
aliasesInput = aliases;
|
||||
selectedAliases = parseAliases(page.url.searchParams.get('aliases'));
|
||||
connectStream();
|
||||
});
|
||||
|
||||
@@ -218,6 +239,19 @@
|
||||
let isStale = $derived.by(() => staleSeconds == null || staleSeconds > 12);
|
||||
|
||||
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(() => {
|
||||
nowMs = Date.now();
|
||||
}, 1000);
|
||||
@@ -249,15 +283,31 @@
|
||||
</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>
|
||||
<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>
|
||||
<Button variant="outline" size="sm" onclick={() => void applyControls()}>
|
||||
Apply
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user