feat(web): complete fleet and instance management slices
This commit is contained in:
+47
-84
@@ -1,12 +1,14 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
FleetPage,
|
||||
canonicalHttpOrigin,
|
||||
type FleetDataSource,
|
||||
type FleetSnapshot,
|
||||
} from './fleet/fleet-page.js';
|
||||
import { FleetPage, type FleetDataSource, type FleetSnapshot } from './fleet/fleet-page.js';
|
||||
import { createFleetApiDataSource } from './fleet/fleet-api-data-source.js';
|
||||
import { InstanceEditor, type InstanceDataSource } from './instances/instance-crud.js';
|
||||
import {
|
||||
InstanceDetail,
|
||||
INSTANCE_MODULE_LABELS,
|
||||
type CapabilityDataSource,
|
||||
type InstanceCapabilityMap,
|
||||
} from './instances/instance-detail.js';
|
||||
|
||||
export type GlobalSection = 'fleet' | 'jobs' | 'audit' | 'settings';
|
||||
export type InstanceModule =
|
||||
@@ -52,19 +54,12 @@ export interface AppShellProps {
|
||||
instance?: InstanceContext;
|
||||
fleetDataSource?: FleetDataSource;
|
||||
fleetData?: FleetSnapshot;
|
||||
instanceDataSource?: InstanceDataSource;
|
||||
capabilities?: InstanceCapabilityMap;
|
||||
capabilityDataSource?: CapabilityDataSource;
|
||||
}
|
||||
|
||||
const 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 MODULE_LABELS = INSTANCE_MODULE_LABELS;
|
||||
const INSTANCE_MODULES = new Set(Object.keys(MODULE_LABELS));
|
||||
|
||||
function normalize(pathname: string): string {
|
||||
@@ -120,65 +115,22 @@ function section(route: ResolvedRoute): GlobalSection | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function InstanceSidebar({
|
||||
instance,
|
||||
module,
|
||||
}: {
|
||||
instance: InstanceContext;
|
||||
module: InstanceModule;
|
||||
}) {
|
||||
const origin = canonicalHttpOrigin(instance.origin);
|
||||
return (
|
||||
<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>
|
||||
{Object.entries(MODULE_LABELS).map(([key, label]) => (
|
||||
<li key={key}>
|
||||
<a
|
||||
href={`/instances/${encodeURIComponent(instance.id)}/${key}`}
|
||||
aria-current={module === key ? 'page' : undefined}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function Page({
|
||||
route,
|
||||
fleetDataSource,
|
||||
fleetData,
|
||||
instance,
|
||||
instanceDataSource,
|
||||
capabilities,
|
||||
capabilityDataSource,
|
||||
}: {
|
||||
route: ResolvedRoute;
|
||||
fleetDataSource: FleetDataSource | undefined;
|
||||
fleetData: FleetSnapshot | undefined;
|
||||
instance: InstanceContext | undefined;
|
||||
instanceDataSource: InstanceDataSource | undefined;
|
||||
capabilities: InstanceCapabilityMap | undefined;
|
||||
capabilityDataSource: CapabilityDataSource | undefined;
|
||||
}): ReactNode {
|
||||
if (route.kind === 'fleet')
|
||||
return (
|
||||
@@ -187,6 +139,21 @@ function Page({
|
||||
{...(fleetData ? { initialData: fleetData } : {})}
|
||||
/>
|
||||
);
|
||||
if (route.kind === 'instance-new')
|
||||
return (
|
||||
<InstanceEditor
|
||||
mode="create"
|
||||
{...(instanceDataSource ? { dataSource: instanceDataSource } : {})}
|
||||
/>
|
||||
);
|
||||
if (route.kind === 'settings-instance-detail')
|
||||
return (
|
||||
<InstanceEditor
|
||||
mode="edit"
|
||||
{...(route.params?.instanceId ? { instanceId: route.params.instanceId } : {})}
|
||||
{...(instanceDataSource ? { dataSource: instanceDataSource } : {})}
|
||||
/>
|
||||
);
|
||||
if (route.kind === 'not-found')
|
||||
return (
|
||||
<section>
|
||||
@@ -195,18 +162,16 @@ function Page({
|
||||
<a href="/fleet">Return to Fleet</a>
|
||||
</section>
|
||||
);
|
||||
if (route.kind.startsWith('instance-') && route.kind !== 'instance-new') {
|
||||
if (route.kind.startsWith('instance-')) {
|
||||
const module = route.kind.slice('instance-'.length) as InstanceModule;
|
||||
const ownsRoute = instance?.id === route.params?.instanceId;
|
||||
return (
|
||||
<>
|
||||
<h1>{MODULE_LABELS[module]}</h1>
|
||||
{ownsRoute ? (
|
||||
<p>Inspect {MODULE_LABELS[module].toLowerCase()} data and available operations.</p>
|
||||
) : (
|
||||
<p>Instance context is unavailable for this route.</p>
|
||||
)}
|
||||
</>
|
||||
<InstanceDetail
|
||||
instanceId={route.params?.instanceId ?? ''}
|
||||
module={module}
|
||||
{...(instance ? { instance } : {})}
|
||||
{...(capabilities ? { capabilities } : {})}
|
||||
{...(capabilityDataSource ? { capabilityDataSource } : {})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const labels: Partial<Record<RouteKind, string>> = {
|
||||
@@ -233,15 +198,13 @@ export function AppShell({
|
||||
instance,
|
||||
fleetDataSource,
|
||||
fleetData,
|
||||
instanceDataSource,
|
||||
capabilities,
|
||||
capabilityDataSource,
|
||||
}: AppShellProps) {
|
||||
const resolved = resolveRoute(pathname);
|
||||
const route = resolved.kind === 'redirect' ? resolveRoute(resolved.to ?? '/fleet') : resolved;
|
||||
const currentSection = section(route);
|
||||
const instanceModule =
|
||||
route.kind.startsWith('instance-') && route.kind !== 'instance-new'
|
||||
? (route.kind.slice('instance-'.length) as InstanceModule)
|
||||
: null;
|
||||
const owner = instanceModule && instance?.id === route.params?.instanceId ? instance : undefined;
|
||||
return (
|
||||
<div className="app-shell" data-route={route.kind}>
|
||||
<a className="skip-link" href="#main-content">
|
||||
@@ -273,15 +236,15 @@ export function AppShell({
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
{owner && instanceModule ? (
|
||||
<InstanceSidebar instance={owner} module={instanceModule} />
|
||||
) : null}
|
||||
<main id="main-content" tabIndex={-1}>
|
||||
<Page
|
||||
route={route}
|
||||
fleetDataSource={fleetDataSource ?? createFleetApiDataSource()}
|
||||
fleetData={fleetData}
|
||||
instance={instance}
|
||||
instanceDataSource={instanceDataSource}
|
||||
capabilities={capabilities}
|
||||
capabilityDataSource={capabilityDataSource}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user