Compare commits
2 Commits
9e66f91128
...
f95b1683e7
| Author | SHA1 | Date | |
|---|---|---|---|
| f95b1683e7 | |||
|
|
f6a256d483 |
@ -28,6 +28,14 @@ describe("Teamserver 工作区", () => {
|
|||||||
expect(screen.getByText("10.42.8.17")).toBeInTheDocument();
|
expect(screen.getByText("10.42.8.17")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("从 Session 表格打开上下文锁定的详情标签", () => {
|
||||||
|
render(<App />);
|
||||||
|
fireEvent.doubleClick(screen.getByRole("row", { name: /NW-038/ }));
|
||||||
|
expect(screen.getAllByRole("tab", { name: "Session Overview" }).find((tab) => tab.getAttribute("aria-selected") === "true")).toBeDefined();
|
||||||
|
expect(screen.getByLabelText("锁定的操作上下文")).toHaveTextContent("Northwind / NW-038 / Session Overview");
|
||||||
|
expect(screen.getByRole("heading", { name: "NW-038" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("通过拆分后的侧边栏进入主线新增的连接中心", () => {
|
it("通过拆分后的侧边栏进入主线新增的连接中心", () => {
|
||||||
render(<App />);
|
render(<App />);
|
||||||
fireEvent.click(screen.getByRole("button", { name: "连接中心" }));
|
fireEvent.click(screen.getByRole("button", { name: "连接中心" }));
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export function App() {
|
|||||||
dispatch({ type: "navigate", serverId: server.id, project: server.activeProject, navigation: page });
|
dispatch({ type: "navigate", serverId: server.id, project: server.activeProject, navigation: page });
|
||||||
dispatch({ type: "openTab", serverId: server.id, tab: { id: `${server.activeProject.toLowerCase()}-${page.toLowerCase().replaceAll(" ", "-")}`, title, context: { serverId: server.id, project: server.activeProject, session: "Project" } } });
|
dispatch({ type: "openTab", serverId: server.id, tab: { id: `${server.activeProject.toLowerCase()}-${page.toLowerCase().replaceAll(" ", "-")}`, title, context: { serverId: server.id, project: server.activeProject, session: "Project" } } });
|
||||||
};
|
};
|
||||||
const sessionsPage = <><div className="content-heading"><div><h2>Sessions</h2><p>{activeTab.context.project} 中的 248 个 Session · 4 个实时更新</p></div></div><p className="demo-boundary" role="status">演示数据 · 内存 adapter · 未连接真实 Teamserver</p><SessionTable rows={sessions} state={tableState} capabilities={{ filtering: true, sortableColumns: ["status", "id", "seen"], columnVisibility: true, pagination: true }} page={{ start: 1, end: 50, total: 248, hasPrevious: false, hasNext: true }} onStateChange={(state) => { if (state.filter !== server.filter) dispatch({ type: "setFilter", serverId: server.id, filter: state.filter }); setTableStates((current) => updateSessionTableState(current, server.id, state)); }} onQueryIntent={(intent) => setLastQueryIntent((current) => ({ ...current, [server.id]: intent }))} /><output className="query-intent" aria-live="polite">{lastQueryIntent[server.id] ? `查询意图:${lastQueryIntent[server.id]?.type}` : "等待服务端查询意图"}</output></>;
|
const sessionsPage = <><div className="content-heading"><div><h2>Sessions</h2><p>{activeTab.context.project} 中的 248 个 Session · 4 个实时更新</p></div></div><p className="demo-boundary" role="status">演示数据 · 内存 adapter · 未连接真实 Teamserver</p><SessionTable rows={sessions} state={tableState} capabilities={{ filtering: true, sortableColumns: ["status", "id", "seen"], columnVisibility: true, pagination: true }} page={{ start: 1, end: 50, total: 248, hasPrevious: false, hasNext: true }} onStateChange={(state) => { if (state.filter !== server.filter) dispatch({ type: "setFilter", serverId: server.id, filter: state.filter }); setTableStates((current) => updateSessionTableState(current, server.id, state)); }} onOpenSession={(session) => dispatch({ type: "openTab", serverId: server.id, tab: { id: `${server.id}-${activeTab.context.project}-${session.id}-overview`, title: "Session Overview", context: { serverId: server.id, project: activeTab.context.project, session: session.id } } })} onQueryIntent={(intent) => setLastQueryIntent((current) => ({ ...current, [server.id]: intent }))} /><output className="query-intent" aria-live="polite">{lastQueryIntent[server.id] ? `查询意图:${lastQueryIntent[server.id]?.type}` : "等待服务端查询意图"}</output></>;
|
||||||
const pages: Record<string, ReactNode> = { Overview: <OverviewPage />, Sessions: sessionsPage, "Session Overview": <SessionDetailPage />, Tasks: <TasksPage />, "Task Output": <TasksPage />, Events: <EventsPage />, Artifacts: <ArtifactsPage />, Audit: <AuditPage />, "连接中心": <ConnectionCenter adapter={connectionAdapter} developmentMode={import.meta.env.DEV} /> };
|
const pages: Record<string, ReactNode> = { Overview: <OverviewPage />, Sessions: sessionsPage, "Session Overview": <SessionDetailPage sessionId={activeTab.context.session} />, Tasks: <TasksPage />, "Task Output": <TasksPage />, Events: <EventsPage />, Artifacts: <ArtifactsPage />, Audit: <AuditPage />, "连接中心": <ConnectionCenter adapter={connectionAdapter} developmentMode={import.meta.env.DEV} /> };
|
||||||
return <WorkspaceLayout servers={workspace.serverOrder.map((id) => workspace.servers[id])} server={server} activeTab={activeTab} activePage={server.navigation} onNavigate={navigate} onSwitchServer={(serverId) => dispatch({ type: "switchServer", serverId })} onActivateTab={(tabId) => dispatch({ type: "activateTab", serverId: server.id, tabId })} onCloseTab={(tabId) => dispatch({ type: "closeTab", serverId: server.id, tabId })} onTogglePin={(tabId) => dispatch({ type: "togglePinTab", serverId: server.id, tabId })}><article className="content" aria-label={`${activeTab.title} 页面`}>{pages[activeTab.title] ?? <OverviewPage />}</article></WorkspaceLayout>;
|
return <WorkspaceLayout servers={workspace.serverOrder.map((id) => workspace.servers[id])} server={server} activeTab={activeTab} activePage={server.navigation} onNavigate={navigate} onSwitchServer={(serverId) => dispatch({ type: "switchServer", serverId })} onActivateTab={(tabId) => dispatch({ type: "activateTab", serverId: server.id, tabId })} onCloseTab={(tabId) => dispatch({ type: "closeTab", serverId: server.id, tabId })} onTogglePin={(tabId) => dispatch({ type: "togglePinTab", serverId: server.id, tabId })}><article className="content" aria-label={`${activeTab.title} 页面`}>{pages[activeTab.title] ?? <OverviewPage />}</article></WorkspaceLayout>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,9 +8,9 @@ const rows: SessionRow[] = [
|
|||||||
{ id: "S-2", status: "OFFLINE", host: "beta", user: "two", os: "Windows", address: "10.0.0.2", seen: "2m", tags: ["b"] },
|
{ id: "S-2", status: "OFFLINE", host: "beta", user: "two", os: "Windows", address: "10.0.0.2", seen: "2m", tags: ["b"] },
|
||||||
];
|
];
|
||||||
|
|
||||||
function Harness({ onIntent = () => undefined }: { onIntent?: (intent: SessionQueryIntent) => void }) {
|
function Harness({ onIntent = () => undefined, onOpenSession }: { onIntent?: (intent: SessionQueryIntent) => void; onOpenSession?: (session: SessionRow) => void }) {
|
||||||
const [state, setState] = useState(createSessionTableViewState);
|
const [state, setState] = useState(createSessionTableViewState);
|
||||||
return <SessionTable rows={rows} state={state} capabilities={{ filtering: true, sortableColumns: ["status", "id", "seen"], columnVisibility: true, pagination: true }} page={{ start: 1, end: 2, total: 200, hasPrevious: false, hasNext: true }} onStateChange={setState} onQueryIntent={onIntent} />;
|
return <SessionTable rows={rows} state={state} capabilities={{ filtering: true, sortableColumns: ["status", "id", "seen"], columnVisibility: true, pagination: true }} page={{ start: 1, end: 2, total: 200, hasPrevious: false, hasNext: true }} onStateChange={setState} onQueryIntent={onIntent} onOpenSession={onOpenSession} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("SessionTable", () => {
|
describe("SessionTable", () => {
|
||||||
@ -61,4 +61,15 @@ describe("SessionTable", () => {
|
|||||||
fireEvent.click(screen.getByRole("checkbox", { name: "来源地址" }));
|
fireEvent.click(screen.getByRole("checkbox", { name: "来源地址" }));
|
||||||
expect(screen.queryByText("10.0.0.1")).not.toBeInTheDocument();
|
expect(screen.queryByText("10.0.0.1")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("双击或按 Enter 打开当前 Session", () => {
|
||||||
|
const onOpenSession = vi.fn();
|
||||||
|
render(<Harness onOpenSession={onOpenSession} />);
|
||||||
|
const first = screen.getByRole("row", { name: /S-1/ });
|
||||||
|
fireEvent.doubleClick(first);
|
||||||
|
first.focus();
|
||||||
|
fireEvent.keyDown(first, { key: "Enter" });
|
||||||
|
expect(onOpenSession).toHaveBeenNthCalledWith(1, rows[0]);
|
||||||
|
expect(onOpenSession).toHaveBeenNthCalledWith(2, rows[0]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export interface SessionTableProps {
|
|||||||
page: { start: number; end: number; total: number; hasPrevious: boolean; hasNext: boolean };
|
page: { start: number; end: number; total: number; hasPrevious: boolean; hasNext: boolean };
|
||||||
onStateChange: (state: SessionTableViewState) => void;
|
onStateChange: (state: SessionTableViewState) => void;
|
||||||
onQueryIntent: (intent: SessionQueryIntent) => void;
|
onQueryIntent: (intent: SessionQueryIntent) => void;
|
||||||
|
onOpenSession?: (session: SessionRow) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionTableCapabilities {
|
export interface SessionTableCapabilities {
|
||||||
@ -64,7 +65,7 @@ function nextSort(state: SessionTableViewState, column: SessionColumnId): SortDi
|
|||||||
return state.sort?.column === column && state.sort.direction === "ascending" ? "descending" : "ascending";
|
return state.sort?.column === column && state.sort.direction === "ascending" ? "descending" : "ascending";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SessionTable({ rows, state, capabilities, page, onStateChange, onQueryIntent }: SessionTableProps) {
|
export function SessionTable({ rows, state, capabilities, page, onStateChange, onQueryIntent, onOpenSession }: SessionTableProps) {
|
||||||
const rowRefs = useRef(new Map<string, HTMLDivElement>());
|
const rowRefs = useRef(new Map<string, HTMLDivElement>());
|
||||||
const selected = new Set(state.selectedSessionIds);
|
const selected = new Set(state.selectedSessionIds);
|
||||||
const visible = new Set(state.visibleColumns);
|
const visible = new Set(state.visibleColumns);
|
||||||
@ -97,6 +98,10 @@ export function SessionTable({ rows, state, capabilities, page, onStateChange, o
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRowKeyDown = (event: KeyboardEvent<HTMLDivElement>, index: number, id: string) => {
|
const handleRowKeyDown = (event: KeyboardEvent<HTMLDivElement>, index: number, id: string) => {
|
||||||
|
if (event.key === "Enter" && event.target === event.currentTarget) {
|
||||||
|
event.preventDefault();
|
||||||
|
const session = rows[index]; if (session) onOpenSession?.(session);
|
||||||
|
}
|
||||||
if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Home" || event.key === "End") {
|
if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Home" || event.key === "End") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const target = event.key === "Home" ? 0 : event.key === "End" ? rows.length - 1 : index + (event.key === "ArrowDown" ? 1 : -1);
|
const target = event.key === "Home" ? 0 : event.key === "End" ? rows.length - 1 : index + (event.key === "ArrowDown" ? 1 : -1);
|
||||||
@ -138,7 +143,7 @@ export function SessionTable({ rows, state, capabilities, page, onStateChange, o
|
|||||||
})}
|
})}
|
||||||
<span role="columnheader" />
|
<span role="columnheader" />
|
||||||
</div>
|
</div>
|
||||||
</div><div role="rowgroup">{rows.map((session, index) => <div className={`table-row session-data-row ${session.id === state.focusedSessionId ? "focused" : ""}`} role="row" aria-selected={selected.has(session.id)} key={session.id} tabIndex={session.id === state.focusedSessionId || (!state.focusedSessionId && index === 0) ? 0 : -1} ref={(element) => { if (element) rowRefs.current.set(session.id, element); else rowRefs.current.delete(session.id); }} onFocus={() => state.focusedSessionId !== session.id && onStateChange({ ...state, focusedSessionId: session.id })} onClick={(event) => handleRowClick(event, index, session.id)} onKeyDown={(event) => handleRowKeyDown(event, index, session.id)}>
|
</div><div role="rowgroup">{rows.map((session, index) => <div className={`table-row session-data-row ${session.id === state.focusedSessionId ? "focused" : ""}`} role="row" aria-selected={selected.has(session.id)} aria-label={`${session.id},双击或按 Enter 打开详情`} key={session.id} tabIndex={session.id === state.focusedSessionId || (!state.focusedSessionId && index === 0) ? 0 : -1} ref={(element) => { if (element) rowRefs.current.set(session.id, element); else rowRefs.current.delete(session.id); }} onFocus={() => state.focusedSessionId !== session.id && onStateChange({ ...state, focusedSessionId: session.id })} onClick={(event) => handleRowClick(event, index, session.id)} onDoubleClick={() => onOpenSession?.(session)} onKeyDown={(event) => handleRowKeyDown(event, index, session.id)}>
|
||||||
{visible.has("status") && <span role="cell"><em className={`status-mark ${session.status.toLowerCase()}`} aria-hidden="true" /><span className="sr-only">状态:</span>{session.status}</span>}
|
{visible.has("status") && <span role="cell"><em className={`status-mark ${session.status.toLowerCase()}`} aria-hidden="true" /><span className="sr-only">状态:</span>{session.status}</span>}
|
||||||
{visible.has("id") && <strong role="cell" className="mono">{session.id}</strong>}
|
{visible.has("id") && <strong role="cell" className="mono">{session.id}</strong>}
|
||||||
{visible.has("identity") && <span role="cell"><b>{session.host}</b><small>{session.user}</small></span>}
|
{visible.has("identity") && <span role="cell"><b>{session.host}</b><small>{session.user}</small></span>}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { Event, Task } from "../../protocol/model";
|
import type { Event, Task } from "../../protocol/model";
|
||||||
import { demoSession } from "../../protocol/fixtures";
|
import { demoSession, demoSessions } from "../../protocol/fixtures";
|
||||||
import { EventDetail, EventFilters, EventStream, TaskDetail, TaskList, type EventFiltersValue, type TaskOutputEntry } from "../activity";
|
import { EventDetail, EventFilters, EventStream, TaskDetail, TaskList, type EventFiltersValue, type TaskOutputEntry } from "../activity";
|
||||||
import { ArtifactList, ArtifactMetadata, ArtifactPreview, AuditDetail, AuditTable, type ArtifactRecord, type AuditRecord } from "../records";
|
import { ArtifactList, ArtifactMetadata, ArtifactPreview, AuditDetail, AuditTable, type ArtifactRecord, type AuditRecord } from "../records";
|
||||||
import { SessionOverview, type SessionOverviewData } from "../sessions";
|
import { SessionOverview, type SessionOverviewData } from "../sessions";
|
||||||
@ -16,9 +16,10 @@ function Heading({ title, detail }: { title: string; detail: string }) { return
|
|||||||
|
|
||||||
export function OverviewPage() { return <><Heading title="Project Overview" detail="Northwind 项目健康与活动摘要" /><div className="overview-grid"><section><h3>连接状态</h3><p>CONNECTED · 42 ms · Protocol 1.0</p></section><section><h3>当前活动</h3><p>248 Sessions · 7 Tasks · 2 未读事件</p></section></div></>; }
|
export function OverviewPage() { return <><Heading title="Project Overview" detail="Northwind 项目健康与活动摘要" /><div className="overview-grid"><section><h3>连接状态</h3><p>CONNECTED · 42 ms · Protocol 1.0</p></section><section><h3>当前活动</h3><p>248 Sessions · 7 Tasks · 2 未读事件</p></section></div></>; }
|
||||||
|
|
||||||
export function SessionDetailPage() {
|
export function SessionDetailPage({ sessionId }: { sessionId?: string }) {
|
||||||
|
const session = demoSessions.find((item) => item.session_id === sessionId) ?? demoSession;
|
||||||
const data = { capabilities: [], tasks: [task], events: [event], artifacts: [], notes: "服务端演示备注", tags: demoSession.tags } as unknown as SessionOverviewData;
|
const data = { capabilities: [], tasks: [task], events: [event], artifacts: [], notes: "服务端演示备注", tags: demoSession.tags } as unknown as SessionOverviewData;
|
||||||
return <><Heading title="Session Overview" detail="标准协议 Session 详情" /><SessionOverview session={demoSession} data={data} /></>;
|
return <><Heading title="Session Overview" detail={`${session.name} · 标准协议 Session 详情`} /><SessionOverview session={session} data={data} /></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TasksPage() {
|
export function TasksPage() {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user