Files
ThothII/frontend/src/shell/WorkspaceManager.tsx
T

563 lines
30 KiB
TypeScript

import { useEffect, 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 WorkspaceRuntimeConfiguration,
} from "../api/workspaces";
import { captureAuthOperation, isAuthOperationCurrent, StaleAuthOperationError } 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 (
<div role="alert" aria-label={name} className="grid gap-2 rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm">
<p>{message}</p>
<div><Button size="sm" variant="outline" onClick={onRetry}>{retryLabel}</Button></div>
</div>
);
}
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";
export function WorkspaceManager({
open,
onClose,
canManageWorkspace = false,
canManageSecrets = false,
}: {
open: boolean;
onClose: () => void;
canManageWorkspace?: boolean;
canManageSecrets?: boolean;
}) {
const queryClient = useQueryClient();
const [selectedId, setSelectedId] = useState<string>();
const [secretValues, setSecretValues] = useState<Record<string, string>>({});
const [notice, setNotice] = useState<string>();
const [diagnostics, setDiagnostics] = useState<string[]>([]);
const [validationNotice, setValidationNotice] = useState<string>();
const [validationDiagnostics, setValidationDiagnostics] = useState<string[]>([]);
const [connectionNotice, setConnectionNotice] = useState<string>();
const [connectionDiagnostics, setConnectionDiagnostics] = useState<string[]>([]);
const [busyAction, setBusyAction] = useState<string>();
const operationEpochRef = useRef(0);
const selectedIdRef = useRef(selectedId);
selectedIdRef.current = selectedId;
useEffect(() => () => { operationEpochRef.current += 1; }, []);
async function guardedQuery<T>(request: () => Promise<T>, targetId?: string): Promise<T> {
const guard = captureAuthOperation({
sessionId: targetId ?? null,
disposalEpoch: operationEpochRef.current,
});
if (!guard) throw new StaleAuthOperationError();
const result = await request();
const currentSessionId = targetId === undefined ? null : selectedIdRef.current;
if (!isAuthOperationCurrent(guard, {
sessionId: currentSessionId,
disposalEpoch: operationEpochRef.current,
})) 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 clearMessages = () => {
setNotice(undefined);
setDiagnostics([]);
setValidationNotice(undefined);
setValidationDiagnostics([]);
setConnectionNotice(undefined);
setConnectionDiagnostics([]);
};
const clearGlobalMessages = () => {
setNotice(undefined);
setDiagnostics([]);
};
const close = () => {
setSecretValues({});
clearMessages();
onClose();
};
const selectWorkspace = (id: string) => {
setSelectedId(id);
setSecretValues({});
clearMessages();
};
const showLevelOne = () => {
setSelectedId(undefined);
setSecretValues({});
clearMessages();
};
async function updateRepository() {
const guard = captureAuthOperation({ disposalEpoch: operationEpochRef.current });
if (!guard) return;
setBusyAction("repository");
clearMessages();
try {
await pullWorkspaceRegistry();
if (!isAuthOperationCurrent(guard, { disposalEpoch: operationEpochRef.current })) return;
await Promise.all([
statusQuery.refetch(),
workspacesQuery.refetch(),
selectedId ? detailQuery.refetch() : Promise.resolve(),
selectedId ? runtimeQuery.refetch() : Promise.resolve(),
]);
if (!isAuthOperationCurrent(guard, { disposalEpoch: operationEpochRef.current })) return;
setNotice("Workspace repository updated and validated.");
} catch (error) {
if (isAuthOperationCurrent(guard, { disposalEpoch: operationEpochRef.current })) {
setDiagnostics([publicError(error, "git_unavailable: Workspace repository could not be updated")]);
}
} finally {
if (isAuthOperationCurrent(guard, { disposalEpoch: operationEpochRef.current })) setBusyAction(undefined);
}
}
async function validateSource() {
if (!detailQuery.data) return;
const guard = captureAuthOperation({ sessionId: selectedId, disposalEpoch: operationEpochRef.current });
if (!guard) return;
setBusyAction("validate");
clearGlobalMessages();
setValidationNotice(undefined);
setValidationDiagnostics([]);
try {
await validateWorkspace(detailQuery.data.workspace);
if (!isAuthOperationCurrent(guard, { sessionId: selectedIdRef.current, disposalEpoch: operationEpochRef.current })) return;
setValidationNotice("Workspace source is valid.");
} catch (error) {
if (isAuthOperationCurrent(guard, { sessionId: selectedIdRef.current, disposalEpoch: operationEpochRef.current })) {
setValidationDiagnostics([publicError(error, "workspace_invalid: Workspace validation could not be completed")]);
}
} finally {
if (isAuthOperationCurrent(guard, { sessionId: selectedIdRef.current, disposalEpoch: operationEpochRef.current })) setBusyAction(undefined);
}
}
async function testConnections() {
if (!selectedId) return;
const targetId = selectedId;
const guard = captureAuthOperation({ sessionId: targetId, disposalEpoch: operationEpochRef.current });
if (!guard) return;
setBusyAction("test");
clearGlobalMessages();
setConnectionNotice(undefined);
setConnectionDiagnostics([]);
try {
const result = await testWorkspace(selectedId);
if (!isAuthOperationCurrent(guard, { sessionId: selectedIdRef.current, disposalEpoch: operationEpochRef.current })) return;
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 (isAuthOperationCurrent(guard, { sessionId: selectedIdRef.current, disposalEpoch: operationEpochRef.current })) {
setConnectionDiagnostics([publicError(error, "connector_unavailable: Workspace connections could not be tested")]);
}
} finally {
if (isAuthOperationCurrent(guard, { sessionId: selectedIdRef.current, disposalEpoch: operationEpochRef.current })) setBusyAction(undefined);
}
}
async function saveSecrets() {
if (!selectedId) return;
const targetId = selectedId;
const guard = captureAuthOperation({ sessionId: targetId, disposalEpoch: operationEpochRef.current });
if (!guard) 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(targetId, values);
if (!isAuthOperationCurrent(guard, { sessionId: selectedIdRef.current, disposalEpoch: operationEpochRef.current })) return;
queryClient.setQueryData(
["workspace-runtime-configuration", targetId],
configuration,
);
setSecretValues({});
await workspacesQuery.refetch();
if (!isAuthOperationCurrent(guard, { sessionId: selectedIdRef.current, disposalEpoch: operationEpochRef.current })) return;
setNotice("Runtime secrets saved. Stored values remain hidden.");
} catch (error) {
if (isAuthOperationCurrent(guard, { sessionId: selectedIdRef.current, disposalEpoch: operationEpochRef.current })) {
setDiagnostics([publicError(error, "workspace_invalid: Runtime secrets could not be saved")]);
}
} finally {
if (isAuthOperationCurrent(guard, { sessionId: selectedIdRef.current, disposalEpoch: operationEpochRef.current })) setBusyAction(undefined);
}
}
async function forgetSecret(requirementId: string) {
if (!selectedId) return;
const targetId = selectedId;
const guard = captureAuthOperation({ sessionId: targetId, disposalEpoch: operationEpochRef.current });
if (!guard) return;
setBusyAction(`forget:${requirementId}`);
clearMessages();
try {
const configuration = await forgetWorkspaceSecret(targetId, requirementId);
if (!isAuthOperationCurrent(guard, { sessionId: selectedIdRef.current, disposalEpoch: operationEpochRef.current })) return;
queryClient.setQueryData(
["workspace-runtime-configuration", targetId],
configuration,
);
setSecretValues((current) => ({ ...current, [requirementId]: "" }));
await workspacesQuery.refetch();
if (!isAuthOperationCurrent(guard, { sessionId: selectedIdRef.current, disposalEpoch: operationEpochRef.current })) return;
setNotice("Stored secret forgotten.");
} catch (error) {
if (isAuthOperationCurrent(guard, { sessionId: selectedIdRef.current, disposalEpoch: operationEpochRef.current })) {
setDiagnostics([publicError(error, "workspace_invalid: Stored secret could not be forgotten")]);
}
} finally {
if (isAuthOperationCurrent(guard, { sessionId: selectedIdRef.current, disposalEpoch: operationEpochRef.current })) 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 (
<Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) close(); }}>
<DialogContent
showCloseButton={false}
className="h-[86vh] w-[94vw] max-h-[92vh] max-w-[94vw] grid-rows-[auto_minmax(0,1fr)] gap-0 overflow-hidden p-0 sm:w-[70vw] sm:max-w-[94vw]"
>
<DialogHeader className="border-b border-border/70 px-6 pb-4 pt-5 pr-12">
<DialogTitle>Workspace management</DialogTitle>
<DialogDescription>
Read, validate, and complete the runtime configuration of workspaces supplied by the installation repository.
</DialogDescription>
</DialogHeader>
<button type="button" aria-label="Close workspace management" onClick={close} className="absolute right-3 top-3 grid size-7 place-items-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground">
<X className="size-4" />
</button>
<div className="grid min-h-0 grid-cols-[15rem_minmax(0,1fr)] overflow-hidden">
<nav aria-label="Workspaces" className="flex min-h-0 flex-col border-r border-border/70 bg-muted/30 p-3">
<Button
type="button"
size="sm"
variant="outline"
className="mb-3 w-full justify-start"
onClick={showLevelOne}
>
<ArrowLeft />Back to Level 1
</Button>
<p className="mb-2 px-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">Available workspaces</p>
<div className="min-h-0 flex-1 overflow-y-auto">
{workspacesQuery.isLoading && <p className="p-2 text-xs text-muted-foreground">Loading workspaces…</p>}
{workspacesQuery.isError && (
<QueryError name="Workspace list failed" message="Could not load workspaces." retryLabel="Retry workspace list" onRetry={() => { void workspacesQuery.refetch(); }} />
)}
{workspaces.map((workspace) => (
<button
key={workspace.id}
type="button"
aria-label={workspace.displayName}
aria-current={selectedId === workspace.id ? "page" : undefined}
onClick={() => selectWorkspace(workspace.id)}
className="mb-1 w-full rounded-md px-2.5 py-2 text-left text-sm hover:bg-muted aria-[current=page]:bg-primary/10 aria-[current=page]:font-semibold"
>
<span className="block truncate">{workspace.displayName}</span>
<span className="block truncate text-xs text-muted-foreground">{workspace.id}</span>
<span className="mt-1 block text-[0.7rem] text-muted-foreground">{stateLabel(workspace.configurationState)}</span>
</button>
))}
</div>
</nav>
<main className="min-h-0 min-w-0 overflow-y-auto overscroll-contain px-6 py-5">
{notice && (
<p role="status" className="mb-4 flex items-center gap-2 rounded-md border border-border bg-muted/50 px-3 py-2 text-sm">
<CheckCircle2 className="size-4 text-primary" />{notice}
</p>
)}
{diagnostics.length > 0 && (
<div data-testid="workspace-diagnostics" aria-live="polite" className="mb-4 grid gap-1 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm">
{diagnostics.map((diagnostic) => (
<p key={diagnostic} className="flex items-start gap-2">
<AlertCircle className="mt-0.5 size-4 shrink-0 text-amber-700" />{diagnostic}
</p>
))}
</div>
)}
{!selectedSummary ? (
<section data-testid="workspace-overview" className="mx-auto max-w-3xl space-y-5">
<div>
<p className="thot-label">Level 1 · Repository</p>
<h2 className="font-heading text-xl font-semibold">How workspaces reach ThothII</h2>
</div>
<ol className="grid list-decimal gap-3 pl-5 text-sm leading-6 text-muted-foreground">
<li><span className="font-medium text-foreground">Create a workspace repository</span> in any local directory you choose. Add one directory for each workspace you want ThothII to manage. At the repository root, <code>thoth-workspaces.yaml</code> lists those workspaces; each workspace directory contains its own <code>workspace.yaml</code>, which declares the database connection, the Evidence sources, and the ThothII vector-database collection used during the process.</li>
<li>Publish that source by committing and pushing it to a repository hosted by a Git server such as GitHub, GitLab, or Gitea.</li>
<li>The repository address, branch, and read-only Git credentials are configured during ThothII installation. This installation reads <span className="font-medium text-foreground">{repositoryLabel}</span> on branch <span className="font-mono text-foreground">{statusQuery.data?.branch ?? "main"}</span>.</li>
<li>ThothII fetches the configured branch into its managed read-only checkout, validates the complete candidate revision, and activates it only when validation succeeds. It never edits, commits, pushes, or publishes workspace source.</li>
</ol>
<p className="text-sm leading-6 text-muted-foreground">
Follow the <a href={workspaceAuthoringGuideUrl} target="_blank" rel="noreferrer" className="font-medium text-foreground underline underline-offset-4 hover:text-primary">workspace authoring instructions on GitHub</a> for the required layout and validation rules.
</p>
{canManageWorkspace && <div className="rounded-lg border border-border bg-muted/30 p-4">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="max-w-2xl">
<h3 className="font-heading font-semibold">Update workspace repository</h3>
<p className="mt-1 text-sm leading-6 text-muted-foreground">Fetches the configured branch directly into the managed read-only checkout and validates it. No workspace selection is required. If candidate validation fails, the current active revision remains unchanged.</p>
</div>
<Button disabled={busyAction === "repository"} onClick={() => { void updateRepository(); }}>
<GitPullRequest />Update workspace repository
</Button>
</div>
</div>}
{!canManageWorkspace && (
<p className="rounded-lg border border-border bg-muted/30 p-4 text-sm text-muted-foreground">
You can inspect workspaces. Workspace updates, validation, and connection tests require workspace management permission.
</p>
)}
<p className="text-sm text-muted-foreground">Select a workspace from the left only for workspace-specific validation, runtime credentials, and connection tests.</p>
</section>
) : (
<section className="mx-auto max-w-4xl space-y-5">
<div className="border-b border-border/70 pb-4">
<p className="thot-label">Level 2 · Selected workspace</p>
<h2 className="font-heading text-xl font-semibold">{selectedSummary.displayName}</h2>
<p className="font-mono text-xs text-muted-foreground">{selectedSummary.id}</p>
</div>
{(detailQuery.isLoading || runtimeQuery.isLoading) && <p className="text-sm text-muted-foreground">Loading workspace configuration…</p>}
{(detailQuery.isError || runtimeQuery.isError) && (
<QueryError name="Workspace configuration failed" message="Could not load this workspace configuration." retryLabel="Retry workspace configuration" onRetry={() => { void Promise.all([detailQuery.refetch(), runtimeQuery.refetch()]); }} />
)}
{detailQuery.data && runtime && (
<>
<div>
<h3 className="font-heading text-lg font-semibold">Workspace-specific actions</h3>
<p className="mt-1 text-sm text-muted-foreground">The actions below apply only to <span className="font-medium text-foreground">{selectedSummary.displayName}</span>. ThothII reads this revision without modifying or publishing it.</p>
</div>
<dl className="grid gap-3 rounded-lg border border-border bg-muted/20 p-4 text-sm sm:grid-cols-2">
<div><dt className="text-xs text-muted-foreground">Source file</dt><dd className="font-mono">{selectedSummary.file}</dd></div>
<div><dt className="text-xs text-muted-foreground">Active revision</dt><dd className="truncate font-mono">{detailQuery.data.revision.commit}</dd></div>
<div>
<dt className="text-xs text-muted-foreground">Database</dt>
<dd className="grid gap-0.5 font-mono text-xs">
<span>engine: {detailQuery.data.workspace.dwh.engine}</span>
<span>database: {detailQuery.data.workspace.dwh.database}</span>
<span>schema: {detailQuery.data.workspace.dwh.schema}</span>
</dd>
</div>
<div><dt className="text-xs text-muted-foreground">Runtime status</dt><dd>{stateLabel(runtime.configurationState)}</dd></div>
</dl>
<div className="grid gap-3 lg:grid-cols-2">
<div data-testid="workspace-validation-card" className="rounded-lg border border-border p-4">
<h4 className="font-heading font-semibold">Validate workspace source</h4>
<p className="mt-1 min-h-12 text-sm leading-5 text-muted-foreground">Checks workspace.yaml and the required workspace directories against the supported workspace schema. No source file is changed.</p>
{validationNotice && (
<p role="status" className="mt-3 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700">
<CheckCircle2 className="mr-1 inline size-4 text-emerald-600" />{validationNotice}
</p>
)}
{validationDiagnostics.length > 0 && (
<div role="alert" aria-live="polite" className="mt-3 grid gap-1 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm">
{validationDiagnostics.map((diagnostic) => (
<p key={diagnostic} className="flex items-start gap-2">
<AlertCircle className="mt-0.5 size-4 shrink-0 text-amber-700" />{diagnostic}
</p>
))}
</div>
)}
{canManageWorkspace && <Button className="mt-3" size="sm" variant="outline" disabled={busyAction === "validate"} onClick={() => { void validateSource(); }}>
<ClipboardCheck />Validate workspace source
</Button>}
</div>
<div data-testid="workspace-connection-card" className="rounded-lg border border-border p-4">
<h4 className="font-heading font-semibold">Test workspace connections</h4>
<p className="mt-1 min-h-12 text-sm leading-5 text-muted-foreground">Uses temporary decrypted credentials to verify the configured data warehouse and Evidence source. Temporary files are deleted after the test.</p>
{connectionNotice && (
<p role="status" className="mt-3 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700">
<CheckCircle2 className="mr-1 inline size-4 text-emerald-600" />{connectionNotice}
</p>
)}
{connectionDiagnostics.length > 0 && (
<div role="alert" aria-live="polite" className="mt-3 grid gap-1 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm">
{connectionDiagnostics.map((diagnostic) => (
<p key={diagnostic} className="flex items-start gap-2">
<AlertCircle className="mt-0.5 size-4 shrink-0 text-amber-700" />{diagnostic}
</p>
))}
</div>
)}
{canManageWorkspace && <Button className="mt-3" size="sm" variant="outline" disabled={busyAction === "test"} onClick={() => { void testConnections(); }}>
<FlaskConical />Test workspace connections
</Button>}
</div>
</div>
{canManageSecrets && <div className="rounded-lg border border-border p-4">
<div className="mb-4 flex items-start gap-3">
<KeyRound className="mt-0.5 size-5 text-primary" />
<div>
<h3 className="font-heading font-semibold">Runtime secrets</h3>
<p className="mt-1 text-sm leading-5 text-muted-foreground">Enter only new or replacement values. Stored values are never displayed. Saving replaces the selected secret and clears the form field.</p>
</div>
</div>
{runtime.requirements.length === 0 ? (
<p className="text-sm text-muted-foreground">This workspace does not require user-provided runtime secrets for its selected connectors.</p>
) : (
<div className="grid gap-4">
{runtime.requirements.map((requirement) => (
<div key={requirement.id} className="grid gap-2 rounded-md bg-muted/30 p-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<label htmlFor={`workspace-secret-${requirement.id}`} className="font-medium">{requirement.label}</label>
<span className={requirement.configured ? "text-xs font-medium text-emerald-700" : "text-xs font-medium text-amber-700"}>
{requirement.configured ? "Configured" : "Not configured"}
</span>
</div>
<p className="text-xs text-muted-foreground">{requirement.description}{requirement.required ? " Required for this workspace." : " Optional."}</p>
{requirement.input === "textarea" ? (
<textarea
id={`workspace-secret-${requirement.id}`}
value={secretValues[requirement.id] ?? ""}
placeholder="Enter a new or replacement value"
autoComplete="off"
rows={4}
onChange={(event) => setSecretValues((current) => ({ ...current, [requirement.id]: event.target.value }))}
className="w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
/>
) : (
<input
id={`workspace-secret-${requirement.id}`}
type="password"
value={secretValues[requirement.id] ?? ""}
placeholder="Enter a new or replacement value"
autoComplete="new-password"
onChange={(event) => setSecretValues((current) => ({ ...current, [requirement.id]: event.target.value }))}
className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
/>
)}
{requirement.configured && (
<div>
<Button
size="sm"
variant="ghost"
disabled={busyAction === `forget:${requirement.id}`}
aria-label={`Forget stored ${requirement.label}`}
onClick={() => { void forgetSecret(requirement.id); }}
>
<Trash2 />Forget stored value
</Button>
</div>
)}
</div>
))}
<div>
<Button disabled={!hasEnteredSecrets || busyAction === "save-secrets"} onClick={() => { void saveSecrets(); }}>
<KeyRound />Save entered secrets
</Button>
</div>
</div>
)}
</div>}
{!canManageSecrets && (
<p className="rounded-lg border border-border bg-muted/30 p-4 text-sm text-muted-foreground">
Runtime secret controls are available only to authorized workspace operators.
</p>
)}
</>
)}
</section>
)}
</main>
</div>
</DialogContent>
</Dialog>
);
}