feat: complete catalog fleet management workflow
This commit is contained in:
@@ -5,6 +5,7 @@ import { CentralStatus } from "./CentralStatus";
|
||||
import { ModelActivityPanel } from "./ModelActivityPanel";
|
||||
import { WorkspaceManager } from "./WorkspaceManager";
|
||||
import { PiManagement } from "./PiManagement";
|
||||
import { WorkAreaPanelHost } from "./WorkAreaPanel";
|
||||
import { useActivityPanelResize } from "./useActivityPanelResize";
|
||||
import { useSessionPanelResize } from "./useSessionPanelResize";
|
||||
import { NavSessions } from "./NavSessions";
|
||||
@@ -41,6 +42,35 @@ interface AppShellProps {
|
||||
}
|
||||
|
||||
type ActiveSurface = "core" | "database-management";
|
||||
type ActiveManagementPanel = "workspace" | "pi" | null;
|
||||
|
||||
type DatabaseManagementPresentation = "legacy" | "fleet";
|
||||
|
||||
interface DatabaseManagementPresentationEnvironment {
|
||||
isDevelopment: boolean;
|
||||
mode: string;
|
||||
legacyFlag?: string | boolean;
|
||||
search: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fleet Ledger is the release-safe default. The old surface can only be
|
||||
* requested explicitly from a development server or a staging build, so an
|
||||
* accidentally retained flag can never switch the production UI back.
|
||||
*/
|
||||
export function resolveDatabaseManagementPresentation({
|
||||
isDevelopment,
|
||||
mode,
|
||||
legacyFlag,
|
||||
search,
|
||||
}: DatabaseManagementPresentationEnvironment): DatabaseManagementPresentation {
|
||||
const legacyAllowed = isDevelopment || mode.trim().toLowerCase() === "staging";
|
||||
if (!legacyAllowed) return "fleet";
|
||||
|
||||
const legacyRequestedByQuery = new URLSearchParams(search).get("db-ui")?.toLowerCase() === "legacy";
|
||||
const legacyRequestedByEnvironment = String(legacyFlag ?? "").trim().toLowerCase() === "true";
|
||||
return legacyRequestedByQuery || legacyRequestedByEnvironment ? "legacy" : "fleet";
|
||||
}
|
||||
|
||||
export function AppShell({ canLogout }: AppShellProps) {
|
||||
const authenticatedUser = useAuthUser();
|
||||
@@ -123,12 +153,17 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const [showActivity, setShowActivity] = useState(false);
|
||||
const [activeSurface, setActiveSurface] = useState<ActiveSurface>("core");
|
||||
const databaseManagementPresentation = resolveDatabaseManagementPresentation({
|
||||
isDevelopment: import.meta.env.DEV,
|
||||
mode: import.meta.env.MODE,
|
||||
legacyFlag: import.meta.env.VITE_DB_MANAGEMENT_LEGACY,
|
||||
search: window.location.search,
|
||||
});
|
||||
const databaseNavigationRef = useRef({ dirty: false, busy: false });
|
||||
const updateDatabaseNavigationState = useCallback((state: { dirty: boolean; busy: boolean }) => {
|
||||
databaseNavigationRef.current = state;
|
||||
}, []);
|
||||
const [workspaceManagerOpen, setWorkspaceManagerOpen] = useState(false);
|
||||
const [piManagementOpen, setPiManagementOpen] = useState(false);
|
||||
const [activeManagementPanel, setActiveManagementPanel] = useState<ActiveManagementPanel>(null);
|
||||
const [activeOpen, setActiveOpen] = useState(true);
|
||||
const [archiveOpen, setArchiveOpen] = useState(false);
|
||||
const [renameTarget, setRenameTarget] = useState<SessionSummary | null>(null);
|
||||
@@ -218,6 +253,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
const s = sessions.find((x) => x.id === id);
|
||||
if (!s) return;
|
||||
setActiveSurface("core");
|
||||
setActiveManagementPanel(null);
|
||||
// A session with a live Pi runtime opens straight into its live view: doResume
|
||||
// reconnects to the already-active runtime and replays its pending gate, so an
|
||||
// in-progress session never shows an empty screen that reads as "stopped". Cold or
|
||||
@@ -239,6 +275,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
async function doResume(id: string, databaseExitApproved = false) {
|
||||
if (!databaseExitApproved && !canLeaveDatabaseManagement()) return;
|
||||
setActiveSurface("core");
|
||||
setActiveManagementPanel(null);
|
||||
const guard = captureAuthOperation({ sessionId: id, disposalEpoch: operationEpochRef.current });
|
||||
if (!guard) return;
|
||||
const token = ++resumeInvocationRef.current;
|
||||
@@ -522,6 +559,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
function startNewSession() {
|
||||
if (!canLeaveDatabaseManagement()) return;
|
||||
setActiveSurface("core");
|
||||
setActiveManagementPanel(null);
|
||||
invalidateResumeIntent();
|
||||
newSessionOperationRef.current = null;
|
||||
resetSession();
|
||||
@@ -623,7 +661,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showActivity && <ModelActivityPanel desktopSplit={desktopSplit} onClose={() => setShowActivity(false)} onOpenWorkspaceManager={() => setWorkspaceManagerOpen(true)} hidden={activeSurface !== "core"} />}
|
||||
{showActivity && <ModelActivityPanel desktopSplit={desktopSplit} onClose={() => setShowActivity(false)} onOpenWorkspaceManager={() => setActiveManagementPanel("workspace")} hidden={activeSurface !== "core"} />}
|
||||
{activeSurface === "core" && showActivity && desktopSplit && (
|
||||
<div className="relative w-0 shrink-0">
|
||||
<div
|
||||
@@ -638,12 +676,13 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
)}
|
||||
|
||||
{/* Conversation column */}
|
||||
<div data-testid="conversation-column" className="flex min-w-0 flex-1 flex-col">
|
||||
<WorkAreaPanelHost data-testid="conversation-column" className="flex min-w-0 flex-1 flex-col">
|
||||
{activeSurface === "database-management" && (
|
||||
<DatabaseManagementPage
|
||||
canManage={canManageDatabase}
|
||||
canManageSecrets={canManageWorkspaceSecrets}
|
||||
onNavigationStateChange={updateDatabaseNavigationState}
|
||||
presentation={databaseManagementPresentation}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
@@ -727,11 +766,23 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<WorkspaceManager
|
||||
open={activeManagementPanel === "workspace"}
|
||||
onClose={() => setActiveManagementPanel(null)}
|
||||
canManageWorkspace={canManageWorkspace}
|
||||
canManageSecrets={canManageWorkspaceSecrets}
|
||||
/>
|
||||
{canManagePi && (
|
||||
<PiManagement
|
||||
open={activeManagementPanel === "pi"}
|
||||
onClose={() => setActiveManagementPanel(null)}
|
||||
/>
|
||||
)}
|
||||
</WorkAreaPanelHost>
|
||||
|
||||
{/* Right session rail */}
|
||||
{!showActivity && (
|
||||
<aside className="flex w-[15vw] shrink-0 flex-col border-l border-border bg-sidebar">
|
||||
{(activeSurface === "database-management" || !showActivity) && (
|
||||
<aside aria-label="Session navigation" className="flex w-[15vw] shrink-0 flex-col bg-sidebar">
|
||||
<div className="relative px-4 pb-3 pt-5 text-center">
|
||||
<h1 className="font-heading text-xl font-semibold leading-none tracking-tight text-foreground">
|
||||
Thoth<span className="text-primary">II</span>
|
||||
@@ -767,25 +818,28 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
if (!canLeaveDatabaseManagement()) return;
|
||||
setActiveSurface("core");
|
||||
setWorkspaceManagerOpen(true);
|
||||
}}
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
if (!canLeaveDatabaseManagement()) return;
|
||||
setActiveSurface("core");
|
||||
setActiveManagementPanel("workspace");
|
||||
}}
|
||||
>
|
||||
Workspace management
|
||||
</Button>
|
||||
{canManageDatabase && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
aria-current={activeSurface === "database-management" ? "page" : undefined}
|
||||
onClick={() => setActiveSurface("database-management")}
|
||||
>
|
||||
Database management
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeSurface === "database-management" ? "secondary" : "outline"}
|
||||
size="sm"
|
||||
className="w-full"
|
||||
aria-current={activeSurface === "database-management" ? "page" : undefined}
|
||||
onClick={() => {
|
||||
setActiveManagementPanel(null);
|
||||
setActiveSurface("database-management");
|
||||
}}
|
||||
>
|
||||
Database management
|
||||
</Button>
|
||||
)}
|
||||
{canManagePi && (
|
||||
<Button
|
||||
@@ -795,7 +849,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
onClick={() => {
|
||||
if (!canLeaveDatabaseManagement()) return;
|
||||
setActiveSurface("core");
|
||||
setPiManagementOpen(true);
|
||||
setActiveManagementPanel("pi");
|
||||
}}
|
||||
>
|
||||
Pi management
|
||||
@@ -951,14 +1005,6 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
<WorkspaceManager
|
||||
open={workspaceManagerOpen}
|
||||
onClose={() => setWorkspaceManagerOpen(false)}
|
||||
canManageWorkspace={canManageWorkspace}
|
||||
canManageSecrets={canManageWorkspaceSecrets}
|
||||
/>
|
||||
{canManagePi && <PiManagement open={piManagementOpen} onClose={() => setPiManagementOpen(false)} />}
|
||||
|
||||
<Toaster />
|
||||
|
||||
<StopConfirmDialog
|
||||
|
||||
Reference in New Issue
Block a user