This commit is contained in:
@@ -439,12 +439,12 @@ test("context panels stay inside the manager and the Tables grid sits in a sideb
|
||||
|
||||
await administration.click();
|
||||
await expect(administration).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(adminNavigationRail.getByRole("button", { name: "Database management", exact: true })).toHaveCount(0);
|
||||
await expect(adminNavigationRail.getByRole("button", { name: "Database", exact: true })).toHaveCount(0);
|
||||
await administration.click();
|
||||
await expect(administration).toHaveAttribute("aria-expanded", "true");
|
||||
|
||||
await adminNavigationRail
|
||||
.getByRole("button", { name: "Database management", exact: true })
|
||||
.getByRole("button", { name: "Database", exact: true })
|
||||
.click();
|
||||
await expectCurrentNavigation(page, "Database management");
|
||||
|
||||
@@ -795,7 +795,7 @@ test("Workspace and Pi management share the centered work-area panel without cov
|
||||
await expectWorkAreaPanelGeometry(page, "Workspace management");
|
||||
await expectWorkAreaPanelHeaderUsesPrimary(page, "Workspace management");
|
||||
|
||||
const piTrigger = page.getByRole("button", { name: "Pi management", exact: true });
|
||||
const piTrigger = page.getByRole("button", { name: "Pi configuration", exact: true });
|
||||
await piTrigger.click();
|
||||
await expectCurrentNavigation(page, "Pi management");
|
||||
await expect(
|
||||
@@ -812,7 +812,7 @@ test("Workspace and Pi management share the centered work-area panel without cov
|
||||
}
|
||||
|
||||
await page.setViewportSize({ width: 720, height: 800 });
|
||||
await page.getByRole("button", { name: "Workspace management", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Workspace", exact: true }).click();
|
||||
await expectCurrentNavigation(page, "Workspace management");
|
||||
const compactWorkArea = page.getByTestId("conversation-column");
|
||||
const compactPanel = compactWorkArea.getByRole("dialog", { name: "Workspace management" });
|
||||
|
||||
@@ -112,7 +112,7 @@ LocalEvidenceArchive(root).initialize()
|
||||
}).allTextContents()).map(text => text.trim())).toEqual([
|
||||
"Database management", "Memory management", "Evidence management",
|
||||
]);
|
||||
await page.getByRole("button", { name: "Memory management", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Memory", exact: true }).click();
|
||||
await page.screenshot({ path: "/private/tmp/thothii-m1-memory-opening.png", fullPage: true });
|
||||
const memory = page.getByRole("main", { name: "Memory management" });
|
||||
await expect(memory).toBeVisible();
|
||||
@@ -157,9 +157,9 @@ finally: service.close()
|
||||
THT_PRINCIPAL_ISSUER: "test", THT_PRINCIPAL_SUBJECT: "admin", THT_PRINCIPAL_IS_ADMIN: "true" });
|
||||
expect(JSON.parse(recalled)).toEqual([]);
|
||||
await expect(page.getByRole("button", { name: "New card", exact: true })).toBeEnabled();
|
||||
if (!await page.getByRole("button", { name: "Evidence management", exact: true }).isVisible())
|
||||
if (!await page.getByRole("button", { name: "Evidence", exact: true }).isVisible())
|
||||
await page.getByRole("button", { name: "Administration", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Evidence management", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Evidence", exact: true }).click();
|
||||
const evidence = page.getByRole("main", { name: "Evidence management" });
|
||||
await expect(evidence).toBeVisible();
|
||||
await evidence.getByRole("combobox", { name: /Workspace/ }).selectOption("fixture-workspace");
|
||||
@@ -179,12 +179,12 @@ finally: service.close()
|
||||
await page.screenshot({ path: "/private/tmp/thothii-acceptance-evidence-mobile.png", fullPage: true });
|
||||
await page.getByRole("button", { name: "Navigation", exact: true }).click();
|
||||
const navigation = page.getByRole("dialog", { name: "Navigation", exact: true });
|
||||
await expect(navigation.getByRole("button", { name: "Memory management", exact: true })).toBeVisible();
|
||||
await expect(navigation.getByRole("button", { name: "Memory", exact: true })).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(navigation).toBeHidden();
|
||||
await expect(page.getByRole("button", { name: "Navigation", exact: true })).toBeFocused();
|
||||
await page.getByRole("button", { name: "Navigation", exact: true }).click();
|
||||
await navigation.getByRole("button", { name: "Memory management", exact: true }).click();
|
||||
await navigation.getByRole("button", { name: "Memory", exact: true }).click();
|
||||
await expect(navigation).toBeHidden();
|
||||
const mobileMemory = page.getByRole("main", { name: "Memory management" });
|
||||
await expect(mobileMemory).toBeVisible();
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface PiInstallationConfig {
|
||||
}
|
||||
|
||||
export interface PiManagementStatus {
|
||||
hostPlatform?: "linux" | "macos" | "windows";
|
||||
version?: string;
|
||||
ready: boolean;
|
||||
credentials: "present" | "missing";
|
||||
|
||||
@@ -39,6 +39,17 @@ beforeEach(() => {
|
||||
});
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
test("uses short Admin buttons while keeping management in page titles", async () => {
|
||||
renderShell();
|
||||
await screen.findByRole("textbox", { name: /new question/i });
|
||||
await userEvent.click(screen.getByRole("button", { name: "Administration" }));
|
||||
for (const [label, title] of [["Workspace", "Workspace"], ["Evidence", "Evidence"], ["Memory", "Memory"], ["Database", "Database"], ["Pi configuration", "Pi"]]) {
|
||||
await userEvent.click(screen.getByRole("button", { name: label }));
|
||||
expect(await screen.findByRole("main", { name: `${title} management` })).toBeVisible();
|
||||
expect(screen.queryByRole("button", { name: `${title} management` })).not.toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
test.each(["Workspace", "Evidence", "Memory", "Database", "Pi"])("reloads the %s route as a full page", async name => {
|
||||
window.history.replaceState({ portal: "kept" }, "", `/portal?lang=it&thoth_route=administration/${name.toLowerCase()}#host`);
|
||||
renderShell();
|
||||
@@ -52,8 +63,8 @@ test("preserves host state and query parameters through Back and Forward", async
|
||||
window.history.replaceState({ portal: { view: "thoth" } }, "", "/portal?lang=it#host");
|
||||
renderShell();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Administration" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Workspace management" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Pi management" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Workspace" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Pi configuration" }));
|
||||
act(() => window.history.back());
|
||||
expect(await screen.findByRole("main", { name: "Workspace management" })).toBeVisible();
|
||||
act(() => window.history.forward());
|
||||
@@ -75,8 +86,8 @@ test.each(["workspace", "database", "memory", "evidence", "pi"])("denies a direc
|
||||
test("Back and Core navigation preserve a Memory draft until the editor cancels it", async () => {
|
||||
renderShell();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Administration" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Workspace management" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Memory management" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Workspace" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Memory" }));
|
||||
const page = await screen.findByRole("main", { name: "Memory management" });
|
||||
await userEvent.click(await within(page).findByRole("button", { name: "New card" }));
|
||||
await userEvent.type(within(page).getByLabelText("Title"), "Keep this draft");
|
||||
@@ -122,7 +133,7 @@ test("option A has one collapsible global context and preserves the Core draft a
|
||||
await userEvent.click(screen.getByRole("button", { name: "Done" }));
|
||||
expect(screen.queryByRole("combobox", { name: "Model" })).not.toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Administration" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Memory management" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Memory" }));
|
||||
expect(await screen.findByRole("main", { name: "Memory management" })).toBeVisible();
|
||||
expect(screen.queryByRole("textbox", { name: /new question/i })).not.toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Return to session" }));
|
||||
|
||||
@@ -52,7 +52,7 @@ describe("authenticated shell permissions", () => {
|
||||
renderShell({ subject: "admin", isAdmin: true, roles: ["admin"],
|
||||
permissions: ["session.use", "memory.manage", "database.manage"] });
|
||||
await userEvent.click(await screen.findByRole("button", { name: "Administration" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Memory management" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Memory" }));
|
||||
const memory = screen.getByRole("main", { name: "Memory management" });
|
||||
expect(memory).toBeVisible();
|
||||
expect(within(memory).queryByRole("combobox", { name: "Workspace" })).not.toBeInTheDocument();
|
||||
@@ -111,9 +111,9 @@ describe("authenticated shell permissions", () => {
|
||||
|
||||
expect(await screen.findByRole("button", { name: "New session" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Administration" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Database management" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Workspace management" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Pi management" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Database" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Workspace" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Pi configuration" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("tab", { name: "All sessions" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -137,12 +137,12 @@ describe("authenticated shell permissions", () => {
|
||||
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
||||
|
||||
const panel = within(sessionNavigation).getByRole("region", { name: "Administration" });
|
||||
const database = within(panel).getByRole("button", { name: "Database management" });
|
||||
const memory = within(panel).getByRole("button", { name: "Memory management" });
|
||||
const evidence = within(panel).getByRole("button", { name: "Evidence management" });
|
||||
const database = within(panel).getByRole("button", { name: "Database" });
|
||||
const memory = within(panel).getByRole("button", { name: "Memory" });
|
||||
const evidence = within(panel).getByRole("button", { name: "Evidence" });
|
||||
const separator = within(panel).getByRole("separator");
|
||||
const workspace = within(panel).getByRole("button", { name: "Workspace management" });
|
||||
const pi = within(panel).getByRole("button", { name: "Pi management" });
|
||||
const workspace = within(panel).getByRole("button", { name: "Workspace" });
|
||||
const pi = within(panel).getByRole("button", { name: "Pi configuration" });
|
||||
expect(panel.children[0]).toBe(database);
|
||||
expect(panel.children[1]).toBe(memory);
|
||||
expect(panel.children[2]).toBe(evidence);
|
||||
@@ -156,7 +156,7 @@ describe("authenticated shell permissions", () => {
|
||||
await user.keyboard(" ");
|
||||
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
||||
expect(within(sessionNavigation).queryByRole("region", { name: "Administration" })).not.toBeInTheDocument();
|
||||
expect(within(sessionNavigation).queryByRole("button", { name: "Database management" })).not.toBeInTheDocument();
|
||||
expect(within(sessionNavigation).queryByRole("button", { name: "Database" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("keeps identity but hides logout outside local authentication", async () => {
|
||||
@@ -277,8 +277,8 @@ describe("authenticated shell permissions", () => {
|
||||
|
||||
act(() => clearAuthState());
|
||||
expect(screen.queryByRole("button", { name: "Administration" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Workspace management" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Pi management" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Workspace" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Pi configuration" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("tab", { name: "All sessions" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ test("starts on the core activity after a hard refresh even if metadata manageme
|
||||
const sessionNavigation = screen.getByRole("complementary", { name: "Session navigation" });
|
||||
expect(within(sessionNavigation).getByRole("button", { name: "New session" }))
|
||||
.toHaveAttribute("aria-current", "page");
|
||||
expect(within(sessionNavigation).getByRole("button", { name: "Database management" }))
|
||||
expect(within(sessionNavigation).getByRole("button", { name: "Database" }))
|
||||
.not.toHaveAttribute("aria-current");
|
||||
expect(screen.queryByRole("main", { name: "Database management" })).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -119,7 +119,7 @@ test("replaces the core conversation while keeping the session navigation", asyn
|
||||
const conversationColumn = screen.getByTestId("conversation-column");
|
||||
const sessionNavigation = screen.getByRole("complementary", { name: "Session navigation" });
|
||||
const newSession = within(sessionNavigation).getByRole("button", { name: "New session" });
|
||||
const databaseManagement = within(sessionNavigation).getByRole("button", { name: "Database management" });
|
||||
const databaseManagement = within(sessionNavigation).getByRole("button", { name: "Database" });
|
||||
|
||||
expect(newSession).toHaveAttribute("aria-current", "page");
|
||||
expect(newSession).toHaveAttribute("data-navigation-state", "current");
|
||||
@@ -178,7 +178,7 @@ test("keeps a live core session connected while returning from database manageme
|
||||
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
|
||||
const source = FakeEventSource.instances[0];
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Database management" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Database" }));
|
||||
|
||||
expect(screen.getByRole("main", { name: "Database management" })).toBeVisible();
|
||||
expect(source.closed).toBe(false);
|
||||
@@ -210,9 +210,9 @@ test("hides the complete administrative navigation from a regular user", () => {
|
||||
|
||||
expect(screen.getByRole("button", { name: "New session" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Administration" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Workspace management" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Database management" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Pi management" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Workspace" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Database" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Pi configuration" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("requires saving or cancelling a dirty database form before leaving", async () => {
|
||||
@@ -250,7 +250,8 @@ test("requires saving or cancelling a dirty database form before leaving", async
|
||||
renderShell();
|
||||
await expandAdministration();
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Database management" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Database" }));
|
||||
await userEvent.click(await screen.findByRole("button", { name: "Edit Policlinico San Donato" }));
|
||||
const schema = await screen.findByLabelText("Schema");
|
||||
await userEvent.clear(schema);
|
||||
await userEvent.type(schema, "reporting");
|
||||
|
||||
@@ -204,13 +204,13 @@ test("opens Workspace management from the right sidebar without interrupting the
|
||||
renderShell();
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Administration" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Workspace management" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Workspace" }));
|
||||
|
||||
expect(await screen.findByRole("heading", { name: "Workspace management" })).toBeVisible();
|
||||
const dialog = screen.getByRole("main", { name: "Workspace management" });
|
||||
const workArea = screen.getByTestId("conversation-column");
|
||||
const sessionNavigation = screen.getByRole("complementary", { name: "Session navigation" });
|
||||
const workspaceManagement = within(sessionNavigation).getByRole("button", { name: "Workspace management" });
|
||||
const workspaceManagement = within(sessionNavigation).getByRole("button", { name: "Workspace" });
|
||||
const newSession = within(sessionNavigation).getByRole("button", { name: "New session" });
|
||||
expect(workArea).toContainElement(dialog);
|
||||
expect(sessionNavigation).not.toContainElement(dialog);
|
||||
|
||||
@@ -122,12 +122,12 @@ test("Pi management preserves the open session summary and the model activity ti
|
||||
await user.click(await screen.findByText("Attiva uno"));
|
||||
expect(await screen.findByRole("complementary", { name: "Session summary" })).toHaveTextContent("Domanda originale");
|
||||
await user.click(screen.getByRole("button", { name: "Administration" }));
|
||||
await user.click(screen.getByRole("button", { name: "Pi management" }));
|
||||
await user.click(screen.getByRole("button", { name: "Pi configuration" }));
|
||||
expect(await screen.findByRole("heading", { name: "Pi management" })).toBeVisible();
|
||||
const dialog = screen.getByRole("main", { name: "Pi management" });
|
||||
const workArea = screen.getByTestId("conversation-column");
|
||||
const sessionNavigation = screen.getByRole("complementary", { name: "Session navigation" });
|
||||
const piManagement = within(sessionNavigation).getByRole("button", { name: "Pi management" });
|
||||
const piManagement = within(sessionNavigation).getByRole("button", { name: "Pi configuration" });
|
||||
const newSession = within(sessionNavigation).getByRole("button", { name: "New session" });
|
||||
expect(workArea).toContainElement(dialog);
|
||||
expect(sessionNavigation).not.toContainElement(dialog);
|
||||
@@ -151,7 +151,7 @@ test("Pi management preserves the open session summary and the model activity ti
|
||||
await user.click(screen.getByRole("button", { name: "Resume" }));
|
||||
await screen.findByRole("button", { name: "Show model activity" });
|
||||
act(() => useSessionStore.getState().setLastUserEntry({ kind: "input", text: "Preserved activity" }));
|
||||
await user.click(screen.getByRole("button", { name: "Pi management" }));
|
||||
await user.click(screen.getByRole("button", { name: "Pi configuration" }));
|
||||
await user.click(await screen.findByRole("button", { name: "Return to session" }));
|
||||
await waitFor(() => expect(screen.queryByRole("heading", { name: "Pi management" })).not.toBeInTheDocument());
|
||||
await user.click(screen.getByRole("button", { name: "Show model activity" }));
|
||||
|
||||
@@ -999,7 +999,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
navigate({ surface: "database-management" }, true);
|
||||
}}
|
||||
>
|
||||
Database management
|
||||
Database
|
||||
</Button>
|
||||
<Button
|
||||
variant={currentNavigation === "memory" ? "navigationActive" : "outline"}
|
||||
@@ -1013,13 +1013,13 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
if (!canLeaveDatabaseManagement()) return;
|
||||
navigate({ surface: "memory-management" }, true);
|
||||
}}
|
||||
>Memory management</Button>
|
||||
>Memory</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={() => navigate({ surface: "evidence-management" })}
|
||||
>Evidence management</Button>
|
||||
>Evidence</Button>
|
||||
<div role="separator" aria-orientation="horizontal" className="mx-1 h-px bg-border/90" />
|
||||
<Button
|
||||
variant={currentNavigation === "workspace" ? "navigationActive" : "outline"}
|
||||
@@ -1032,7 +1032,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
navigate({ surface: "workspace-management" }, true);
|
||||
}}
|
||||
>
|
||||
Workspace management
|
||||
Workspace
|
||||
</Button>
|
||||
<Button
|
||||
variant={currentNavigation === "pi" ? "navigationActive" : "outline"}
|
||||
@@ -1047,7 +1047,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
||||
navigate({ surface: "pi-management" }, true);
|
||||
}}
|
||||
>
|
||||
Pi management
|
||||
Pi configuration
|
||||
</Button>
|
||||
<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"}
|
||||
|
||||
@@ -90,12 +90,14 @@ function renderPage({
|
||||
canManageSecrets = true,
|
||||
onNavigationStateChange,
|
||||
presentation = "legacy",
|
||||
initialWorkspaceId,
|
||||
}: {
|
||||
rows?: CatalogDatabase[] | (() => CatalogDatabase[]);
|
||||
canManage?: boolean;
|
||||
canManageSecrets?: boolean;
|
||||
onNavigationStateChange?: (state: { dirty: boolean; busy: boolean }) => void;
|
||||
presentation?: "legacy" | "fleet";
|
||||
initialWorkspaceId?: string;
|
||||
} = {}) {
|
||||
server.use(http.get("/api/catalog/databases", () => HttpResponse.json(
|
||||
typeof rows === "function" ? rows() : rows,
|
||||
@@ -104,6 +106,7 @@ function renderPage({
|
||||
const view = render(
|
||||
<QueryClientProvider client={client}>
|
||||
<DatabaseManagementPage
|
||||
initialWorkspaceId={initialWorkspaceId}
|
||||
canManage={canManage}
|
||||
canManageSecrets={canManageSecrets}
|
||||
onNavigationStateChange={onNavigationStateChange}
|
||||
@@ -115,6 +118,15 @@ function renderPage({
|
||||
return { ...view, client };
|
||||
}
|
||||
|
||||
test("fleet entry opens the database list without workspace preparation or an automatic editor", async () => {
|
||||
renderPage({ presentation: "fleet", initialWorkspaceId: "psd-clinical" });
|
||||
expect(await screen.findByRole("button", { name: "View tables for Policlinico San Donato" })).toBeVisible();
|
||||
expect(screen.getByText("Research laboratory")).toBeVisible();
|
||||
expect(screen.queryByRole("button", { name: "Back to list" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/Workspace preparation/)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("link", { name: "Open workspace preprocessing" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
function makeSyncRun(
|
||||
scope: CatalogSyncRun["scope"],
|
||||
tableIds: string[] = [],
|
||||
|
||||
@@ -10,7 +10,6 @@ import { CheckCircle2, CircleAlert, History, RefreshCw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { workspacePreferences } from "../workspaces/preferences";
|
||||
import { useInteractionModelBusy } from "../models/useInteractionModelBusy";
|
||||
import { WorkspacePreparation } from "./administration/WorkspacePreparation";
|
||||
import { Button } from "../components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -161,7 +160,6 @@ function isDescriptionGenerationActive(run?: DescriptionGenerationRun | null): b
|
||||
}
|
||||
|
||||
export function DatabaseManagementPage({
|
||||
initialWorkspaceId,
|
||||
canManage,
|
||||
canManageSecrets,
|
||||
onNavigationStateChange,
|
||||
@@ -411,15 +409,6 @@ export function DatabaseManagementPage({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const initialWorkspaceOpened = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!initialWorkspaceId || initialWorkspaceOpened.current || !data) return;
|
||||
initialWorkspaceOpened.current = true;
|
||||
const row = data.find(item => item.workspaceId === initialWorkspaceId);
|
||||
if (row) openForm(row.configured ? "edit" : "configure", row, document.body);
|
||||
else toast.info("This workspace's database configuration is unavailable");
|
||||
}, [initialWorkspaceId, data, openForm]);
|
||||
|
||||
const viewRow = useCallback((row: CatalogDatabase, origin: HTMLButtonElement) => {
|
||||
openForm("view", row, origin);
|
||||
}, [openForm]);
|
||||
@@ -1224,7 +1213,6 @@ export function DatabaseManagementPage({
|
||||
onOpenSensitiveHistory={openSensitivityReviewItemHistory}
|
||||
activeSyncRun={currentActiveRun}
|
||||
/>
|
||||
<WorkspacePreparation workspaceId={activeRow.workspaceId} canManage={canManage} databaseSummary />
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
@@ -1403,8 +1391,6 @@ export function DatabaseManagementPage({
|
||||
)}
|
||||
children={<>
|
||||
{fleetDrawer ?? <FleetLedgerContent busy={isLoading || isFetching}>{fleetContent}</FleetLedgerContent>}
|
||||
{!fleetDrawer && (activeRow ?? rows.find(row => row.id === selectedDatabaseId)) &&
|
||||
<WorkspacePreparation workspaceId={(activeRow ?? rows.find(row => row.id === selectedDatabaseId))!.workspaceId} canManage={canManage} databaseSummary />}
|
||||
</>}
|
||||
drawer={(
|
||||
<>
|
||||
|
||||
@@ -13,11 +13,11 @@ const readyStatus = {
|
||||
checkedAt: "2026-08-05T10:00:00.000Z",
|
||||
};
|
||||
|
||||
function renderManagement() {
|
||||
function renderManagement(page = false) {
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
return render(
|
||||
<QueryClientProvider client={client}>
|
||||
<PiManagement open onClose={() => undefined} />
|
||||
<PiManagement open page={page} onClose={() => undefined} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
@@ -52,6 +52,20 @@ test("shows the catalog default as read-only installation state", async () => {
|
||||
expect(document.querySelector('input[type="password"]')).toBeNull();
|
||||
});
|
||||
|
||||
test.each(["linux", "macos", "windows"])("opens host instructions for the installation platform %s", async hostPlatform => {
|
||||
server.use(http.get("/api/pi-management/status", () => HttpResponse.json({ ...readyStatus, hostPlatform })));
|
||||
renderManagement(true);
|
||||
await screen.findByText("Pi 0.80.3");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Host maintenance" }));
|
||||
const label = hostPlatform === "macos" ? "macOS" : hostPlatform === "windows" ? "Windows" : "Linux";
|
||||
expect(screen.getByRole("tab", { name: label })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByRole("tabpanel", { name: label })).toBeVisible();
|
||||
await userEvent.click(screen.getByRole("tab", { name: "Windows" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Runtime and models" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Host maintenance" }));
|
||||
expect(screen.getByRole("tabpanel", { name: "Windows" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows every session model exposed by Core", async () => {
|
||||
server.use(http.get("/api/models", () => HttpResponse.json({ models: [
|
||||
{ provider: "zai", id: "glm-5.3", name: "GLM 5.3", reasoning: true },
|
||||
|
||||
@@ -202,13 +202,12 @@ function PiInstructionSteps({ details }: { details: PiPlatformDetails }) {
|
||||
</ol>;
|
||||
}
|
||||
|
||||
function PiPlatformInstructions() {
|
||||
const [activePlatform, setActivePlatform] = useState<PiPlatform>();
|
||||
const [focusedPlatform, setFocusedPlatform] = useState<PiPlatform>("linux");
|
||||
function PiPlatformInstructions({ hostPlatform }: { hostPlatform?: PiPlatform }) {
|
||||
const [chosenPlatform, setActivePlatform] = useState<PiPlatform>();
|
||||
const activePlatform = chosenPlatform ?? hostPlatform ?? "linux";
|
||||
const tabRefs = useRef<Record<PiPlatform, HTMLButtonElement | null>>({ linux: null, macos: null, windows: null });
|
||||
|
||||
function activateAndFocus(platform: PiPlatform) {
|
||||
setFocusedPlatform(platform);
|
||||
setActivePlatform(platform);
|
||||
tabRefs.current[platform]?.focus();
|
||||
}
|
||||
@@ -242,13 +241,11 @@ function PiPlatformInstructions() {
|
||||
role="tab"
|
||||
aria-selected={activePlatform === platform.id}
|
||||
aria-controls={`pi-${platform.id}-panel`}
|
||||
tabIndex={focusedPlatform === platform.id ? 0 : -1}
|
||||
tabIndex={activePlatform === platform.id ? 0 : -1}
|
||||
className={activePlatform === platform.id ? "rounded bg-background px-2 py-1.5 font-semibold text-foreground shadow-sm" : "rounded px-2 py-1.5 font-semibold text-muted-foreground hover:bg-background/70 hover:text-foreground"}
|
||||
onFocus={() => setFocusedPlatform(platform.id)}
|
||||
onKeyDown={(event) => handleTabKeyDown(event, platform.id)}
|
||||
onClick={() => {
|
||||
setFocusedPlatform(platform.id);
|
||||
setActivePlatform((current) => current === platform.id ? undefined : platform.id);
|
||||
setActivePlatform(platform.id);
|
||||
}}
|
||||
>
|
||||
{platform.label}
|
||||
@@ -348,7 +345,9 @@ export function PiManagement({ open, onClose, page = false }: { open: boolean; o
|
||||
<Button variant={section === "instructions" ? "navigationActive" : "ghost"} className="w-full justify-start whitespace-normal" aria-current={section === "instructions" ? "page" : undefined} onClick={() => setSection("instructions")}>Host maintenance</Button>
|
||||
</nav>}
|
||||
<div className={page ? "thot-pi-detail" : undefined}>
|
||||
{(!page || section === "instructions") && <PiPlatformInstructions />}
|
||||
<div hidden={page && section !== "instructions"}>
|
||||
<PiPlatformInstructions hostPlatform={statusQuery.data?.hostPlatform} />
|
||||
</div>
|
||||
<div className={page ? undefined : "mt-5"} hidden={page && section !== "runtime"}>
|
||||
{forbidden ? (
|
||||
<div role="alert" aria-label="Pi management unavailable" className="rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm">
|
||||
|
||||
@@ -10,6 +10,30 @@
|
||||
border-inline-end: 1px solid oklch(var(--border));
|
||||
}
|
||||
|
||||
.thot-workspace-area-tabs {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
overflow-x: auto;
|
||||
border-bottom: 1px solid oklch(var(--border));
|
||||
}
|
||||
.thot-workspace-area-tabs [role="tab"] {
|
||||
flex: none;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: oklch(var(--muted-foreground));
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.thot-workspace-area-tabs [aria-selected="true"] {
|
||||
border-bottom-color: oklch(var(--primary));
|
||||
color: oklch(var(--primary));
|
||||
background: oklch(var(--muted) / 0.4);
|
||||
}
|
||||
.thot-workspace-area-tabs [role="tab"]:focus-visible {
|
||||
outline: 2px solid oklch(var(--ring));
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
@container work-area-panel (max-width: 45rem) {
|
||||
.thot-workspace-manager__layout {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
|
||||
@@ -136,6 +136,28 @@ test("defaults workspace mutations and secrets to denied", async () => {
|
||||
expect(screen.queryByRole("heading", { name: "Runtime secrets" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("page tabs group workspace areas and preserve unsaved secrets across tabs", async () => {
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}>
|
||||
<WorkspaceManager open page initialWorkspaceId="psd-clinical" onClose={vi.fn()} canManageWorkspace canManageSecrets />
|
||||
</QueryClientProvider>);
|
||||
const tabs = await screen.findByRole("tablist", { name: "Workspace areas" });
|
||||
expect(within(tabs).getAllByRole("tab").map(tab => tab.textContent)).toEqual([
|
||||
"Preparation", "Specific actions", "About", "Authentication",
|
||||
]);
|
||||
expect(screen.getByRole("tab", { name: "Preparation" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByRole("tabpanel", { name: "Preparation" })).toBeVisible();
|
||||
await userEvent.click(screen.getByRole("tab", { name: "Specific actions" }));
|
||||
expect(await screen.findByRole("button", { name: "Validate workspace source" })).toBeVisible();
|
||||
await userEvent.click(screen.getByRole("tab", { name: "Authentication" }));
|
||||
await userEvent.type(await screen.findByLabelText("Data warehouse password"), "unsaved-value");
|
||||
await userEvent.click(screen.getByRole("tab", { name: "About" }));
|
||||
expect(screen.getByRole("heading", { name: "About this workspace" })).toBeVisible();
|
||||
await userEvent.keyboard("{ArrowRight}");
|
||||
expect(screen.getByRole("tab", { name: "Authentication" })).toHaveFocus();
|
||||
expect(screen.getByLabelText("Data warehouse password")).toHaveValue("unsaved-value");
|
||||
});
|
||||
|
||||
test("uses the shared modeless work-area panel and keeps workspace content scrollable", () => {
|
||||
renderManager();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -76,6 +76,8 @@ type WorkspaceOperationGuard = Readonly<{
|
||||
|
||||
import { useWorkingContextScope } from "../workspaces/WorkingContext";
|
||||
|
||||
const workspaceAreas = ["Preparation", "Specific actions", "About", "Authentication"] as const;
|
||||
|
||||
export function WorkspaceManager({
|
||||
open,
|
||||
onClose,
|
||||
@@ -96,6 +98,18 @@ export function WorkspaceManager({
|
||||
canManageSecrets?: boolean;
|
||||
}) {
|
||||
const workingContext = useWorkingContextScope();
|
||||
const tabsId = useId();
|
||||
const [activeArea, setActiveArea] = useState<typeof workspaceAreas[number]>("Preparation");
|
||||
const areaTabs = useRef<Array<HTMLButtonElement | null>>([]);
|
||||
const areaId = (area: typeof workspaceAreas[number]) => `${tabsId}-${workspaceAreas.indexOf(area)}`;
|
||||
const areaPanel = (area: typeof workspaceAreas[number]) => ({
|
||||
role: page ? "tabpanel" : undefined,
|
||||
id: `${areaId(area)}-panel`,
|
||||
"aria-labelledby": page ? `${areaId(area)}-tab` : undefined,
|
||||
hidden: page && activeArea !== area,
|
||||
tabIndex: page ? 0 : undefined,
|
||||
className: "space-y-5",
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
const [selectedId, setSelectedId] = useState<string | undefined>(initialWorkspaceId);
|
||||
const [secretValues, setSecretValues] = useState<Record<string, string>>({});
|
||||
@@ -530,7 +544,31 @@ export function WorkspaceManager({
|
||||
<p className="font-mono text-xs text-muted-foreground">{selectedSummary.id}</p>
|
||||
</div>
|
||||
|
||||
{page && selectedId && <WorkspacePreparation workspaceId={selectedId} canManage={canPreprocess} />}
|
||||
{page && <div role="tablist" aria-label="Workspace areas" className="thot-workspace-area-tabs">
|
||||
{workspaceAreas.map((area, index) => <button
|
||||
key={area}
|
||||
ref={element => { areaTabs.current[index] = element; }}
|
||||
type="button"
|
||||
role="tab"
|
||||
id={`${areaId(area)}-tab`}
|
||||
aria-controls={`${areaId(area)}-panel`}
|
||||
aria-selected={activeArea === area}
|
||||
tabIndex={activeArea === area ? 0 : -1}
|
||||
onClick={() => setActiveArea(area)}
|
||||
onKeyDown={event => {
|
||||
const next = event.key === "ArrowRight" ? (index + 1) % workspaceAreas.length
|
||||
: event.key === "ArrowLeft" ? (index + workspaceAreas.length - 1) % workspaceAreas.length
|
||||
: event.key === "Home" ? 0 : event.key === "End" ? workspaceAreas.length - 1 : undefined;
|
||||
if (next === undefined) return;
|
||||
event.preventDefault();
|
||||
setActiveArea(workspaceAreas[next]);
|
||||
areaTabs.current[next]?.focus();
|
||||
}}
|
||||
>{area}</button>)}
|
||||
</div>}
|
||||
{page && selectedId && <div {...areaPanel("Preparation")}>
|
||||
<WorkspacePreparation workspaceId={selectedId} canManage={canPreprocess} />
|
||||
</div>}
|
||||
|
||||
{(detailQuery.isLoading || runtimeQuery.isLoading) && <p className="text-sm text-muted-foreground">Loading workspace configuration…</p>}
|
||||
{(detailQuery.isError || runtimeQuery.isError) && (
|
||||
@@ -539,6 +577,7 @@ export function WorkspaceManager({
|
||||
|
||||
{detailQuery.data && runtime && (
|
||||
<>
|
||||
<div {...areaPanel("About")}>
|
||||
<div className="max-w-3xl">
|
||||
<h3 className="font-heading text-lg font-semibold">About this workspace</h3>
|
||||
<p className="mt-1 text-sm leading-6 text-muted-foreground">
|
||||
@@ -555,7 +594,9 @@ export function WorkspaceManager({
|
||||
<div><dt className="text-xs text-muted-foreground">Workspace language</dt><dd className="font-mono text-xs">{detailQuery.data.workspace.workspace.language}</dd></div>
|
||||
<div><dt className="text-xs text-muted-foreground">Runtime status</dt><dd>{stateLabel(runtime.configurationState)}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div {...areaPanel("Specific actions")}>
|
||||
<div>
|
||||
<h3 className="font-heading text-lg font-semibold">Workspace-specific actions</h3>
|
||||
<p className="mt-1 text-sm leading-6 text-muted-foreground">The validation and connection checks below apply only to <span className="font-medium text-foreground">{selectedSummary.displayName}</span> and read its active revision without changing or publishing it.</p>
|
||||
@@ -606,6 +647,10 @@ export function WorkspaceManager({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div {...areaPanel("Authentication")}>
|
||||
{!authentication && <p className="text-sm text-muted-foreground">Run a connection test in Specific actions to inspect authentication diagnostics.</p>}
|
||||
{authentication && <section data-testid="workspace-authentication" className="rounded-lg border border-border p-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<h4 className="font-heading font-semibold">Authentication</h4>
|
||||
@@ -693,6 +738,7 @@ export function WorkspaceManager({
|
||||
Runtime secret controls are available only to authorized workspace operators.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -82,36 +82,41 @@
|
||||
.thot-administration-dependency p { max-width: 72ch; margin-bottom: 0.5rem; }
|
||||
.thot-administration-mobile-nav { position: absolute; inset: 0 0 auto; z-index: 30; display: flex; height: 3rem; align-items: center; justify-content: space-between; gap: 0.5rem; padding: 0 1rem; border-bottom: 1px solid oklch(var(--border)); background: oklch(var(--background)); }
|
||||
.thot-app-shell[data-administration-narrow="true"] { padding-top: 3rem; }
|
||||
.thot-work-area-page .thot-workspace-manager__layout { flex: 1; min-height: 22rem; }
|
||||
.thot-work-area-page .thot-work-area-panel__body--flush { display: flex; overflow: hidden; }
|
||||
.thot-work-area-page .thot-workspace-manager__layout { flex: 1; min-height: 0; }
|
||||
.thot-work-area-page .thot-workspace-manager__content { padding: 1.4rem; }
|
||||
.thot-work-area-page__body--pi { padding: 1.5rem 1.75rem; }
|
||||
.thot-pi-workbench { display: grid; grid-template-columns: minmax(12rem, 0.55fr) minmax(0, 1.5fr); border: 1px solid oklch(var(--border)); background: oklch(var(--card)); }
|
||||
.thot-work-area-page__body--pi { display: flex; padding: 1.5rem 1.75rem; overflow: hidden; }
|
||||
.thot-pi-workbench { display: grid; flex: 1; min-width: 0; min-height: 0; grid-template-columns: minmax(12rem, 0.55fr) minmax(0, 1.5fr); border: 1px solid oklch(var(--border)); background: oklch(var(--card)); }
|
||||
.thot-pi-index { background: oklch(var(--muted) / 0.45); border-inline-end: 1px solid oklch(var(--border)); padding: 1.25rem; }
|
||||
.thot-pi-index a { display: block; padding: 0.65rem 0; font-size: 0.875rem; }
|
||||
.thot-pi-detail { min-width: 0; padding: 1.35rem; }
|
||||
.thot-pi-index { min-height: 0; overflow-y: auto; }
|
||||
.thot-pi-detail { min-width: 0; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 1.35rem; }
|
||||
.thot-pi-detail :where(section, ol, li, div, pre, dl, dd) { min-width: 0; max-width: 100%; }
|
||||
.thot-pi-detail :where(p, dt, dd) { overflow-wrap: anywhere; }
|
||||
.thot-pi-detail [data-testid="pi-platform-instructions-scroll"] { max-height: none; overflow: visible; }
|
||||
.thot-workspace-preparation { border-block-start: 1px solid oklch(var(--border)); padding-top: 1.25rem; }
|
||||
.thot-workspace-preparation .thot-preprocessing-control { max-width: 40rem; }
|
||||
.thot-preprocessing-control[data-presentation="page"] { padding: 1rem; margin-top: 0.75rem; }
|
||||
.thot-preprocessing-control[data-presentation="page"] :where(strong, span, small, button) { font-size: 0.8125rem; line-height: 1.5; }
|
||||
.thot-preprocessing-control[data-presentation="page"] button { height: auto; min-height: 2.25rem; white-space: normal; }
|
||||
.thot-administration-database .thot-fleet-ledger__workspace { overflow-y: auto; }
|
||||
.thot-administration-database .thot-fleet-ledger__content { flex: 1 0 26rem; min-height: 26rem; }
|
||||
.thot-administration-database .thot-fleet-ledger__kpi-value { font-size: 1.125rem; }
|
||||
.thot-administration-database .thot-administration-dependency { flex: none; }
|
||||
.thot-database-workbench { grid-template-columns: minmax(10rem, 0.55fr) minmax(0, 1.5fr); }
|
||||
.thot-database-workbench form > .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
@container administration (max-width: 48rem) {
|
||||
.thot-administration-split, .thot-pi-workbench { grid-template-columns: minmax(0, 1fr); }
|
||||
.thot-pi-workbench { grid-template-rows: auto minmax(0, 1fr); }
|
||||
.thot-database-workbench form > .grid { grid-template-columns: minmax(0, 1fr); }
|
||||
.thot-administration-index, .thot-pi-index { border-inline-end: 0; border-block-end: 1px solid oklch(var(--border)); }
|
||||
.thot-administration-index { max-height: 22rem; overflow-y: auto; }
|
||||
.thot-administration-header { padding: 1.25rem; }
|
||||
.thot-administration-split { margin: 1rem; }
|
||||
.thot-work-area-page .thot-workspace-manager__layout { display: flex; flex-direction: column; overflow: visible; }
|
||||
.thot-work-area-page .thot-workspace-manager__navigation { max-height: 18rem; flex: none; border-inline-end: 0; border-block-end: 1px solid oklch(var(--border)); }
|
||||
.thot-work-area-page .thot-workspace-manager__content { overflow: visible; }
|
||||
.thot-work-area-page .thot-workspace-manager__layout { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, auto) minmax(0, 1fr); }
|
||||
.thot-work-area-page .thot-workspace-manager__navigation { max-height: 10rem; border-inline-end: 0; border-block-end: 1px solid oklch(var(--border)); }
|
||||
}
|
||||
@container administration (max-width: 30rem) {
|
||||
.thot-pi-detail dl { display: block; }
|
||||
.thot-pi-detail dt { margin-top: 0.75rem; }
|
||||
.thot-administration-header h1 { font-size: 1.5rem; }
|
||||
.thot-administration-header, .thot-administration-toolbar, .thot-work-area-page__body--pi { padding: 1rem; }
|
||||
.thot-administration-detail, .thot-pi-detail { padding: 1rem; }
|
||||
|
||||
@@ -3,18 +3,12 @@ import { getWorkspacePreprocessingStatus } from "../../api/workspace-preprocessi
|
||||
import { WorkspacePreprocessingControl } from "../WorkspacePreprocessingControl";
|
||||
import { AdministrationLink } from "./AdministrationNavigation";
|
||||
|
||||
export function WorkspacePreparation({ workspaceId, canManage, databaseSummary = false }: {
|
||||
workspaceId: string; canManage: boolean; databaseSummary?: boolean;
|
||||
export function WorkspacePreparation({ workspaceId, canManage }: {
|
||||
workspaceId: string; canManage: boolean;
|
||||
}) {
|
||||
const query = useQuery({ queryKey: ["workspace-preprocessing", workspaceId],
|
||||
queryFn: () => getWorkspacePreprocessingStatus(workspaceId), enabled: canManage, retry: false, refetchInterval: 5000 });
|
||||
if (!canManage) return <p className="text-sm text-muted-foreground">Database management permission is required to inspect or run preprocessing.</p>;
|
||||
if (databaseSummary) return <section aria-label="Workspace preparation dependency" className="thot-administration-dependency">
|
||||
<p><strong>Workspace preparation</strong> · {workspaceId}</p>
|
||||
<p role="status">{query.isLoading ? "Checking workspace readiness…" : query.isError ? "Workspace readiness is unavailable." : query.data?.detail}</p>
|
||||
{query.isError && <button className="thot-administration-link" onClick={() => void query.refetch()}>Retry readiness</button>}
|
||||
<AdministrationLink surface="workspace-management" workspace={workspaceId} className="thot-administration-link">Open workspace preprocessing</AdministrationLink>
|
||||
</section>;
|
||||
return <section aria-label="Workspace readiness" className="thot-workspace-preparation">
|
||||
<p className="thot-administration-eyebrow">Preparation</p>
|
||||
<h3 className="font-heading text-lg font-semibold mt-1">Ready for analysis</h3>
|
||||
|
||||
Reference in New Issue
Block a user