Files
paperclip/ui/src/pages/ExecutionWorkspaceDetail.tsx
2026-03-16 20:12:22 -05:00

83 lines
3.6 KiB
TypeScript

import { Link, useParams } from "@/lib/router";
import { useQuery } from "@tanstack/react-query";
import { ExternalLink } from "lucide-react";
import { executionWorkspacesApi } from "../api/execution-workspaces";
import { queryKeys } from "../lib/queryKeys";
function isSafeExternalUrl(value: string | null | undefined) {
if (!value) return false;
try {
const parsed = new URL(value);
return parsed.protocol === "http:" || parsed.protocol === "https:";
} catch {
return false;
}
}
function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="flex items-start gap-3 py-1.5">
<div className="w-28 shrink-0 text-xs text-muted-foreground">{label}</div>
<div className="min-w-0 flex-1 text-sm">{children}</div>
</div>
);
}
export function ExecutionWorkspaceDetail() {
const { workspaceId } = useParams<{ workspaceId: string }>();
const { data: workspace, isLoading, error } = useQuery({
queryKey: queryKeys.executionWorkspaces.detail(workspaceId!),
queryFn: () => executionWorkspacesApi.get(workspaceId!),
enabled: Boolean(workspaceId),
});
if (isLoading) return <p className="text-sm text-muted-foreground">Loading...</p>;
if (error) return <p className="text-sm text-destructive">{error instanceof Error ? error.message : "Failed to load workspace"}</p>;
if (!workspace) return null;
return (
<div className="max-w-2xl space-y-4">
<div className="space-y-1">
<div className="text-xs text-muted-foreground">Execution workspace</div>
<h1 className="text-2xl font-semibold">{workspace.name}</h1>
<div className="text-sm text-muted-foreground">
{workspace.status} · {workspace.mode} · {workspace.providerType}
</div>
</div>
<div className="rounded-lg border border-border p-4">
<DetailRow label="Project">
{workspace.projectId ? <Link to={`/projects/${workspace.projectId}`} className="hover:underline">{workspace.projectId}</Link> : "None"}
</DetailRow>
<DetailRow label="Source issue">
{workspace.sourceIssueId ? <Link to={`/issues/${workspace.sourceIssueId}`} className="hover:underline">{workspace.sourceIssueId}</Link> : "None"}
</DetailRow>
<DetailRow label="Branch">{workspace.branchName ?? "None"}</DetailRow>
<DetailRow label="Base ref">{workspace.baseRef ?? "None"}</DetailRow>
<DetailRow label="Working dir">
<span className="break-all font-mono text-xs">{workspace.cwd ?? "None"}</span>
</DetailRow>
<DetailRow label="Provider ref">
<span className="break-all font-mono text-xs">{workspace.providerRef ?? "None"}</span>
</DetailRow>
<DetailRow label="Repo URL">
{workspace.repoUrl && isSafeExternalUrl(workspace.repoUrl) ? (
<a href={workspace.repoUrl} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 hover:underline">
{workspace.repoUrl}
<ExternalLink className="h-3 w-3" />
</a>
) : workspace.repoUrl ? (
<span className="break-all font-mono text-xs">{workspace.repoUrl}</span>
) : "None"}
</DetailRow>
<DetailRow label="Opened">{new Date(workspace.openedAt).toLocaleString()}</DetailRow>
<DetailRow label="Last used">{new Date(workspace.lastUsedAt).toLocaleString()}</DetailRow>
<DetailRow label="Cleanup">
{workspace.cleanupEligibleAt ? `${new Date(workspace.cleanupEligibleAt).toLocaleString()}${workspace.cleanupReason ? ` · ${workspace.cleanupReason}` : ""}` : "Not scheduled"}
</DetailRow>
</div>
</div>
);
}