Files
stack/apps/web/src/components/mission-control/MissionControlLayout.tsx
Jason Woltje e4f942dde7
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
feat(web): MS23-P2-008 panel grid responsive layout
2026-03-07 14:47:39 -06:00

116 lines
3.4 KiB
TypeScript

"use client";
import { useCallback, useState } from "react";
import { AuditLogDrawer } from "@/components/mission-control/AuditLogDrawer";
import { GlobalAgentRoster } from "@/components/mission-control/GlobalAgentRoster";
import {
MAX_PANEL_COUNT,
MIN_PANEL_COUNT,
MissionControlPanel,
type PanelConfig,
} from "@/components/mission-control/MissionControlPanel";
import { Button } from "@/components/ui/button";
const INITIAL_PANELS: PanelConfig[] = [{}];
export function MissionControlLayout(): React.JSX.Element {
const [panels, setPanels] = useState<PanelConfig[]>(INITIAL_PANELS);
const [selectedSessionId, setSelectedSessionId] = useState<string>();
const handleSelectSession = useCallback((sessionId: string): void => {
setSelectedSessionId(sessionId);
setPanels((currentPanels) => {
if (currentPanels.some((panel) => panel.sessionId === sessionId)) {
return currentPanels;
}
const firstEmptyPanelIndex = currentPanels.findIndex(
(panel) => panel.sessionId === undefined
);
if (firstEmptyPanelIndex >= 0) {
return currentPanels.map((panel, index) =>
index === firstEmptyPanelIndex ? { ...panel, sessionId } : panel
);
}
if (currentPanels.length >= MAX_PANEL_COUNT) {
return currentPanels;
}
return [...currentPanels, { sessionId }];
});
}, []);
const handleAddPanel = useCallback((): void => {
setPanels((currentPanels) => {
if (currentPanels.length >= MAX_PANEL_COUNT) {
return currentPanels;
}
return [...currentPanels, {}];
});
}, []);
const handleRemovePanel = useCallback((panelIndex: number): void => {
setPanels((currentPanels) => {
if (panelIndex < 0 || panelIndex >= currentPanels.length) {
return currentPanels;
}
if (currentPanels.length <= MIN_PANEL_COUNT) {
return currentPanels;
}
const nextPanels = currentPanels.filter((_, index) => index !== panelIndex);
return nextPanels.length === 0 ? INITIAL_PANELS : nextPanels;
});
}, []);
const handleExpandPanel = useCallback((panelIndex: number): void => {
setPanels((currentPanels) => {
if (panelIndex < 0 || panelIndex >= currentPanels.length) {
return currentPanels;
}
const shouldExpand = !currentPanels[panelIndex]?.expanded;
return currentPanels.map((panel, index) => ({
...panel,
expanded: shouldExpand && index === panelIndex,
}));
});
}, []);
return (
<section className="flex h-full min-h-0 flex-col overflow-hidden" aria-label="Mission Control">
<header className="mb-3 flex items-center justify-end">
<AuditLogDrawer
trigger={
<Button variant="outline" size="sm">
Audit Log
</Button>
}
/>
</header>
<div className="grid min-h-0 flex-1 gap-4 xl:grid-cols-[280px_minmax(0,1fr)]">
<aside className="h-full min-h-0">
<GlobalAgentRoster
onSelectSession={handleSelectSession}
{...(selectedSessionId !== undefined ? { selectedSessionId } : {})}
/>
</aside>
<main className="h-full min-h-0 overflow-hidden">
<MissionControlPanel
panels={panels}
onAddPanel={handleAddPanel}
onRemovePanel={handleRemovePanel}
onExpandPanel={handleExpandPanel}
/>
</main>
</div>
</section>
);
}