feat(web): complete fleet and instance management slices

This commit is contained in:
chick
2026-07-17 17:12:35 +08:00
parent 4093634e13
commit 6e817f380a
15 changed files with 1737 additions and 203 deletions
+180
View File
@@ -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>
);
}