feat(web): prioritize SMS operations workspace
This commit is contained in:
@@ -10,6 +10,11 @@ import {
|
||||
type SortDirection,
|
||||
} from './fleet-table-view-model.js';
|
||||
import type { InstanceDataSource } from '../instances/instance-crud.js';
|
||||
import type {
|
||||
FleetMessageLoadState,
|
||||
FleetMessagesDataSource,
|
||||
} from './fleet-messages-api-data-source.js';
|
||||
import { loadFleetMessageSummaries } from './fleet-messages-api-data-source.js';
|
||||
|
||||
export interface FleetSnapshot {
|
||||
readonly instances: readonly FleetInstance[];
|
||||
@@ -20,10 +25,12 @@ export interface FleetDataSource {
|
||||
}
|
||||
export interface FleetPageProps {
|
||||
readonly dataSource?: FleetDataSource;
|
||||
readonly messagesDataSource?: FleetMessagesDataSource;
|
||||
readonly instanceDataSource?: InstanceDataSource;
|
||||
readonly initialData?: FleetSnapshot;
|
||||
readonly refreshSignal?: number;
|
||||
}
|
||||
type FleetMessageState = FleetMessageLoadState;
|
||||
|
||||
const EMPTY_SNAPSHOT: FleetSnapshot = { instances: [], statuses: new Map() };
|
||||
const STATUS_LABELS: Readonly<Record<string, string>> = {
|
||||
@@ -69,6 +76,18 @@ const percent = (value: number | undefined): string =>
|
||||
value === undefined ? '暂未获取' : `${value.toFixed(1)}%`;
|
||||
const temperature = (value: number | undefined): string =>
|
||||
value === undefined ? '暂未获取' : `${value.toFixed(1)} °C`;
|
||||
const messageDirection = (value: string): string =>
|
||||
value === 'incoming' || value === 'received'
|
||||
? '收到'
|
||||
: value === 'outgoing' || value === 'sent'
|
||||
? '发送'
|
||||
: '未知方向';
|
||||
const messageExcerpt = (value: string): string =>
|
||||
value.length > 36 ? `${value.slice(0, 36)}…` : value;
|
||||
const messageTime = (value: string): string => {
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN');
|
||||
};
|
||||
|
||||
export function canonicalHttpOrigin(value: string): string | null {
|
||||
try {
|
||||
@@ -84,6 +103,7 @@ export function canonicalHttpOrigin(value: string): string | null {
|
||||
|
||||
export function FleetPage({
|
||||
dataSource,
|
||||
messagesDataSource,
|
||||
instanceDataSource,
|
||||
initialData,
|
||||
refreshSignal = 0,
|
||||
@@ -114,6 +134,10 @@ export function FleetPage({
|
||||
const [deleteStatus, setDeleteStatus] =
|
||||
useState<Readonly<{ id: string; kind: 'success' | 'error'; message: string }>>();
|
||||
const selectAllRef = useRef<HTMLInputElement>(null);
|
||||
const messagesOwner = useRef(0);
|
||||
const [messageStates, setMessageStates] = useState<ReadonlyMap<string, FleetMessageState>>(
|
||||
new Map(),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialData && refreshSignal === 0) {
|
||||
@@ -141,6 +165,23 @@ export function FleetPage({
|
||||
};
|
||||
}, [attempt, dataSource, initialData, refreshSignal]);
|
||||
|
||||
useEffect(() => {
|
||||
const request = ++messagesOwner.current;
|
||||
const controller = new AbortController();
|
||||
const instances = snapshot?.instances;
|
||||
setMessageStates(new Map<string, FleetMessageState>());
|
||||
if (!messagesDataSource || !instances) return () => controller.abort();
|
||||
void loadFleetMessageSummaries(
|
||||
messagesDataSource,
|
||||
instances.map((instance) => instance.id),
|
||||
controller.signal,
|
||||
).then((entries) => {
|
||||
if (request === messagesOwner.current && !controller.signal.aborted)
|
||||
setMessageStates(entries);
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [messagesDataSource, snapshot]);
|
||||
|
||||
const model = useMemo(
|
||||
() =>
|
||||
buildFleetTableViewModel(snapshot?.instances ?? [], snapshot?.statuses ?? new Map(), {
|
||||
@@ -445,21 +486,47 @@ export function FleetPage({
|
||||
<dd>{temperature(row.status?.summary?.resources?.maxTemperatureCelsius)}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="capability-tags" aria-label="能力">
|
||||
{row.capabilities.length > 0 ? (
|
||||
row.capabilities.map((item) => (
|
||||
{row.capabilities.length > 0 ? (
|
||||
<div className="capability-tags" aria-label="能力">
|
||||
{row.capabilities.map((item) => (
|
||||
<span key={item}>{capabilityLabel(item)}</span>
|
||||
))
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<section className="fleet-card-sms" aria-label="最近短信">
|
||||
<h3>最近短信</h3>
|
||||
{messageStates.get(row.id)?.unavailable ? (
|
||||
<p className="fleet-card-sms-empty">短信暂不可用</p>
|
||||
) : messageStates.get(row.id)?.latest ? (
|
||||
<dl>
|
||||
<div>
|
||||
<dt>方向</dt>
|
||||
<dd>{messageDirection(messageStates.get(row.id)!.latest!.direction)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>号码</dt>
|
||||
<dd>{messageStates.get(row.id)!.latest!.phoneNumber}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>内容</dt>
|
||||
<dd>{messageExcerpt(messageStates.get(row.id)!.latest!.content)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>时间</dt>
|
||||
<dd>{messageTime(messageStates.get(row.id)!.latest!.timestamp)}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
) : (
|
||||
<span>能力未知</span>
|
||||
<p className="fleet-card-sms-empty">暂无短信</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<div className="fleet-card-actions">
|
||||
<a
|
||||
href={`/instances/${encodeURIComponent(row.id)}/overview`}
|
||||
aria-label={`管理 ${row.displayName}`}
|
||||
className="primary-action"
|
||||
href={`/instances/${encodeURIComponent(row.id)}/messages`}
|
||||
aria-label={`查看 ${row.displayName} 的短信`}
|
||||
>
|
||||
管理
|
||||
查看短信
|
||||
</a>
|
||||
<a
|
||||
href={`/settings/instances/${encodeURIComponent(row.id)}`}
|
||||
|
||||
Reference in New Issue
Block a user