import { useMemo, useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
AlertCircle,
CheckCircle2,
ClipboardCheck,
FlaskConical,
GitPullRequest,
KeyRound,
Trash2,
X,
} from "lucide-react";
import {
asWorkspaceApiError,
forgetWorkspaceSecret,
getWorkspace,
getWorkspaceRegistryStatus,
getWorkspaceRuntimeConfiguration,
listWorkspaces,
pullWorkspaceRegistry,
saveWorkspaceSecrets,
testWorkspace,
validateWorkspace,
type WorkspaceRuntimeConfiguration,
} from "../api/workspaces";
import { Button } from "../components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "../components/ui/dialog";
function QueryError({ name, message, retryLabel, onRetry }: {
name: string;
message: string;
retryLabel: string;
onRetry: () => void;
}) {
return (
);
}
function publicError(error: unknown, fallback: string): string {
const safe = asWorkspaceApiError(error);
return safe ? `${safe.code}: ${safe.message}` : fallback;
}
function stateLabel(state: "ready" | "configuration_required"): string {
return state === "ready" ? "Ready" : "Runtime configuration required";
}
export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: () => void }) {
const queryClient = useQueryClient();
const [selectedId, setSelectedId] = useState();
const [secretValues, setSecretValues] = useState>({});
const [notice, setNotice] = useState();
const [diagnostics, setDiagnostics] = useState([]);
const [busyAction, setBusyAction] = useState();
const statusQuery = useQuery({
queryKey: ["workspace-repository-status"],
queryFn: getWorkspaceRegistryStatus,
enabled: open,
});
const workspacesQuery = useQuery({
queryKey: ["workspaces"],
queryFn: listWorkspaces,
enabled: open,
});
const workspaces = workspacesQuery.data ?? [];
const selectedSummary = useMemo(
() => workspaces.find(({ id }) => id === selectedId),
[selectedId, workspaces],
);
const detailQuery = useQuery({
queryKey: ["workspace", selectedId],
queryFn: () => getWorkspace(selectedId!),
enabled: Boolean(open && selectedId),
});
const runtimeQuery = useQuery({
queryKey: ["workspace-runtime-configuration", selectedId],
queryFn: () => getWorkspaceRuntimeConfiguration(selectedId!),
enabled: Boolean(open && selectedId),
});
const clearMessages = () => {
setNotice(undefined);
setDiagnostics([]);
};
const close = () => {
setSecretValues({});
clearMessages();
onClose();
};
const selectWorkspace = (id: string) => {
setSelectedId(id);
setSecretValues({});
clearMessages();
};
async function updateRepository() {
setBusyAction("repository");
clearMessages();
try {
await pullWorkspaceRegistry();
await Promise.all([
statusQuery.refetch(),
workspacesQuery.refetch(),
selectedId ? detailQuery.refetch() : Promise.resolve(),
selectedId ? runtimeQuery.refetch() : Promise.resolve(),
]);
setNotice("Workspace repository updated and validated.");
} catch (error) {
setDiagnostics([publicError(error, "git_unavailable: Workspace repository could not be updated")]);
} finally {
setBusyAction(undefined);
}
}
async function validateSource() {
if (!detailQuery.data) return;
setBusyAction("validate");
clearMessages();
try {
await validateWorkspace(detailQuery.data.workspace);
setNotice("Workspace source is valid.");
} catch (error) {
setDiagnostics([publicError(error, "workspace_invalid: Workspace validation could not be completed")]);
} finally {
setBusyAction(undefined);
}
}
async function testConnections() {
if (!selectedId) return;
setBusyAction("test");
clearMessages();
try {
const result = await testWorkspace(selectedId);
setDiagnostics(result.diagnostics.map(({ code, message }) => `${code}: ${message}`));
if (result.diagnostics.length === 0) {
setNotice(result.activatable
? "Workspace connections are valid."
: "Workspace connection test completed.");
}
} catch (error) {
setDiagnostics([publicError(error, "connector_unavailable: Workspace connections could not be tested")]);
} finally {
setBusyAction(undefined);
}
}
async function saveSecrets() {
if (!selectedId) return;
const values = Object.fromEntries(
Object.entries(secretValues).filter(([, value]) => value.length > 0),
);
if (Object.keys(values).length === 0) return;
setBusyAction("save-secrets");
clearMessages();
try {
const configuration = await saveWorkspaceSecrets(selectedId, values);
queryClient.setQueryData(
["workspace-runtime-configuration", selectedId],
configuration,
);
setSecretValues({});
await workspacesQuery.refetch();
setNotice("Runtime secrets saved. Stored values remain hidden.");
} catch (error) {
setDiagnostics([publicError(error, "workspace_invalid: Runtime secrets could not be saved")]);
} finally {
setBusyAction(undefined);
}
}
async function forgetSecret(requirementId: string) {
if (!selectedId) return;
setBusyAction(`forget:${requirementId}`);
clearMessages();
try {
const configuration = await forgetWorkspaceSecret(selectedId, requirementId);
queryClient.setQueryData(
["workspace-runtime-configuration", selectedId],
configuration,
);
setSecretValues((current) => ({ ...current, [requirementId]: "" }));
await workspacesQuery.refetch();
setNotice("Stored secret forgotten.");
} catch (error) {
setDiagnostics([publicError(error, "workspace_invalid: Stored secret could not be forgotten")]);
} finally {
setBusyAction(undefined);
}
}
const repository = statusQuery.data?.repository;
const repositoryLabel = repository
? `${repository.host}/${repository.repository}`
: "the repository configured for this ThothII installation";
const runtime = runtimeQuery.data;
const hasEnteredSecrets = Object.values(secretValues).some((value) => value.length > 0);
return (
);
}