feat: establish unified administration and model context baseline
This commit is contained in:
+121
-81
@@ -20,7 +20,6 @@ import { DatabaseManagementPage } from "./DatabaseManagementPage";
|
||||
import { MemoryManagementPage } from "./MemoryManagementPage";
|
||||
import { EvidenceManagementPage } from "./EvidenceManagementPage";
|
||||
import { ArchiveNavigation } from "./ArchiveNavigation";
|
||||
import { WorkspacePreprocessingControl } from "./WorkspacePreprocessingControl";
|
||||
import { Pencil, ArrowLeft, ArrowRight, ChevronDown, Trash2 } from "lucide-react";
|
||||
import { Accordion } from "@base-ui/react/accordion";
|
||||
import { Button, buttonVariants } from "../components/ui/button";
|
||||
@@ -37,20 +36,23 @@ import {
|
||||
} from "../components/ui/dialog";
|
||||
import type { SessionScope, SessionSummary } from "../api/types";
|
||||
import { useAuthGeneration, useAuthUser } from "../auth/authState";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useIsMutating, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { CSSProperties, KeyboardEvent } from "react";
|
||||
import { captureAuthOperation, isAuthOperationCurrent, StaleAuthOperationError, type AuthOperationGuard } from "../auth/authOperation";
|
||||
import { getSettings } from "../api/settings";
|
||||
import { workspacePreferences, type WorkspacePreference } from "../workspaces/preferences";
|
||||
import { workspacePreferences } from "../workspaces/preferences";
|
||||
import { getWorkspacePreprocessingStatus } from "../api/workspace-preprocessing";
|
||||
import { AdministrationNavigationProvider, useAdministrationNavigation } from "./administration/AdministrationNavigation";
|
||||
import { AdministrationHeader } from "./administration/AdministrationPage";
|
||||
import { WorkingContextProvider, useWorkingContext } from "../workspaces/WorkingContext";
|
||||
import { WorkingContextShelf } from "./WorkingContextShelf";
|
||||
import { useInteractionModelBusy } from "../models/useInteractionModelBusy";
|
||||
|
||||
interface AppShellProps {
|
||||
canLogout: boolean;
|
||||
}
|
||||
|
||||
type ActiveSurface = "core" | "database-management" | "memory-management" | "evidence-management";
|
||||
type ActiveManagementPanel = "workspace" | "pi" | null;
|
||||
const SESSION_SCOPES: readonly SessionScope[] = ["mine", "all"];
|
||||
|
||||
function sessionScopeTabClass(selected: boolean): string {
|
||||
@@ -89,6 +91,10 @@ export function resolveDatabaseManagementPresentation({
|
||||
}
|
||||
|
||||
export function AppShell({ canLogout }: AppShellProps) {
|
||||
const workingContext = useWorkingContext();
|
||||
const interactionBusy = useInteractionModelBusy();
|
||||
const mutationBusy = useIsMutating() > 0;
|
||||
const [adminBusy, setAdminBusy] = useState({ database: false, workspace: false, evidence: false, pi: false });
|
||||
const authenticatedUser = useAuthUser();
|
||||
const [panelSession, setPanelSession] = useState<SessionSummary | null>(null);
|
||||
const {
|
||||
@@ -166,18 +172,9 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
queryKey: ["settings"],
|
||||
queryFn: getSettings,
|
||||
});
|
||||
const [workspacePreference, setWorkspacePreference] = useState<WorkspacePreference>(
|
||||
() => workspacePreferences.load(),
|
||||
);
|
||||
useEffect(() => {
|
||||
const unsubscribe = workspacePreferences.subscribe(setWorkspacePreference);
|
||||
// Child effects may seed the singleton before this parent effect subscribes.
|
||||
setWorkspacePreference(workspacePreferences.load());
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
// Only an ID selected from the active Workspace Registry may address workspace-scoped APIs.
|
||||
// Installation settings are a legacy hint and can name a removed/non-catalog workspace.
|
||||
const selectedWorkspaceId = workspacePreference.workspaceId;
|
||||
const selectedWorkspaceId = workingContext.workspace?.id;
|
||||
const preprocessingQuery = useQuery({
|
||||
queryKey: ["workspace-preprocessing", selectedWorkspaceId],
|
||||
queryFn: () => getWorkspacePreprocessingStatus(selectedWorkspaceId!),
|
||||
@@ -204,7 +201,6 @@ 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,
|
||||
@@ -214,8 +210,25 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
const databaseNavigationRef = useRef({ dirty: false, busy: false });
|
||||
const updateDatabaseNavigationState = useCallback((state: { dirty: boolean; busy: boolean }) => {
|
||||
databaseNavigationRef.current = state;
|
||||
setAdminBusy(previous => previous.database === state.busy ? previous : { ...previous, database: state.busy });
|
||||
}, []);
|
||||
const [activeManagementPanel, setActiveManagementPanel] = useState<ActiveManagementPanel>(null);
|
||||
const workspaceNavigationRef = useRef({ dirty: false, busy: false });
|
||||
const updateWorkspaceNavigationState = useCallback((state: { dirty: boolean; busy: boolean }) => {
|
||||
workspaceNavigationRef.current = state;
|
||||
setAdminBusy(previous => previous.workspace === state.busy ? previous : { ...previous, workspace: state.busy });
|
||||
}, []);
|
||||
const { route, navigate } = useAdministrationNavigation(canLeaveDatabaseManagement, () => {
|
||||
if (activeSurface === "memory-management") return memoryDirty || memoryBusy;
|
||||
const state = activeSurface === "database-management" ? databaseNavigationRef.current
|
||||
: activeSurface === "workspace-management" ? workspaceNavigationRef.current : null;
|
||||
return Boolean(state && (state.dirty || state.busy));
|
||||
});
|
||||
const activeSurface = route.surface;
|
||||
const [visited, setVisited] = useState<Set<string>>(() => new Set([activeSurface]));
|
||||
useEffect(() => { setVisited(previous => previous.has(activeSurface) ? previous : new Set([...previous, activeSurface])); }, [activeSurface]);
|
||||
const administrationBusy = mutationBusy || memoryBusy || Object.values(adminBusy).some(Boolean);
|
||||
const contextLocked = interactionBusy || creatingSession || administrationBusy
|
||||
|| preprocessingQuery.data?.state === "running";
|
||||
const [adminNavigationValue, setAdminNavigationValue] = useState<string[]>([]);
|
||||
const [activeOpen, setActiveOpen] = useState(true);
|
||||
const [archiveOpen, setArchiveOpen] = useState(false);
|
||||
@@ -306,30 +319,22 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
}
|
||||
|
||||
function canLeaveDatabaseManagement(): boolean {
|
||||
if (activeSurface === "memory-management" && memoryBusy) {
|
||||
toast.info("Wait for the Memory operation to finish before leaving this page");
|
||||
const dirty = activeSurface === "workspace-management" ? workspaceNavigationRef.current.dirty
|
||||
: activeSurface === "memory-management" ? memoryDirty
|
||||
: activeSurface === "database-management" ? databaseNavigationRef.current.dirty : false;
|
||||
if (dirty) {
|
||||
toast.warning("Save your administration changes, or cancel the edit, before leaving this page.");
|
||||
return false;
|
||||
}
|
||||
if (activeSurface === "memory-management" && memoryDirty) {
|
||||
return window.confirm("Discard unsaved Memory changes and leave Memory management?");
|
||||
}
|
||||
if (activeSurface !== "database-management") return true;
|
||||
if (databaseNavigationRef.current.busy) {
|
||||
toast.info("Wait for the database operation to finish before leaving this page");
|
||||
return false;
|
||||
}
|
||||
if (databaseNavigationRef.current.dirty) {
|
||||
return window.confirm("Discard unsaved database changes and leave database management?");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function openPanel(id: string) {
|
||||
if (!workingContext.ready) return;
|
||||
if (!canLeaveDatabaseManagement()) return;
|
||||
const s = sessions.find((x) => x.id === id);
|
||||
if (!s) return;
|
||||
setActiveSurface("core");
|
||||
setActiveManagementPanel(null);
|
||||
navigate({ surface: "core" }, true);
|
||||
// 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
|
||||
@@ -349,9 +354,9 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
});
|
||||
}
|
||||
async function doResume(id: string, databaseExitApproved = false) {
|
||||
if (!workingContext.ready) return;
|
||||
if (!databaseExitApproved && !canLeaveDatabaseManagement()) return;
|
||||
setActiveSurface("core");
|
||||
setActiveManagementPanel(null);
|
||||
navigate({ surface: "core" }, true);
|
||||
const guard = captureAuthOperation({ sessionId: id, disposalEpoch: operationEpochRef.current });
|
||||
if (!guard) return;
|
||||
const token = ++resumeInvocationRef.current;
|
||||
@@ -388,6 +393,9 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
if (!isAuthOperationCurrent(guard, { sessionId: id, disposalEpoch: operationEpochRef.current })) return;
|
||||
const latest = latestResumeIntentRef.current;
|
||||
if (latest?.token !== operation.latestToken || latest.id !== id) return;
|
||||
// Bind context atomically with Resume, before displaying any session or Admin actions.
|
||||
// Older servers omit this field; their manifest remains the compatibility fallback.
|
||||
if (result.workspaceId) workspacePreferences.selectWorkspace(result.workspaceId);
|
||||
const reconnectSameSession = activeSessionIdRef.current === id;
|
||||
setPanelSession(null);
|
||||
setAwaitingQuestion(false);
|
||||
@@ -417,7 +425,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
// Paint the persisted re-entry phase while the replacement stream starts replaying.
|
||||
// The manifest's `phase` is the 1-based current phase (1..8).
|
||||
try {
|
||||
const m = (await getSession(id)) as { phase?: number };
|
||||
const m = (await getSession(id)) as { phase?: number; workspace_id?: string };
|
||||
if (!isAuthOperationCurrent(guard, { sessionId: id, disposalEpoch: operationEpochRef.current })) return;
|
||||
const latestAfterManifest = latestResumeIntentRef.current;
|
||||
if (
|
||||
@@ -425,6 +433,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
|| latestAfterManifest.id !== id
|
||||
|| activeSessionIdRef.current !== id
|
||||
) return;
|
||||
if (!result.workspaceId && m.workspace_id) workspacePreferences.selectWorkspace(m.workspace_id);
|
||||
if (typeof m.phase === "number" && m.phase >= 1 && m.phase <= 8) {
|
||||
setPhase(`F${m.phase}`);
|
||||
}
|
||||
@@ -557,7 +566,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
session={s}
|
||||
groups={groups}
|
||||
onResume={() => doResume(s.id)}
|
||||
onView={() => setPanelSession(s)}
|
||||
onView={() => { if (canLeaveDatabaseManagement()) { navigate({ surface: "core" }, true); setPanelSession(s); } }}
|
||||
onRename={() => setRenameTarget(s)}
|
||||
onMove={(g) => move(s, g)}
|
||||
onNewGroup={() => newGroup(s)}
|
||||
@@ -633,13 +642,13 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
}, [lastSystemEvent]);
|
||||
|
||||
function startNewSession() {
|
||||
if (!workingContext.ready || administrationBusy) return;
|
||||
if (preprocessingBlocksNewSession) {
|
||||
toast.warning(preprocessingQuery.data?.detail ?? "Workspace preprocessing is required.");
|
||||
return;
|
||||
}
|
||||
if (!canLeaveDatabaseManagement()) return;
|
||||
setActiveSurface("core");
|
||||
setActiveManagementPanel(null);
|
||||
navigate({ surface: "core" }, true);
|
||||
invalidateResumeIntent();
|
||||
newSessionOperationRef.current = null;
|
||||
resetSession();
|
||||
@@ -707,13 +716,35 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
await logoutUser();
|
||||
}
|
||||
|
||||
const currentNavigation = activeSurface === "database-management"
|
||||
? "database"
|
||||
: activeSurface === "memory-management" ? "memory" : activeSurface === "evidence-management" ? "evidence" : activeManagementPanel ?? "core";
|
||||
const currentNavigation = activeSurface.replace("-management", "");
|
||||
const administrationPermitted = activeSurface === "core" || (isAdmin && (
|
||||
activeSurface === "workspace-management" ||
|
||||
(activeSurface === "database-management" && canManageDatabase) ||
|
||||
(activeSurface === "memory-management" && canManageMemory) ||
|
||||
(activeSurface === "evidence-management" && canManageEvidence) ||
|
||||
(activeSurface === "pi-management" && canManagePi)
|
||||
));
|
||||
const adminNavigationOpen = adminNavigationValue.includes("administration");
|
||||
const managementNavigationCurrent = currentNavigation !== "core";
|
||||
|
||||
function changeContextWorkspace(id: string) {
|
||||
if (contextLocked || !canLeaveDatabaseManagement() || id === workingContext.workspaceId) return;
|
||||
invalidateResumeIntent();
|
||||
setPanelSession(null);
|
||||
selectActiveSession(null);
|
||||
resetSession();
|
||||
setAwaitingQuestion(false);
|
||||
workspacePreferences.selectWorkspace(id);
|
||||
}
|
||||
function changeContextModel(id: string) {
|
||||
if (!contextLocked && canLeaveDatabaseManagement()) workspacePreferences.selectModel(id);
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkingContextProvider value={{ workspaceId: workingContext.workspaceId, ready: workingContext.ready, locked: contextLocked, administrationBusy: administrationBusy || (interactionBusy && !agentActive) }}>
|
||||
<AdministrationNavigationProvider navigate={navigate}>
|
||||
<div className="thot-context-layout">
|
||||
<WorkingContextShelf context={workingContext} locked={contextLocked} onWorkspace={changeContextWorkspace} onModel={changeContextModel} />
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={panelWidthsStyle}
|
||||
@@ -722,8 +753,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
data-activity-resizing={activityResizing}
|
||||
data-session-resizing={sessionResizing}
|
||||
className={[
|
||||
"flex h-screen bg-background text-foreground",
|
||||
["memory-management", "evidence-management"].includes(activeSurface) ? "max-md:pt-12" : "",
|
||||
"thot-app-shell flex bg-background text-foreground",
|
||||
activityResizing || sessionResizing ? "select-none cursor-col-resize" : "",
|
||||
].join(" ")}
|
||||
>
|
||||
@@ -748,7 +778,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showActivity && <ModelActivityPanel desktopSplit={desktopSplit} onClose={() => setShowActivity(false)} onOpenWorkspaceManager={() => setActiveManagementPanel("workspace")} hidden={activeSurface !== "core"} />}
|
||||
{showActivity && <ModelActivityPanel desktopSplit={desktopSplit} onClose={() => setShowActivity(false)} onOpenWorkspaceManager={() => navigate({ surface: "workspace-management" })} hidden={activeSurface !== "core"} />}
|
||||
{activeSurface === "core" && showActivity && desktopSplit && (
|
||||
<div className="relative w-0 shrink-0">
|
||||
<div
|
||||
@@ -764,21 +794,29 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
|
||||
{/* Conversation column */}
|
||||
<WorkAreaPanelHost data-testid="conversation-column" className="flex min-w-0 flex-1 flex-col">
|
||||
{activeSurface === "memory-management" && (
|
||||
<MemoryManagementPage canManage={canManageMemory} onDirtyChange={setMemoryDirty} onBusyChange={setMemoryBusy} />
|
||||
{!workingContext.ready && <main className="thot-administration-page" aria-label="Working context required"><AdministrationHeader title="Choose your working context" description="Select an available workspace and AI model above to enable Core and Administration." /></main>}
|
||||
{!administrationPermitted && <main className="thot-administration-page" aria-label="Administration unavailable">
|
||||
<AdministrationHeader title="Administration unavailable" description="Your account does not have permission to open this page." />
|
||||
</main>}
|
||||
{workingContext.ready && isAdmin && canManageMemory && (activeSurface === "memory-management" || visited.has("memory-management")) && (
|
||||
<div style={{ display: activeSurface === "memory-management" ? "contents" : "none" }}><MemoryManagementPage key={selectedWorkspaceId} initialWorkspaceId={selectedWorkspaceId} canManage={canManageMemory} onDirtyChange={setMemoryDirty} onBusyChange={setMemoryBusy} /></div>
|
||||
)}
|
||||
{activeSurface === "evidence-management" && <EvidenceManagementPage canManage={canManageEvidence} />}
|
||||
{activeSurface === "database-management" && (
|
||||
{workingContext.ready && isAdmin && canManageEvidence && (activeSurface === "evidence-management" || visited.has("evidence-management")) && <div style={{ display: activeSurface === "evidence-management" ? "contents" : "none" }}><EvidenceManagementPage key={selectedWorkspaceId} initialWorkspaceId={selectedWorkspaceId} canManage={canManageEvidence} /></div>}
|
||||
{workingContext.ready && isAdmin && canManageDatabase && (activeSurface === "database-management" || visited.has("database-management")) && (
|
||||
<div style={{ display: activeSurface === "database-management" ? "contents" : "none" }}>
|
||||
<DatabaseManagementPage
|
||||
key={selectedWorkspaceId}
|
||||
initialWorkspaceId={selectedWorkspaceId}
|
||||
canManage={canManageDatabase}
|
||||
canManageSecrets={canManageWorkspaceSecrets}
|
||||
onNavigationStateChange={updateDatabaseNavigationState}
|
||||
presentation={databaseManagementPresentation}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
aria-hidden={activeSurface !== "core"}
|
||||
className={activeSurface === "core" ? "contents" : "hidden"}
|
||||
aria-hidden={activeSurface !== "core" || !workingContext.ready}
|
||||
className={activeSurface === "core" && workingContext.ready ? "contents" : "hidden"}
|
||||
>
|
||||
{activeSessionId && (
|
||||
<div className="relative shrink-0 border-b border-border/70 bg-background/80 px-6 py-2.5 backdrop-blur">
|
||||
@@ -857,23 +895,29 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<WorkspaceManager
|
||||
open={activeManagementPanel === "workspace"}
|
||||
onClose={() => setActiveManagementPanel(null)}
|
||||
<div style={{ display: activeSurface === "workspace-management" && workingContext.ready ? "contents" : "none" }}><WorkspaceManager
|
||||
key={selectedWorkspaceId}
|
||||
page
|
||||
initialWorkspaceId={selectedWorkspaceId}
|
||||
open={workingContext.ready && isAdmin && (activeSurface === "workspace-management" || visited.has("workspace-management"))}
|
||||
onClose={() => navigate({ surface: "core" })}
|
||||
onNavigationStateChange={updateWorkspaceNavigationState}
|
||||
canManageWorkspace={canManageWorkspace}
|
||||
canManageSecrets={canManageWorkspaceSecrets}
|
||||
/>
|
||||
{canManagePi && (
|
||||
<PiManagement
|
||||
open={activeManagementPanel === "pi"}
|
||||
onClose={() => setActiveManagementPanel(null)}
|
||||
/>
|
||||
canPreprocess={canManageDatabase}
|
||||
/></div>
|
||||
{isAdmin && canManagePi && (
|
||||
<div style={{ display: activeSurface === "pi-management" && workingContext.ready ? "contents" : "none" }}><PiManagement
|
||||
page
|
||||
open={workingContext.ready && (activeSurface === "pi-management" || visited.has("pi-management"))}
|
||||
onClose={() => navigate({ surface: "core" })}
|
||||
/></div>
|
||||
)}
|
||||
</WorkAreaPanelHost>
|
||||
|
||||
{/* Right session rail */}
|
||||
{(activeSurface !== "core" || !showActivity) && (
|
||||
<ArchiveNavigation surface={activeSurface}>
|
||||
{(
|
||||
<ArchiveNavigation surface={activeSurface} forceDrawer={activeSurface === "core" && showActivity}>
|
||||
<aside aria-label="Session navigation" className="flex w-64 shrink-0 flex-col bg-sidebar">
|
||||
<div className="thot-session-navigation__header relative text-center">
|
||||
<h1 className="font-heading text-xl font-semibold leading-none tracking-tight text-foreground">
|
||||
@@ -899,15 +943,18 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
</div>
|
||||
|
||||
<div className="thot-session-navigation__primary-controls">
|
||||
{activeSurface !== "core" && <Button variant="outline" size="sm" className="w-full" onClick={() => navigate({ surface: "core" })}>
|
||||
Return to session
|
||||
</Button>}
|
||||
<Button
|
||||
variant={currentNavigation === "core" ? "navigationActive" : "outline"}
|
||||
size="sm"
|
||||
className="w-full min-w-0 max-w-full gap-[0.25rem] px-[0.75rem]"
|
||||
aria-current={currentNavigation === "core" ? "page" : undefined}
|
||||
data-navigation-state={preprocessingBlocksNewSession
|
||||
data-navigation-state={preprocessingBlocksNewSession || !workingContext.ready || administrationBusy
|
||||
? "unavailable"
|
||||
: currentNavigation === "core" ? "current" : "available"}
|
||||
disabled={preprocessingBlocksNewSession}
|
||||
disabled={preprocessingBlocksNewSession || !workingContext.ready || administrationBusy}
|
||||
title={preprocessingBlocksNewSession ? preprocessingQuery.data?.detail : undefined}
|
||||
onClick={startNewSession}
|
||||
>
|
||||
@@ -949,8 +996,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
title={canManageDatabase ? undefined : "Database management permission is required"}
|
||||
onClick={() => {
|
||||
if (!canLeaveDatabaseManagement()) return;
|
||||
setActiveManagementPanel(null);
|
||||
setActiveSurface("database-management");
|
||||
navigate({ surface: "database-management" }, true);
|
||||
}}
|
||||
>
|
||||
Database management
|
||||
@@ -965,15 +1011,14 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
data-navigation-state={currentNavigation === "memory" ? "current" : "available"}
|
||||
onClick={() => {
|
||||
if (!canLeaveDatabaseManagement()) return;
|
||||
setActiveManagementPanel(null);
|
||||
setActiveSurface("memory-management");
|
||||
navigate({ surface: "memory-management" }, true);
|
||||
}}
|
||||
>Memory management</Button>
|
||||
<Button variant={currentNavigation === "evidence" ? "navigationActive" : "outline"}
|
||||
size="sm" className="w-full min-w-0 max-w-full gap-[0.25rem] px-[0.75rem]"
|
||||
aria-current={currentNavigation === "evidence" ? "page" : undefined}
|
||||
disabled={!canManageEvidence} title={canManageEvidence ? undefined : "Evidence management permission is required"}
|
||||
onClick={() => { if (!canLeaveDatabaseManagement()) return; setActiveManagementPanel(null); setActiveSurface("evidence-management"); }}
|
||||
onClick={() => navigate({ surface: "evidence-management" })}
|
||||
>Evidence management</Button>
|
||||
<div role="separator" aria-orientation="horizontal" className="mx-1 h-px bg-border/90" />
|
||||
<Button
|
||||
@@ -981,12 +1026,10 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
size="sm"
|
||||
className="w-full min-w-0 max-w-full gap-[0.25rem] px-[0.75rem]"
|
||||
aria-current={currentNavigation === "workspace" ? "page" : undefined}
|
||||
aria-expanded={activeManagementPanel === "workspace"}
|
||||
data-navigation-state={currentNavigation === "workspace" ? "current" : "available"}
|
||||
onClick={() => {
|
||||
if (!canLeaveDatabaseManagement()) return;
|
||||
setActiveSurface("core");
|
||||
setActiveManagementPanel("workspace");
|
||||
navigate({ surface: "workspace-management" }, true);
|
||||
}}
|
||||
>
|
||||
Workspace management
|
||||
@@ -996,25 +1039,19 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
size="sm"
|
||||
className="w-full min-w-0 max-w-full gap-[0.25rem] px-[0.75rem]"
|
||||
aria-current={currentNavigation === "pi" ? "page" : undefined}
|
||||
aria-expanded={activeManagementPanel === "pi"}
|
||||
data-navigation-state={currentNavigation === "pi" ? "current" : canManagePi ? "available" : "unavailable"}
|
||||
disabled={!canManagePi}
|
||||
title={canManagePi ? undefined : "Pi management permission is required"}
|
||||
onClick={() => {
|
||||
if (!canLeaveDatabaseManagement()) return;
|
||||
setActiveSurface("core");
|
||||
setActiveManagementPanel("pi");
|
||||
navigate({ surface: "pi-management" }, true);
|
||||
}}
|
||||
>
|
||||
Pi management
|
||||
</Button>
|
||||
<WorkspacePreprocessingControl
|
||||
workspaceId={selectedWorkspaceId}
|
||||
status={preprocessingQuery.data}
|
||||
loading={preprocessingQuery.isLoading}
|
||||
unavailable={preprocessingQuery.isError}
|
||||
canManage={canManageDatabase}
|
||||
/>
|
||||
<Button variant="ghost" size="sm" className="w-full min-w-0 whitespace-normal" onClick={() => navigate({ surface: "workspace-management", workspace: selectedWorkspaceId })}>
|
||||
Workspace readiness: {preprocessingQuery.isError ? "unavailable" : preprocessingQuery.data?.state ?? "checking"}
|
||||
</Button>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion.Root>
|
||||
@@ -1241,6 +1278,9 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AdministrationNavigationProvider>
|
||||
</WorkingContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user