feat(web): complete fleet and instance management slices
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { InstanceContext, InstanceModule } from '../app-shell.js';
|
||||
import { canonicalHttpOrigin } from '../fleet/fleet-page.js';
|
||||
|
||||
export type CapabilityState = 'supported' | 'degraded' | 'unsupported' | 'unknown';
|
||||
|
||||
export interface InstanceCapability {
|
||||
readonly state: CapabilityState;
|
||||
readonly explanation?: string;
|
||||
}
|
||||
|
||||
export type InstanceCapabilityMap = Readonly<Partial<Record<InstanceModule, InstanceCapability>>>;
|
||||
|
||||
export interface CapabilityDataSource {
|
||||
load(instanceId: string, signal: AbortSignal): Promise<InstanceCapabilityMap>;
|
||||
}
|
||||
|
||||
export interface InstanceDetailProps {
|
||||
readonly instanceId: string;
|
||||
readonly module: InstanceModule;
|
||||
readonly instance?: InstanceContext;
|
||||
readonly capabilities?: InstanceCapabilityMap;
|
||||
readonly capabilityDataSource?: CapabilityDataSource;
|
||||
}
|
||||
|
||||
export const INSTANCE_MODULE_LABELS: Readonly<Record<InstanceModule, string>> = {
|
||||
overview: 'Overview',
|
||||
cellular: 'Cellular',
|
||||
'device-network': 'Device Network',
|
||||
messages: 'Messages',
|
||||
calls: 'Calls',
|
||||
esim: 'eSIM',
|
||||
notifications: 'Notifications',
|
||||
automation: 'Automation',
|
||||
ota: 'OTA',
|
||||
};
|
||||
|
||||
const MODULES = Object.keys(INSTANCE_MODULE_LABELS) as readonly InstanceModule[];
|
||||
|
||||
const DEFAULT_EXPLANATIONS: Readonly<Record<Exclude<CapabilityState, 'supported'>, string>> = {
|
||||
degraded: 'This module is available with limited functionality.',
|
||||
unsupported: 'This instance does not support this module.',
|
||||
unknown: 'Capability state is unknown.',
|
||||
};
|
||||
|
||||
function capabilityFor(map: InstanceCapabilityMap, module: InstanceModule): InstanceCapability {
|
||||
return map[module] ?? { state: 'unknown' };
|
||||
}
|
||||
|
||||
function explanation(capability: InstanceCapability): string | null {
|
||||
if (capability.state === 'supported') return null;
|
||||
return capability.explanation?.trim() || DEFAULT_EXPLANATIONS[capability.state];
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner-fenced instance context and capability navigation for every instance route.
|
||||
* Capability discovery is injected: this framework deliberately assumes no API or operation IDs.
|
||||
*/
|
||||
export function InstanceDetail({
|
||||
instanceId,
|
||||
module,
|
||||
instance,
|
||||
capabilities,
|
||||
capabilityDataSource,
|
||||
}: InstanceDetailProps) {
|
||||
const ownsRoute = instance?.id === instanceId;
|
||||
const [loadedCapabilities, setLoadedCapabilities] = useState<InstanceCapabilityMap | undefined>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const requestRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
const request = ++requestRef.current;
|
||||
setLoadedCapabilities(undefined);
|
||||
setLoadError(null);
|
||||
|
||||
if (!ownsRoute || capabilities || !capabilityDataSource) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
setLoading(true);
|
||||
void capabilityDataSource.load(instanceId, controller.signal).then(
|
||||
(result) => {
|
||||
if (!controller.signal.aborted && requestRef.current === request) {
|
||||
setLoadedCapabilities(result);
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
(error: unknown) => {
|
||||
if (!controller.signal.aborted && requestRef.current === request) {
|
||||
setLoadError(error instanceof Error ? error.message : 'Capability discovery failed.');
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
return () => controller.abort();
|
||||
}, [capabilities, capabilityDataSource, instanceId, ownsRoute]);
|
||||
|
||||
if (!ownsRoute) {
|
||||
return (
|
||||
<section>
|
||||
<h1>{INSTANCE_MODULE_LABELS[module]}</h1>
|
||||
<p>Instance context is unavailable for this route.</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const map = capabilities ?? loadedCapabilities ?? {};
|
||||
const origin = canonicalHttpOrigin(instance.origin);
|
||||
const activeCapability = capabilityFor(map, module);
|
||||
|
||||
return (
|
||||
<section className="instance-detail">
|
||||
<aside className="instance-context" aria-label="Current instance">
|
||||
<strong>{instance.name}</strong>
|
||||
<code>{instance.id}</code>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Status</dt>
|
||||
<dd>{instance.status}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Authentication</dt>
|
||||
<dd>{instance.authentication}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Freshness</dt>
|
||||
<dd>{instance.freshness}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{origin ? (
|
||||
<a href={origin} target="_blank" rel="noopener noreferrer">
|
||||
Open original site
|
||||
</a>
|
||||
) : null}
|
||||
<nav aria-label="Instance modules">
|
||||
<ul>
|
||||
{MODULES.map((item) => {
|
||||
const capability = capabilityFor(map, item);
|
||||
const reason = explanation(capability);
|
||||
return (
|
||||
<li key={item} data-capability-state={capability.state}>
|
||||
{capability.state === 'supported' ? (
|
||||
<a
|
||||
href={`/instances/${encodeURIComponent(instanceId)}/${item}`}
|
||||
aria-current={module === item ? 'page' : undefined}
|
||||
>
|
||||
{INSTANCE_MODULE_LABELS[item]}
|
||||
</a>
|
||||
) : (
|
||||
<>
|
||||
<span>{INSTANCE_MODULE_LABELS[item]}</span>
|
||||
<small>{reason}</small>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
<div className="instance-module-detail">
|
||||
<h1>{INSTANCE_MODULE_LABELS[module]}</h1>
|
||||
{loading ? <p role="status">Loading capabilities…</p> : null}
|
||||
{loadError ? <p role="alert">Capabilities unavailable: {loadError}</p> : null}
|
||||
{!loading && activeCapability.state === 'supported' ? (
|
||||
<p>
|
||||
Inspect {INSTANCE_MODULE_LABELS[module].toLowerCase()} data and available operations.
|
||||
</p>
|
||||
) : null}
|
||||
{!loading && activeCapability.state !== 'supported' ? (
|
||||
<p data-capability-state={activeCapability.state}>{explanation(activeCapability)}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user