diff --git a/src/App.test.tsx b/src/App.test.tsx index d2551e9..1f4891c 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -28,6 +28,14 @@ describe("Teamserver 工作区", () => { expect(screen.getByText("10.42.8.17")).toBeInTheDocument(); }); + it("从 Session 表格打开上下文锁定的详情标签", () => { + render(); + 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("通过拆分后的侧边栏进入主线新增的连接中心", () => { render(); fireEvent.click(screen.getByRole("button", { name: "连接中心" })); diff --git a/src/App.tsx b/src/App.tsx index 69df922..afcfb7e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,7 +24,7 @@ export function App() { 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" } } }); }; - const sessionsPage = <>

Sessions

{activeTab.context.project} 中的 248 个 Session · 4 个实时更新

演示数据 · 内存 adapter · 未连接真实 Teamserver

{ 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 }))} />{lastQueryIntent[server.id] ? `查询意图:${lastQueryIntent[server.id]?.type}` : "等待服务端查询意图"}; - const pages: Record = { Overview: , Sessions: sessionsPage, "Session Overview": , Tasks: , "Task Output": , Events: , Artifacts: , Audit: , "连接中心": }; + const sessionsPage = <>

Sessions

{activeTab.context.project} 中的 248 个 Session · 4 个实时更新

演示数据 · 内存 adapter · 未连接真实 Teamserver

{ 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 }))} />{lastQueryIntent[server.id] ? `查询意图:${lastQueryIntent[server.id]?.type}` : "等待服务端查询意图"}; + const pages: Record = { Overview: , Sessions: sessionsPage, "Session Overview": , Tasks: , "Task Output": , Events: , Artifacts: , Audit: , "连接中心": }; return 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 })}>
{pages[activeTab.title] ?? }
; } diff --git a/src/SessionTable.test.tsx b/src/SessionTable.test.tsx index 27678b0..ff50857 100644 --- a/src/SessionTable.test.tsx +++ b/src/SessionTable.test.tsx @@ -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"] }, ]; -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); - return ; + return ; } describe("SessionTable", () => { @@ -61,4 +61,15 @@ describe("SessionTable", () => { fireEvent.click(screen.getByRole("checkbox", { name: "来源地址" })); expect(screen.queryByText("10.0.0.1")).not.toBeInTheDocument(); }); + + it("双击或按 Enter 打开当前 Session", () => { + const onOpenSession = vi.fn(); + render(); + 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]); + }); }); diff --git a/src/SessionTable.tsx b/src/SessionTable.tsx index 2f59e93..92de4c4 100644 --- a/src/SessionTable.tsx +++ b/src/SessionTable.tsx @@ -37,6 +37,7 @@ export interface SessionTableProps { page: { start: number; end: number; total: number; hasPrevious: boolean; hasNext: boolean }; onStateChange: (state: SessionTableViewState) => void; onQueryIntent: (intent: SessionQueryIntent) => void; + onOpenSession?: (session: SessionRow) => void; } 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"; } -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()); const selected = new Set(state.selectedSessionIds); const visible = new Set(state.visibleColumns); @@ -97,6 +98,10 @@ export function SessionTable({ rows, state, capabilities, page, onStateChange, o }; const handleRowKeyDown = (event: KeyboardEvent, 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") { event.preventDefault(); 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 })} -
{rows.map((session, index) =>
{ 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)}> +
{rows.map((session, index) =>
{ 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") &&