feat(ui): simplify fleet cards by moving edit/delete into detail
Hide instance IDs and config/delete controls on overview cards; keep service/system restart only, with edit still available from detail.
This commit is contained in:
@@ -29,15 +29,16 @@ const snapshot: FleetSnapshot = {
|
||||
afterEach(cleanup);
|
||||
|
||||
describe('FleetPage card navigation', () => {
|
||||
it('makes the whole instance card open its dashboard while admin controls stay separately interactive', () => {
|
||||
it('makes the whole instance card open its dashboard while ops stay separate and config stays out of the card', () => {
|
||||
render(<FleetPage initialData={snapshot} />);
|
||||
|
||||
const card = screen.getByRole('article', { name: 'Alpha modem 实例概览' });
|
||||
const entry = within(card).getByRole('link', { name: '打开 Alpha modem 实例仪表盘' });
|
||||
expect(entry.getAttribute('href')).toBe('/instances/alpha/overview');
|
||||
expect(entry.classList.contains('fleet-card-entry')).toBe(true);
|
||||
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(within(card).queryByRole('button', { name: /删除/ })).toBeNull();
|
||||
expect(within(card).queryByText('alpha')).toBeNull();
|
||||
expect(within(card).queryByRole('link', { name: /查看.*短信/ })).toBeNull();
|
||||
expect(screen.getByRole('region', { name: '实例状态摘要' }).textContent).toMatch(
|
||||
/实例总数\s*1.*在线\s*1.*需处理\s*0/s,
|
||||
@@ -49,7 +50,9 @@ describe('FleetPage card navigation', () => {
|
||||
'51',
|
||||
);
|
||||
expect(within(card).getByText('进入仪表盘')).toBeTruthy();
|
||||
expect(within(card).getByRole('group', { name: '实例管理操作' })).toBeTruthy();
|
||||
expect(within(card).getByRole('group', { name: '实例运维操作' })).toBeTruthy();
|
||||
expect(within(card).getByRole('button', { name: '重启服务 Alpha modem' })).toBeTruthy();
|
||||
expect(within(card).getByRole('button', { name: '系统重启 Alpha modem' })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
type FleetStatus,
|
||||
type SortDirection,
|
||||
} from './fleet-table-view-model.js';
|
||||
import type { InstanceDataSource } from '../instances/instance-crud.js';
|
||||
import type {
|
||||
FleetMessageLoadState,
|
||||
FleetMessagesDataSource,
|
||||
@@ -35,7 +34,6 @@ export interface FleetDataSource {
|
||||
export interface FleetPageProps {
|
||||
readonly dataSource?: FleetDataSource;
|
||||
readonly messagesDataSource?: FleetMessagesDataSource;
|
||||
readonly instanceDataSource?: InstanceDataSource;
|
||||
readonly initialData?: FleetSnapshot;
|
||||
readonly refreshSignal?: number;
|
||||
readonly operationClient?: OperationClient;
|
||||
@@ -128,7 +126,6 @@ const SYSTEM_REBOOT = {
|
||||
export function FleetPage({
|
||||
dataSource,
|
||||
messagesDataSource,
|
||||
instanceDataSource,
|
||||
initialData,
|
||||
refreshSignal = 0,
|
||||
operationClient,
|
||||
@@ -157,13 +154,6 @@ export function FleetPage({
|
||||
message?: string;
|
||||
error?: string;
|
||||
} | null>(null);
|
||||
const [deleteId, setDeleteId] = useState<string>();
|
||||
const [deleteOwner, setDeleteOwner] = useState<Readonly<{ id: string; revision: number }>>();
|
||||
const [deleteConfirmation, setDeleteConfirmation] = useState('');
|
||||
const [deleteBusy, setDeleteBusy] = useState(false);
|
||||
const deleteRequestRef = useRef(0);
|
||||
const [deleteStatus, setDeleteStatus] =
|
||||
useState<Readonly<{ id: string; kind: 'success' | 'error'; message: string }>>();
|
||||
const messagesOwner = useRef(0);
|
||||
const [messageStates, setMessageStates] = useState<ReadonlyMap<string, FleetMessageState>>(
|
||||
new Map(),
|
||||
@@ -382,47 +372,6 @@ export function FleetPage({
|
||||
});
|
||||
}
|
||||
|
||||
async function beginDelete(id: string): Promise<void> {
|
||||
const request = ++deleteRequestRef.current;
|
||||
setDeleteId(id);
|
||||
setDeleteOwner(undefined);
|
||||
setDeleteConfirmation('');
|
||||
setDeleteStatus(undefined);
|
||||
if (!instanceDataSource) {
|
||||
setDeleteStatus({ id, kind: 'error', message: '无法准备删除,请稍后重试。' });
|
||||
return;
|
||||
}
|
||||
setDeleteBusy(true);
|
||||
try {
|
||||
const current = await instanceDataSource.get(id);
|
||||
if (request !== deleteRequestRef.current) return;
|
||||
if (current.id !== id) throw new Error('owner mismatch');
|
||||
setDeleteOwner({ id: current.id, revision: current.revision });
|
||||
} catch {
|
||||
if (request === deleteRequestRef.current) {
|
||||
setDeleteStatus({ id, kind: 'error', message: '无法准备删除,请稍后重试。' });
|
||||
}
|
||||
} finally {
|
||||
if (request === deleteRequestRef.current) setDeleteBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(id: string): Promise<void> {
|
||||
if (!instanceDataSource || deleteOwner?.id !== id || deleteConfirmation !== id) return;
|
||||
setDeleteBusy(true);
|
||||
setDeleteStatus(undefined);
|
||||
try {
|
||||
await instanceDataSource.delete(id, deleteOwner.revision);
|
||||
setDeleteStatus({ id, kind: 'success', message: '删除请求已提交。' });
|
||||
setDeleteId(undefined);
|
||||
setDeleteConfirmation('');
|
||||
} catch {
|
||||
setDeleteStatus({ id, kind: 'error', message: '删除请求失败,请稍后重试。' });
|
||||
} finally {
|
||||
setDeleteBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
function selectedTargets(): Array<{ instanceId: string; revision: number }> {
|
||||
const instances = snapshot?.instances ?? [];
|
||||
return model.selectedIds.flatMap((id) => {
|
||||
@@ -870,7 +819,6 @@ export function FleetPage({
|
||||
aria-label={`打开 ${row.displayName} 实例仪表盘`}
|
||||
>
|
||||
<h2>{row.displayName}</h2>
|
||||
<code>{row.id}</code>
|
||||
<span className="fleet-card-entry-label">进入仪表盘</span>
|
||||
</a>
|
||||
<span className={`status status-${row.statusKind}`}>
|
||||
@@ -971,7 +919,7 @@ export function FleetPage({
|
||||
<div
|
||||
className="fleet-card-actions fleet-card-admin-actions"
|
||||
role="group"
|
||||
aria-label="实例管理操作"
|
||||
aria-label="实例运维操作"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -990,20 +938,6 @@ export function FleetPage({
|
||||
>
|
||||
系统重启
|
||||
</button>
|
||||
<a
|
||||
href={`/settings/instances/${encodeURIComponent(row.id)}`}
|
||||
aria-label={`编辑 ${row.displayName}`}
|
||||
>
|
||||
编辑
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="danger-button"
|
||||
aria-label={`删除 ${row.displayName}`}
|
||||
onClick={() => void beginDelete(row.id)}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
{cardAction?.id === row.id ? (
|
||||
<p
|
||||
@@ -1013,55 +947,6 @@ export function FleetPage({
|
||||
{cardAction.error ?? cardAction.message}
|
||||
</p>
|
||||
) : null}
|
||||
{deleteId === row.id ? (
|
||||
<div className="card-delete-confirmation">
|
||||
<p>删除后无法撤销。</p>
|
||||
<label>
|
||||
输入 {row.id} 以确认删除
|
||||
<input
|
||||
value={deleteConfirmation}
|
||||
onChange={(event) => setDeleteConfirmation(event.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
<div className="fleet-card-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="danger-button"
|
||||
disabled={
|
||||
deleteBusy ||
|
||||
deleteOwner?.id !== row.id ||
|
||||
deleteConfirmation !== row.id
|
||||
}
|
||||
aria-label={`确认删除 ${row.displayName}`}
|
||||
onClick={() => void remove(row.id)}
|
||||
>
|
||||
确认删除
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setDeleteId(undefined);
|
||||
setDeleteConfirmation('');
|
||||
setDeleteStatus(undefined);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{deleteStatus?.id === row.id ? (
|
||||
<p
|
||||
role={deleteStatus.kind === 'error' ? 'alert' : 'status'}
|
||||
className={
|
||||
deleteStatus.kind === 'error'
|
||||
? 'card-operation-error'
|
||||
: 'card-operation-success'
|
||||
}
|
||||
>
|
||||
{deleteStatus.message}
|
||||
</p>
|
||||
) : null}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user