feat: complete catalog-driven preprocessing
Publish documentation / publish (push) Successful in 2m12s
Publish documentation / publish (push) Successful in 2m12s
This commit is contained in:
@@ -17,6 +17,7 @@ import { StopConfirmDialog } from "./StopConfirmDialog";
|
||||
import { SteerInput, ComposerFooter } from "./SteerInput";
|
||||
import { WorkflowBar } from "./WorkflowBar";
|
||||
import { DatabaseManagementPage } from "./DatabaseManagementPage";
|
||||
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,6 +38,9 @@ import { 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 { getWorkspacePreprocessingStatus } from "../api/workspace-preprocessing";
|
||||
|
||||
interface AppShellProps {
|
||||
canLogout: boolean;
|
||||
@@ -45,23 +49,6 @@ interface AppShellProps {
|
||||
type ActiveSurface = "core" | "database-management";
|
||||
type ActiveManagementPanel = "workspace" | "pi" | null;
|
||||
const SESSION_SCOPES: readonly SessionScope[] = ["mine", "all"];
|
||||
const APP_NAVIGATION_STORAGE_KEY = "thothii:app-navigation";
|
||||
|
||||
function readPersistedNavigation(): { surface: ActiveSurface; panel: ActiveManagementPanel } {
|
||||
if (import.meta.env.MODE === "test") return { surface: "core", panel: null };
|
||||
try {
|
||||
const parsed = JSON.parse(window.localStorage.getItem(APP_NAVIGATION_STORAGE_KEY) ?? "null") as {
|
||||
surface?: unknown;
|
||||
panel?: unknown;
|
||||
} | null;
|
||||
return {
|
||||
surface: parsed?.surface === "database-management" ? "database-management" : "core",
|
||||
panel: parsed?.panel === "workspace" || parsed?.panel === "pi" ? parsed.panel : null,
|
||||
};
|
||||
} catch {
|
||||
return { surface: "core", panel: null };
|
||||
}
|
||||
}
|
||||
|
||||
function sessionScopeTabClass(selected: boolean): string {
|
||||
const base = "relative -mb-px flex h-8 w-full cursor-pointer items-center justify-center rounded-t-md border border-b-2 px-2 text-xs font-semibold tracking-[0.005em] outline-none focus-visible:z-10 focus-visible:ring-3 focus-visible:ring-[oklch(var(--nav-active-border)/0.28)]";
|
||||
@@ -168,6 +155,33 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
const canManageDatabase = permissions.includes("database.manage");
|
||||
const canManagePi = permissions.includes("pi.manage");
|
||||
const authGeneration = useAuthGeneration();
|
||||
const { data: installationSettings } = useQuery({
|
||||
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 preprocessingQuery = useQuery({
|
||||
queryKey: ["workspace-preprocessing", selectedWorkspaceId],
|
||||
queryFn: () => getWorkspacePreprocessingStatus(selectedWorkspaceId!),
|
||||
enabled: Boolean(authenticatedUser && selectedWorkspaceId),
|
||||
refetchInterval: (query) => query.state.data?.state === "running" ? 1_000 : 5_000,
|
||||
});
|
||||
const preprocessingBlocksNewSession = Boolean(
|
||||
selectedWorkspaceId
|
||||
&& preprocessingQuery.data
|
||||
&& preprocessingQuery.data.state !== "ready",
|
||||
);
|
||||
const { data: sessions = [] } = useQuery<SessionSummary[]>({
|
||||
queryKey: ["sessions", sessionScope],
|
||||
queryFn: async () => {
|
||||
@@ -183,8 +197,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const [showActivity, setShowActivity] = useState(false);
|
||||
const persistedNavigation = readPersistedNavigation();
|
||||
const [activeSurface, setActiveSurface] = useState<ActiveSurface>(persistedNavigation.surface);
|
||||
const [activeSurface, setActiveSurface] = useState<ActiveSurface>("core");
|
||||
const databaseManagementPresentation = resolveDatabaseManagementPresentation({
|
||||
isDevelopment: import.meta.env.DEV,
|
||||
mode: import.meta.env.MODE,
|
||||
@@ -195,7 +208,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
const updateDatabaseNavigationState = useCallback((state: { dirty: boolean; busy: boolean }) => {
|
||||
databaseNavigationRef.current = state;
|
||||
}, []);
|
||||
const [activeManagementPanel, setActiveManagementPanel] = useState<ActiveManagementPanel>(persistedNavigation.panel);
|
||||
const [activeManagementPanel, setActiveManagementPanel] = useState<ActiveManagementPanel>(null);
|
||||
const [adminNavigationValue, setAdminNavigationValue] = useState<string[]>([]);
|
||||
const [activeOpen, setActiveOpen] = useState(true);
|
||||
const [archiveOpen, setArchiveOpen] = useState(false);
|
||||
@@ -206,10 +219,6 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
|
||||
const [renameGroupTarget, setRenameGroupTarget] = useState<string | null>(null);
|
||||
const operationEpochRef = useRef(0);
|
||||
useEffect(() => {
|
||||
if (import.meta.env.MODE === "test") return;
|
||||
window.localStorage.setItem(APP_NAVIGATION_STORAGE_KEY, JSON.stringify({ surface: activeSurface, panel: activeManagementPanel }));
|
||||
}, [activeManagementPanel, activeSurface]);
|
||||
useEffect(() => () => { operationEpochRef.current += 1; }, []);
|
||||
|
||||
const groups = useMemo(
|
||||
@@ -610,6 +619,10 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
}, [lastSystemEvent]);
|
||||
|
||||
function startNewSession() {
|
||||
if (preprocessingBlocksNewSession) {
|
||||
toast.warning(preprocessingQuery.data?.detail ?? "Workspace preprocessing is required.");
|
||||
return;
|
||||
}
|
||||
if (!canLeaveDatabaseManagement()) return;
|
||||
setActiveSurface("core");
|
||||
setActiveManagementPanel(null);
|
||||
@@ -841,7 +854,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
|
||||
{/* Right session rail */}
|
||||
{(activeSurface === "database-management" || !showActivity) && (
|
||||
<aside aria-label="Session navigation" className="flex w-[15vw] shrink-0 flex-col bg-sidebar">
|
||||
<aside aria-label="Session navigation" className="flex w-64 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>
|
||||
@@ -871,7 +884,11 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
size="sm"
|
||||
className="w-full"
|
||||
aria-current={currentNavigation === "core" ? "page" : undefined}
|
||||
data-navigation-state={currentNavigation === "core" ? "current" : "available"}
|
||||
data-navigation-state={preprocessingBlocksNewSession
|
||||
? "unavailable"
|
||||
: currentNavigation === "core" ? "current" : "available"}
|
||||
disabled={preprocessingBlocksNewSession}
|
||||
title={preprocessingBlocksNewSession ? preprocessingQuery.data?.detail : undefined}
|
||||
onClick={startNewSession}
|
||||
>
|
||||
New session
|
||||
@@ -949,6 +966,13 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
>
|
||||
Pi management
|
||||
</Button>
|
||||
<WorkspacePreprocessingControl
|
||||
workspaceId={selectedWorkspaceId}
|
||||
status={preprocessingQuery.data}
|
||||
loading={preprocessingQuery.isLoading}
|
||||
unavailable={preprocessingQuery.isError}
|
||||
canManage={canManageDatabase}
|
||||
/>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion.Root>
|
||||
|
||||
Reference in New Issue
Block a user