feat(web): redesign fleet cards and instance workspace
This commit is contained in:
@@ -18,11 +18,9 @@ const snapshot: FleetSnapshot = {
|
||||
'alpha',
|
||||
{
|
||||
instanceId: 'alpha',
|
||||
connectivity: 'online',
|
||||
authentication: 'authenticated',
|
||||
freshness: 'fresh',
|
||||
capabilities: ['overview', 'messages'],
|
||||
anomalies: [],
|
||||
reachable: true,
|
||||
authenticated: true,
|
||||
summary: { freshness: 'fresh', resources: { cpuPercent: 24, memoryPercent: 51 } },
|
||||
},
|
||||
],
|
||||
]),
|
||||
@@ -41,5 +39,16 @@ describe('FleetPage card navigation', () => {
|
||||
expect(within(card).getByRole('link', { name: '编辑 Alpha modem' })).toBeTruthy();
|
||||
expect(within(card).getByRole('button', { name: '删除 Alpha modem' })).toBeTruthy();
|
||||
expect(within(card).queryByRole('link', { name: /查看.*短信/ })).toBeNull();
|
||||
expect(screen.getByRole('region', { name: '实例状态摘要' }).textContent).toMatch(
|
||||
/实例总数\s*1.*在线\s*1.*需处理\s*0/s,
|
||||
);
|
||||
expect(within(card).getByRole('meter', { name: 'CPU 使用率' }).getAttribute('value')).toBe(
|
||||
'24',
|
||||
);
|
||||
expect(within(card).getByRole('meter', { name: '内存使用率' }).getAttribute('value')).toBe(
|
||||
'51',
|
||||
);
|
||||
expect(within(card).getByText('进入仪表盘')).toBeTruthy();
|
||||
expect(within(card).getByRole('group', { name: '实例管理操作' })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,8 +82,7 @@ const messageDirection = (value: string): string =>
|
||||
: 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');
|
||||
@@ -198,9 +197,22 @@ export function FleetPage({
|
||||
[auth, capability, filter, page, query, selectedIds, snapshot, sort, tag, version],
|
||||
);
|
||||
|
||||
const fleetSummary = useMemo(() => {
|
||||
const statuses = [...(snapshot?.statuses.values() ?? [])];
|
||||
const online = statuses.filter(
|
||||
(status) => status.reachable && status.authenticated !== false,
|
||||
).length;
|
||||
return {
|
||||
total: snapshot?.instances.length ?? 0,
|
||||
online,
|
||||
attention: (snapshot?.instances.length ?? 0) - online,
|
||||
};
|
||||
}, [snapshot]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectAllRef.current)
|
||||
if (selectAllRef.current) {
|
||||
selectAllRef.current.indeterminate = model.visibleSelection.indeterminate;
|
||||
}
|
||||
}, [model.visibleSelection.indeterminate]);
|
||||
useEffect(() => {
|
||||
if (page !== model.page) setPage(model.page);
|
||||
@@ -332,6 +344,22 @@ export function FleetPage({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{snapshot ? (
|
||||
<section className="fleet-status-summary" aria-label="实例状态摘要">
|
||||
<div>
|
||||
<span>实例总数</span>
|
||||
<strong>{fleetSummary.total}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>在线</span>
|
||||
<strong>{fleetSummary.online}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>需处理</span>
|
||||
<strong>{fleetSummary.attention}</strong>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
{batchOpen ? (
|
||||
<div className="batch-entry" role="region" aria-label="批量操作入口">
|
||||
请为已选择的 {model.selectedIds.length} 项选择操作。
|
||||
@@ -465,6 +493,7 @@ export function FleetPage({
|
||||
>
|
||||
<h2>{row.displayName}</h2>
|
||||
<code>{row.id}</code>
|
||||
<span className="fleet-card-entry-label">进入仪表盘</span>
|
||||
</a>
|
||||
<span className={`status status-${row.statusKind}`}>
|
||||
{STATUS_LABELS[row.statusKind]}
|
||||
@@ -479,11 +508,39 @@ export function FleetPage({
|
||||
</div>
|
||||
<div>
|
||||
<dt>CPU</dt>
|
||||
<dd>{percent(row.status?.summary?.resources?.cpuPercent)}</dd>
|
||||
<dd>
|
||||
{row.status?.summary?.resources?.cpuPercent === undefined ? (
|
||||
'暂未获取'
|
||||
) : (
|
||||
<>
|
||||
<meter
|
||||
min="0"
|
||||
max="100"
|
||||
value={row.status.summary.resources.cpuPercent}
|
||||
aria-label="CPU 使用率"
|
||||
/>
|
||||
{percent(row.status.summary.resources.cpuPercent)}
|
||||
</>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>内存</dt>
|
||||
<dd>{percent(row.status?.summary?.resources?.memoryPercent)}</dd>
|
||||
<dd>
|
||||
{row.status?.summary?.resources?.memoryPercent === undefined ? (
|
||||
'暂未获取'
|
||||
) : (
|
||||
<>
|
||||
<meter
|
||||
min="0"
|
||||
max="100"
|
||||
value={row.status.summary.resources.memoryPercent}
|
||||
aria-label="内存使用率"
|
||||
/>
|
||||
{percent(row.status.summary.resources.memoryPercent)}
|
||||
</>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>最高温度</dt>
|
||||
@@ -511,10 +568,6 @@ export function FleetPage({
|
||||
<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>
|
||||
@@ -524,7 +577,11 @@ export function FleetPage({
|
||||
<p className="fleet-card-sms-empty">暂无短信</p>
|
||||
)}
|
||||
</section>
|
||||
<div className="fleet-card-actions fleet-card-admin-actions">
|
||||
<div
|
||||
className="fleet-card-actions fleet-card-admin-actions"
|
||||
role="group"
|
||||
aria-label="实例管理操作"
|
||||
>
|
||||
<a
|
||||
href={`/settings/instances/${encodeURIComponent(row.id)}`}
|
||||
aria-label={`编辑 ${row.displayName}`}
|
||||
@@ -665,22 +722,7 @@ export function FleetPage({
|
||||
tags: <td>{row.tags.join(', ') || '—'}</td>,
|
||||
freshness: <td>{FRESHNESS_LABELS[row.freshness] ?? row.freshness}</td>,
|
||||
anomalies: <td>{row.anomalies.join(', ') || '—'}</td>,
|
||||
origin: (
|
||||
<td>
|
||||
{origin ? (
|
||||
<a
|
||||
href={origin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`打开 ${row.displayName} 的源站`}
|
||||
>
|
||||
{origin}
|
||||
</a>
|
||||
) : (
|
||||
<span>源地址无效</span>
|
||||
)}
|
||||
</td>
|
||||
),
|
||||
origin: <td>{origin ? <span>{origin}</span> : <span>源地址无效</span>}</td>,
|
||||
};
|
||||
return (
|
||||
<tr key={row.id} aria-selected={row.selected}>
|
||||
|
||||
Reference in New Issue
Block a user