import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
AlertCircle,
ArrowLeft,
CheckCircle2,
ClipboardCheck,
FlaskConical,
GitPullRequest,
KeyRound,
Trash2,
X,
} from "lucide-react";
import {
asWorkspaceApiError,
forgetWorkspaceSecret,
getWorkspace,
getWorkspaceRegistryStatus,
getWorkspaceRuntimeConfiguration,
listWorkspaces,
pullWorkspaceRegistry,
saveWorkspaceSecrets,
testWorkspace,
validateWorkspace,
type AuthDiagnostics,
type WorkspaceRuntimeConfiguration,
} from "../api/workspaces";
import {
captureAuthOperation,
isAuthOperationCurrent,
StaleAuthOperationError,
type AuthOperationGuard,
} from "../auth/authOperation";
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";
}
const workspaceAuthoringGuideUrl =
"https://github.com/mptyl/ThothII/blob/main/docs/install/local-workspace-registry.md#prepare-and-publish-a-workspace-source";
type WorkspaceOperationContext = Readonly<{
open: boolean;
canManageWorkspace: boolean;
canManageSecrets: boolean;
}>;
type WorkspaceOperationGuard = Readonly<{
operation: AuthOperationGuard;
context: WorkspaceOperationContext;
}>;
export function WorkspaceManager({
open,
onClose,
canManageWorkspace = false,
canManageSecrets = false,
}: {
open: boolean;
onClose: () => void;
canManageWorkspace?: boolean;
canManageSecrets?: boolean;
}) {
const queryClient = useQueryClient();
const [selectedId, setSelectedId] = useState();
const [secretValues, setSecretValues] = useState>({});
const [notice, setNotice] = useState();
const [diagnostics, setDiagnostics] = useState([]);
const [validationNotice, setValidationNotice] = useState();
const [validationDiagnostics, setValidationDiagnostics] = useState([]);
const [connectionNotice, setConnectionNotice] = useState();
const [connectionDiagnostics, setConnectionDiagnostics] = useState([]);
const [authentication, setAuthentication] = useState();
const [busyAction, setBusyAction] = useState();
const operationEpochRef = useRef(0);
const diagnosticEpochRef = useRef(0);
const selectedIdRef = useRef(selectedId);
const contextRef = useRef({ open, canManageWorkspace, canManageSecrets });
const committedContextRef = useRef(contextRef.current);
selectedIdRef.current = selectedId;
contextRef.current = { open, canManageWorkspace, canManageSecrets };
const clearWorkspacePresentation = () => {
setNotice(undefined);
setDiagnostics([]);
setValidationNotice(undefined);
setValidationDiagnostics([]);
setConnectionNotice(undefined);
setConnectionDiagnostics([]);
setAuthentication(undefined);
};
const cancelWorkspaceQueries = () => {
void queryClient.cancelQueries({ queryKey: ["workspace-repository-status"] });
void queryClient.cancelQueries({ queryKey: ["workspaces"] });
void queryClient.cancelQueries({ queryKey: ["workspace"] });
void queryClient.cancelQueries({ queryKey: ["workspace-runtime-configuration"] });
};
const clearWorkspaceCaches = () => {
queryClient.removeQueries({ queryKey: ["workspace-repository-status"] });
queryClient.removeQueries({ queryKey: ["workspaces"] });
queryClient.removeQueries({ queryKey: ["workspace"] });
queryClient.removeQueries({ queryKey: ["workspace-runtime-configuration"] });
};
// This is the sole invalidation boundary for deferred workspace work. Call it before capturing
// an operation guard; doing it after capture would immediately invalidate the new operation.
const invalidateWorkspaceContext = ({
clearSecrets = true,
clearCaches = false,
}: { clearSecrets?: boolean; clearCaches?: boolean } = {}) => {
operationEpochRef.current += 1;
diagnosticEpochRef.current += 1;
cancelWorkspaceQueries();
if (clearCaches) clearWorkspaceCaches();
setBusyAction(undefined);
if (clearSecrets) {
setSecretValues({});
}
clearWorkspacePresentation();
};
useEffect(() => () => {
operationEpochRef.current += 1;
diagnosticEpochRef.current += 1;
}, []);
useLayoutEffect(() => {
const previous = committedContextRef.current;
const current = contextRef.current;
const openChanged = previous.open !== current.open;
const permissionsChanged = previous.canManageWorkspace !== current.canManageWorkspace
|| previous.canManageSecrets !== current.canManageSecrets;
if (openChanged || permissionsChanged) {
invalidateWorkspaceContext({ clearCaches: permissionsChanged });
}
committedContextRef.current = current;
}, [open, canManageWorkspace, canManageSecrets]);
function captureWorkspaceOperation(targetId?: string): WorkspaceOperationGuard | null {
const context = contextRef.current;
if (!context.open) return null;
const operation = captureAuthOperation({
sessionId: targetId ?? null,
disposalEpoch: operationEpochRef.current,
});
return operation ? { operation, context } : null;
}
function isWorkspaceOperationCurrent(guard: WorkspaceOperationGuard | null, targetId?: string): boolean {
if (!guard) return false;
const context = contextRef.current;
return context.open === guard.context.open
&& context.canManageWorkspace === guard.context.canManageWorkspace
&& context.canManageSecrets === guard.context.canManageSecrets
&& isAuthOperationCurrent(guard.operation, {
sessionId: targetId ?? null,
disposalEpoch: operationEpochRef.current,
});
}
async function guardedQuery(request: () => Promise, targetId?: string): Promise {
const guard = captureWorkspaceOperation(targetId);
if (!guard) throw new StaleAuthOperationError();
const result = await request();
const currentTargetId = targetId === undefined ? undefined : selectedIdRef.current;
if (!isWorkspaceOperationCurrent(guard, currentTargetId)) throw new StaleAuthOperationError();
return result;
}
const statusQuery = useQuery({
queryKey: ["workspace-repository-status"],
queryFn: () => guardedQuery(getWorkspaceRegistryStatus),
enabled: open,
});
const workspacesQuery = useQuery({
queryKey: ["workspaces"],
queryFn: () => guardedQuery(listWorkspaces),
enabled: open,
});
const workspaces = workspacesQuery.data ?? [];
const selectedSummary = useMemo(
() => workspaces.find(({ id }) => id === selectedId),
[selectedId, workspaces],
);
const detailQuery = useQuery({
queryKey: ["workspace", selectedId],
queryFn: () => guardedQuery(() => getWorkspace(selectedId!), selectedId),
enabled: Boolean(open && selectedId),
});
const runtimeQuery = useQuery({
queryKey: ["workspace-runtime-configuration", selectedId],
queryFn: () => guardedQuery(() => getWorkspaceRuntimeConfiguration(selectedId!), selectedId),
enabled: Boolean(open && selectedId),
});
const close = () => {
invalidateWorkspaceContext();
onClose();
};
const selectWorkspace = (id: string) => {
invalidateWorkspaceContext();
setSelectedId(id);
};
const showLevelOne = () => {
invalidateWorkspaceContext();
setSelectedId(undefined);
};
async function updateRepository() {
invalidateWorkspaceContext({ clearCaches: true });
const guard = captureWorkspaceOperation();
if (!guard) return;
const targetId = selectedIdRef.current;
setBusyAction("repository");
try {
await pullWorkspaceRegistry();
if (!isWorkspaceOperationCurrent(guard)) return;
await Promise.all([
statusQuery.refetch(),
workspacesQuery.refetch(),
targetId ? detailQuery.refetch() : Promise.resolve(),
targetId ? runtimeQuery.refetch() : Promise.resolve(),
]);
if (!isWorkspaceOperationCurrent(guard)) return;
setNotice("Workspace repository updated and validated.");
} catch (error) {
if (isWorkspaceOperationCurrent(guard)) {
setDiagnostics([publicError(error, "git_unavailable: Workspace repository could not be updated")]);
}
} finally {
if (isWorkspaceOperationCurrent(guard)) setBusyAction(undefined);
}
}
async function validateSource() {
if (!detailQuery.data) return;
const targetId = selectedId;
const source = detailQuery.data.workspace;
invalidateWorkspaceContext({ clearSecrets: false });
const guard = captureWorkspaceOperation(targetId);
if (!guard) return;
const diagnosticEpoch = diagnosticEpochRef.current;
setBusyAction("validate");
try {
const result = await validateWorkspace(source);
if (diagnosticEpoch !== diagnosticEpochRef.current ||
!isWorkspaceOperationCurrent(guard, selectedIdRef.current)) return;
setAuthentication(result.authentication);
setValidationNotice(result.activatable ? "Workspace source and authentication are valid." : "Workspace source is valid.");
} catch (error) {
if (diagnosticEpoch === diagnosticEpochRef.current &&
isWorkspaceOperationCurrent(guard, selectedIdRef.current)) {
setValidationDiagnostics([publicError(error, "workspace_invalid: Workspace validation could not be completed")]);
}
} finally {
if (diagnosticEpoch === diagnosticEpochRef.current &&
isWorkspaceOperationCurrent(guard, selectedIdRef.current)) setBusyAction(undefined);
}
}
async function testConnections() {
if (!selectedId) return;
const targetId = selectedId;
invalidateWorkspaceContext({ clearSecrets: false });
const guard = captureWorkspaceOperation(targetId);
if (!guard) return;
const diagnosticEpoch = diagnosticEpochRef.current;
setBusyAction("test");
try {
const result = await testWorkspace(targetId);
if (diagnosticEpoch !== diagnosticEpochRef.current ||
!isWorkspaceOperationCurrent(guard, selectedIdRef.current)) return;
setAuthentication(result.authentication);
const issues = result.diagnostics.filter(({ level }) => level !== "info");
const informational = result.diagnostics.find(({ level }) => level === "info");
setConnectionDiagnostics(issues.map(({ code, message }) => `${code}: ${message}`));
if (issues.length === 0) {
setConnectionNotice(result.activatable
? informational
? `${informational.code}: ${informational.message}`
: "Workspace connections are valid."
: "Workspace connection test completed.");
}
} catch (error) {
if (diagnosticEpoch === diagnosticEpochRef.current &&
isWorkspaceOperationCurrent(guard, selectedIdRef.current)) {
setConnectionDiagnostics([publicError(error, "connector_unavailable: Workspace connections could not be tested")]);
}
} finally {
if (diagnosticEpoch === diagnosticEpochRef.current &&
isWorkspaceOperationCurrent(guard, selectedIdRef.current)) setBusyAction(undefined);
}
}
async function saveSecrets() {
if (!selectedId) return;
const targetId = selectedId;
const values = Object.fromEntries(
Object.entries(secretValues).filter(([, value]) => value.length > 0),
);
if (Object.keys(values).length === 0) return;
invalidateWorkspaceContext({ clearSecrets: false });
const guard = captureWorkspaceOperation(targetId);
if (!guard) return;
setBusyAction("save-secrets");
try {
const configuration = await saveWorkspaceSecrets(targetId, values);
if (!isWorkspaceOperationCurrent(guard, selectedIdRef.current)) return;
queryClient.setQueryData(
["workspace-runtime-configuration", targetId],
configuration,
);
setSecretValues({});
await workspacesQuery.refetch();
if (!isWorkspaceOperationCurrent(guard, selectedIdRef.current)) return;
setNotice("Runtime secrets saved. Stored values remain hidden.");
} catch (error) {
if (isWorkspaceOperationCurrent(guard, selectedIdRef.current)) {
setDiagnostics([publicError(error, "workspace_invalid: Runtime secrets could not be saved")]);
}
} finally {
if (isWorkspaceOperationCurrent(guard, selectedIdRef.current)) setBusyAction(undefined);
}
}
async function forgetSecret(requirementId: string) {
if (!selectedId) return;
const targetId = selectedId;
invalidateWorkspaceContext({ clearSecrets: false });
const guard = captureWorkspaceOperation(targetId);
if (!guard) return;
setBusyAction(`forget:${requirementId}`);
try {
const configuration = await forgetWorkspaceSecret(targetId, requirementId);
if (!isWorkspaceOperationCurrent(guard, selectedIdRef.current)) return;
queryClient.setQueryData(
["workspace-runtime-configuration", targetId],
configuration,
);
setSecretValues((current) => ({ ...current, [requirementId]: "" }));
await workspacesQuery.refetch();
if (!isWorkspaceOperationCurrent(guard, selectedIdRef.current)) return;
setNotice("Stored secret forgotten.");
} catch (error) {
if (isWorkspaceOperationCurrent(guard, selectedIdRef.current)) {
setDiagnostics([publicError(error, "workspace_invalid: Stored secret could not be forgotten")]);
}
} finally {
if (isWorkspaceOperationCurrent(guard, selectedIdRef.current)) setBusyAction(undefined);
}
}
const repository = statusQuery.data?.repository;
const repositoryLabel = repository
? `${repository.host}/${repository.repository}`
: "the repository configured for this ThothII installation";
const repositoryWebUrl = repository && repository.transport !== "local"
? `https://${repository.host}/${repository.repository}`
: undefined;
const runtime = runtimeQuery.data;
const hasEnteredSecrets = Object.values(secretValues).some((value) => value.length > 0);
return (
);
}