feat(web): add jobs audit and settings workspaces
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
// @vitest-environment jsdom
|
||||
import { cleanup, render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
InstanceSettingsPage,
|
||||
sanitizeFleetSnapshot,
|
||||
type FleetDataSource,
|
||||
type FleetSnapshot,
|
||||
} from './instance-settings-page.js';
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
function deferredSource() {
|
||||
let resolve!: (value: FleetSnapshot) => void;
|
||||
let reject!: (reason: unknown) => void;
|
||||
const load = vi.fn<FleetDataSource['load']>(
|
||||
() =>
|
||||
new Promise((done, fail) => {
|
||||
resolve = done;
|
||||
reject = fail;
|
||||
}),
|
||||
);
|
||||
return {
|
||||
source: { load },
|
||||
load,
|
||||
resolve: (value: FleetSnapshot) => resolve(value),
|
||||
reject: (reason: unknown) => reject(reason),
|
||||
};
|
||||
}
|
||||
|
||||
const snapshot: FleetSnapshot = {
|
||||
instances: [
|
||||
{
|
||||
id: 'west/one',
|
||||
name: 'West modem',
|
||||
url: 'https://operator:secret@west.example:8443/admin?token=secret',
|
||||
description: 'must not appear',
|
||||
tags: ['private-tag'],
|
||||
},
|
||||
{ id: 'offline', url: 'javascript:alert(1)' },
|
||||
],
|
||||
statuses: new Map([
|
||||
[
|
||||
'west/one',
|
||||
{
|
||||
reachable: true,
|
||||
authenticated: true,
|
||||
latencyMs: 12,
|
||||
summary: {
|
||||
capabilities: ['messages', 'calls', 7],
|
||||
freshness: 'fresh',
|
||||
password: 'never-render',
|
||||
credentialConfigured: true,
|
||||
version: 'also-not-part-of-this-page',
|
||||
},
|
||||
},
|
||||
],
|
||||
['offline', { reachable: false }],
|
||||
]),
|
||||
};
|
||||
|
||||
describe('Settings instances page', () => {
|
||||
it('rejects identity values instead of trimming, truncating, or admitting controls', () => {
|
||||
const longId = 'x'.repeat(257);
|
||||
const source = (id: string, name?: string) => ({
|
||||
instances: [{ id, url: 'https://example.test', ...(name === undefined ? {} : { name }) }],
|
||||
statuses: new Map(),
|
||||
});
|
||||
|
||||
for (const id of [' padded', 'padded ', longId, 'line\nbreak', 'null\0byte']) {
|
||||
expect(sanitizeFleetSnapshot(source(id))).toEqual({ instances: [], statuses: new Map() });
|
||||
}
|
||||
expect(sanitizeFleetSnapshot(source('stable-id', ` ${'n'.repeat(300)} `))?.instances).toEqual(
|
||||
[{ id: 'stable-id', url: 'https://example.test', name: 'n'.repeat(256) }],
|
||||
);
|
||||
});
|
||||
|
||||
it('is explicitly unavailable without an injected fleet source', () => {
|
||||
render(<InstanceSettingsPage />);
|
||||
expect(screen.getByRole('heading', { name: 'Instances' })).toBeTruthy();
|
||||
expect(screen.getByRole('status', { name: 'Instances unavailable' }).textContent).toMatch(
|
||||
/no fleet data source/i,
|
||||
);
|
||||
expect(screen.getByRole('link', { name: 'Add instance' }).getAttribute('href')).toBe(
|
||||
'/instances/new',
|
||||
);
|
||||
});
|
||||
|
||||
it('loads and renders an accessible, safe settings list using only fleet fields', async () => {
|
||||
const pending = deferredSource();
|
||||
render(<InstanceSettingsPage dataSource={pending.source} />);
|
||||
expect(screen.getByRole('status', { name: 'Instances loading status' })).toBeTruthy();
|
||||
expect(pending.load).toHaveBeenCalledWith(expect.any(AbortSignal));
|
||||
pending.resolve(snapshot);
|
||||
|
||||
const list = await screen.findByRole('list', { name: 'Configured instances' });
|
||||
const west = within(list).getByRole('listitem', { name: 'West modem' });
|
||||
expect(within(west).getByRole('link', { name: 'West modem' }).getAttribute('href')).toBe(
|
||||
'/settings/instances/west%2Fone',
|
||||
);
|
||||
const origin = within(west).getByRole('link', { name: 'Open West modem origin' });
|
||||
expect(origin.getAttribute('href')).toBe('https://west.example:8443');
|
||||
expect(origin.getAttribute('rel')).toBe('noopener noreferrer');
|
||||
for (const value of ['Online', 'Authenticated', 'Fresh', 'messages, calls'])
|
||||
expect(within(west).getByText(value)).toBeTruthy();
|
||||
|
||||
const offline = within(list).getByRole('listitem', { name: 'offline' });
|
||||
expect(within(offline).getByText('Invalid origin')).toBeTruthy();
|
||||
expect(within(offline).getByText('Offline')).toBeTruthy();
|
||||
expect(within(offline).queryByText(/authentication|freshness|capabilities/i)).toBeNull();
|
||||
|
||||
expect(document.body.textContent).not.toMatch(
|
||||
/secret|must not appear|private-tag|never-render|credentialConfigured|also-not-part/,
|
||||
);
|
||||
});
|
||||
|
||||
it('shows a fixed error, retries, and never exposes rejection details', async () => {
|
||||
const user = userEvent.setup();
|
||||
const load = vi
|
||||
.fn<FleetDataSource['load']>()
|
||||
.mockRejectedValueOnce(new Error('password=top-secret'))
|
||||
.mockResolvedValueOnce({ instances: [], statuses: new Map() });
|
||||
render(<InstanceSettingsPage dataSource={{ load }} />);
|
||||
const alert = await screen.findByRole('alert');
|
||||
expect(alert.textContent).toBe('Instances could not be loaded.Retry loading instances');
|
||||
expect(document.body.textContent).not.toContain('top-secret');
|
||||
await user.click(screen.getByRole('button', { name: 'Retry loading instances' }));
|
||||
expect(await screen.findByText('No instances are configured.')).toBeTruthy();
|
||||
expect(load).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('aborts superseded loads and fences late results', async () => {
|
||||
const first = deferredSource();
|
||||
const second = deferredSource();
|
||||
let calls = 0;
|
||||
const load = vi.fn<FleetDataSource['load']>((signal) =>
|
||||
(calls++ === 0 ? first : second).source.load(signal),
|
||||
);
|
||||
const source = { load };
|
||||
const { rerender } = render(<InstanceSettingsPage dataSource={source} refreshSignal={0} />);
|
||||
const oldSignal = load.mock.calls[0]?.[0];
|
||||
rerender(<InstanceSettingsPage dataSource={source} refreshSignal={1} />);
|
||||
expect(oldSignal?.aborted).toBe(true);
|
||||
first.resolve({
|
||||
instances: [{ id: 'stale', url: 'https://stale.example' }],
|
||||
statuses: new Map(),
|
||||
});
|
||||
second.resolve({
|
||||
instances: [{ id: 'current', url: 'https://current.example' }],
|
||||
statuses: new Map(),
|
||||
});
|
||||
expect(await screen.findByRole('link', { name: 'current' })).toBeTruthy();
|
||||
expect(screen.queryByRole('link', { name: 'stale' })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,281 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
canonicalHttpOrigin,
|
||||
type FleetDataSource,
|
||||
type FleetSnapshot,
|
||||
} from '../fleet/fleet-page.js';
|
||||
import type { FleetInstance, FleetStatus } from '../fleet/fleet-table-view-model.js';
|
||||
|
||||
export type { FleetDataSource, FleetSnapshot } from '../fleet/fleet-page.js';
|
||||
|
||||
const MAX_INSTANCES = 1000;
|
||||
const MAX_STRING = 256;
|
||||
const FRESHNESS = new Set(['fresh', 'stale', 'unknown']);
|
||||
|
||||
interface SafeInstance {
|
||||
readonly id: string;
|
||||
readonly name?: string;
|
||||
readonly url: string;
|
||||
}
|
||||
interface SafeStatus {
|
||||
readonly reachable: boolean;
|
||||
readonly authenticated?: boolean;
|
||||
readonly capabilities?: readonly string[];
|
||||
readonly freshness?: 'fresh' | 'stale' | 'unknown';
|
||||
}
|
||||
interface SafeSnapshot {
|
||||
readonly instances: readonly SafeInstance[];
|
||||
readonly statuses: ReadonlyMap<string, SafeStatus>;
|
||||
}
|
||||
|
||||
export interface InstanceSettingsPageProps {
|
||||
readonly dataSource?: FleetDataSource;
|
||||
readonly initialData?: FleetSnapshot;
|
||||
readonly refreshSignal?: number;
|
||||
}
|
||||
|
||||
function record(value: unknown): Record<string, unknown> | null {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function boundedDisplayString(value: unknown): string | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
const clean = value.trim();
|
||||
return clean ? clean.slice(0, MAX_STRING) : null;
|
||||
}
|
||||
|
||||
function identityString(value: unknown): string | null {
|
||||
return typeof value === 'string' &&
|
||||
value.length > 0 &&
|
||||
value.length <= MAX_STRING &&
|
||||
value === value.trim() &&
|
||||
!/[\u0000-\u001f\u007f-\u009f]/u.test(value)
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
function safeInstance(value: unknown): SafeInstance | null {
|
||||
const source = record(value);
|
||||
if (!source) return null;
|
||||
const id = identityString(source.id);
|
||||
const url = boundedDisplayString(source.url);
|
||||
if (!id || !url) return null;
|
||||
const name = boundedDisplayString(source.name);
|
||||
return { id, url, ...(name ? { name } : {}) };
|
||||
}
|
||||
|
||||
function safeStatus(value: unknown): SafeStatus | null {
|
||||
const source = record(value);
|
||||
if (!source || typeof source.reachable !== 'boolean') return null;
|
||||
const summary = record(source.summary);
|
||||
const rawCapabilities = summary?.capabilities;
|
||||
const capabilities = Array.isArray(rawCapabilities)
|
||||
? rawCapabilities
|
||||
.map(boundedDisplayString)
|
||||
.filter((entry): entry is string => entry !== null)
|
||||
.slice(0, 100)
|
||||
: undefined;
|
||||
const rawFreshness = summary?.freshness;
|
||||
const freshness =
|
||||
typeof rawFreshness === 'string' && FRESHNESS.has(rawFreshness)
|
||||
? (rawFreshness as SafeStatus['freshness'])
|
||||
: undefined;
|
||||
return {
|
||||
reachable: source.reachable,
|
||||
...(typeof source.authenticated === 'boolean' ? { authenticated: source.authenticated } : {}),
|
||||
...(capabilities?.length ? { capabilities } : {}),
|
||||
...(freshness ? { freshness } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Retains only fields with defined Fleet semantics; arbitrary summary data is discarded. */
|
||||
export function sanitizeFleetSnapshot(value: unknown): SafeSnapshot | null {
|
||||
const source = record(value);
|
||||
if (!source || !Array.isArray(source.instances) || !(source.statuses instanceof Map)) return null;
|
||||
const instances: SafeInstance[] = [];
|
||||
const ids = new Set<string>();
|
||||
for (const candidate of source.instances) {
|
||||
const instance = safeInstance(candidate);
|
||||
if (!instance || ids.has(instance.id)) continue;
|
||||
instances.push(instance);
|
||||
ids.add(instance.id);
|
||||
if (instances.length === MAX_INSTANCES) break;
|
||||
}
|
||||
const statuses = new Map<string, SafeStatus>();
|
||||
for (const id of ids) {
|
||||
const status = safeStatus(source.statuses.get(id));
|
||||
if (status) statuses.set(id, status);
|
||||
}
|
||||
return { instances, statuses };
|
||||
}
|
||||
|
||||
function statusLabel(status: SafeStatus | undefined): string | null {
|
||||
if (!status) return null;
|
||||
if (!status.reachable) return 'Offline';
|
||||
if (status.authenticated === false) return 'Authentication required';
|
||||
return 'Online';
|
||||
}
|
||||
|
||||
function authLabel(status: SafeStatus | undefined): string | null {
|
||||
if (status?.authenticated === true) return 'Authenticated';
|
||||
if (status?.authenticated === false) return 'Authentication required';
|
||||
return null;
|
||||
}
|
||||
|
||||
function titleCase(value: string): string {
|
||||
return value[0]!.toUpperCase() + value.slice(1);
|
||||
}
|
||||
|
||||
export function InstanceSettingsPage({
|
||||
dataSource,
|
||||
initialData,
|
||||
refreshSignal = 0,
|
||||
}: InstanceSettingsPageProps) {
|
||||
const [snapshot, setSnapshot] = useState<SafeSnapshot | null>(() =>
|
||||
initialData ? sanitizeFleetSnapshot(initialData) : null,
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [failed, setFailed] = useState(false);
|
||||
const [attempt, setAttempt] = useState(0);
|
||||
const request = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData && refreshSignal === 0 && attempt === 0) {
|
||||
const clean = sanitizeFleetSnapshot(initialData);
|
||||
setSnapshot(clean);
|
||||
setFailed(clean === null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!dataSource) return;
|
||||
const controller = new AbortController();
|
||||
const requestId = ++request.current;
|
||||
setLoading(true);
|
||||
setFailed(false);
|
||||
void dataSource.load(controller.signal).then(
|
||||
(raw) => {
|
||||
if (controller.signal.aborted || request.current !== requestId) return;
|
||||
const clean = sanitizeFleetSnapshot(raw);
|
||||
setSnapshot(clean);
|
||||
setFailed(clean === null);
|
||||
setLoading(false);
|
||||
},
|
||||
() => {
|
||||
if (controller.signal.aborted || request.current !== requestId) return;
|
||||
setSnapshot(null);
|
||||
setFailed(true);
|
||||
setLoading(false);
|
||||
},
|
||||
);
|
||||
return () => controller.abort();
|
||||
}, [attempt, dataSource, initialData, refreshSignal]);
|
||||
|
||||
const unavailable = !dataSource && !initialData;
|
||||
|
||||
return (
|
||||
<section aria-labelledby="settings-instances-title">
|
||||
<header>
|
||||
<div>
|
||||
<h1 id="settings-instances-title">Instances</h1>
|
||||
<p>Configure the SimAdmin instances available to this workspace.</p>
|
||||
</div>
|
||||
<a href="/instances/new">Add instance</a>
|
||||
</header>
|
||||
|
||||
{unavailable ? (
|
||||
<p role="status" aria-label="Instances unavailable">
|
||||
Instances are runtime-unavailable because no fleet data source was provided.
|
||||
</p>
|
||||
) : null}
|
||||
{loading ? (
|
||||
<p role="status" aria-label="Instances loading status">
|
||||
Loading instances…
|
||||
</p>
|
||||
) : null}
|
||||
{failed ? (
|
||||
<div role="alert">
|
||||
<p>Instances could not be loaded.</p>
|
||||
{dataSource ? (
|
||||
<button type="button" onClick={() => setAttempt((value) => value + 1)}>
|
||||
Retry loading instances
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{!loading && !failed && snapshot?.instances.length === 0 ? (
|
||||
<p>No instances are configured.</p>
|
||||
) : null}
|
||||
{!loading && !failed && snapshot && snapshot.instances.length > 0 ? (
|
||||
<ul aria-label="Configured instances">
|
||||
{snapshot.instances.map((instance) => {
|
||||
const displayName = instance.name ?? instance.id;
|
||||
const origin = canonicalHttpOrigin(instance.url);
|
||||
const status = snapshot.statuses.get(instance.id);
|
||||
const state = statusLabel(status);
|
||||
const authentication = authLabel(status);
|
||||
return (
|
||||
<li key={instance.id} aria-label={displayName}>
|
||||
<h2>
|
||||
<a href={`/settings/instances/${encodeURIComponent(instance.id)}`}>
|
||||
{displayName}
|
||||
</a>
|
||||
</h2>
|
||||
{instance.name ? <p>{instance.id}</p> : null}
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Origin</dt>
|
||||
<dd>
|
||||
{origin ? (
|
||||
<a
|
||||
href={origin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`Open ${displayName} origin`}
|
||||
>
|
||||
{origin}
|
||||
</a>
|
||||
) : (
|
||||
'Invalid origin'
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
{state ? (
|
||||
<div>
|
||||
<dt>Status</dt>
|
||||
<dd>{state}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{authentication ? (
|
||||
<div>
|
||||
<dt>Authentication</dt>
|
||||
<dd>{authentication}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{status?.freshness ? (
|
||||
<div>
|
||||
<dt>Freshness</dt>
|
||||
<dd>{titleCase(status.freshness)}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{status?.capabilities?.length ? (
|
||||
<div>
|
||||
<dt>Capabilities</dt>
|
||||
<dd>{status.capabilities.join(', ')}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
</dl>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Compile-time checks that the sanitizer's accepted source fields remain Fleet-owned.
|
||||
void ({} as FleetInstance);
|
||||
void ({} as FleetStatus);
|
||||
Reference in New Issue
Block a user