@burdenoff/microfe-cosmicintersection 2026.731.1 → 2026.805.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/cosmicintersection/CosmicIntersectionRoutes.js +1 -1
  2. package/dist/cosmicintersection/CosmicIntersectionRoutes.js.map +1 -1
  3. package/dist/cosmicintersection/pages/collections/CollectionDetailPage.js +1 -1
  4. package/dist/cosmicintersection/pages/collections/CollectionDetailPage.js.map +1 -1
  5. package/dist/cosmicintersection/pages/collections/CollectionFormPage.js +1 -1
  6. package/dist/cosmicintersection/pages/collections/CollectionFormPage.js.map +1 -1
  7. package/dist/cosmicintersection/pages/datasets/DatasetDetailPage.js +1 -1
  8. package/dist/cosmicintersection/pages/datasets/DatasetDetailPage.js.map +1 -1
  9. package/dist/cosmicintersection/pages/datasets/DatasetFormPage.js +1 -1
  10. package/dist/cosmicintersection/pages/datasets/DatasetFormPage.js.map +1 -1
  11. package/dist/cosmicintersection/pages/lineage/LineagePage.js +1 -1
  12. package/dist/cosmicintersection/pages/lineage/LineagePage.js.map +1 -1
  13. package/dist/cosmicintersection/pages/notebooks/NotebookDetailPage.js +1 -1
  14. package/dist/cosmicintersection/pages/notebooks/NotebookDetailPage.js.map +1 -1
  15. package/dist/cosmicintersection/pages/notebooks/NotebookFormPage.js +1 -1
  16. package/dist/cosmicintersection/pages/notebooks/NotebookFormPage.js.map +1 -1
  17. package/dist/cosmicintersection/pages/pipelines/PipelineDetailPage.js +1 -1
  18. package/dist/cosmicintersection/pages/pipelines/PipelineDetailPage.js.map +1 -1
  19. package/dist/cosmicintersection/pages/pipelines/PipelineFormPage.js +1 -1
  20. package/dist/cosmicintersection/pages/pipelines/PipelineFormPage.js.map +1 -1
  21. package/dist/cosmicintersection/pages/runs/RunDetailPage.js +1 -1
  22. package/dist/cosmicintersection/pages/runs/RunDetailPage.js.map +1 -1
  23. package/dist/cosmicintersection/pages/sources/SourceDetailPage.js +1 -1
  24. package/dist/cosmicintersection/pages/sources/SourceDetailPage.js.map +1 -1
  25. package/dist/cosmicintersection/pages/sources/SourceFormPage.js +1 -1
  26. package/dist/cosmicintersection/pages/sources/SourceFormPage.js.map +1 -1
  27. package/package.json +4 -4
@@ -12,7 +12,7 @@ import m from "../../components/StatusBadge.js";
12
12
  import "../../components/index.js";
13
13
  import { useMemo as h, useState as g } from "react";
14
14
  import { Fragment as _, jsx as v, jsxs as y } from "react/jsx-runtime";
15
- import { useParams as b } from "react-router-dom";
15
+ import { useParams as b } from "react-router";
16
16
  import { Beaker as x, Database as S, Download as C, FileCode2 as w, MessageSquare as T, Pencil as E, Play as D, Share2 as O, Sparkles as k } from "lucide-react";
17
17
  import { Badge as A, Button as j, Dialog as M, DialogContent as N, DialogDescription as P, DialogFooter as F, DialogHeader as I, DialogTitle as L, Input as R, Label as z, Switch as B, Table as ee, TableBody as V, TableCell as H, TableHead as U, TableHeader as W, TableRow as G } from "@burdenoff/fe-libs/ui";
18
18
  //#region src/cosmicintersection/pages/notebooks/NotebookDetailPage.tsx
@@ -1 +1 @@
1
- {"version":3,"file":"NotebookDetailPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/notebooks/NotebookDetailPage.tsx"],"sourcesContent":["/**\n * NotebookDetailPage — full record for one notebook.\n *\n * Header renders the notebook's metadata (status, runtime, environment JSON,\n * description, sharing, attached file download). Below it an Analyses section\n * lists `cosListAnalyses(notebookId)` and a \"Run analysis\" dialog kicks off a\n * new `cosRunAnalysis` bound to this notebook. Archive is available from the\n * header action menu. No fabricated data — everything is read from the backend.\n */\nimport { useMemo, useState, type FormEvent } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n Beaker,\n Database,\n Download,\n FileCode2,\n MessageSquare,\n Pencil,\n Play,\n Share2,\n Sparkles,\n} from 'lucide-react';\nimport {\n Badge,\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Label,\n Switch,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useCosAnalysesQuery,\n useCosArchiveNotebookMutation,\n useCosDatasetsQuery,\n useCosNotebookQuery,\n useCosRunAnalysisMutation,\n type CosAnalysis,\n} from '../../api/cosmicApi';\nimport {\n ActionMenu,\n DiscussionPanel,\n EmptyState,\n ErrorState,\n LoadingState,\n PageLayout,\n StatusBadge,\n useFileDownloadUrl,\n} from '../../components';\nimport { useCosmicIntersection } from '../../context/CosmicIntersectionContext';\nimport { notify } from '../../lib/toast';\n\nfunction formatDateTime(value?: string | null): string {\n if (!value) {\n return '—';\n }\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) {\n return '—';\n }\n return date.toLocaleString(undefined, {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n });\n}\n\n/** Renders a presigned download link for an attached wspace-files artifact. */\nfunction FileDownloadLink({ fileId, label }: { fileId: string; label: string }) {\n const { data, loading } = useFileDownloadUrl(fileId);\n const url = data?.getFileDownloadUrl ?? null;\n if (loading) {\n return <span className=\"text-xs text-text-secondary\">Resolving link…</span>;\n }\n if (!url) {\n return <span className=\"text-xs text-text-secondary\">File {fileId}</span>;\n }\n return (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1 text-sm text-text-primary underline-offset-2 hover:underline\"\n >\n <Download className=\"size-3.5\" aria-hidden=\"true\" />\n {label}\n </a>\n );\n}\n\ninterface RunAnalysisDialogProps {\n notebookId: string;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onCompleted: () => void;\n}\n\n/** Dialog to launch a new analysis on this notebook. */\nfunction RunAnalysisDialog({\n notebookId,\n open,\n onOpenChange,\n onCompleted,\n}: RunAnalysisDialogProps) {\n const [title, setTitle] = useState('');\n const [method, setMethod] = useState('');\n const [aiAssisted, setAiAssisted] = useState(false);\n const [runAnalysis, { loading }] = useCosRunAnalysisMutation();\n\n const reset = () => {\n setTitle('');\n setMethod('');\n setAiAssisted(false);\n };\n\n const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n const trimmed = title.trim();\n if (!trimmed) {\n notify.error('Title is required', 'Give the analysis a descriptive title.');\n return;\n }\n const response = await runAnalysis({\n variables: {\n input: {\n notebookId,\n title: trimmed,\n method: method.trim() || undefined,\n aiAssisted,\n },\n },\n });\n if (response.error || !response.data?.cosRunAnalysis?.id) {\n return;\n }\n notify.success('Analysis started', `“${trimmed}” is now running.`);\n reset();\n onOpenChange(false);\n onCompleted();\n };\n\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => {\n if (!loading) {\n onOpenChange(next);\n }\n }}\n >\n <DialogContent>\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n <DialogHeader>\n <DialogTitle>Run analysis</DialogTitle>\n <DialogDescription>\n Launch a new analysis run against this notebook. It starts in the running state and\n can be completed later with results.\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"run-analysis-title\">Title</Label>\n <Input\n id=\"run-analysis-title\"\n value={title}\n onChange={(event) => setTitle(event.target.value)}\n placeholder=\"e.g. Period-folding search\"\n required\n />\n </div>\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"run-analysis-method\">Method</Label>\n <Input\n id=\"run-analysis-method\"\n value={method}\n onChange={(event) => setMethod(event.target.value)}\n placeholder=\"e.g. box-least-squares\"\n />\n </div>\n <div className=\"flex items-center gap-3\">\n <Switch\n id=\"run-analysis-ai\"\n checked={aiAssisted}\n onCheckedChange={(checked) => setAiAssisted(checked)}\n />\n <Label htmlFor=\"run-analysis-ai\" className=\"cursor-pointer\">\n AI-assisted analysis\n </Label>\n </div>\n\n <DialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={loading}\n >\n Cancel\n </Button>\n <Button type=\"submit\" disabled={loading}>\n <Play className=\"size-4\" />\n {loading ? 'Starting…' : 'Run analysis'}\n </Button>\n </DialogFooter>\n </form>\n </DialogContent>\n </Dialog>\n );\n}\n\nexport default function NotebookDetailPage() {\n const { id = '' } = useParams<{ id: string }>();\n const { navigate } = useCosmicIntersection();\n const [runOpen, setRunOpen] = useState(false);\n\n const { data, loading, error, refetch } = useCosNotebookQuery(id);\n const {\n data: analysesData,\n loading: analysesLoading,\n error: analysesError,\n refetch: refetchAnalyses,\n } = useCosAnalysesQuery({ notebookId: id });\n // Resolve the notebook's linked dataset UUIDs to human names for chips.\n const { data: datasetsData } = useCosDatasetsQuery({ pagination: { limit: 100, offset: 0 } });\n const [archiveNotebook] = useCosArchiveNotebookMutation();\n\n const notebook = data?.cosNotebook ?? null;\n const analyses = analysesData?.cosListAnalyses ?? [];\n\n const datasetNameById = useMemo(() => {\n const map = new Map<string, string>();\n for (const dataset of datasetsData?.cosListDatasets.items ?? []) {\n map.set(dataset.id, dataset.name);\n }\n return map;\n }, [datasetsData]);\n\n const handleArchive = async () => {\n if (!notebook) {\n return;\n }\n const response = await archiveNotebook({ variables: { id: notebook.id } });\n if (response.error) {\n throw new Error(response.error.message);\n }\n if (!response.data?.cosArchiveNotebook) {\n notify.error('Could not archive notebook', 'The server did not confirm the change.');\n throw new Error('Archive not confirmed');\n }\n await refetch();\n notify.success('Notebook archived', `“${notebook.title}” was archived.`);\n };\n\n if (loading) {\n return (\n <PageLayout title=\"Notebook\" backPath=\"notebooks\">\n <LoadingState label=\"Loading notebook…\" />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout title=\"Notebook\" backPath=\"notebooks\">\n <ErrorState\n message=\"We could not load this notebook. Please try again.\"\n onRetry={() => void refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!notebook) {\n return (\n <PageLayout title=\"Notebook\" backPath=\"notebooks\">\n <EmptyState\n icon={FileCode2}\n title=\"Notebook not found.\"\n description=\"This notebook may have been removed or is outside your current scope.\"\n action={\n <Button type=\"button\" onClick={() => navigate('notebooks')}>\n Back to notebooks\n </Button>\n }\n />\n </PageLayout>\n );\n }\n\n const environmentJson =\n notebook.environment && Object.keys(notebook.environment).length > 0\n ? JSON.stringify(notebook.environment, null, 2)\n : null;\n\n const actions = (\n <div className=\"flex items-center gap-2\">\n <Button type=\"button\" onClick={() => setRunOpen(true)}>\n <Play className=\"size-4\" />\n Run analysis\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => navigate(`notebooks/${notebook.id}/edit`)}\n >\n <Pencil className=\"size-4\" />\n Edit\n </Button>\n <ActionMenu\n ariaLabel={`Actions for ${notebook.title}`}\n entityName={notebook.title}\n onDelete={notebook.status === 'ARCHIVED' ? undefined : handleArchive}\n deleteLabel=\"Archive notebook\"\n />\n </div>\n );\n\n return (\n <PageLayout\n title={notebook.title}\n description=\"Notebook detail\"\n backPath=\"notebooks\"\n actions={actions}\n >\n <div className=\"space-y-6\">\n {/* Overview */}\n <section className=\"rounded-card border border-border-default bg-bg-elevated p-6\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <StatusBadge status={notebook.status} />\n {notebook.runtime ? <Badge variant=\"outline\">Runtime: {notebook.runtime}</Badge> : null}\n {notebook.isShared ? (\n <Badge variant=\"secondary\" className=\"gap-1\">\n <Share2 className=\"size-3\" aria-hidden=\"true\" />\n Shared{notebook.shareSlug ? `: ${notebook.shareSlug}` : ''}\n </Badge>\n ) : (\n <Badge variant=\"outline\">Private</Badge>\n )}\n </div>\n\n {notebook.description ? (\n <p className=\"mt-4 text-sm text-text-secondary\">{notebook.description}</p>\n ) : (\n <p className=\"mt-4 text-sm text-text-secondary\">No description provided.</p>\n )}\n\n <div className=\"mt-6\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">Linked datasets</p>\n {notebook.datasetIds.length === 0 ? (\n <p className=\"mt-2 text-sm text-text-secondary\">No datasets linked.</p>\n ) : (\n <div className=\"mt-2 flex flex-wrap gap-2\">\n {notebook.datasetIds.map((datasetId) => {\n const name = datasetNameById.get(datasetId);\n return (\n <Button\n key={datasetId}\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-auto gap-1.5 px-2.5 py-1\"\n onClick={() => navigate(`datasets/${datasetId}`)}\n >\n <Database className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n <span className={name ? 'max-w-[16rem] truncate' : 'font-mono text-xs'}>\n {name ?? datasetId}\n </span>\n </Button>\n );\n })}\n </div>\n )}\n </div>\n\n <dl className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n <div>\n <dt className=\"text-xs uppercase tracking-wide text-text-secondary\">Created</dt>\n <dd className=\"mt-1 text-sm text-text-primary\">\n {formatDateTime(notebook.createdAt)}\n </dd>\n </div>\n <div>\n <dt className=\"text-xs uppercase tracking-wide text-text-secondary\">Updated</dt>\n <dd className=\"mt-1 text-sm text-text-primary\">\n {formatDateTime(notebook.updatedAt)}\n </dd>\n </div>\n <div>\n <dt className=\"text-xs uppercase tracking-wide text-text-secondary\">Notebook file</dt>\n <dd className=\"mt-1 text-sm text-text-primary\">\n {notebook.file?.id ? (\n <FileDownloadLink fileId={notebook.file.id} label=\"Download notebook\" />\n ) : (\n <span className=\"text-text-secondary\">No file attached</span>\n )}\n </dd>\n </div>\n </dl>\n\n {notebook.tags.length > 0 ? (\n <div className=\"mt-6\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">Tags</p>\n <div className=\"mt-2 flex flex-wrap gap-2\">\n {notebook.tags.map((tag) => (\n <Badge key={tag} variant=\"outline\">\n {tag}\n </Badge>\n ))}\n </div>\n </div>\n ) : null}\n\n {environmentJson ? (\n <div className=\"mt-6\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">Environment</p>\n <pre className=\"mt-2 max-h-64 overflow-auto rounded-card border border-border-default bg-bg-sunken p-4 font-mono text-xs text-text-primary\">\n {environmentJson}\n </pre>\n </div>\n ) : null}\n </section>\n\n {/* Discussion */}\n <section className=\"space-y-4\">\n <div className=\"flex items-center gap-2\">\n <MessageSquare className=\"size-5 text-text-secondary\" aria-hidden=\"true\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">Discussion</h2>\n </div>\n <DiscussionPanel\n entityType=\"cosmicintersection-notebook\"\n entityId={notebook.id}\n title={notebook.title}\n widgetKey=\"notebook-discussion\"\n />\n </section>\n\n {/* Analyses */}\n <section className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Analyses</h2>\n <p className=\"text-sm text-text-secondary\">\n Analysis runs executed against this notebook.\n </p>\n </div>\n <Button type=\"button\" variant=\"outline\" onClick={() => setRunOpen(true)}>\n <Play className=\"size-4\" />\n Run analysis\n </Button>\n </div>\n\n {analysesLoading ? (\n <LoadingState variant=\"rows\" rows={3} label=\"Loading analyses…\" />\n ) : analysesError ? (\n <ErrorState\n message=\"We could not load analyses for this notebook.\"\n onRetry={() => void refetchAnalyses()}\n />\n ) : analyses.length === 0 ? (\n <EmptyState\n icon={Beaker}\n title=\"No analyses yet.\"\n description=\"Run an analysis to generate results and artifacts from this notebook.\"\n action={\n <Button type=\"button\" onClick={() => setRunOpen(true)}>\n <Play className=\"size-4\" />\n Run analysis\n </Button>\n }\n />\n ) : (\n <>\n {/* Desktop table */}\n <div className=\"hidden rounded-card border border-border-default bg-bg-elevated md:block\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Title</TableHead>\n <TableHead>Method</TableHead>\n <TableHead>AI</TableHead>\n <TableHead>Status</TableHead>\n <TableHead>Started</TableHead>\n <TableHead>Output</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {analyses.map((analysis: CosAnalysis) => (\n <TableRow key={analysis.id}>\n <TableCell className=\"font-medium text-text-primary\">\n {analysis.title}\n </TableCell>\n <TableCell className=\"text-text-secondary\">\n {analysis.method ?? '—'}\n </TableCell>\n <TableCell>\n {analysis.aiAssisted ? (\n <Badge variant=\"secondary\" className=\"gap-1\">\n <Sparkles className=\"size-3\" aria-hidden=\"true\" />\n AI\n </Badge>\n ) : (\n <span className=\"text-text-secondary\">—</span>\n )}\n </TableCell>\n <TableCell>\n <StatusBadge status={analysis.status} />\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-text-secondary\">\n {formatDateTime(analysis.startedAt)}\n </TableCell>\n <TableCell>\n {analysis.outputFile?.id ? (\n <FileDownloadLink fileId={analysis.outputFile.id} label=\"Download\" />\n ) : analysis.results && Object.keys(analysis.results).length > 0 ? (\n <span className=\"text-xs text-text-secondary\">Results recorded</span>\n ) : (\n <span className=\"text-text-secondary\">—</span>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n\n {/* Mobile card list */}\n <ul className=\"space-y-3 md:hidden\">\n {analyses.map((analysis: CosAnalysis) => (\n <li\n key={analysis.id}\n className=\"space-y-2 rounded-card border border-border-default bg-bg-elevated p-4\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate font-medium text-text-primary\">{analysis.title}</p>\n {analysis.method ? (\n <p className=\"truncate text-xs text-text-secondary\">{analysis.method}</p>\n ) : null}\n </div>\n <StatusBadge status={analysis.status} />\n </div>\n <div className=\"flex flex-wrap items-center gap-2 text-xs text-text-secondary\">\n {analysis.aiAssisted ? (\n <Badge variant=\"secondary\" className=\"gap-1\">\n <Sparkles className=\"size-3\" aria-hidden=\"true\" />\n AI\n </Badge>\n ) : null}\n <span>Started {formatDateTime(analysis.startedAt)}</span>\n </div>\n <div>\n {analysis.outputFile?.id ? (\n <FileDownloadLink fileId={analysis.outputFile.id} label=\"Download\" />\n ) : analysis.results && Object.keys(analysis.results).length > 0 ? (\n <span className=\"text-xs text-text-secondary\">Results recorded</span>\n ) : (\n <span className=\"text-xs text-text-secondary\">No output</span>\n )}\n </div>\n </li>\n ))}\n </ul>\n </>\n )}\n </section>\n </div>\n\n <RunAnalysisDialog\n notebookId={notebook.id}\n open={runOpen}\n onOpenChange={setRunOpen}\n onCompleted={() => void refetchAnalyses()}\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"NotebookDetailPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/notebooks/NotebookDetailPage.tsx"],"sourcesContent":["/**\n * NotebookDetailPage — full record for one notebook.\n *\n * Header renders the notebook's metadata (status, runtime, environment JSON,\n * description, sharing, attached file download). Below it an Analyses section\n * lists `cosListAnalyses(notebookId)` and a \"Run analysis\" dialog kicks off a\n * new `cosRunAnalysis` bound to this notebook. Archive is available from the\n * header action menu. No fabricated data — everything is read from the backend.\n */\nimport { useMemo, useState, type FormEvent } from 'react';\nimport { useParams } from 'react-router';\nimport {\n Beaker,\n Database,\n Download,\n FileCode2,\n MessageSquare,\n Pencil,\n Play,\n Share2,\n Sparkles,\n} from 'lucide-react';\nimport {\n Badge,\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Label,\n Switch,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useCosAnalysesQuery,\n useCosArchiveNotebookMutation,\n useCosDatasetsQuery,\n useCosNotebookQuery,\n useCosRunAnalysisMutation,\n type CosAnalysis,\n} from '../../api/cosmicApi';\nimport {\n ActionMenu,\n DiscussionPanel,\n EmptyState,\n ErrorState,\n LoadingState,\n PageLayout,\n StatusBadge,\n useFileDownloadUrl,\n} from '../../components';\nimport { useCosmicIntersection } from '../../context/CosmicIntersectionContext';\nimport { notify } from '../../lib/toast';\n\nfunction formatDateTime(value?: string | null): string {\n if (!value) {\n return '—';\n }\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) {\n return '—';\n }\n return date.toLocaleString(undefined, {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n });\n}\n\n/** Renders a presigned download link for an attached wspace-files artifact. */\nfunction FileDownloadLink({ fileId, label }: { fileId: string; label: string }) {\n const { data, loading } = useFileDownloadUrl(fileId);\n const url = data?.getFileDownloadUrl ?? null;\n if (loading) {\n return <span className=\"text-xs text-text-secondary\">Resolving link…</span>;\n }\n if (!url) {\n return <span className=\"text-xs text-text-secondary\">File {fileId}</span>;\n }\n return (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1 text-sm text-text-primary underline-offset-2 hover:underline\"\n >\n <Download className=\"size-3.5\" aria-hidden=\"true\" />\n {label}\n </a>\n );\n}\n\ninterface RunAnalysisDialogProps {\n notebookId: string;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onCompleted: () => void;\n}\n\n/** Dialog to launch a new analysis on this notebook. */\nfunction RunAnalysisDialog({\n notebookId,\n open,\n onOpenChange,\n onCompleted,\n}: RunAnalysisDialogProps) {\n const [title, setTitle] = useState('');\n const [method, setMethod] = useState('');\n const [aiAssisted, setAiAssisted] = useState(false);\n const [runAnalysis, { loading }] = useCosRunAnalysisMutation();\n\n const reset = () => {\n setTitle('');\n setMethod('');\n setAiAssisted(false);\n };\n\n const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n const trimmed = title.trim();\n if (!trimmed) {\n notify.error('Title is required', 'Give the analysis a descriptive title.');\n return;\n }\n const response = await runAnalysis({\n variables: {\n input: {\n notebookId,\n title: trimmed,\n method: method.trim() || undefined,\n aiAssisted,\n },\n },\n });\n if (response.error || !response.data?.cosRunAnalysis?.id) {\n return;\n }\n notify.success('Analysis started', `“${trimmed}” is now running.`);\n reset();\n onOpenChange(false);\n onCompleted();\n };\n\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => {\n if (!loading) {\n onOpenChange(next);\n }\n }}\n >\n <DialogContent>\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n <DialogHeader>\n <DialogTitle>Run analysis</DialogTitle>\n <DialogDescription>\n Launch a new analysis run against this notebook. It starts in the running state and\n can be completed later with results.\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"run-analysis-title\">Title</Label>\n <Input\n id=\"run-analysis-title\"\n value={title}\n onChange={(event) => setTitle(event.target.value)}\n placeholder=\"e.g. Period-folding search\"\n required\n />\n </div>\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"run-analysis-method\">Method</Label>\n <Input\n id=\"run-analysis-method\"\n value={method}\n onChange={(event) => setMethod(event.target.value)}\n placeholder=\"e.g. box-least-squares\"\n />\n </div>\n <div className=\"flex items-center gap-3\">\n <Switch\n id=\"run-analysis-ai\"\n checked={aiAssisted}\n onCheckedChange={(checked) => setAiAssisted(checked)}\n />\n <Label htmlFor=\"run-analysis-ai\" className=\"cursor-pointer\">\n AI-assisted analysis\n </Label>\n </div>\n\n <DialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={loading}\n >\n Cancel\n </Button>\n <Button type=\"submit\" disabled={loading}>\n <Play className=\"size-4\" />\n {loading ? 'Starting…' : 'Run analysis'}\n </Button>\n </DialogFooter>\n </form>\n </DialogContent>\n </Dialog>\n );\n}\n\nexport default function NotebookDetailPage() {\n const { id = '' } = useParams<{ id: string }>();\n const { navigate } = useCosmicIntersection();\n const [runOpen, setRunOpen] = useState(false);\n\n const { data, loading, error, refetch } = useCosNotebookQuery(id);\n const {\n data: analysesData,\n loading: analysesLoading,\n error: analysesError,\n refetch: refetchAnalyses,\n } = useCosAnalysesQuery({ notebookId: id });\n // Resolve the notebook's linked dataset UUIDs to human names for chips.\n const { data: datasetsData } = useCosDatasetsQuery({ pagination: { limit: 100, offset: 0 } });\n const [archiveNotebook] = useCosArchiveNotebookMutation();\n\n const notebook = data?.cosNotebook ?? null;\n const analyses = analysesData?.cosListAnalyses ?? [];\n\n const datasetNameById = useMemo(() => {\n const map = new Map<string, string>();\n for (const dataset of datasetsData?.cosListDatasets.items ?? []) {\n map.set(dataset.id, dataset.name);\n }\n return map;\n }, [datasetsData]);\n\n const handleArchive = async () => {\n if (!notebook) {\n return;\n }\n const response = await archiveNotebook({ variables: { id: notebook.id } });\n if (response.error) {\n throw new Error(response.error.message);\n }\n if (!response.data?.cosArchiveNotebook) {\n notify.error('Could not archive notebook', 'The server did not confirm the change.');\n throw new Error('Archive not confirmed');\n }\n await refetch();\n notify.success('Notebook archived', `“${notebook.title}” was archived.`);\n };\n\n if (loading) {\n return (\n <PageLayout title=\"Notebook\" backPath=\"notebooks\">\n <LoadingState label=\"Loading notebook…\" />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout title=\"Notebook\" backPath=\"notebooks\">\n <ErrorState\n message=\"We could not load this notebook. Please try again.\"\n onRetry={() => void refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!notebook) {\n return (\n <PageLayout title=\"Notebook\" backPath=\"notebooks\">\n <EmptyState\n icon={FileCode2}\n title=\"Notebook not found.\"\n description=\"This notebook may have been removed or is outside your current scope.\"\n action={\n <Button type=\"button\" onClick={() => navigate('notebooks')}>\n Back to notebooks\n </Button>\n }\n />\n </PageLayout>\n );\n }\n\n const environmentJson =\n notebook.environment && Object.keys(notebook.environment).length > 0\n ? JSON.stringify(notebook.environment, null, 2)\n : null;\n\n const actions = (\n <div className=\"flex items-center gap-2\">\n <Button type=\"button\" onClick={() => setRunOpen(true)}>\n <Play className=\"size-4\" />\n Run analysis\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => navigate(`notebooks/${notebook.id}/edit`)}\n >\n <Pencil className=\"size-4\" />\n Edit\n </Button>\n <ActionMenu\n ariaLabel={`Actions for ${notebook.title}`}\n entityName={notebook.title}\n onDelete={notebook.status === 'ARCHIVED' ? undefined : handleArchive}\n deleteLabel=\"Archive notebook\"\n />\n </div>\n );\n\n return (\n <PageLayout\n title={notebook.title}\n description=\"Notebook detail\"\n backPath=\"notebooks\"\n actions={actions}\n >\n <div className=\"space-y-6\">\n {/* Overview */}\n <section className=\"rounded-card border border-border-default bg-bg-elevated p-6\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <StatusBadge status={notebook.status} />\n {notebook.runtime ? <Badge variant=\"outline\">Runtime: {notebook.runtime}</Badge> : null}\n {notebook.isShared ? (\n <Badge variant=\"secondary\" className=\"gap-1\">\n <Share2 className=\"size-3\" aria-hidden=\"true\" />\n Shared{notebook.shareSlug ? `: ${notebook.shareSlug}` : ''}\n </Badge>\n ) : (\n <Badge variant=\"outline\">Private</Badge>\n )}\n </div>\n\n {notebook.description ? (\n <p className=\"mt-4 text-sm text-text-secondary\">{notebook.description}</p>\n ) : (\n <p className=\"mt-4 text-sm text-text-secondary\">No description provided.</p>\n )}\n\n <div className=\"mt-6\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">Linked datasets</p>\n {notebook.datasetIds.length === 0 ? (\n <p className=\"mt-2 text-sm text-text-secondary\">No datasets linked.</p>\n ) : (\n <div className=\"mt-2 flex flex-wrap gap-2\">\n {notebook.datasetIds.map((datasetId) => {\n const name = datasetNameById.get(datasetId);\n return (\n <Button\n key={datasetId}\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-auto gap-1.5 px-2.5 py-1\"\n onClick={() => navigate(`datasets/${datasetId}`)}\n >\n <Database className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n <span className={name ? 'max-w-[16rem] truncate' : 'font-mono text-xs'}>\n {name ?? datasetId}\n </span>\n </Button>\n );\n })}\n </div>\n )}\n </div>\n\n <dl className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n <div>\n <dt className=\"text-xs uppercase tracking-wide text-text-secondary\">Created</dt>\n <dd className=\"mt-1 text-sm text-text-primary\">\n {formatDateTime(notebook.createdAt)}\n </dd>\n </div>\n <div>\n <dt className=\"text-xs uppercase tracking-wide text-text-secondary\">Updated</dt>\n <dd className=\"mt-1 text-sm text-text-primary\">\n {formatDateTime(notebook.updatedAt)}\n </dd>\n </div>\n <div>\n <dt className=\"text-xs uppercase tracking-wide text-text-secondary\">Notebook file</dt>\n <dd className=\"mt-1 text-sm text-text-primary\">\n {notebook.file?.id ? (\n <FileDownloadLink fileId={notebook.file.id} label=\"Download notebook\" />\n ) : (\n <span className=\"text-text-secondary\">No file attached</span>\n )}\n </dd>\n </div>\n </dl>\n\n {notebook.tags.length > 0 ? (\n <div className=\"mt-6\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">Tags</p>\n <div className=\"mt-2 flex flex-wrap gap-2\">\n {notebook.tags.map((tag) => (\n <Badge key={tag} variant=\"outline\">\n {tag}\n </Badge>\n ))}\n </div>\n </div>\n ) : null}\n\n {environmentJson ? (\n <div className=\"mt-6\">\n <p className=\"text-xs uppercase tracking-wide text-text-secondary\">Environment</p>\n <pre className=\"mt-2 max-h-64 overflow-auto rounded-card border border-border-default bg-bg-sunken p-4 font-mono text-xs text-text-primary\">\n {environmentJson}\n </pre>\n </div>\n ) : null}\n </section>\n\n {/* Discussion */}\n <section className=\"space-y-4\">\n <div className=\"flex items-center gap-2\">\n <MessageSquare className=\"size-5 text-text-secondary\" aria-hidden=\"true\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">Discussion</h2>\n </div>\n <DiscussionPanel\n entityType=\"cosmicintersection-notebook\"\n entityId={notebook.id}\n title={notebook.title}\n widgetKey=\"notebook-discussion\"\n />\n </section>\n\n {/* Analyses */}\n <section className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Analyses</h2>\n <p className=\"text-sm text-text-secondary\">\n Analysis runs executed against this notebook.\n </p>\n </div>\n <Button type=\"button\" variant=\"outline\" onClick={() => setRunOpen(true)}>\n <Play className=\"size-4\" />\n Run analysis\n </Button>\n </div>\n\n {analysesLoading ? (\n <LoadingState variant=\"rows\" rows={3} label=\"Loading analyses…\" />\n ) : analysesError ? (\n <ErrorState\n message=\"We could not load analyses for this notebook.\"\n onRetry={() => void refetchAnalyses()}\n />\n ) : analyses.length === 0 ? (\n <EmptyState\n icon={Beaker}\n title=\"No analyses yet.\"\n description=\"Run an analysis to generate results and artifacts from this notebook.\"\n action={\n <Button type=\"button\" onClick={() => setRunOpen(true)}>\n <Play className=\"size-4\" />\n Run analysis\n </Button>\n }\n />\n ) : (\n <>\n {/* Desktop table */}\n <div className=\"hidden rounded-card border border-border-default bg-bg-elevated md:block\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Title</TableHead>\n <TableHead>Method</TableHead>\n <TableHead>AI</TableHead>\n <TableHead>Status</TableHead>\n <TableHead>Started</TableHead>\n <TableHead>Output</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {analyses.map((analysis: CosAnalysis) => (\n <TableRow key={analysis.id}>\n <TableCell className=\"font-medium text-text-primary\">\n {analysis.title}\n </TableCell>\n <TableCell className=\"text-text-secondary\">\n {analysis.method ?? '—'}\n </TableCell>\n <TableCell>\n {analysis.aiAssisted ? (\n <Badge variant=\"secondary\" className=\"gap-1\">\n <Sparkles className=\"size-3\" aria-hidden=\"true\" />\n AI\n </Badge>\n ) : (\n <span className=\"text-text-secondary\">—</span>\n )}\n </TableCell>\n <TableCell>\n <StatusBadge status={analysis.status} />\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-text-secondary\">\n {formatDateTime(analysis.startedAt)}\n </TableCell>\n <TableCell>\n {analysis.outputFile?.id ? (\n <FileDownloadLink fileId={analysis.outputFile.id} label=\"Download\" />\n ) : analysis.results && Object.keys(analysis.results).length > 0 ? (\n <span className=\"text-xs text-text-secondary\">Results recorded</span>\n ) : (\n <span className=\"text-text-secondary\">—</span>\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n\n {/* Mobile card list */}\n <ul className=\"space-y-3 md:hidden\">\n {analyses.map((analysis: CosAnalysis) => (\n <li\n key={analysis.id}\n className=\"space-y-2 rounded-card border border-border-default bg-bg-elevated p-4\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate font-medium text-text-primary\">{analysis.title}</p>\n {analysis.method ? (\n <p className=\"truncate text-xs text-text-secondary\">{analysis.method}</p>\n ) : null}\n </div>\n <StatusBadge status={analysis.status} />\n </div>\n <div className=\"flex flex-wrap items-center gap-2 text-xs text-text-secondary\">\n {analysis.aiAssisted ? (\n <Badge variant=\"secondary\" className=\"gap-1\">\n <Sparkles className=\"size-3\" aria-hidden=\"true\" />\n AI\n </Badge>\n ) : null}\n <span>Started {formatDateTime(analysis.startedAt)}</span>\n </div>\n <div>\n {analysis.outputFile?.id ? (\n <FileDownloadLink fileId={analysis.outputFile.id} label=\"Download\" />\n ) : analysis.results && Object.keys(analysis.results).length > 0 ? (\n <span className=\"text-xs text-text-secondary\">Results recorded</span>\n ) : (\n <span className=\"text-xs text-text-secondary\">No output</span>\n )}\n </div>\n </li>\n ))}\n </ul>\n </>\n )}\n </section>\n </div>\n\n <RunAnalysisDialog\n notebookId={notebook.id}\n open={runOpen}\n onOpenChange={setRunOpen}\n onCompleted={() => void refetchAnalyses()}\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -7,7 +7,7 @@ import c from "../../components/FileUploadField.js";
7
7
  import "../../components/index.js";
8
8
  import { useState as l } from "react";
9
9
  import { jsx as u, jsxs as d } from "react/jsx-runtime";
10
- import { useParams as f } from "react-router-dom";
10
+ import { useParams as f } from "react-router";
11
11
  import { Checkbox as p, Input as m, Label as h, Select as g, SelectContent as _, SelectItem as v, SelectTrigger as y, SelectValue as b, Switch as x, Textarea as S } from "@burdenoff/fe-libs/ui";
12
12
  //#region src/cosmicintersection/pages/notebooks/NotebookFormPage.tsx
13
13
  var C = [
@@ -1 +1 @@
1
- {"version":3,"file":"NotebookFormPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/notebooks/NotebookFormPage.tsx"],"sourcesContent":["/**\n * NotebookFormPage — create / edit a computational notebook.\n *\n * Notebook artifacts (the .ipynb-style file) are NOT stored by this MFE — they\n * live in wspace-files and are attached via FileUploadField, which hands back a\n * `fileId`. Datasets are linked by id (multi-select from `cosListDatasets`),\n * tags use the shared chips editor, and `environment` is free-form JSON. On\n * submit the form calls `cosCreateNotebook` / `cosUpdateNotebook`.\n */\nimport { useState, type FormEvent } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n Checkbox,\n Input,\n Label,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Switch,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useCosCreateNotebookMutation,\n useCosDatasetsQuery,\n useCosNotebookQuery,\n useCosUpdateNotebookMutation,\n type CosCreateNotebookInput,\n type CosNotebookStatus,\n type CosUpdateNotebookInput,\n} from '../../api/cosmicApi';\nimport { EntityFormShell, EntityTagsField, FileUploadField } from '../../components';\nimport { useCosmicIntersection } from '../../context/CosmicIntersectionContext';\nimport { notify } from '../../lib/toast';\n\nconst STATUS_OPTIONS: { value: CosNotebookStatus; label: string }[] = [\n { value: 'DRAFT', label: 'Draft' },\n { value: 'RUNNING', label: 'Running' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\ninterface FormState {\n title: string;\n description: string;\n runtime: string;\n environment: string;\n datasetIds: string[];\n tags: string[];\n fileId: string;\n status: CosNotebookStatus;\n isShared: boolean;\n shareSlug: string;\n}\n\nconst INITIAL: FormState = {\n title: '',\n description: '',\n runtime: '',\n environment: '',\n datasetIds: [],\n tags: [],\n fileId: '',\n status: 'DRAFT',\n isShared: false,\n shareSlug: '',\n};\n\n/** Parse the optional environment JSON. Returns `undefined` when blank, or an\n * error string when the text is not a valid JSON object. */\nfunction parseEnvironment(\n raw: string\n): { ok: true; value?: Record<string, unknown> } | { ok: false; error: string } {\n const trimmed = raw.trim();\n if (!trimmed) {\n return { ok: true, value: undefined };\n }\n try {\n const parsed: unknown = JSON.parse(trimmed);\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return { ok: false, error: 'Environment must be a JSON object.' };\n }\n return { ok: true, value: parsed as Record<string, unknown> };\n } catch {\n return { ok: false, error: 'Environment is not valid JSON.' };\n }\n}\n\nexport default function NotebookFormPage() {\n const { id = '' } = useParams<{ id: string }>();\n const isEdit = Boolean(id);\n const { navigate } = useCosmicIntersection();\n\n const { data: notebookData, loading: loadingNotebook } = useCosNotebookQuery(id);\n const { data: datasetsData, loading: loadingDatasets } = useCosDatasetsQuery({\n pagination: { limit: 100, offset: 0 },\n });\n const [createNotebook, { loading: creating }] = useCosCreateNotebookMutation();\n const [updateNotebook, { loading: updating }] = useCosUpdateNotebookMutation();\n\n const [form, setForm] = useState<FormState>(INITIAL);\n const datasets = datasetsData?.cosListDatasets.items ?? [];\n\n // Hydrate the form from the fetched notebook exactly once per loaded record,\n // using React's documented \"adjust state while rendering\" pattern (no effect,\n // no cascading render) rather than a setState-in-effect.\n const notebook = notebookData?.cosNotebook ?? null;\n const [hydratedId, setHydratedId] = useState<string | null>(null);\n if (isEdit && notebook && hydratedId !== notebook.id) {\n setHydratedId(notebook.id);\n setForm({\n title: notebook.title ?? '',\n description: notebook.description ?? '',\n runtime: notebook.runtime ?? '',\n environment: notebook.environment ? JSON.stringify(notebook.environment, null, 2) : '',\n datasetIds: notebook.datasetIds ?? [],\n tags: notebook.tags ?? [],\n fileId: notebook.file?.id ?? '',\n status: notebook.status ?? 'DRAFT',\n isShared: notebook.isShared ?? false,\n shareSlug: notebook.shareSlug ?? '',\n });\n }\n\n const setField = <K extends keyof FormState>(key: K, value: FormState[K]) =>\n setForm((prev) => ({ ...prev, [key]: value }));\n\n const toggleDataset = (datasetId: string) =>\n setForm((prev) => ({\n ...prev,\n datasetIds: prev.datasetIds.includes(datasetId)\n ? prev.datasetIds.filter((existing) => existing !== datasetId)\n : [...prev.datasetIds, datasetId],\n }));\n\n const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n const title = form.title.trim();\n if (!title) {\n notify.error('Title is required', 'Give the notebook a descriptive title.');\n return;\n }\n\n const env = parseEnvironment(form.environment);\n if (!env.ok) {\n notify.error('Invalid environment', env.error);\n return;\n }\n\n if (isEdit) {\n const input: CosUpdateNotebookInput = {\n title,\n description: form.description.trim() || undefined,\n runtime: form.runtime.trim() || undefined,\n environment: env.value,\n datasetIds: form.datasetIds,\n tags: form.tags,\n status: form.status,\n isShared: form.isShared,\n shareSlug: form.shareSlug.trim() || undefined,\n };\n const response = await updateNotebook({ variables: { id, input } });\n if (response.error || !response.data?.cosUpdateNotebook) {\n return;\n }\n notify.success('Notebook updated', `“${title}” was saved.`);\n navigate(`notebooks/${id}`);\n return;\n }\n\n const input: CosCreateNotebookInput = {\n title,\n description: form.description.trim() || undefined,\n runtime: form.runtime.trim() || undefined,\n environment: env.value,\n datasetIds: form.datasetIds,\n tags: form.tags,\n fileId: form.fileId || undefined,\n };\n const response = await createNotebook({ variables: { input } });\n const createdId = response.data?.cosCreateNotebook?.id;\n if (response.error || !createdId) {\n return;\n }\n notify.success('Notebook created', `“${title}” is ready.`);\n navigate(`notebooks/${createdId}`);\n };\n\n const saving = creating || updating;\n\n return (\n <EntityFormShell\n title={isEdit ? 'Edit notebook' : 'New notebook'}\n description=\"Set up a computational notebook and link the datasets it analyses.\"\n purpose=\"Notebooks capture the code and environment behind an analysis. Attach the notebook file, pick which federated datasets it reads, and record the runtime so results are reproducible.\"\n backLabel={isEdit ? 'Back to notebook' : 'Back to notebooks'}\n onBack={() => navigate(isEdit ? `notebooks/${id}` : 'notebooks')}\n onSubmit={handleSubmit}\n submitLabel={isEdit ? 'Save notebook' : 'Create notebook'}\n saving={saving}\n submitDisabled={isEdit && loadingNotebook}\n >\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"notebook-title\">Title</Label>\n <Input\n id=\"notebook-title\"\n value={form.title}\n onChange={(event) => setField('title', event.target.value)}\n placeholder=\"e.g. Kepler light-curve detrending\"\n required\n />\n </div>\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"notebook-runtime\">Runtime</Label>\n <Input\n id=\"notebook-runtime\"\n value={form.runtime}\n onChange={(event) => setField('runtime', event.target.value)}\n placeholder=\"e.g. python-3.11\"\n />\n </div>\n </div>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"notebook-description\">Description</Label>\n <Textarea\n id=\"notebook-description\"\n value={form.description}\n onChange={(event) => setField('description', event.target.value)}\n rows={3}\n placeholder=\"What does this notebook do?\"\n />\n </div>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"notebook-environment\">Environment (JSON)</Label>\n <Textarea\n id=\"notebook-environment\"\n value={form.environment}\n onChange={(event) => setField('environment', event.target.value)}\n rows={4}\n className=\"font-mono text-xs\"\n placeholder='{ \"numpy\": \"1.26\", \"astropy\": \"6.0\" }'\n />\n <p className=\"text-xs text-text-secondary\">\n Optional dependency / kernel metadata as a JSON object.\n </p>\n </div>\n\n <div className=\"space-y-2\">\n <Label>Linked datasets</Label>\n {loadingDatasets ? (\n <p className=\"text-sm text-text-secondary\">Loading datasets…</p>\n ) : datasets.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n No datasets available yet. Create a dataset first to link it here.\n </p>\n ) : (\n <div className=\"max-h-56 space-y-1 overflow-auto rounded-card border border-border-default bg-bg-sunken p-2\">\n {datasets.map((dataset) => {\n const checked = form.datasetIds.includes(dataset.id);\n return (\n <label\n key={dataset.id}\n className=\"flex cursor-pointer items-center gap-3 rounded-button px-2 py-1.5 hover:bg-bg-elevated\"\n >\n <Checkbox\n checked={checked}\n onCheckedChange={() => toggleDataset(dataset.id)}\n aria-label={`Link ${dataset.name}`}\n />\n <span className=\"min-w-0 flex-1 truncate text-sm text-text-primary\">\n {dataset.name}\n </span>\n <span className=\"text-xs text-text-secondary\">{dataset.status}</span>\n </label>\n );\n })}\n </div>\n )}\n {form.datasetIds.length > 0 ? (\n <p className=\"text-xs text-text-secondary\">\n {form.datasetIds.length} dataset{form.datasetIds.length === 1 ? '' : 's'} linked.\n </p>\n ) : null}\n </div>\n\n <EntityTagsField value={form.tags} onChange={(tags) => setField('tags', tags)} />\n\n <FileUploadField\n value={form.fileId}\n onChange={(fileId) => setField('fileId', fileId)}\n label=\"Attach notebook file\"\n hint=\"Upload the notebook artifact (.ipynb or any file). Stored in workspace files.\"\n />\n\n {isEdit ? (\n <div className=\"space-y-4 rounded-card border border-border-default bg-bg-sunken p-4\">\n <p className=\"text-sm font-medium text-text-primary\">Lifecycle &amp; sharing</p>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"notebook-status\">Status</Label>\n <Select\n value={form.status}\n onValueChange={(value) => setField('status', value as CosNotebookStatus)}\n >\n <SelectTrigger id=\"notebook-status\" aria-label=\"Notebook status\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {STATUS_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"notebook-share-slug\">Share slug</Label>\n <Input\n id=\"notebook-share-slug\"\n value={form.shareSlug}\n onChange={(event) => setField('shareSlug', event.target.value)}\n placeholder=\"e.g. kepler-detrend\"\n disabled={!form.isShared}\n />\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <Switch\n id=\"notebook-shared\"\n checked={form.isShared}\n onCheckedChange={(checked) => setField('isShared', checked)}\n />\n <Label htmlFor=\"notebook-shared\" className=\"cursor-pointer\">\n Shared with the workspace\n </Label>\n </div>\n </div>\n ) : null}\n </EntityFormShell>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"NotebookFormPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/notebooks/NotebookFormPage.tsx"],"sourcesContent":["/**\n * NotebookFormPage — create / edit a computational notebook.\n *\n * Notebook artifacts (the .ipynb-style file) are NOT stored by this MFE — they\n * live in wspace-files and are attached via FileUploadField, which hands back a\n * `fileId`. Datasets are linked by id (multi-select from `cosListDatasets`),\n * tags use the shared chips editor, and `environment` is free-form JSON. On\n * submit the form calls `cosCreateNotebook` / `cosUpdateNotebook`.\n */\nimport { useState, type FormEvent } from 'react';\nimport { useParams } from 'react-router';\nimport {\n Checkbox,\n Input,\n Label,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Switch,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useCosCreateNotebookMutation,\n useCosDatasetsQuery,\n useCosNotebookQuery,\n useCosUpdateNotebookMutation,\n type CosCreateNotebookInput,\n type CosNotebookStatus,\n type CosUpdateNotebookInput,\n} from '../../api/cosmicApi';\nimport { EntityFormShell, EntityTagsField, FileUploadField } from '../../components';\nimport { useCosmicIntersection } from '../../context/CosmicIntersectionContext';\nimport { notify } from '../../lib/toast';\n\nconst STATUS_OPTIONS: { value: CosNotebookStatus; label: string }[] = [\n { value: 'DRAFT', label: 'Draft' },\n { value: 'RUNNING', label: 'Running' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\ninterface FormState {\n title: string;\n description: string;\n runtime: string;\n environment: string;\n datasetIds: string[];\n tags: string[];\n fileId: string;\n status: CosNotebookStatus;\n isShared: boolean;\n shareSlug: string;\n}\n\nconst INITIAL: FormState = {\n title: '',\n description: '',\n runtime: '',\n environment: '',\n datasetIds: [],\n tags: [],\n fileId: '',\n status: 'DRAFT',\n isShared: false,\n shareSlug: '',\n};\n\n/** Parse the optional environment JSON. Returns `undefined` when blank, or an\n * error string when the text is not a valid JSON object. */\nfunction parseEnvironment(\n raw: string\n): { ok: true; value?: Record<string, unknown> } | { ok: false; error: string } {\n const trimmed = raw.trim();\n if (!trimmed) {\n return { ok: true, value: undefined };\n }\n try {\n const parsed: unknown = JSON.parse(trimmed);\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return { ok: false, error: 'Environment must be a JSON object.' };\n }\n return { ok: true, value: parsed as Record<string, unknown> };\n } catch {\n return { ok: false, error: 'Environment is not valid JSON.' };\n }\n}\n\nexport default function NotebookFormPage() {\n const { id = '' } = useParams<{ id: string }>();\n const isEdit = Boolean(id);\n const { navigate } = useCosmicIntersection();\n\n const { data: notebookData, loading: loadingNotebook } = useCosNotebookQuery(id);\n const { data: datasetsData, loading: loadingDatasets } = useCosDatasetsQuery({\n pagination: { limit: 100, offset: 0 },\n });\n const [createNotebook, { loading: creating }] = useCosCreateNotebookMutation();\n const [updateNotebook, { loading: updating }] = useCosUpdateNotebookMutation();\n\n const [form, setForm] = useState<FormState>(INITIAL);\n const datasets = datasetsData?.cosListDatasets.items ?? [];\n\n // Hydrate the form from the fetched notebook exactly once per loaded record,\n // using React's documented \"adjust state while rendering\" pattern (no effect,\n // no cascading render) rather than a setState-in-effect.\n const notebook = notebookData?.cosNotebook ?? null;\n const [hydratedId, setHydratedId] = useState<string | null>(null);\n if (isEdit && notebook && hydratedId !== notebook.id) {\n setHydratedId(notebook.id);\n setForm({\n title: notebook.title ?? '',\n description: notebook.description ?? '',\n runtime: notebook.runtime ?? '',\n environment: notebook.environment ? JSON.stringify(notebook.environment, null, 2) : '',\n datasetIds: notebook.datasetIds ?? [],\n tags: notebook.tags ?? [],\n fileId: notebook.file?.id ?? '',\n status: notebook.status ?? 'DRAFT',\n isShared: notebook.isShared ?? false,\n shareSlug: notebook.shareSlug ?? '',\n });\n }\n\n const setField = <K extends keyof FormState>(key: K, value: FormState[K]) =>\n setForm((prev) => ({ ...prev, [key]: value }));\n\n const toggleDataset = (datasetId: string) =>\n setForm((prev) => ({\n ...prev,\n datasetIds: prev.datasetIds.includes(datasetId)\n ? prev.datasetIds.filter((existing) => existing !== datasetId)\n : [...prev.datasetIds, datasetId],\n }));\n\n const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n const title = form.title.trim();\n if (!title) {\n notify.error('Title is required', 'Give the notebook a descriptive title.');\n return;\n }\n\n const env = parseEnvironment(form.environment);\n if (!env.ok) {\n notify.error('Invalid environment', env.error);\n return;\n }\n\n if (isEdit) {\n const input: CosUpdateNotebookInput = {\n title,\n description: form.description.trim() || undefined,\n runtime: form.runtime.trim() || undefined,\n environment: env.value,\n datasetIds: form.datasetIds,\n tags: form.tags,\n status: form.status,\n isShared: form.isShared,\n shareSlug: form.shareSlug.trim() || undefined,\n };\n const response = await updateNotebook({ variables: { id, input } });\n if (response.error || !response.data?.cosUpdateNotebook) {\n return;\n }\n notify.success('Notebook updated', `“${title}” was saved.`);\n navigate(`notebooks/${id}`);\n return;\n }\n\n const input: CosCreateNotebookInput = {\n title,\n description: form.description.trim() || undefined,\n runtime: form.runtime.trim() || undefined,\n environment: env.value,\n datasetIds: form.datasetIds,\n tags: form.tags,\n fileId: form.fileId || undefined,\n };\n const response = await createNotebook({ variables: { input } });\n const createdId = response.data?.cosCreateNotebook?.id;\n if (response.error || !createdId) {\n return;\n }\n notify.success('Notebook created', `“${title}” is ready.`);\n navigate(`notebooks/${createdId}`);\n };\n\n const saving = creating || updating;\n\n return (\n <EntityFormShell\n title={isEdit ? 'Edit notebook' : 'New notebook'}\n description=\"Set up a computational notebook and link the datasets it analyses.\"\n purpose=\"Notebooks capture the code and environment behind an analysis. Attach the notebook file, pick which federated datasets it reads, and record the runtime so results are reproducible.\"\n backLabel={isEdit ? 'Back to notebook' : 'Back to notebooks'}\n onBack={() => navigate(isEdit ? `notebooks/${id}` : 'notebooks')}\n onSubmit={handleSubmit}\n submitLabel={isEdit ? 'Save notebook' : 'Create notebook'}\n saving={saving}\n submitDisabled={isEdit && loadingNotebook}\n >\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"notebook-title\">Title</Label>\n <Input\n id=\"notebook-title\"\n value={form.title}\n onChange={(event) => setField('title', event.target.value)}\n placeholder=\"e.g. Kepler light-curve detrending\"\n required\n />\n </div>\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"notebook-runtime\">Runtime</Label>\n <Input\n id=\"notebook-runtime\"\n value={form.runtime}\n onChange={(event) => setField('runtime', event.target.value)}\n placeholder=\"e.g. python-3.11\"\n />\n </div>\n </div>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"notebook-description\">Description</Label>\n <Textarea\n id=\"notebook-description\"\n value={form.description}\n onChange={(event) => setField('description', event.target.value)}\n rows={3}\n placeholder=\"What does this notebook do?\"\n />\n </div>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"notebook-environment\">Environment (JSON)</Label>\n <Textarea\n id=\"notebook-environment\"\n value={form.environment}\n onChange={(event) => setField('environment', event.target.value)}\n rows={4}\n className=\"font-mono text-xs\"\n placeholder='{ \"numpy\": \"1.26\", \"astropy\": \"6.0\" }'\n />\n <p className=\"text-xs text-text-secondary\">\n Optional dependency / kernel metadata as a JSON object.\n </p>\n </div>\n\n <div className=\"space-y-2\">\n <Label>Linked datasets</Label>\n {loadingDatasets ? (\n <p className=\"text-sm text-text-secondary\">Loading datasets…</p>\n ) : datasets.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n No datasets available yet. Create a dataset first to link it here.\n </p>\n ) : (\n <div className=\"max-h-56 space-y-1 overflow-auto rounded-card border border-border-default bg-bg-sunken p-2\">\n {datasets.map((dataset) => {\n const checked = form.datasetIds.includes(dataset.id);\n return (\n <label\n key={dataset.id}\n className=\"flex cursor-pointer items-center gap-3 rounded-button px-2 py-1.5 hover:bg-bg-elevated\"\n >\n <Checkbox\n checked={checked}\n onCheckedChange={() => toggleDataset(dataset.id)}\n aria-label={`Link ${dataset.name}`}\n />\n <span className=\"min-w-0 flex-1 truncate text-sm text-text-primary\">\n {dataset.name}\n </span>\n <span className=\"text-xs text-text-secondary\">{dataset.status}</span>\n </label>\n );\n })}\n </div>\n )}\n {form.datasetIds.length > 0 ? (\n <p className=\"text-xs text-text-secondary\">\n {form.datasetIds.length} dataset{form.datasetIds.length === 1 ? '' : 's'} linked.\n </p>\n ) : null}\n </div>\n\n <EntityTagsField value={form.tags} onChange={(tags) => setField('tags', tags)} />\n\n <FileUploadField\n value={form.fileId}\n onChange={(fileId) => setField('fileId', fileId)}\n label=\"Attach notebook file\"\n hint=\"Upload the notebook artifact (.ipynb or any file). Stored in workspace files.\"\n />\n\n {isEdit ? (\n <div className=\"space-y-4 rounded-card border border-border-default bg-bg-sunken p-4\">\n <p className=\"text-sm font-medium text-text-primary\">Lifecycle &amp; sharing</p>\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"notebook-status\">Status</Label>\n <Select\n value={form.status}\n onValueChange={(value) => setField('status', value as CosNotebookStatus)}\n >\n <SelectTrigger id=\"notebook-status\" aria-label=\"Notebook status\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {STATUS_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"notebook-share-slug\">Share slug</Label>\n <Input\n id=\"notebook-share-slug\"\n value={form.shareSlug}\n onChange={(event) => setField('shareSlug', event.target.value)}\n placeholder=\"e.g. kepler-detrend\"\n disabled={!form.isShared}\n />\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <Switch\n id=\"notebook-shared\"\n checked={form.isShared}\n onCheckedChange={(checked) => setField('isShared', checked)}\n />\n <Label htmlFor=\"notebook-shared\" className=\"cursor-pointer\">\n Shared with the workspace\n </Label>\n </div>\n </div>\n ) : null}\n </EntityFormShell>\n );\n}\n"],"mappings":"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"}
@@ -12,7 +12,7 @@ import ne from "../../components/StepDAG.js";
12
12
  import { mutationSucceeded as p } from "../../lib/mutationResult.js";
13
13
  import { useMemo as m, useState as re } from "react";
14
14
  import { Fragment as h, jsx as g, jsxs as _ } from "react/jsx-runtime";
15
- import { useParams as ie } from "react-router-dom";
15
+ import { useParams as ie } from "react-router";
16
16
  import { Archive as ae, Clock as oe, Hash as se, Pencil as v, Play as y } from "lucide-react";
17
17
  import { AlertDialog as ce, AlertDialogAction as le, AlertDialogCancel as ue, AlertDialogContent as de, AlertDialogDescription as fe, AlertDialogFooter as pe, AlertDialogHeader as me, AlertDialogTitle as he, Button as b, Card as x, CardContent as S, CardHeader as C, CardTitle as w, Table as ge, TableBody as _e, TableCell as T, TableHead as E, TableHeader as ve, TableRow as D, buttonVariants as ye } from "@burdenoff/fe-libs/ui";
18
18
  //#region src/cosmicintersection/pages/pipelines/PipelineDetailPage.tsx
@@ -1 +1 @@
1
- {"version":3,"file":"PipelineDetailPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/pipelines/PipelineDetailPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { Archive, Clock, Hash, Pencil, Play } from 'lucide-react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n Button,\n buttonVariants,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useCosArchivePipelineMutation,\n useCosPipelineQuery,\n useCosRunStepsQuery,\n useCosRunsQuery,\n useCosStartRunMutation,\n type CosStepKind,\n} from '../../api/cosmicApi';\nimport {\n EmptyState,\n ErrorState,\n LoadingState,\n PageLayout,\n Pagination,\n StatusBadge,\n formatEnumLabel,\n usePagination,\n} from '../../components';\nimport { StepDAG, type StepDAGStep } from '../../components/StepDAG';\nimport { useCosmicIntersection } from '../../context/CosmicIntersectionContext';\nimport { mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\nconst STEP_KINDS: CosStepKind[] = [\n 'INGEST',\n 'CALIBRATE',\n 'TRANSFORM',\n 'ANALYZE',\n 'EXPORT',\n 'NOTIFY',\n];\n\nfunction isStepKind(value: unknown): value is CosStepKind {\n return typeof value === 'string' && (STEP_KINDS as string[]).includes(value);\n}\n\n/**\n * Parse an ordered step list out of a pipeline definition object. Tolerates\n * unknown/legacy shapes — only well-formed `{ kind, name? }` entries survive.\n */\nfunction parseDefinitionSteps(definition?: Record<string, unknown> | null): StepDAGStep[] {\n if (!definition) {\n return [];\n }\n const raw = (definition as { steps?: unknown }).steps;\n if (!Array.isArray(raw)) {\n return [];\n }\n const out: StepDAGStep[] = [];\n for (const item of raw) {\n if (item && typeof item === 'object' && !Array.isArray(item)) {\n const kind = (item as { kind?: unknown }).kind;\n const name = (item as { name?: unknown }).name;\n if (isStepKind(kind)) {\n out.push({ kind, name: typeof name === 'string' ? name : undefined });\n }\n }\n }\n return out;\n}\n\nfunction formatDate(value?: string | null): string {\n if (!value) {\n return '—';\n }\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? '—' : date.toLocaleString();\n}\n\nfunction formatDuration(ms?: number | null): string {\n if (ms === null || ms === undefined) {\n return '—';\n }\n if (ms < 1000) {\n return `${ms} ms`;\n }\n const seconds = ms / 1000;\n if (seconds < 60) {\n return `${seconds.toFixed(1)} s`;\n }\n const minutes = Math.floor(seconds / 60);\n const rest = Math.round(seconds % 60);\n return `${minutes}m ${rest}s`;\n}\n\nfunction DetailRow({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <div className=\"flex flex-col gap-1 border-b border-border-seam py-3 last:border-0 sm:flex-row sm:items-start sm:gap-4\">\n <span className=\"w-40 shrink-0 text-sm font-medium text-text-secondary\">{label}</span>\n <span className=\"min-w-0 text-sm text-text-primary\">{children}</span>\n </div>\n );\n}\n\n/** Small pill used in the header meta row (trigger / cron / version). */\nfunction MetaPill({ icon: Icon, children }: { icon?: typeof Clock; children: React.ReactNode }) {\n return (\n <span className=\"inline-flex items-center gap-1.5 rounded-full border border-border-default bg-bg-elevated px-2.5 py-0.5 text-xs text-text-secondary\">\n {Icon ? <Icon className=\"size-3.5\" aria-hidden=\"true\" /> : null}\n {children}\n </span>\n );\n}\n\nexport default function PipelineDetailPage() {\n const { id = '' } = useParams<{ id: string }>();\n const { navigate } = useCosmicIntersection();\n\n const { data, loading, error, refetch } = useCosPipelineQuery(id);\n const runsPagination = usePagination({ pageSize: 10, resetKey: id });\n const {\n data: runsData,\n loading: runsLoading,\n error: runsError,\n refetch: refetchRuns,\n } = useCosRunsQuery({\n pipelineId: id,\n pagination: { limit: runsPagination.limit, offset: runsPagination.offset },\n });\n\n const pipeline = data?.cosPipeline;\n const runs = useMemo(() => runsData?.cosListRuns.items ?? [], [runsData]);\n const runsPageInfo = runsData?.cosListRuns.pageInfo;\n\n // Prefer the authored definition steps. If the definition has none, fall back\n // to the most recent run's recorded steps so the flow is never empty when the\n // pipeline has actually executed.\n const definitionSteps = useMemo(\n () => parseDefinitionSteps(pipeline?.definition),\n [pipeline?.definition]\n );\n const latestRunId = runs[0]?.id ?? '';\n const shouldDerive = Boolean(pipeline) && definitionSteps.length === 0 && Boolean(latestRunId);\n const { data: latestStepsData } = useCosRunStepsQuery(shouldDerive ? latestRunId : '');\n const derivedSteps = useMemo<StepDAGStep[]>(() => {\n const list = latestStepsData?.cosListRunSteps ?? [];\n return [...list]\n .sort((a, b) => a.position - b.position)\n .map((step) => ({ kind: step.kind, name: step.name, status: step.status }));\n }, [latestStepsData]);\n const dagSteps = definitionSteps.length > 0 ? definitionSteps : derivedSteps;\n const derivedFromRun = definitionSteps.length === 0 && derivedSteps.length > 0;\n\n const [startRun, { loading: starting }] = useCosStartRunMutation();\n const [archivePipeline, { loading: archiving }] = useCosArchivePipelineMutation();\n const [confirmArchive, setConfirmArchive] = useState(false);\n\n const handleStartRun = async () => {\n if (!pipeline) {\n return;\n }\n const response = await startRun({ variables: { input: { pipelineId: pipeline.id } } });\n const runId = response.data?.cosStartRun?.id;\n if (!mutationSucceeded(response, runId, 'Could not start a run')) {\n return;\n }\n notify.success('Run started', 'A new run was queued.');\n await Promise.all([refetch(), refetchRuns()]);\n if (runId) {\n navigate(`runs/${runId}`);\n }\n };\n\n const handleArchive = async () => {\n if (!pipeline) {\n return;\n }\n const response = await archivePipeline({ variables: { id: pipeline.id } });\n if (\n !mutationSucceeded(response, response.data?.cosArchivePipeline, 'Could not archive pipeline')\n ) {\n return;\n }\n setConfirmArchive(false);\n notify.success('Pipeline archived', `“${pipeline.name}” was archived.`);\n navigate('pipelines');\n };\n\n if (loading) {\n return (\n <PageLayout title=\"Pipeline\" backPath=\"pipelines\">\n <LoadingState label=\"Loading pipeline…\" />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout title=\"Pipeline\" backPath=\"pipelines\">\n <ErrorState onRetry={() => void refetch()} />\n </PageLayout>\n );\n }\n\n if (!pipeline) {\n return (\n <PageLayout title=\"Pipeline\" backPath=\"pipelines\">\n <EmptyState\n title=\"Pipeline not found\"\n description=\"This pipeline may have been archived or you may not have access to it.\"\n action={\n <Button type=\"button\" variant=\"outline\" onClick={() => navigate('pipelines')}>\n Back to pipelines\n </Button>\n }\n />\n </PageLayout>\n );\n }\n\n const isArchived = pipeline.status === 'ARCHIVED';\n const hasDefinition = Boolean(pipeline.definition && Object.keys(pipeline.definition).length > 0);\n\n return (\n <PageLayout\n title={pipeline.name}\n description={pipeline.description ?? undefined}\n backPath=\"pipelines\"\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button\n type=\"button\"\n onClick={() => void handleStartRun()}\n disabled={starting || isArchived}\n >\n <Play className=\"size-4\" />\n {starting ? 'Starting…' : 'Start run'}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => navigate(`pipelines/${pipeline.id}/edit`)}\n >\n <Pencil className=\"size-4\" />\n Edit\n </Button>\n {!isArchived ? (\n <Button type=\"button\" variant=\"outline\" onClick={() => setConfirmArchive(true)}>\n <Archive className=\"size-4\" />\n Archive\n </Button>\n ) : null}\n </div>\n }\n >\n <div className=\"space-y-6\">\n {/* Header meta — status, trigger, cron, version — mirrors the mock's badge row. */}\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={pipeline.status} />\n <MetaPill icon={Clock}>{formatEnumLabel(pipeline.trigger)}</MetaPill>\n {pipeline.schedule ? (\n <MetaPill>\n <span className=\"font-mono\">{pipeline.schedule}</span>\n </MetaPill>\n ) : null}\n <MetaPill icon={Hash}>v{pipeline.version}</MetaPill>\n </div>\n\n {/* Overview — the step-flow DAG (mockup 11). */}\n <Card>\n <CardHeader>\n <CardTitle>Overview</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"flex flex-wrap items-baseline justify-between gap-2\">\n <span className=\"text-sm font-medium text-text-secondary\">Step flow</span>\n {derivedFromRun ? (\n <span className=\"text-xs text-text-secondary\">Derived from the latest run.</span>\n ) : null}\n </div>\n\n {dagSteps.length > 0 ? (\n <StepDAG steps={dagSteps} />\n ) : (\n <div className=\"rounded-card border border-dashed border-border-default bg-bg-sunken px-4 py-8 text-center\">\n <p className=\"text-sm text-text-secondary\">\n No steps defined yet. Edit the pipeline to add ingest, calibrate, transform,\n analyze or export steps.\n </p>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"mt-3\"\n onClick={() => navigate(`pipelines/${pipeline.id}/edit`)}\n >\n <Pencil className=\"size-4\" />\n Add steps\n </Button>\n </div>\n )}\n\n {hasDefinition ? (\n <details className=\"group rounded-card border border-border-default bg-bg-sunken\">\n <summary className=\"flex cursor-pointer list-none items-center justify-between px-4 py-2.5 text-sm font-medium text-text-secondary transition-colors hover:text-text-primary\">\n Advanced — raw definition JSON\n <span className=\"text-xs text-text-secondary transition-transform group-open:rotate-180\">\n ▾\n </span>\n </summary>\n <pre className=\"overflow-x-auto border-t border-border-seam p-4 font-mono text-xs text-text-primary\">\n {JSON.stringify(pipeline.definition, null, 2)}\n </pre>\n </details>\n ) : null}\n </CardContent>\n </Card>\n\n {/* Details — the full attribute list (app value-add over the mock). */}\n <Card>\n <CardHeader>\n <CardTitle>Details</CardTitle>\n </CardHeader>\n <CardContent>\n <DetailRow label=\"Status\">\n <StatusBadge status={pipeline.status} />\n </DetailRow>\n <DetailRow label=\"Trigger\">{formatEnumLabel(pipeline.trigger)}</DetailRow>\n <DetailRow label=\"Schedule\">\n {pipeline.schedule ? (\n <span className=\"font-mono text-xs\">{pipeline.schedule}</span>\n ) : (\n '—'\n )}\n </DetailRow>\n <DetailRow label=\"Version\">v{pipeline.version}</DetailRow>\n <DetailRow label=\"Last run\">{formatDate(pipeline.lastRunAt)}</DetailRow>\n <DetailRow label=\"Created\">{formatDate(pipeline.createdAt)}</DetailRow>\n <DetailRow label=\"Tags\">\n {pipeline.tags.length === 0 ? (\n '—'\n ) : (\n <span className=\"flex flex-wrap gap-1\">\n {pipeline.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </span>\n )}\n </DetailRow>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Runs</CardTitle>\n </CardHeader>\n <CardContent>\n {runsLoading ? (\n <LoadingState variant=\"rows\" rows={4} label=\"Loading runs…\" />\n ) : runsError ? (\n <ErrorState message=\"Could not load runs.\" onRetry={() => void refetchRuns()} />\n ) : runs.length === 0 ? (\n <EmptyState\n icon={Play}\n title=\"No runs yet\"\n description=\"Start a run to process data through this pipeline.\"\n action={\n <Button type=\"button\" onClick={() => void handleStartRun()} disabled={isArchived}>\n <Play className=\"size-4\" />\n Start run\n </Button>\n }\n />\n ) : (\n <>\n {/* Desktop: table. */}\n <div className=\"hidden rounded-card border border-border-default md:block\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Run</TableHead>\n <TableHead>Status</TableHead>\n <TableHead>Started</TableHead>\n <TableHead>Duration</TableHead>\n <TableHead>Triggered by</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {runs.map((run) => (\n <TableRow\n key={run.id}\n className=\"cursor-pointer\"\n onClick={() => navigate(`runs/${run.id}`)}\n >\n <TableCell className=\"font-medium text-text-primary\">\n #{run.runNumber}\n </TableCell>\n <TableCell>\n <StatusBadge status={run.status} />\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-text-secondary\">\n {formatDate(run.startedAt)}\n </TableCell>\n <TableCell className=\"tabular-nums text-text-secondary\">\n {formatDuration(run.durationMs)}\n </TableCell>\n <TableCell className=\"text-text-secondary\">\n {run.triggeredBy || '—'}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n\n {/* Mobile: stacked cards. */}\n <ul className=\"space-y-3 md:hidden\">\n {runs.map((run) => (\n <li key={run.id}>\n <button\n type=\"button\"\n onClick={() => navigate(`runs/${run.id}`)}\n className=\"flex w-full flex-col gap-2 rounded-card border border-border-default bg-bg-elevated p-4 text-left transition-colors hover:border-border-strong\"\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"font-medium text-text-primary\">#{run.runNumber}</span>\n <StatusBadge status={run.status} />\n </div>\n <dl className=\"grid grid-cols-2 gap-x-3 gap-y-1 text-xs text-text-secondary\">\n <div className=\"flex flex-col\">\n <dt className=\"text-text-secondary\">Started</dt>\n <dd className=\"text-text-primary\">{formatDate(run.startedAt)}</dd>\n </div>\n <div className=\"flex flex-col\">\n <dt className=\"text-text-secondary\">Duration</dt>\n <dd className=\"tabular-nums text-text-primary\">\n {formatDuration(run.durationMs)}\n </dd>\n </div>\n <div className=\"col-span-2 flex flex-col\">\n <dt className=\"text-text-secondary\">Triggered by</dt>\n <dd className=\"text-text-primary\">{run.triggeredBy || '—'}</dd>\n </div>\n </dl>\n </button>\n </li>\n ))}\n </ul>\n\n <Pagination\n offset={runsPagination.offset}\n limit={runsPagination.limit}\n loadedCount={runs.length}\n hasMore={runsPageInfo?.hasMore ?? false}\n onPrev={runsPagination.prevPage}\n onNext={runsPagination.nextPage}\n itemLabel=\"runs\"\n loading={runsLoading}\n />\n </>\n )}\n </CardContent>\n </Card>\n </div>\n\n <AlertDialog\n open={confirmArchive}\n onOpenChange={(next) => (archiving ? undefined : setConfirmArchive(next))}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Archive “{pipeline.name}”?</AlertDialogTitle>\n <AlertDialogDescription>\n Archiving stops the pipeline from being triggered. Existing runs are kept for\n reference.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={archiving}>Cancel</AlertDialogCancel>\n <AlertDialogAction\n onClick={(event) => {\n event.preventDefault();\n void handleArchive();\n }}\n disabled={archiving}\n className={buttonVariants({ variant: 'destructive' })}\n >\n {archiving ? 'Archiving…' : 'Archive pipeline'}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"PipelineDetailPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/pipelines/PipelineDetailPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { Archive, Clock, Hash, Pencil, Play } from 'lucide-react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n Button,\n buttonVariants,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useCosArchivePipelineMutation,\n useCosPipelineQuery,\n useCosRunStepsQuery,\n useCosRunsQuery,\n useCosStartRunMutation,\n type CosStepKind,\n} from '../../api/cosmicApi';\nimport {\n EmptyState,\n ErrorState,\n LoadingState,\n PageLayout,\n Pagination,\n StatusBadge,\n formatEnumLabel,\n usePagination,\n} from '../../components';\nimport { StepDAG, type StepDAGStep } from '../../components/StepDAG';\nimport { useCosmicIntersection } from '../../context/CosmicIntersectionContext';\nimport { mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\nconst STEP_KINDS: CosStepKind[] = [\n 'INGEST',\n 'CALIBRATE',\n 'TRANSFORM',\n 'ANALYZE',\n 'EXPORT',\n 'NOTIFY',\n];\n\nfunction isStepKind(value: unknown): value is CosStepKind {\n return typeof value === 'string' && (STEP_KINDS as string[]).includes(value);\n}\n\n/**\n * Parse an ordered step list out of a pipeline definition object. Tolerates\n * unknown/legacy shapes — only well-formed `{ kind, name? }` entries survive.\n */\nfunction parseDefinitionSteps(definition?: Record<string, unknown> | null): StepDAGStep[] {\n if (!definition) {\n return [];\n }\n const raw = (definition as { steps?: unknown }).steps;\n if (!Array.isArray(raw)) {\n return [];\n }\n const out: StepDAGStep[] = [];\n for (const item of raw) {\n if (item && typeof item === 'object' && !Array.isArray(item)) {\n const kind = (item as { kind?: unknown }).kind;\n const name = (item as { name?: unknown }).name;\n if (isStepKind(kind)) {\n out.push({ kind, name: typeof name === 'string' ? name : undefined });\n }\n }\n }\n return out;\n}\n\nfunction formatDate(value?: string | null): string {\n if (!value) {\n return '—';\n }\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? '—' : date.toLocaleString();\n}\n\nfunction formatDuration(ms?: number | null): string {\n if (ms === null || ms === undefined) {\n return '—';\n }\n if (ms < 1000) {\n return `${ms} ms`;\n }\n const seconds = ms / 1000;\n if (seconds < 60) {\n return `${seconds.toFixed(1)} s`;\n }\n const minutes = Math.floor(seconds / 60);\n const rest = Math.round(seconds % 60);\n return `${minutes}m ${rest}s`;\n}\n\nfunction DetailRow({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <div className=\"flex flex-col gap-1 border-b border-border-seam py-3 last:border-0 sm:flex-row sm:items-start sm:gap-4\">\n <span className=\"w-40 shrink-0 text-sm font-medium text-text-secondary\">{label}</span>\n <span className=\"min-w-0 text-sm text-text-primary\">{children}</span>\n </div>\n );\n}\n\n/** Small pill used in the header meta row (trigger / cron / version). */\nfunction MetaPill({ icon: Icon, children }: { icon?: typeof Clock; children: React.ReactNode }) {\n return (\n <span className=\"inline-flex items-center gap-1.5 rounded-full border border-border-default bg-bg-elevated px-2.5 py-0.5 text-xs text-text-secondary\">\n {Icon ? <Icon className=\"size-3.5\" aria-hidden=\"true\" /> : null}\n {children}\n </span>\n );\n}\n\nexport default function PipelineDetailPage() {\n const { id = '' } = useParams<{ id: string }>();\n const { navigate } = useCosmicIntersection();\n\n const { data, loading, error, refetch } = useCosPipelineQuery(id);\n const runsPagination = usePagination({ pageSize: 10, resetKey: id });\n const {\n data: runsData,\n loading: runsLoading,\n error: runsError,\n refetch: refetchRuns,\n } = useCosRunsQuery({\n pipelineId: id,\n pagination: { limit: runsPagination.limit, offset: runsPagination.offset },\n });\n\n const pipeline = data?.cosPipeline;\n const runs = useMemo(() => runsData?.cosListRuns.items ?? [], [runsData]);\n const runsPageInfo = runsData?.cosListRuns.pageInfo;\n\n // Prefer the authored definition steps. If the definition has none, fall back\n // to the most recent run's recorded steps so the flow is never empty when the\n // pipeline has actually executed.\n const definitionSteps = useMemo(\n () => parseDefinitionSteps(pipeline?.definition),\n [pipeline?.definition]\n );\n const latestRunId = runs[0]?.id ?? '';\n const shouldDerive = Boolean(pipeline) && definitionSteps.length === 0 && Boolean(latestRunId);\n const { data: latestStepsData } = useCosRunStepsQuery(shouldDerive ? latestRunId : '');\n const derivedSteps = useMemo<StepDAGStep[]>(() => {\n const list = latestStepsData?.cosListRunSteps ?? [];\n return [...list]\n .sort((a, b) => a.position - b.position)\n .map((step) => ({ kind: step.kind, name: step.name, status: step.status }));\n }, [latestStepsData]);\n const dagSteps = definitionSteps.length > 0 ? definitionSteps : derivedSteps;\n const derivedFromRun = definitionSteps.length === 0 && derivedSteps.length > 0;\n\n const [startRun, { loading: starting }] = useCosStartRunMutation();\n const [archivePipeline, { loading: archiving }] = useCosArchivePipelineMutation();\n const [confirmArchive, setConfirmArchive] = useState(false);\n\n const handleStartRun = async () => {\n if (!pipeline) {\n return;\n }\n const response = await startRun({ variables: { input: { pipelineId: pipeline.id } } });\n const runId = response.data?.cosStartRun?.id;\n if (!mutationSucceeded(response, runId, 'Could not start a run')) {\n return;\n }\n notify.success('Run started', 'A new run was queued.');\n await Promise.all([refetch(), refetchRuns()]);\n if (runId) {\n navigate(`runs/${runId}`);\n }\n };\n\n const handleArchive = async () => {\n if (!pipeline) {\n return;\n }\n const response = await archivePipeline({ variables: { id: pipeline.id } });\n if (\n !mutationSucceeded(response, response.data?.cosArchivePipeline, 'Could not archive pipeline')\n ) {\n return;\n }\n setConfirmArchive(false);\n notify.success('Pipeline archived', `“${pipeline.name}” was archived.`);\n navigate('pipelines');\n };\n\n if (loading) {\n return (\n <PageLayout title=\"Pipeline\" backPath=\"pipelines\">\n <LoadingState label=\"Loading pipeline…\" />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout title=\"Pipeline\" backPath=\"pipelines\">\n <ErrorState onRetry={() => void refetch()} />\n </PageLayout>\n );\n }\n\n if (!pipeline) {\n return (\n <PageLayout title=\"Pipeline\" backPath=\"pipelines\">\n <EmptyState\n title=\"Pipeline not found\"\n description=\"This pipeline may have been archived or you may not have access to it.\"\n action={\n <Button type=\"button\" variant=\"outline\" onClick={() => navigate('pipelines')}>\n Back to pipelines\n </Button>\n }\n />\n </PageLayout>\n );\n }\n\n const isArchived = pipeline.status === 'ARCHIVED';\n const hasDefinition = Boolean(pipeline.definition && Object.keys(pipeline.definition).length > 0);\n\n return (\n <PageLayout\n title={pipeline.name}\n description={pipeline.description ?? undefined}\n backPath=\"pipelines\"\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button\n type=\"button\"\n onClick={() => void handleStartRun()}\n disabled={starting || isArchived}\n >\n <Play className=\"size-4\" />\n {starting ? 'Starting…' : 'Start run'}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => navigate(`pipelines/${pipeline.id}/edit`)}\n >\n <Pencil className=\"size-4\" />\n Edit\n </Button>\n {!isArchived ? (\n <Button type=\"button\" variant=\"outline\" onClick={() => setConfirmArchive(true)}>\n <Archive className=\"size-4\" />\n Archive\n </Button>\n ) : null}\n </div>\n }\n >\n <div className=\"space-y-6\">\n {/* Header meta — status, trigger, cron, version — mirrors the mock's badge row. */}\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={pipeline.status} />\n <MetaPill icon={Clock}>{formatEnumLabel(pipeline.trigger)}</MetaPill>\n {pipeline.schedule ? (\n <MetaPill>\n <span className=\"font-mono\">{pipeline.schedule}</span>\n </MetaPill>\n ) : null}\n <MetaPill icon={Hash}>v{pipeline.version}</MetaPill>\n </div>\n\n {/* Overview — the step-flow DAG (mockup 11). */}\n <Card>\n <CardHeader>\n <CardTitle>Overview</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"flex flex-wrap items-baseline justify-between gap-2\">\n <span className=\"text-sm font-medium text-text-secondary\">Step flow</span>\n {derivedFromRun ? (\n <span className=\"text-xs text-text-secondary\">Derived from the latest run.</span>\n ) : null}\n </div>\n\n {dagSteps.length > 0 ? (\n <StepDAG steps={dagSteps} />\n ) : (\n <div className=\"rounded-card border border-dashed border-border-default bg-bg-sunken px-4 py-8 text-center\">\n <p className=\"text-sm text-text-secondary\">\n No steps defined yet. Edit the pipeline to add ingest, calibrate, transform,\n analyze or export steps.\n </p>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"mt-3\"\n onClick={() => navigate(`pipelines/${pipeline.id}/edit`)}\n >\n <Pencil className=\"size-4\" />\n Add steps\n </Button>\n </div>\n )}\n\n {hasDefinition ? (\n <details className=\"group rounded-card border border-border-default bg-bg-sunken\">\n <summary className=\"flex cursor-pointer list-none items-center justify-between px-4 py-2.5 text-sm font-medium text-text-secondary transition-colors hover:text-text-primary\">\n Advanced — raw definition JSON\n <span className=\"text-xs text-text-secondary transition-transform group-open:rotate-180\">\n ▾\n </span>\n </summary>\n <pre className=\"overflow-x-auto border-t border-border-seam p-4 font-mono text-xs text-text-primary\">\n {JSON.stringify(pipeline.definition, null, 2)}\n </pre>\n </details>\n ) : null}\n </CardContent>\n </Card>\n\n {/* Details — the full attribute list (app value-add over the mock). */}\n <Card>\n <CardHeader>\n <CardTitle>Details</CardTitle>\n </CardHeader>\n <CardContent>\n <DetailRow label=\"Status\">\n <StatusBadge status={pipeline.status} />\n </DetailRow>\n <DetailRow label=\"Trigger\">{formatEnumLabel(pipeline.trigger)}</DetailRow>\n <DetailRow label=\"Schedule\">\n {pipeline.schedule ? (\n <span className=\"font-mono text-xs\">{pipeline.schedule}</span>\n ) : (\n '—'\n )}\n </DetailRow>\n <DetailRow label=\"Version\">v{pipeline.version}</DetailRow>\n <DetailRow label=\"Last run\">{formatDate(pipeline.lastRunAt)}</DetailRow>\n <DetailRow label=\"Created\">{formatDate(pipeline.createdAt)}</DetailRow>\n <DetailRow label=\"Tags\">\n {pipeline.tags.length === 0 ? (\n '—'\n ) : (\n <span className=\"flex flex-wrap gap-1\">\n {pipeline.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </span>\n )}\n </DetailRow>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Runs</CardTitle>\n </CardHeader>\n <CardContent>\n {runsLoading ? (\n <LoadingState variant=\"rows\" rows={4} label=\"Loading runs…\" />\n ) : runsError ? (\n <ErrorState message=\"Could not load runs.\" onRetry={() => void refetchRuns()} />\n ) : runs.length === 0 ? (\n <EmptyState\n icon={Play}\n title=\"No runs yet\"\n description=\"Start a run to process data through this pipeline.\"\n action={\n <Button type=\"button\" onClick={() => void handleStartRun()} disabled={isArchived}>\n <Play className=\"size-4\" />\n Start run\n </Button>\n }\n />\n ) : (\n <>\n {/* Desktop: table. */}\n <div className=\"hidden rounded-card border border-border-default md:block\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>Run</TableHead>\n <TableHead>Status</TableHead>\n <TableHead>Started</TableHead>\n <TableHead>Duration</TableHead>\n <TableHead>Triggered by</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {runs.map((run) => (\n <TableRow\n key={run.id}\n className=\"cursor-pointer\"\n onClick={() => navigate(`runs/${run.id}`)}\n >\n <TableCell className=\"font-medium text-text-primary\">\n #{run.runNumber}\n </TableCell>\n <TableCell>\n <StatusBadge status={run.status} />\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-text-secondary\">\n {formatDate(run.startedAt)}\n </TableCell>\n <TableCell className=\"tabular-nums text-text-secondary\">\n {formatDuration(run.durationMs)}\n </TableCell>\n <TableCell className=\"text-text-secondary\">\n {run.triggeredBy || '—'}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n </div>\n\n {/* Mobile: stacked cards. */}\n <ul className=\"space-y-3 md:hidden\">\n {runs.map((run) => (\n <li key={run.id}>\n <button\n type=\"button\"\n onClick={() => navigate(`runs/${run.id}`)}\n className=\"flex w-full flex-col gap-2 rounded-card border border-border-default bg-bg-elevated p-4 text-left transition-colors hover:border-border-strong\"\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"font-medium text-text-primary\">#{run.runNumber}</span>\n <StatusBadge status={run.status} />\n </div>\n <dl className=\"grid grid-cols-2 gap-x-3 gap-y-1 text-xs text-text-secondary\">\n <div className=\"flex flex-col\">\n <dt className=\"text-text-secondary\">Started</dt>\n <dd className=\"text-text-primary\">{formatDate(run.startedAt)}</dd>\n </div>\n <div className=\"flex flex-col\">\n <dt className=\"text-text-secondary\">Duration</dt>\n <dd className=\"tabular-nums text-text-primary\">\n {formatDuration(run.durationMs)}\n </dd>\n </div>\n <div className=\"col-span-2 flex flex-col\">\n <dt className=\"text-text-secondary\">Triggered by</dt>\n <dd className=\"text-text-primary\">{run.triggeredBy || '—'}</dd>\n </div>\n </dl>\n </button>\n </li>\n ))}\n </ul>\n\n <Pagination\n offset={runsPagination.offset}\n limit={runsPagination.limit}\n loadedCount={runs.length}\n hasMore={runsPageInfo?.hasMore ?? false}\n onPrev={runsPagination.prevPage}\n onNext={runsPagination.nextPage}\n itemLabel=\"runs\"\n loading={runsLoading}\n />\n </>\n )}\n </CardContent>\n </Card>\n </div>\n\n <AlertDialog\n open={confirmArchive}\n onOpenChange={(next) => (archiving ? undefined : setConfirmArchive(next))}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Archive “{pipeline.name}”?</AlertDialogTitle>\n <AlertDialogDescription>\n Archiving stops the pipeline from being triggered. Existing runs are kept for\n reference.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={archiving}>Cancel</AlertDialogCancel>\n <AlertDialogAction\n onClick={(event) => {\n event.preventDefault();\n void handleArchive();\n }}\n disabled={archiving}\n className={buttonVariants({ variant: 'destructive' })}\n >\n {archiving ? 'Archiving…' : 'Archive pipeline'}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -8,7 +8,7 @@ import "../../components/index.js";
8
8
  import { describeError as s, mutationSucceeded as c } from "../../lib/mutationResult.js";
9
9
  import { useMemo as l, useState as u } from "react";
10
10
  import { jsx as d, jsxs as f } from "react/jsx-runtime";
11
- import { useParams as p } from "react-router-dom";
11
+ import { useParams as p } from "react-router";
12
12
  import { ArrowDown as m, ArrowUp as te, GripVertical as h, Plus as g, Trash2 as _ } from "lucide-react";
13
13
  import { Button as v, Input as y, Label as b, Select as x, SelectContent as S, SelectItem as C, SelectTrigger as w, SelectValue as T, Textarea as E } from "@burdenoff/fe-libs/ui";
14
14
  //#region src/cosmicintersection/pages/pipelines/PipelineFormPage.tsx
@@ -1 +1 @@
1
- {"version":3,"file":"PipelineFormPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/pipelines/PipelineFormPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowDown, ArrowUp, GripVertical, Plus, Trash2 } from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useCosCreatePipelineMutation,\n useCosPipelineQuery,\n useCosUpdatePipelineMutation,\n type CosPipelineTrigger,\n type CosStepKind,\n} from '../../api/cosmicApi';\nimport { EntityFormShell, EntityTagsField, formatEnumLabel } from '../../components';\nimport { useCosmicIntersection } from '../../context/CosmicIntersectionContext';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\nconst TRIGGER_OPTIONS: CosPipelineTrigger[] = ['MANUAL', 'SCHEDULED', 'ON_INGEST', 'WEBHOOK'];\nconst STEP_KINDS: CosStepKind[] = [\n 'INGEST',\n 'CALIBRATE',\n 'TRANSFORM',\n 'ANALYZE',\n 'EXPORT',\n 'NOTIFY',\n];\n\nfunction isStepKind(value: unknown): value is CosStepKind {\n return typeof value === 'string' && (STEP_KINDS as string[]).includes(value);\n}\n\nlet stepIdSeq = 0;\nconst makeStepId = () => `step-${(stepIdSeq += 1)}`;\n\ninterface StepRow {\n id: string;\n kind: CosStepKind;\n name: string;\n}\n\ninterface PipelineFormState {\n name: string;\n description: string;\n trigger: CosPipelineTrigger;\n schedule: string;\n steps: StepRow[];\n tags: string[];\n}\n\nconst INITIAL_STATE: PipelineFormState = {\n name: '',\n description: '',\n trigger: 'MANUAL',\n schedule: '',\n steps: [],\n tags: [],\n};\n\n/** Parse an existing definition's `steps[]` into editable builder rows. */\nfunction definitionToRows(definition?: Record<string, unknown> | null): StepRow[] {\n if (!definition) {\n return [];\n }\n const raw = (definition as { steps?: unknown }).steps;\n if (!Array.isArray(raw)) {\n return [];\n }\n const rows: StepRow[] = [];\n for (const item of raw) {\n if (item && typeof item === 'object' && !Array.isArray(item)) {\n const kind = (item as { kind?: unknown }).kind;\n const name = (item as { name?: unknown }).name;\n if (isStepKind(kind)) {\n rows.push({ id: makeStepId(), kind, name: typeof name === 'string' ? name : '' });\n }\n }\n }\n return rows;\n}\n\n/** Serialize builder rows to the `steps[]` payload — omitting blank names. */\nfunction rowsToSteps(rows: StepRow[]): Record<string, unknown>[] {\n return rows.map((row) => {\n const step: Record<string, unknown> = { kind: row.kind };\n const name = row.name.trim();\n if (name) {\n step.name = name;\n }\n return step;\n });\n}\n\nexport default function PipelineFormPage() {\n const { id = '' } = useParams<{ id: string }>();\n const isEdit = Boolean(id);\n const { navigate } = useCosmicIntersection();\n\n const { data, loading: loadingPipeline } = useCosPipelineQuery(id);\n const [createPipeline, { loading: creating }] = useCosCreatePipelineMutation();\n const [updatePipeline, { loading: updating }] = useCosUpdatePipelineMutation();\n\n const [form, setForm] = useState<PipelineFormState>(INITIAL_STATE);\n // Extra top-level definition keys (anything other than `steps`) preserved\n // verbatim so editing steps never silently drops other configuration.\n const [baseDefinition, setBaseDefinition] = useState<Record<string, unknown>>({});\n const [jsonText, setJsonText] = useState('');\n const [jsonError, setJsonError] = useState<string | null>(null);\n\n // Hydrate the form once the edited pipeline loads. Derive-during-render keyed\n // on the loaded id so a later refetch never clobbers in-progress edits.\n const loaded = isEdit ? data?.cosPipeline : null;\n const [hydratedId, setHydratedId] = useState<string | null>(null);\n if (loaded && loaded.id !== hydratedId) {\n setHydratedId(loaded.id);\n setForm({\n name: loaded.name ?? '',\n description: loaded.description ?? '',\n trigger: loaded.trigger ?? 'MANUAL',\n schedule: loaded.schedule ?? '',\n steps: definitionToRows(loaded.definition),\n tags: loaded.tags ?? [],\n });\n const extra: Record<string, unknown> = { ...(loaded.definition ?? {}) };\n delete extra.steps;\n setBaseDefinition(extra);\n }\n\n const setField = <K extends keyof PipelineFormState>(field: K, value: PipelineFormState[K]) => {\n setForm((prev) => ({ ...prev, [field]: value }));\n };\n\n const serializedSteps = useMemo(\n () => JSON.stringify({ ...baseDefinition, steps: rowsToSteps(form.steps) }, null, 2),\n [baseDefinition, form.steps]\n );\n\n const addStep = () => {\n setField('steps', [...form.steps, { id: makeStepId(), kind: 'INGEST', name: '' }]);\n };\n\n const removeStep = (stepId: string) => {\n setField(\n 'steps',\n form.steps.filter((step) => step.id !== stepId)\n );\n };\n\n const updateStep = (stepId: string, patch: Partial<Omit<StepRow, 'id'>>) => {\n setField(\n 'steps',\n form.steps.map((step) => (step.id === stepId ? { ...step, ...patch } : step))\n );\n };\n\n const moveStep = (index: number, direction: -1 | 1) => {\n const target = index + direction;\n if (target < 0 || target >= form.steps.length) {\n return;\n }\n const next = [...form.steps];\n const [moved] = next.splice(index, 1);\n next.splice(target, 0, moved);\n setField('steps', next);\n };\n\n const applyJson = () => {\n let parsed: unknown;\n try {\n parsed = JSON.parse(jsonText);\n } catch (error) {\n setJsonError(describeError(error) ?? 'Invalid JSON.');\n return;\n }\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n setJsonError('Definition must be a JSON object with a \"steps\" array.');\n return;\n }\n const rawSteps = (parsed as { steps?: unknown }).steps;\n if (!Array.isArray(rawSteps)) {\n setJsonError('Definition must contain a \"steps\" array.');\n return;\n }\n const rows: StepRow[] = [];\n for (const item of rawSteps) {\n if (!item || typeof item !== 'object' || Array.isArray(item)) {\n setJsonError('Each step must be an object with a \"kind\".');\n return;\n }\n const kind = (item as { kind?: unknown }).kind;\n if (!isStepKind(kind)) {\n setJsonError(`Invalid step kind \"${String(kind)}\". Allowed: ${STEP_KINDS.join(', ')}.`);\n return;\n }\n const name = (item as { name?: unknown }).name;\n rows.push({ id: makeStepId(), kind, name: typeof name === 'string' ? name : '' });\n }\n const extra: Record<string, unknown> = { ...(parsed as Record<string, unknown>) };\n delete extra.steps;\n setBaseDefinition(extra);\n setField('steps', rows);\n setJsonError(null);\n notify.success('Steps updated', 'The builder now reflects your JSON.');\n };\n\n const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n if (!form.name.trim()) {\n notify.error('Name is required', 'Give the pipeline a name before saving.');\n return;\n }\n\n // Build the definition from the builder (the single source of truth),\n // preserving any extra top-level keys.\n const stepsPayload = rowsToSteps(form.steps);\n const base: Record<string, unknown> = { ...baseDefinition };\n delete base.steps;\n let definition: Record<string, unknown> | undefined;\n if (stepsPayload.length > 0) {\n definition = { ...base, steps: stepsPayload };\n } else if (Object.keys(base).length > 0) {\n definition = base;\n } else {\n definition = undefined;\n }\n\n const schedule = form.trigger === 'SCHEDULED' ? form.schedule.trim() : '';\n\n try {\n if (isEdit) {\n const response = await updatePipeline({\n variables: {\n id,\n input: {\n name: form.name.trim(),\n description: form.description.trim() || undefined,\n trigger: form.trigger,\n schedule: schedule || undefined,\n definition,\n tags: form.tags,\n },\n },\n });\n if (\n !mutationSucceeded(\n response,\n response.data?.cosUpdatePipeline?.id,\n 'Could not update pipeline'\n )\n ) {\n return;\n }\n notify.success('Pipeline updated', `“${form.name.trim()}” was saved.`);\n navigate(`pipelines/${id}`);\n return;\n }\n\n const response = await createPipeline({\n variables: {\n input: {\n name: form.name.trim(),\n description: form.description.trim() || undefined,\n trigger: form.trigger,\n schedule: schedule || undefined,\n definition,\n tags: form.tags,\n },\n },\n });\n const createdId = response.data?.cosCreatePipeline?.id;\n if (!mutationSucceeded(response, createdId, 'Could not create pipeline')) {\n return;\n }\n notify.success('Pipeline created', `“${form.name.trim()}” is ready.`);\n navigate(createdId ? `pipelines/${createdId}` : 'pipelines');\n } catch (error) {\n notify.error('Could not save pipeline', describeError(error));\n }\n };\n\n const saving = creating || updating;\n\n return (\n <EntityFormShell\n title={isEdit ? 'Edit pipeline' : 'New pipeline'}\n description=\"Define how raw data is ingested, processed and released.\"\n purpose=\"A pipeline chains ordered steps (ingest, calibrate, transform, analyze, export) over your datasets. Choose how it is triggered, add an optional cron schedule, and build its steps below.\"\n backLabel={isEdit ? 'Back to pipeline' : 'Back to pipelines'}\n onBack={() => navigate(isEdit ? `pipelines/${id}` : 'pipelines')}\n onSubmit={onSubmit}\n submitLabel={isEdit ? 'Save changes' : 'Create pipeline'}\n saving={saving}\n submitDisabled={isEdit && loadingPipeline}\n >\n <div className=\"space-y-1\">\n <Label htmlFor=\"pipeline-name\">Name</Label>\n <Input\n id=\"pipeline-name\"\n value={form.name}\n onChange={(event) => setField('name', event.target.value)}\n placeholder=\"e.g. Kepler DR3 ingest\"\n required\n />\n </div>\n\n <div className=\"space-y-1\">\n <Label htmlFor=\"pipeline-description\">Description</Label>\n <Textarea\n id=\"pipeline-description\"\n value={form.description}\n onChange={(event) => setField('description', event.target.value)}\n rows={3}\n placeholder=\"What this pipeline does and when it runs.\"\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <div className=\"space-y-1\">\n <Label htmlFor=\"pipeline-trigger\">Trigger</Label>\n <Select\n value={form.trigger}\n onValueChange={(value) => setField('trigger', value as CosPipelineTrigger)}\n >\n <SelectTrigger id=\"pipeline-trigger\" aria-label=\"Trigger\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {TRIGGER_OPTIONS.map((option) => (\n <SelectItem key={option} value={option}>\n {formatEnumLabel(option)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {form.trigger === 'SCHEDULED' ? (\n <div className=\"space-y-1\">\n <Label htmlFor=\"pipeline-schedule\">Schedule (cron)</Label>\n <Input\n id=\"pipeline-schedule\"\n value={form.schedule}\n onChange={(event) => setField('schedule', event.target.value)}\n placeholder=\"0 2 * * *\"\n className=\"font-mono\"\n />\n <p className=\"text-xs text-text-secondary\">\n Standard 5-field cron expression, e.g. <span className=\"font-mono\">0 2 * * *</span>{' '}\n for daily at 02:00.\n </p>\n </div>\n ) : null}\n </div>\n\n {/* Structured step builder (mockup 12). */}\n <div className=\"space-y-2\">\n <Label>Definition — steps</Label>\n <p className=\"text-xs text-text-secondary\">\n The ordered stages this pipeline runs. Add a step, pick its kind, optionally name it, and\n reorder with the arrows.\n </p>\n\n {form.steps.length === 0 ? (\n <div className=\"rounded-card border border-dashed border-border-default bg-bg-sunken px-4 py-6 text-center\">\n <p className=\"text-sm text-text-secondary\">\n No steps yet. Add the first stage to start building the flow.\n </p>\n </div>\n ) : (\n <ol className=\"space-y-2\">\n {form.steps.map((step, index) => (\n <li\n key={step.id}\n className=\"flex items-center gap-2 rounded-card border border-border-default bg-bg-elevated p-2.5\"\n >\n <span className=\"flex items-center gap-1.5 text-text-secondary\">\n <GripVertical className=\"size-4 shrink-0\" aria-hidden=\"true\" />\n <span className=\"w-4 text-xs font-semibold tabular-nums\">{index + 1}</span>\n </span>\n\n <div className=\"grid min-w-0 flex-1 grid-cols-1 gap-2 sm:grid-cols-[10rem_1fr]\">\n <Select\n value={step.kind}\n onValueChange={(value) => updateStep(step.id, { kind: value as CosStepKind })}\n >\n <SelectTrigger aria-label={`Kind for step ${index + 1}`}>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {STEP_KINDS.map((option) => (\n <SelectItem key={option} value={option}>\n {formatEnumLabel(option)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Input\n value={step.name}\n onChange={(event) => updateStep(step.id, { name: event.target.value })}\n placeholder=\"Optional step name\"\n aria-label={`Name for step ${index + 1}`}\n />\n </div>\n\n <div className=\"flex shrink-0 items-center gap-0.5\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={() => moveStep(index, -1)}\n disabled={index === 0}\n aria-label={`Move step ${index + 1} up`}\n >\n <ArrowUp className=\"size-4\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={() => moveStep(index, 1)}\n disabled={index === form.steps.length - 1}\n aria-label={`Move step ${index + 1} down`}\n >\n <ArrowDown className=\"size-4\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={() => removeStep(step.id)}\n aria-label={`Remove step ${index + 1}`}\n className=\"text-text-secondary hover:text-status-error-text\"\n >\n <Trash2 className=\"size-4\" />\n </Button>\n </div>\n </li>\n ))}\n </ol>\n )}\n\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={addStep}>\n <Plus className=\"size-4\" />\n Add step\n </Button>\n\n <details\n className=\"group mt-2 rounded-card border border-border-default bg-bg-sunken\"\n onToggle={(event) => {\n if (event.currentTarget.open) {\n setJsonText(serializedSteps);\n setJsonError(null);\n }\n }}\n >\n <summary className=\"flex cursor-pointer list-none items-center justify-between px-4 py-2.5 text-sm font-medium text-text-secondary transition-colors hover:text-text-primary\">\n Advanced — edit as JSON\n <span className=\"text-xs transition-transform group-open:rotate-180\">▾</span>\n </summary>\n <div className=\"space-y-2 border-t border-border-seam p-4\">\n <Textarea\n value={jsonText}\n onChange={(event) => {\n setJsonText(event.target.value);\n setJsonError(null);\n }}\n rows={8}\n spellCheck={false}\n className=\"font-mono text-xs\"\n aria-label=\"Definition JSON\"\n />\n {jsonError ? <p className=\"text-xs text-status-error-text\">{jsonError}</p> : null}\n <div className=\"flex items-center justify-between gap-2\">\n <p className=\"text-xs text-text-secondary\">\n Edit the raw definition, then apply it back to the builder above.\n </p>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={applyJson}>\n Apply to builder\n </Button>\n </div>\n </div>\n </details>\n </div>\n\n <EntityTagsField value={form.tags} onChange={(tags) => setField('tags', tags)} />\n </EntityFormShell>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"PipelineFormPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/pipelines/PipelineFormPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { ArrowDown, ArrowUp, GripVertical, Plus, Trash2 } from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useCosCreatePipelineMutation,\n useCosPipelineQuery,\n useCosUpdatePipelineMutation,\n type CosPipelineTrigger,\n type CosStepKind,\n} from '../../api/cosmicApi';\nimport { EntityFormShell, EntityTagsField, formatEnumLabel } from '../../components';\nimport { useCosmicIntersection } from '../../context/CosmicIntersectionContext';\nimport { describeError, mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\nconst TRIGGER_OPTIONS: CosPipelineTrigger[] = ['MANUAL', 'SCHEDULED', 'ON_INGEST', 'WEBHOOK'];\nconst STEP_KINDS: CosStepKind[] = [\n 'INGEST',\n 'CALIBRATE',\n 'TRANSFORM',\n 'ANALYZE',\n 'EXPORT',\n 'NOTIFY',\n];\n\nfunction isStepKind(value: unknown): value is CosStepKind {\n return typeof value === 'string' && (STEP_KINDS as string[]).includes(value);\n}\n\nlet stepIdSeq = 0;\nconst makeStepId = () => `step-${(stepIdSeq += 1)}`;\n\ninterface StepRow {\n id: string;\n kind: CosStepKind;\n name: string;\n}\n\ninterface PipelineFormState {\n name: string;\n description: string;\n trigger: CosPipelineTrigger;\n schedule: string;\n steps: StepRow[];\n tags: string[];\n}\n\nconst INITIAL_STATE: PipelineFormState = {\n name: '',\n description: '',\n trigger: 'MANUAL',\n schedule: '',\n steps: [],\n tags: [],\n};\n\n/** Parse an existing definition's `steps[]` into editable builder rows. */\nfunction definitionToRows(definition?: Record<string, unknown> | null): StepRow[] {\n if (!definition) {\n return [];\n }\n const raw = (definition as { steps?: unknown }).steps;\n if (!Array.isArray(raw)) {\n return [];\n }\n const rows: StepRow[] = [];\n for (const item of raw) {\n if (item && typeof item === 'object' && !Array.isArray(item)) {\n const kind = (item as { kind?: unknown }).kind;\n const name = (item as { name?: unknown }).name;\n if (isStepKind(kind)) {\n rows.push({ id: makeStepId(), kind, name: typeof name === 'string' ? name : '' });\n }\n }\n }\n return rows;\n}\n\n/** Serialize builder rows to the `steps[]` payload — omitting blank names. */\nfunction rowsToSteps(rows: StepRow[]): Record<string, unknown>[] {\n return rows.map((row) => {\n const step: Record<string, unknown> = { kind: row.kind };\n const name = row.name.trim();\n if (name) {\n step.name = name;\n }\n return step;\n });\n}\n\nexport default function PipelineFormPage() {\n const { id = '' } = useParams<{ id: string }>();\n const isEdit = Boolean(id);\n const { navigate } = useCosmicIntersection();\n\n const { data, loading: loadingPipeline } = useCosPipelineQuery(id);\n const [createPipeline, { loading: creating }] = useCosCreatePipelineMutation();\n const [updatePipeline, { loading: updating }] = useCosUpdatePipelineMutation();\n\n const [form, setForm] = useState<PipelineFormState>(INITIAL_STATE);\n // Extra top-level definition keys (anything other than `steps`) preserved\n // verbatim so editing steps never silently drops other configuration.\n const [baseDefinition, setBaseDefinition] = useState<Record<string, unknown>>({});\n const [jsonText, setJsonText] = useState('');\n const [jsonError, setJsonError] = useState<string | null>(null);\n\n // Hydrate the form once the edited pipeline loads. Derive-during-render keyed\n // on the loaded id so a later refetch never clobbers in-progress edits.\n const loaded = isEdit ? data?.cosPipeline : null;\n const [hydratedId, setHydratedId] = useState<string | null>(null);\n if (loaded && loaded.id !== hydratedId) {\n setHydratedId(loaded.id);\n setForm({\n name: loaded.name ?? '',\n description: loaded.description ?? '',\n trigger: loaded.trigger ?? 'MANUAL',\n schedule: loaded.schedule ?? '',\n steps: definitionToRows(loaded.definition),\n tags: loaded.tags ?? [],\n });\n const extra: Record<string, unknown> = { ...(loaded.definition ?? {}) };\n delete extra.steps;\n setBaseDefinition(extra);\n }\n\n const setField = <K extends keyof PipelineFormState>(field: K, value: PipelineFormState[K]) => {\n setForm((prev) => ({ ...prev, [field]: value }));\n };\n\n const serializedSteps = useMemo(\n () => JSON.stringify({ ...baseDefinition, steps: rowsToSteps(form.steps) }, null, 2),\n [baseDefinition, form.steps]\n );\n\n const addStep = () => {\n setField('steps', [...form.steps, { id: makeStepId(), kind: 'INGEST', name: '' }]);\n };\n\n const removeStep = (stepId: string) => {\n setField(\n 'steps',\n form.steps.filter((step) => step.id !== stepId)\n );\n };\n\n const updateStep = (stepId: string, patch: Partial<Omit<StepRow, 'id'>>) => {\n setField(\n 'steps',\n form.steps.map((step) => (step.id === stepId ? { ...step, ...patch } : step))\n );\n };\n\n const moveStep = (index: number, direction: -1 | 1) => {\n const target = index + direction;\n if (target < 0 || target >= form.steps.length) {\n return;\n }\n const next = [...form.steps];\n const [moved] = next.splice(index, 1);\n next.splice(target, 0, moved);\n setField('steps', next);\n };\n\n const applyJson = () => {\n let parsed: unknown;\n try {\n parsed = JSON.parse(jsonText);\n } catch (error) {\n setJsonError(describeError(error) ?? 'Invalid JSON.');\n return;\n }\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n setJsonError('Definition must be a JSON object with a \"steps\" array.');\n return;\n }\n const rawSteps = (parsed as { steps?: unknown }).steps;\n if (!Array.isArray(rawSteps)) {\n setJsonError('Definition must contain a \"steps\" array.');\n return;\n }\n const rows: StepRow[] = [];\n for (const item of rawSteps) {\n if (!item || typeof item !== 'object' || Array.isArray(item)) {\n setJsonError('Each step must be an object with a \"kind\".');\n return;\n }\n const kind = (item as { kind?: unknown }).kind;\n if (!isStepKind(kind)) {\n setJsonError(`Invalid step kind \"${String(kind)}\". Allowed: ${STEP_KINDS.join(', ')}.`);\n return;\n }\n const name = (item as { name?: unknown }).name;\n rows.push({ id: makeStepId(), kind, name: typeof name === 'string' ? name : '' });\n }\n const extra: Record<string, unknown> = { ...(parsed as Record<string, unknown>) };\n delete extra.steps;\n setBaseDefinition(extra);\n setField('steps', rows);\n setJsonError(null);\n notify.success('Steps updated', 'The builder now reflects your JSON.');\n };\n\n const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n if (!form.name.trim()) {\n notify.error('Name is required', 'Give the pipeline a name before saving.');\n return;\n }\n\n // Build the definition from the builder (the single source of truth),\n // preserving any extra top-level keys.\n const stepsPayload = rowsToSteps(form.steps);\n const base: Record<string, unknown> = { ...baseDefinition };\n delete base.steps;\n let definition: Record<string, unknown> | undefined;\n if (stepsPayload.length > 0) {\n definition = { ...base, steps: stepsPayload };\n } else if (Object.keys(base).length > 0) {\n definition = base;\n } else {\n definition = undefined;\n }\n\n const schedule = form.trigger === 'SCHEDULED' ? form.schedule.trim() : '';\n\n try {\n if (isEdit) {\n const response = await updatePipeline({\n variables: {\n id,\n input: {\n name: form.name.trim(),\n description: form.description.trim() || undefined,\n trigger: form.trigger,\n schedule: schedule || undefined,\n definition,\n tags: form.tags,\n },\n },\n });\n if (\n !mutationSucceeded(\n response,\n response.data?.cosUpdatePipeline?.id,\n 'Could not update pipeline'\n )\n ) {\n return;\n }\n notify.success('Pipeline updated', `“${form.name.trim()}” was saved.`);\n navigate(`pipelines/${id}`);\n return;\n }\n\n const response = await createPipeline({\n variables: {\n input: {\n name: form.name.trim(),\n description: form.description.trim() || undefined,\n trigger: form.trigger,\n schedule: schedule || undefined,\n definition,\n tags: form.tags,\n },\n },\n });\n const createdId = response.data?.cosCreatePipeline?.id;\n if (!mutationSucceeded(response, createdId, 'Could not create pipeline')) {\n return;\n }\n notify.success('Pipeline created', `“${form.name.trim()}” is ready.`);\n navigate(createdId ? `pipelines/${createdId}` : 'pipelines');\n } catch (error) {\n notify.error('Could not save pipeline', describeError(error));\n }\n };\n\n const saving = creating || updating;\n\n return (\n <EntityFormShell\n title={isEdit ? 'Edit pipeline' : 'New pipeline'}\n description=\"Define how raw data is ingested, processed and released.\"\n purpose=\"A pipeline chains ordered steps (ingest, calibrate, transform, analyze, export) over your datasets. Choose how it is triggered, add an optional cron schedule, and build its steps below.\"\n backLabel={isEdit ? 'Back to pipeline' : 'Back to pipelines'}\n onBack={() => navigate(isEdit ? `pipelines/${id}` : 'pipelines')}\n onSubmit={onSubmit}\n submitLabel={isEdit ? 'Save changes' : 'Create pipeline'}\n saving={saving}\n submitDisabled={isEdit && loadingPipeline}\n >\n <div className=\"space-y-1\">\n <Label htmlFor=\"pipeline-name\">Name</Label>\n <Input\n id=\"pipeline-name\"\n value={form.name}\n onChange={(event) => setField('name', event.target.value)}\n placeholder=\"e.g. Kepler DR3 ingest\"\n required\n />\n </div>\n\n <div className=\"space-y-1\">\n <Label htmlFor=\"pipeline-description\">Description</Label>\n <Textarea\n id=\"pipeline-description\"\n value={form.description}\n onChange={(event) => setField('description', event.target.value)}\n rows={3}\n placeholder=\"What this pipeline does and when it runs.\"\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n <div className=\"space-y-1\">\n <Label htmlFor=\"pipeline-trigger\">Trigger</Label>\n <Select\n value={form.trigger}\n onValueChange={(value) => setField('trigger', value as CosPipelineTrigger)}\n >\n <SelectTrigger id=\"pipeline-trigger\" aria-label=\"Trigger\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {TRIGGER_OPTIONS.map((option) => (\n <SelectItem key={option} value={option}>\n {formatEnumLabel(option)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {form.trigger === 'SCHEDULED' ? (\n <div className=\"space-y-1\">\n <Label htmlFor=\"pipeline-schedule\">Schedule (cron)</Label>\n <Input\n id=\"pipeline-schedule\"\n value={form.schedule}\n onChange={(event) => setField('schedule', event.target.value)}\n placeholder=\"0 2 * * *\"\n className=\"font-mono\"\n />\n <p className=\"text-xs text-text-secondary\">\n Standard 5-field cron expression, e.g. <span className=\"font-mono\">0 2 * * *</span>{' '}\n for daily at 02:00.\n </p>\n </div>\n ) : null}\n </div>\n\n {/* Structured step builder (mockup 12). */}\n <div className=\"space-y-2\">\n <Label>Definition — steps</Label>\n <p className=\"text-xs text-text-secondary\">\n The ordered stages this pipeline runs. Add a step, pick its kind, optionally name it, and\n reorder with the arrows.\n </p>\n\n {form.steps.length === 0 ? (\n <div className=\"rounded-card border border-dashed border-border-default bg-bg-sunken px-4 py-6 text-center\">\n <p className=\"text-sm text-text-secondary\">\n No steps yet. Add the first stage to start building the flow.\n </p>\n </div>\n ) : (\n <ol className=\"space-y-2\">\n {form.steps.map((step, index) => (\n <li\n key={step.id}\n className=\"flex items-center gap-2 rounded-card border border-border-default bg-bg-elevated p-2.5\"\n >\n <span className=\"flex items-center gap-1.5 text-text-secondary\">\n <GripVertical className=\"size-4 shrink-0\" aria-hidden=\"true\" />\n <span className=\"w-4 text-xs font-semibold tabular-nums\">{index + 1}</span>\n </span>\n\n <div className=\"grid min-w-0 flex-1 grid-cols-1 gap-2 sm:grid-cols-[10rem_1fr]\">\n <Select\n value={step.kind}\n onValueChange={(value) => updateStep(step.id, { kind: value as CosStepKind })}\n >\n <SelectTrigger aria-label={`Kind for step ${index + 1}`}>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {STEP_KINDS.map((option) => (\n <SelectItem key={option} value={option}>\n {formatEnumLabel(option)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Input\n value={step.name}\n onChange={(event) => updateStep(step.id, { name: event.target.value })}\n placeholder=\"Optional step name\"\n aria-label={`Name for step ${index + 1}`}\n />\n </div>\n\n <div className=\"flex shrink-0 items-center gap-0.5\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={() => moveStep(index, -1)}\n disabled={index === 0}\n aria-label={`Move step ${index + 1} up`}\n >\n <ArrowUp className=\"size-4\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={() => moveStep(index, 1)}\n disabled={index === form.steps.length - 1}\n aria-label={`Move step ${index + 1} down`}\n >\n <ArrowDown className=\"size-4\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={() => removeStep(step.id)}\n aria-label={`Remove step ${index + 1}`}\n className=\"text-text-secondary hover:text-status-error-text\"\n >\n <Trash2 className=\"size-4\" />\n </Button>\n </div>\n </li>\n ))}\n </ol>\n )}\n\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={addStep}>\n <Plus className=\"size-4\" />\n Add step\n </Button>\n\n <details\n className=\"group mt-2 rounded-card border border-border-default bg-bg-sunken\"\n onToggle={(event) => {\n if (event.currentTarget.open) {\n setJsonText(serializedSteps);\n setJsonError(null);\n }\n }}\n >\n <summary className=\"flex cursor-pointer list-none items-center justify-between px-4 py-2.5 text-sm font-medium text-text-secondary transition-colors hover:text-text-primary\">\n Advanced — edit as JSON\n <span className=\"text-xs transition-transform group-open:rotate-180\">▾</span>\n </summary>\n <div className=\"space-y-2 border-t border-border-seam p-4\">\n <Textarea\n value={jsonText}\n onChange={(event) => {\n setJsonText(event.target.value);\n setJsonError(null);\n }}\n rows={8}\n spellCheck={false}\n className=\"font-mono text-xs\"\n aria-label=\"Definition JSON\"\n />\n {jsonError ? <p className=\"text-xs text-status-error-text\">{jsonError}</p> : null}\n <div className=\"flex items-center justify-between gap-2\">\n <p className=\"text-xs text-text-secondary\">\n Edit the raw definition, then apply it back to the builder above.\n </p>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={applyJson}>\n Apply to builder\n </Button>\n </div>\n </div>\n </details>\n </div>\n\n <EntityTagsField value={form.tags} onChange={(tags) => setField('tags', tags)} />\n </EntityFormShell>\n );\n}\n"],"mappings":"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"}
@@ -12,7 +12,7 @@ import "../../components/index.js";
12
12
  import { describeError as d, mutationSucceeded as f } from "../../lib/mutationResult.js";
13
13
  import { useMemo as re, useState as p } from "react";
14
14
  import { jsx as m, jsxs as h } from "react/jsx-runtime";
15
- import { useParams as g } from "react-router-dom";
15
+ import { useParams as g } from "react-router";
16
16
  import { Check as _, Circle as v, ListTree as ie, Loader2 as y, Plus as b, X as x } from "lucide-react";
17
17
  import { Button as S, Card as C, CardContent as w, CardHeader as T, CardTitle as E, Dialog as ae, DialogContent as D, DialogDescription as O, DialogFooter as oe, DialogHeader as se, DialogTitle as ce, Input as k, Label as A, Select as le, SelectContent as ue, SelectItem as de, SelectTrigger as fe, SelectValue as pe, Textarea as me } from "@burdenoff/fe-libs/ui";
18
18
  //#region src/cosmicintersection/pages/runs/RunDetailPage.tsx