Files
multi-simadmin/apps/web/src/instances/instance-detail.tsx
T

182 lines
6.0 KiB
TypeScript

import type { ReactNode } from 'react';
import { useEffect, useRef, useState } from 'react';
import type { InstanceContext, InstanceModule } from '../app-shell.js';
import { canonicalHttpOrigin } from '../fleet/fleet-page.js';
import { displayStatus } from '../ui/locale.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;
readonly moduleContent?: ReactNode;
}
export const INSTANCE_MODULE_LABELS: Readonly<Record<InstanceModule, string>> = {
overview: '设备信息',
cellular: '蜂窝网络',
'device-network': '设备网络',
messages: '短信',
calls: '通话',
esim: 'eSIM',
notifications: '通知',
automation: '自动化',
ota: 'OTA',
};
const PRIMARY_MODULES = ['messages', 'overview'] as const satisfies readonly InstanceModule[];
const DEFAULT_EXPLANATIONS: Readonly<Record<Exclude<CapabilityState, 'supported'>, string>> = {
degraded: '此模块可用,但功能受限。',
unsupported: '此实例不支持该模块。',
unknown: '能力状态未知。',
};
function canRender(capability: InstanceCapability | undefined): boolean {
if (!capability) return true;
return capability.state === 'supported' || capability.state === 'degraded';
}
const CORE_MODULES = new Set<InstanceModule>(['overview', 'messages']);
function canOpen(module: InstanceModule, capability: InstanceCapability | undefined): boolean {
return capability ? canRender(capability) : CORE_MODULES.has(module);
}
function explanation(capability: InstanceCapability | undefined): string | null {
if (!capability) return 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,
moduleContent,
}: 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) => {
void error;
if (!controller.signal.aborted && requestRef.current === request) {
setLoadError('能力探测失败。');
setLoading(false);
}
},
);
return () => controller.abort();
}, [capabilities, capabilityDataSource, instanceId, ownsRoute]);
if (!ownsRoute) {
return (
<section>
<h1>{INSTANCE_MODULE_LABELS[module]}</h1>
<p>此路由缺少实例上下文。</p>
</section>
);
}
const map = capabilities ?? loadedCapabilities ?? {};
const origin = canonicalHttpOrigin(instance.origin);
const activeCapability = map[module];
return (
<section className="instance-detail">
<aside className="instance-context" aria-label="当前实例">
<strong>{instance.name}</strong>
<code>{instance.id}</code>
{instance.status !== 'unknown' ? (
<dl>
<div>
<dt>状态</dt>
<dd>{displayStatus(instance.status)}</dd>
</div>
</dl>
) : null}
{origin ? (
<a href={origin} target="_blank" rel="noopener noreferrer">
打开源站
</a>
) : null}
<a href={`/settings/instances/${encodeURIComponent(instanceId)}`}>编辑实例</a>
<nav aria-label="实例模块">
<ul>
{PRIMARY_MODULES.map((item) => {
return (
<li key={item}>
<a
href={`/instances/${encodeURIComponent(instanceId)}/${item}`}
aria-current={module === item ? 'page' : undefined}
>
{INSTANCE_MODULE_LABELS[item]}
</a>
</li>
);
})}
</ul>
</nav>
</aside>
<div className="instance-module-detail">
<h1>{INSTANCE_MODULE_LABELS[module]}</h1>
{loading ? <p role="status">正在加载能力…</p> : null}
{loadError ? <p role="alert">能力不可用:{loadError}</p> : null}
{!loading && canOpen(module, activeCapability) ? (
<>
{activeCapability?.state === 'degraded' ? (
<p data-capability-state="degraded">{explanation(activeCapability)}</p>
) : null}
{moduleContent ?? <p>查看{INSTANCE_MODULE_LABELS[module]}数据和可用操作。</p>}
</>
) : null}
{!loading && !canOpen(module, activeCapability) ? (
<p data-capability-state={activeCapability?.state}>{explanation(activeCapability)}</p>
) : null}
</div>
</section>
);
}