Retain host and server scope during investigations
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import { untrack } from 'svelte';
|
||||
import { pollVisible } from '$ui/poll.js';
|
||||
let requestSequence = 0;
|
||||
@@ -57,7 +58,7 @@
|
||||
|
||||
<svelte:head><title>{host?.name ?? 'Host'} · Electric Cherry</title></svelte:head>
|
||||
<section class="page"><a href="/hosts">← All hosts</a><SectionHeader title={host?.name ?? 'Host'} subtitle="Running instances, readiness, inventory, settings, and credentials.">
|
||||
{#snippet actions()}<Button href="/hosts/operations">Operation history</Button>{#if canManage && host && (!cleaning || host.decommission?.retryable)}<Button variant="danger" disabled={busy} onclick={() => deleteOpen = true}>{cleaning ? 'Retry cleanup' : 'Delete host'}</Button>{/if}{/snippet}
|
||||
{#snippet actions()}<Button href={contextHref(`/capacity?host=${encodeURIComponent($page.params.id ?? "")}`, $page.url)}>Capacity</Button><Button href={contextHref(`/hosts/operations?host=${encodeURIComponent($page.params.id ?? "")}`, $page.url)}>Operation history</Button>{#if canManage && host && (!cleaning || host.decommission?.retryable)}<Button variant="danger" disabled={busy} onclick={() => deleteOpen = true}>{cleaning ? 'Retry cleanup' : 'Delete host'}</Button>{/if}{/snippet}
|
||||
</SectionHeader>
|
||||
{#if error}<p class="ui-error" role="alert">{error}</p>{/if}{#if notice}<p role="status">{notice}</p>{/if}
|
||||
{#if loading}<p role="status">Loading host…</p>{:else if host}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { listHostOperationHistory } from '$lib/server/hosts/index.js';
|
||||
import { isHostOperationFilter } from '$lib/hosts.js';
|
||||
import { getHost, listHostOperationHistory } from '$lib/server/hosts/index.js';
|
||||
import { hostOperationScope, isHostOperationFilter } from '$lib/hosts.js';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = ({ url }) => {
|
||||
const requestedPage = url.searchParams.get('page');
|
||||
const requestedFilter = url.searchParams.get('filter');
|
||||
const filter = isHostOperationFilter(requestedFilter) ? requestedFilter : 'important';
|
||||
const history = listHostOperationHistory(Number(requestedPage ?? 1), filter);
|
||||
const scope = hostOperationScope(url.searchParams);
|
||||
const history = listHostOperationHistory(Number(requestedPage ?? 1), filter, scope);
|
||||
if ((requestedPage !== null && requestedPage !== String(history.page))
|
||||
|| (requestedFilter !== null && requestedFilter !== filter)) {
|
||||
const params = new URLSearchParams();
|
||||
const params = new URLSearchParams(url.searchParams);
|
||||
params.delete('page');
|
||||
if (requestedFilter !== null) params.set('filter', filter);
|
||||
if (history.page !== 1) params.set('page', String(history.page));
|
||||
redirect(307, `/hosts/operations${params.size ? `?${params}` : ''}`);
|
||||
}
|
||||
return { history };
|
||||
return { history, scope, hostName: scope.host ? getHost(scope.host)?.name ?? scope.host : null };
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import { navigating } from '$app/stores';
|
||||
import { navigating, page } from '$app/stores';
|
||||
import type { HostOperationFilter, HostOperationHistoryPage } from '$lib/hosts.js';
|
||||
import type { PageData } from './$types';
|
||||
import { pollVisible } from '$ui/poll.js';
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import BackLink from '$ui/components/BackLink.svelte';
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
|
||||
import Button from '$ui/components/primitives/Button.svelte';
|
||||
|
||||
@@ -25,9 +27,10 @@
|
||||
{ id: 'all', label: 'All operations', description: 'Every recorded remote operation.', empty: 'No remote operations yet.' }
|
||||
];
|
||||
const selectedFilter = $derived(filters.find(filter => filter.id === history.filter)!);
|
||||
function pageHref(page: number, filter: HostOperationFilter = history.filter) {
|
||||
const params = new URLSearchParams({ filter });
|
||||
if (page !== 1) params.set('page', String(page));
|
||||
function pageHref(pageNumber: number, filter: HostOperationFilter = history.filter) {
|
||||
const params = new URLSearchParams($page.url.searchParams);
|
||||
params.set('filter', filter); params.delete('page');
|
||||
if (pageNumber !== 1) params.set('page', String(pageNumber));
|
||||
return `/hosts/operations?${params}`;
|
||||
}
|
||||
function cancel() { sequence++; request?.abort(); request = null; refreshing = false; }
|
||||
@@ -40,7 +43,7 @@
|
||||
request = controller;
|
||||
refreshing = true;
|
||||
try {
|
||||
const params = new URLSearchParams({ page: String(history.page), filter: history.filter });
|
||||
const params = new URLSearchParams({ ...data.scope, page: String(history.page), filter: history.filter });
|
||||
const response = await fetch(`/api/hosts/operations?${params}`, {
|
||||
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(15000), ...(signal ? [signal] : [])])
|
||||
});
|
||||
@@ -60,12 +63,14 @@
|
||||
onMount(() => { const stop = pollVisible(refresh, 15000, false); return () => { stop(); cancel(); }; });
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Operation history · Hosts · Electric Cherry</title></svelte:head>
|
||||
<svelte:head><title>Host operation history · Electric Cherry</title></svelte:head>
|
||||
|
||||
<section class="page" aria-busy={changingPage}>
|
||||
<SectionHeader title="Operation history" eyebrow="Hosts" subtitle="Remote operations across all hosts, newest first.">
|
||||
<SectionHeader title="Host operation history" eyebrow="Operations" subtitle="Remote operations, newest first. Filtered views retain their host and server context.">
|
||||
{#snippet actions()}<Button disabled={refreshing || changingPage} onclick={() => void refresh()}>{refreshing ? 'Refreshing…' : 'Refresh'}</Button>{/snippet}
|
||||
</SectionHeader>
|
||||
{#if $page.url.searchParams.has('returnTo')}<BackLink fallback="/hosts" />{/if}
|
||||
{#if data.scope.host || data.scope.server}<div class="scope" role="status"><span>Showing {data.hostName ? `host: ${data.hostName}` : ''}{data.scope.host && data.scope.server ? ' · ' : ''}{data.scope.server ? `server: ${data.scope.server}` : ''}</span><a href={`/hosts/operations?filter=${history.filter}`}>Clear scope</a></div>{/if}
|
||||
<div class="history-filters">
|
||||
<nav class="filter-links" aria-label="Operation history filters">
|
||||
{#each filters as filter}
|
||||
@@ -86,7 +91,7 @@
|
||||
<tbody>{#each history.operations as operation (operation.id)}
|
||||
<tr>
|
||||
<td class="operation">{operation.kind}<small>{operation.id}</small></td>
|
||||
<td>{#if operation.hostName !== null}<a href={`/hosts/${operation.hostId}`}>{operation.hostName}</a>{:else}Deleted host<small>{operation.hostId}</small>{/if}</td>
|
||||
<td>{#if operation.hostName !== null}<a href={contextHref(`/hosts/${operation.hostId}`, $page.url)}>{operation.hostName}</a>{:else}Deleted host<small>{operation.hostId}</small>{/if}</td>
|
||||
<td class="target">
|
||||
<span>{operation.serverName ?? operation.instanceId ?? 'Host-level'}</span>
|
||||
{#if operation.targetPath}<small class="target-path">{operation.targetPath}</small>{/if}
|
||||
@@ -112,6 +117,7 @@
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.scope { display:flex; flex-wrap:wrap; gap:16px; color:var(--text-secondary); font-size:var(--text-sm); }
|
||||
.page { display: grid; gap: 24px; min-width: 0; }
|
||||
.history-filters { min-width: 0; }
|
||||
.history-filters p { margin: 10px 0 0; color: var(--text-secondary); font-size: var(--text-sm); }
|
||||
|
||||
Reference in New Issue
Block a user