feat: implement metadata catalog database management

This commit is contained in:
Codex
2026-08-27 22:43:54 +02:00
parent 705af3aeb2
commit 79c4c925b5
86 changed files with 12566 additions and 135 deletions
+49 -17
View File
@@ -32,7 +32,7 @@ import {
import type { SessionScope, SessionSummary } from "../api/types";
import { useAuthGeneration, useAuthUser } from "../auth/authState";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { CSSProperties } from "react";
import { captureAuthOperation, isAuthOperationCurrent, StaleAuthOperationError, type AuthOperationGuard } from "../auth/authOperation";
@@ -104,8 +104,8 @@ export function AppShell({ canLogout }: AppShellProps) {
const canReadAllSessions = permissions.includes("session.read_all");
const canManageWorkspace = permissions.includes("workspace.manage");
const canManageWorkspaceSecrets = permissions.includes("workspace.secrets.manage");
const canManageDatabase = permissions.includes("database.manage");
const canManagePi = permissions.includes("pi.manage");
const isAdmin = authenticatedUser?.isAdmin === true;
const authGeneration = useAuthGeneration();
const { data: sessions = [] } = useQuery<SessionSummary[]>({
queryKey: ["sessions", sessionScope],
@@ -123,6 +123,10 @@ export function AppShell({ canLogout }: AppShellProps) {
const queryClient = useQueryClient();
const [showActivity, setShowActivity] = useState(false);
const [activeSurface, setActiveSurface] = useState<ActiveSurface>("core");
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 [activeOpen, setActiveOpen] = useState(true);
@@ -197,7 +201,20 @@ export function AppShell({ canLogout }: AppShellProps) {
setSelectedSessionIds(selected ? new Set(sessions.map((session) => session.id)) : new Set());
}
function canLeaveDatabaseManagement(): boolean {
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 (!canLeaveDatabaseManagement()) return;
const s = sessions.find((x) => x.id === id);
if (!s) return;
setActiveSurface("core");
@@ -207,7 +224,7 @@ export function AppShell({ canLogout }: AppShellProps) {
// completed sessions keep the read-only documents panel (with its explicit Resume),
// so a mere click never spawns a runtime.
if (s.active && s.status === "open" && !s.archived && !isForeignSession(s)) {
void doResume(id);
void doResume(id, true);
return;
}
setPanelSession(s); setShowActivity(false);
@@ -219,7 +236,8 @@ export function AppShell({ canLogout }: AppShellProps) {
return next;
});
}
async function doResume(id: string) {
async function doResume(id: string, databaseExitApproved = false) {
if (!databaseExitApproved && !canLeaveDatabaseManagement()) return;
setActiveSurface("core");
const guard = captureAuthOperation({ sessionId: id, disposalEpoch: operationEpochRef.current });
if (!guard) return;
@@ -502,6 +520,7 @@ export function AppShell({ canLogout }: AppShellProps) {
}, [lastSystemEvent]);
function startNewSession() {
if (!canLeaveDatabaseManagement()) return;
setActiveSurface("core");
invalidateResumeIntent();
newSessionOperationRef.current = null;
@@ -566,6 +585,7 @@ export function AppShell({ canLogout }: AppShellProps) {
}
async function signOut() {
if (!canLeaveDatabaseManagement()) return;
await logoutUser();
}
@@ -619,7 +639,13 @@ export function AppShell({ canLogout }: AppShellProps) {
{/* Conversation column */}
<div data-testid="conversation-column" className="flex min-w-0 flex-1 flex-col">
{activeSurface === "database-management" && <DatabaseManagementPage />}
{activeSurface === "database-management" && (
<DatabaseManagementPage
canManage={canManageDatabase}
canManageSecrets={canManageWorkspaceSecrets}
onNavigationStateChange={updateDatabaseNavigationState}
/>
)}
<div
aria-hidden={activeSurface !== "core"}
className={activeSurface === "core" ? "contents" : "hidden"}
@@ -738,16 +764,19 @@ export function AppShell({ canLogout }: AppShellProps) {
>
New session
</Button>
{isAdmin && (
<>
<Button
variant="outline"
size="sm"
<Button
variant="outline"
size="sm"
className="w-full"
onClick={() => setWorkspaceManagerOpen(true)}
>
Workspace management
</Button>
onClick={() => {
if (!canLeaveDatabaseManagement()) return;
setActiveSurface("core");
setWorkspaceManagerOpen(true);
}}
>
Workspace management
</Button>
{canManageDatabase && (
<Button
variant="outline"
size="sm"
@@ -757,14 +786,17 @@ export function AppShell({ canLogout }: AppShellProps) {
>
Database management
</Button>
</>
)}
{isAdmin && canManagePi && (
{canManagePi && (
<Button
variant="outline"
size="sm"
className="w-full"
onClick={() => setPiManagementOpen(true)}
onClick={() => {
if (!canLeaveDatabaseManagement()) return;
setActiveSurface("core");
setPiManagementOpen(true);
}}
>
Pi management
</Button>