36 lines
6.6 KiB
TypeScript
36 lines
6.6 KiB
TypeScript
import { useEffect, useReducer, useState, type ReactNode } from "react";
|
||
import { blankConnection, ConnectionCenter, ConnectionEditor, MemoryMockConnectionAdapter } from "./features/connections";
|
||
import { ArtifactsPage, AuditPage, EventsPage, OverviewPage, SessionDetailPage, TasksPage } from "./features/workbench";
|
||
import { createSessionTableViewState, SessionQueryIntent, SessionTable } from "./SessionTable";
|
||
import { demoSessions } from "./protocol/fixtures";
|
||
import { initializeSessionTableStates, updateSessionTableState } from "./sessionTableState";
|
||
import { toSessionRow } from "./sessionPresentation";
|
||
import { WorkspaceLayout } from "./shell";
|
||
import { Dialog, Toast } from "./ui";
|
||
import { createInitialWorkspaceState, selectActiveTab, selectActiveWorkspace, workspaceReducer } from "./workspace";
|
||
|
||
const fixtureNow = new Date("2026-08-03T19:12:02Z");
|
||
const sessions = demoSessions.map((session, index) => ({ ...toSessionRow(session, fixtureNow), capabilities: index === 0 ? ["session.processes", "session.files"] : [] }));
|
||
const connectionAdapter = new MemoryMockConnectionAdapter();
|
||
|
||
export function App() {
|
||
useEffect(() => { const disableNativeMenu = (event: globalThis.MouseEvent) => event.preventDefault(); document.addEventListener("contextmenu", disableNativeMenu); return () => document.removeEventListener("contextmenu", disableNativeMenu); }, []);
|
||
const [workspace, dispatch] = useReducer(workspaceReducer, undefined, createInitialWorkspaceState);
|
||
const [tableStates, setTableStates] = useState(() => initializeSessionTableStates(workspace.serverOrder));
|
||
const [lastQueryIntent, setLastQueryIntent] = useState<Record<string, SessionQueryIntent | undefined>>({});
|
||
const [addOpen, setAddOpen] = useState(false);
|
||
const [connectionAddresses, setConnectionAddresses] = useState<string[]>([]);
|
||
const [notice, setNotice] = useState("");
|
||
const server = selectActiveWorkspace(workspace); const activeTab = selectActiveTab(server);
|
||
const tableState = { ...(tableStates[server.id] ?? createSessionTableViewState()), filter: server.filter };
|
||
const navigate = (page: string) => {
|
||
const title = page === "Sessions" ? "Sessions" : 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" } } });
|
||
};
|
||
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 } } })} onOpenOperation={(session, operation) => dispatch({ type: "openTab", serverId: server.id, tab: { id: `${server.id}-${activeTab.context.project}-${session.id}-${operation}`, title: operation === "processes" ? "进程" : operation === "files" ? "文件" : "打开操作", context: { serverId: server.id, project: activeTab.context.project, session: session.id }, dirty: operation === "operation" } })} 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 sessionId={activeTab.context.session} />, Tasks: <TasksPage />, "Task Output": <TasksPage />, Events: <EventsPage />, Artifacts: <ArtifactsPage />, Audit: <AuditPage />, "连接中心": <ConnectionCenter adapter={connectionAdapter} developmentMode={import.meta.env.DEV} /> };
|
||
const operationBoundary = ["进程", "文件", "打开操作"].includes(activeTab.title) ? <section className="operation-boundary"><h2>{activeTab.title}</h2><p>{activeTab.context.serverId} / {activeTab.context.project} / {activeTab.context.session}</p><p className="demo-boundary">Mock 界面边界:需要真实 Teamserver adapter 才能执行或读取数据。</p></section> : undefined;
|
||
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 })} onAddServer={() => setAddOpen(true)} 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] ?? operationBoundary ?? <OverviewPage />}</article></WorkspaceLayout><Dialog open={addOpen} onOpenChange={setAddOpen} title="添加 Teamserver"><ConnectionEditor initial={blankConnection()} existingAddresses={connectionAddresses} developmentMode={import.meta.env.DEV} onCancel={() => setAddOpen(false)} onSave={async (config) => { await connectionAdapter.save(config); setConnectionAddresses((current) => [...current, config.address.trim()]); const tab = { id: `${config.id}-connections`, title: "连接中心", context: { serverId: config.id, project: "尚未选择 Project", session: "Project" } }; dispatch({ type: "addServer", server: { id: config.id, name: config.name, shortName: config.name.trim().slice(0, 2).toUpperCase(), connection: "DEGRADED", unread: 0, activeProject: "尚未选择 Project", navigation: "连接中心", filter: "", activeTabId: tab.id, tabs: [tab] } }); setAddOpen(false); setNotice("连接配置已创建,尚未建立真实连接。"); }} /></Dialog>{notice && <Toast tone="info" onDismiss={() => setNotice("")}>{notice}</Toast>}</>;
|
||
}
|