@burdenoff/microfe-cosmicintersection 2026.713.1 → 2026.720.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 (62) hide show
  1. package/dist/cosmicintersection/CosmicIntersectionRoot.js.map +1 -1
  2. package/dist/cosmicintersection/CosmicIntersectionRoutes.js.map +1 -1
  3. package/dist/cosmicintersection/api/cosmicApi.d.ts.map +1 -1
  4. package/dist/cosmicintersection/api/cosmicApi.js.map +1 -1
  5. package/dist/cosmicintersection/components/ActionMenu.js.map +1 -1
  6. package/dist/cosmicintersection/components/ActivityFeedPanel.js.map +1 -1
  7. package/dist/cosmicintersection/components/DiscussionPanel.js.map +1 -1
  8. package/dist/cosmicintersection/components/EmptyState.js.map +1 -1
  9. package/dist/cosmicintersection/components/EntityFormShell.js.map +1 -1
  10. package/dist/cosmicintersection/components/EntityTagsField.js.map +1 -1
  11. package/dist/cosmicintersection/components/ErrorState.js.map +1 -1
  12. package/dist/cosmicintersection/components/FacetPanel.js.map +1 -1
  13. package/dist/cosmicintersection/components/FileUploadField.js.map +1 -1
  14. package/dist/cosmicintersection/components/LineageGraph.d.ts.map +1 -1
  15. package/dist/cosmicintersection/components/LineageGraph.js.map +1 -1
  16. package/dist/cosmicintersection/components/LoadingState.js.map +1 -1
  17. package/dist/cosmicintersection/components/PageLayout.js.map +1 -1
  18. package/dist/cosmicintersection/components/Pagination.js.map +1 -1
  19. package/dist/cosmicintersection/components/ResultsChart.js.map +1 -1
  20. package/dist/cosmicintersection/components/SafeLink.js.map +1 -1
  21. package/dist/cosmicintersection/components/StatusBadge.js.map +1 -1
  22. package/dist/cosmicintersection/components/StepDAG.js.map +1 -1
  23. package/dist/cosmicintersection/context/CosmicIntersectionContext.js.map +1 -1
  24. package/dist/cosmicintersection/lib/mutationResult.js.map +1 -1
  25. package/dist/cosmicintersection/lib/toast.js.map +1 -1
  26. package/dist/cosmicintersection/pages/access/AccessPage.js.map +1 -1
  27. package/dist/cosmicintersection/pages/analyses/AnalysesPage.js.map +1 -1
  28. package/dist/cosmicintersection/pages/collections/CollectionDetailPage.js.map +1 -1
  29. package/dist/cosmicintersection/pages/collections/CollectionFormPage.js.map +1 -1
  30. package/dist/cosmicintersection/pages/collections/CollectionsPage.js.map +1 -1
  31. package/dist/cosmicintersection/pages/dashboard/DashboardPage.js.map +1 -1
  32. package/dist/cosmicintersection/pages/datasets/DatasetDetailPage.js.map +1 -1
  33. package/dist/cosmicintersection/pages/datasets/DatasetFormPage.js.map +1 -1
  34. package/dist/cosmicintersection/pages/datasets/DatasetsPage.js.map +1 -1
  35. package/dist/cosmicintersection/pages/discovery/DiscoveryPage.js.map +1 -1
  36. package/dist/cosmicintersection/pages/lineage/LineagePage.js.map +1 -1
  37. package/dist/cosmicintersection/pages/notebooks/NotebookDetailPage.js.map +1 -1
  38. package/dist/cosmicintersection/pages/notebooks/NotebookFormPage.js.map +1 -1
  39. package/dist/cosmicintersection/pages/notebooks/NotebooksPage.js.map +1 -1
  40. package/dist/cosmicintersection/pages/pipelines/PipelineDetailPage.js.map +1 -1
  41. package/dist/cosmicintersection/pages/pipelines/PipelineFormPage.js.map +1 -1
  42. package/dist/cosmicintersection/pages/pipelines/PipelinesPage.js.map +1 -1
  43. package/dist/cosmicintersection/pages/releases/ReleaseFormPage.js.map +1 -1
  44. package/dist/cosmicintersection/pages/releases/ReleasesPage.js.map +1 -1
  45. package/dist/cosmicintersection/pages/runs/RunDetailPage.js.map +1 -1
  46. package/dist/cosmicintersection/pages/runs/RunsPage.js.map +1 -1
  47. package/dist/cosmicintersection/pages/sources/SourceDetailPage.js.map +1 -1
  48. package/dist/cosmicintersection/pages/sources/SourceFormPage.js.map +1 -1
  49. package/dist/cosmicintersection/pages/sources/SourcesPage.js.map +1 -1
  50. package/dist/cosmicintersection/utils/cn.js.map +1 -1
  51. package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
  52. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1 -1
  53. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
  54. package/dist/services/createSandboxAssistantTransport.js.map +1 -1
  55. package/dist/stories/EmptyState.stories.d.ts +9 -0
  56. package/dist/stories/EmptyState.stories.d.ts.map +1 -0
  57. package/dist/stories/LoadingState.stories.d.ts +9 -0
  58. package/dist/stories/LoadingState.stories.d.ts.map +1 -0
  59. package/dist/stories/StatusBadge.stories.d.ts +12 -0
  60. package/dist/stories/StatusBadge.stories.d.ts.map +1 -0
  61. package/dist/utils/pageContext.js.map +1 -1
  62. package/package.json +14 -3
@@ -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-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 +1 @@
1
- {"version":3,"file":"NotebooksPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/notebooks/NotebooksPage.tsx"],"sourcesContent":["/**\n * NotebooksPage — paginated list of analysis notebooks.\n *\n * Notebooks are computational documents (title, runtime, environment, linked\n * datasets) that scientists run analyses inside. This surface lists them from\n * `cosListNotebooks` with a status filter, per-row View / Edit / Archive\n * actions, and offset pagination. Every value here comes from the backend —\n * there is no fabricated data.\n */\nimport { useState } from 'react';\nimport { NotebookText, Plus, Share2 } from 'lucide-react';\nimport {\n Badge,\n Button,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useCosArchiveNotebookMutation,\n useCosNotebooksQuery,\n type CosNotebook,\n type CosNotebookStatus,\n} from '../../api/cosmicApi';\nimport {\n ActionMenu,\n EmptyState,\n ErrorState,\n LoadingState,\n PageLayout,\n Pagination,\n StatusBadge,\n usePagination,\n} 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\nconst ALL = 'ALL';\n\nfunction formatDate(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.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });\n}\n\nexport default function NotebooksPage() {\n const { navigate } = useCosmicIntersection();\n const [statusFilter, setStatusFilter] = useState<CosNotebookStatus | undefined>(undefined);\n const pagination = usePagination({ pageSize: 50, resetKey: statusFilter });\n\n const { data, loading, error, refetch } = useCosNotebooksQuery({\n status: statusFilter,\n pagination: { limit: pagination.limit, offset: pagination.offset },\n });\n const [archiveNotebook] = useCosArchiveNotebookMutation();\n\n const notebooks = data?.cosListNotebooks.items ?? [];\n const pageInfo = data?.cosListNotebooks.pageInfo;\n\n const handleArchive = async (notebook: CosNotebook) => {\n const response = await archiveNotebook({ variables: { id: notebook.id } });\n if (response.error) {\n // onError already surfaced the toast; rethrow so the confirm dialog stays open.\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 const actions = (\n <Button type=\"button\" onClick={() => navigate('notebooks/create')}>\n <Plus className=\"size-4\" />\n New notebook\n </Button>\n );\n\n return (\n <PageLayout\n title=\"Notebooks\"\n description=\"Computational notebooks for running analyses over federated science datasets.\"\n actions={actions}\n >\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <label className=\"text-sm text-text-secondary\" htmlFor=\"notebook-status-filter\">\n Status\n </label>\n <Select\n value={statusFilter ?? ALL}\n onValueChange={(value) =>\n setStatusFilter(value === ALL ? undefined : (value as CosNotebookStatus))\n }\n >\n <SelectTrigger\n id=\"notebook-status-filter\"\n className=\"w-48\"\n aria-label=\"Filter by status\"\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>All statuses</SelectItem>\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\n {loading ? (\n <LoadingState variant=\"rows\" rows={6} label=\"Loading notebooks…\" />\n ) : error ? (\n <ErrorState\n message=\"We could not load your notebooks. Please try again.\"\n onRetry={() => void refetch()}\n />\n ) : notebooks.length === 0 ? (\n <EmptyState\n icon={NotebookText}\n title={statusFilter ? 'No notebooks match this filter.' : 'No notebooks yet.'}\n description={\n statusFilter\n ? 'Try a different status filter.'\n : 'Create your first notebook to start analysing federated datasets.'\n }\n action={\n statusFilter ? undefined : (\n <Button type=\"button\" onClick={() => navigate('notebooks/create')}>\n <Plus className=\"size-4\" />\n New notebook\n </Button>\n )\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>Status</TableHead>\n <TableHead>Runtime</TableHead>\n <TableHead className=\"text-right\">Datasets</TableHead>\n <TableHead>Sharing</TableHead>\n <TableHead>Tags</TableHead>\n <TableHead>Updated</TableHead>\n <TableHead className=\"w-12 text-right\">Actions</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {notebooks.map((notebook) => (\n <TableRow\n key={notebook.id}\n className=\"cursor-pointer\"\n onClick={() => navigate(`notebooks/${notebook.id}`)}\n >\n <TableCell className=\"font-medium text-text-primary\">\n {notebook.title}\n </TableCell>\n <TableCell>\n <StatusBadge status={notebook.status} />\n </TableCell>\n <TableCell className=\"text-text-secondary\">\n {notebook.runtime ?? '—'}\n </TableCell>\n <TableCell className=\"text-right tabular-nums text-text-secondary\">\n {notebook.datasetIds.length}\n </TableCell>\n <TableCell>\n {notebook.isShared ? (\n <Badge variant=\"secondary\" className=\"gap-1\">\n <Share2 className=\"size-3\" aria-hidden=\"true\" />\n Shared\n </Badge>\n ) : (\n <span className=\"text-text-secondary\">Private</span>\n )}\n </TableCell>\n <TableCell>\n {notebook.tags.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {notebook.tags.slice(0, 3).map((tag) => (\n <Badge key={tag} variant=\"outline\">\n {tag}\n </Badge>\n ))}\n {notebook.tags.length > 3 ? (\n <span className=\"text-xs text-text-secondary\">\n +{notebook.tags.length - 3}\n </span>\n ) : null}\n </div>\n ) : (\n <span className=\"text-text-secondary\">—</span>\n )}\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-text-secondary\">\n {formatDate(notebook.updatedAt)}\n </TableCell>\n <TableCell className=\"text-right\">\n <ActionMenu\n ariaLabel={`Actions for ${notebook.title}`}\n entityName={notebook.title}\n onView={() => navigate(`notebooks/${notebook.id}`)}\n onEdit={() => navigate(`notebooks/${notebook.id}/edit`)}\n onDelete={\n notebook.status === 'ARCHIVED'\n ? undefined\n : () => handleArchive(notebook)\n }\n deleteLabel=\"Archive notebook\"\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 {notebooks.map((notebook) => (\n <li\n key={notebook.id}\n className=\"rounded-card border border-border-default bg-bg-elevated\"\n >\n <div className=\"flex items-start justify-between gap-2 p-4\">\n <button\n type=\"button\"\n onClick={() => navigate(`notebooks/${notebook.id}`)}\n className=\"min-w-0 flex-1 space-y-2 text-left\"\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"truncate font-medium text-text-primary\">\n {notebook.title}\n </span>\n <StatusBadge status={notebook.status} />\n </div>\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-secondary\">\n <span>Runtime: {notebook.runtime ?? '—'}</span>\n <span className=\"tabular-nums\">\n {notebook.datasetIds.length} dataset\n {notebook.datasetIds.length === 1 ? '' : 's'}\n </span>\n {notebook.isShared ? (\n <span className=\"inline-flex items-center gap-1\">\n <Share2 className=\"size-3\" aria-hidden=\"true\" />\n Shared\n </span>\n ) : (\n <span>Private</span>\n )}\n <span>Updated {formatDate(notebook.updatedAt)}</span>\n </div>\n {notebook.tags.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {notebook.tags.slice(0, 4).map((tag) => (\n <Badge key={tag} variant=\"outline\">\n {tag}\n </Badge>\n ))}\n {notebook.tags.length > 4 ? (\n <span className=\"text-xs text-text-secondary\">\n +{notebook.tags.length - 4}\n </span>\n ) : null}\n </div>\n ) : null}\n </button>\n <ActionMenu\n ariaLabel={`Actions for ${notebook.title}`}\n entityName={notebook.title}\n onView={() => navigate(`notebooks/${notebook.id}`)}\n onEdit={() => navigate(`notebooks/${notebook.id}/edit`)}\n onDelete={\n notebook.status === 'ARCHIVED' ? undefined : () => handleArchive(notebook)\n }\n deleteLabel=\"Archive notebook\"\n />\n </div>\n </li>\n ))}\n </ul>\n </>\n )}\n\n {!loading && !error && notebooks.length > 0 ? (\n <Pagination\n offset={pagination.offset}\n limit={pagination.limit}\n loadedCount={notebooks.length}\n hasMore={pageInfo?.hasMore ?? false}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel=\"notebooks\"\n loading={loading}\n />\n ) : null}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"NotebooksPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/notebooks/NotebooksPage.tsx"],"sourcesContent":["/**\n * NotebooksPage — paginated list of analysis notebooks.\n *\n * Notebooks are computational documents (title, runtime, environment, linked\n * datasets) that scientists run analyses inside. This surface lists them from\n * `cosListNotebooks` with a status filter, per-row View / Edit / Archive\n * actions, and offset pagination. Every value here comes from the backend —\n * there is no fabricated data.\n */\nimport { useState } from 'react';\nimport { NotebookText, Plus, Share2 } from 'lucide-react';\nimport {\n Badge,\n Button,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useCosArchiveNotebookMutation,\n useCosNotebooksQuery,\n type CosNotebook,\n type CosNotebookStatus,\n} from '../../api/cosmicApi';\nimport {\n ActionMenu,\n EmptyState,\n ErrorState,\n LoadingState,\n PageLayout,\n Pagination,\n StatusBadge,\n usePagination,\n} 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\nconst ALL = 'ALL';\n\nfunction formatDate(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.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });\n}\n\nexport default function NotebooksPage() {\n const { navigate } = useCosmicIntersection();\n const [statusFilter, setStatusFilter] = useState<CosNotebookStatus | undefined>(undefined);\n const pagination = usePagination({ pageSize: 50, resetKey: statusFilter });\n\n const { data, loading, error, refetch } = useCosNotebooksQuery({\n status: statusFilter,\n pagination: { limit: pagination.limit, offset: pagination.offset },\n });\n const [archiveNotebook] = useCosArchiveNotebookMutation();\n\n const notebooks = data?.cosListNotebooks.items ?? [];\n const pageInfo = data?.cosListNotebooks.pageInfo;\n\n const handleArchive = async (notebook: CosNotebook) => {\n const response = await archiveNotebook({ variables: { id: notebook.id } });\n if (response.error) {\n // onError already surfaced the toast; rethrow so the confirm dialog stays open.\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 const actions = (\n <Button type=\"button\" onClick={() => navigate('notebooks/create')}>\n <Plus className=\"size-4\" />\n New notebook\n </Button>\n );\n\n return (\n <PageLayout\n title=\"Notebooks\"\n description=\"Computational notebooks for running analyses over federated science datasets.\"\n actions={actions}\n >\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <label className=\"text-sm text-text-secondary\" htmlFor=\"notebook-status-filter\">\n Status\n </label>\n <Select\n value={statusFilter ?? ALL}\n onValueChange={(value) =>\n setStatusFilter(value === ALL ? undefined : (value as CosNotebookStatus))\n }\n >\n <SelectTrigger\n id=\"notebook-status-filter\"\n className=\"w-48\"\n aria-label=\"Filter by status\"\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>All statuses</SelectItem>\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\n {loading ? (\n <LoadingState variant=\"rows\" rows={6} label=\"Loading notebooks…\" />\n ) : error ? (\n <ErrorState\n message=\"We could not load your notebooks. Please try again.\"\n onRetry={() => void refetch()}\n />\n ) : notebooks.length === 0 ? (\n <EmptyState\n icon={NotebookText}\n title={statusFilter ? 'No notebooks match this filter.' : 'No notebooks yet.'}\n description={\n statusFilter\n ? 'Try a different status filter.'\n : 'Create your first notebook to start analysing federated datasets.'\n }\n action={\n statusFilter ? undefined : (\n <Button type=\"button\" onClick={() => navigate('notebooks/create')}>\n <Plus className=\"size-4\" />\n New notebook\n </Button>\n )\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>Status</TableHead>\n <TableHead>Runtime</TableHead>\n <TableHead className=\"text-right\">Datasets</TableHead>\n <TableHead>Sharing</TableHead>\n <TableHead>Tags</TableHead>\n <TableHead>Updated</TableHead>\n <TableHead className=\"w-12 text-right\">Actions</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {notebooks.map((notebook) => (\n <TableRow\n key={notebook.id}\n className=\"cursor-pointer\"\n onClick={() => navigate(`notebooks/${notebook.id}`)}\n >\n <TableCell className=\"font-medium text-text-primary\">\n {notebook.title}\n </TableCell>\n <TableCell>\n <StatusBadge status={notebook.status} />\n </TableCell>\n <TableCell className=\"text-text-secondary\">\n {notebook.runtime ?? '—'}\n </TableCell>\n <TableCell className=\"text-right tabular-nums text-text-secondary\">\n {notebook.datasetIds.length}\n </TableCell>\n <TableCell>\n {notebook.isShared ? (\n <Badge variant=\"secondary\" className=\"gap-1\">\n <Share2 className=\"size-3\" aria-hidden=\"true\" />\n Shared\n </Badge>\n ) : (\n <span className=\"text-text-secondary\">Private</span>\n )}\n </TableCell>\n <TableCell>\n {notebook.tags.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {notebook.tags.slice(0, 3).map((tag) => (\n <Badge key={tag} variant=\"outline\">\n {tag}\n </Badge>\n ))}\n {notebook.tags.length > 3 ? (\n <span className=\"text-xs text-text-secondary\">\n +{notebook.tags.length - 3}\n </span>\n ) : null}\n </div>\n ) : (\n <span className=\"text-text-secondary\">—</span>\n )}\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-text-secondary\">\n {formatDate(notebook.updatedAt)}\n </TableCell>\n <TableCell className=\"text-right\">\n <ActionMenu\n ariaLabel={`Actions for ${notebook.title}`}\n entityName={notebook.title}\n onView={() => navigate(`notebooks/${notebook.id}`)}\n onEdit={() => navigate(`notebooks/${notebook.id}/edit`)}\n onDelete={\n notebook.status === 'ARCHIVED'\n ? undefined\n : () => handleArchive(notebook)\n }\n deleteLabel=\"Archive notebook\"\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 {notebooks.map((notebook) => (\n <li\n key={notebook.id}\n className=\"rounded-card border border-border-default bg-bg-elevated\"\n >\n <div className=\"flex items-start justify-between gap-2 p-4\">\n <button\n type=\"button\"\n onClick={() => navigate(`notebooks/${notebook.id}`)}\n className=\"min-w-0 flex-1 space-y-2 text-left\"\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"truncate font-medium text-text-primary\">\n {notebook.title}\n </span>\n <StatusBadge status={notebook.status} />\n </div>\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-secondary\">\n <span>Runtime: {notebook.runtime ?? '—'}</span>\n <span className=\"tabular-nums\">\n {notebook.datasetIds.length} dataset\n {notebook.datasetIds.length === 1 ? '' : 's'}\n </span>\n {notebook.isShared ? (\n <span className=\"inline-flex items-center gap-1\">\n <Share2 className=\"size-3\" aria-hidden=\"true\" />\n Shared\n </span>\n ) : (\n <span>Private</span>\n )}\n <span>Updated {formatDate(notebook.updatedAt)}</span>\n </div>\n {notebook.tags.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {notebook.tags.slice(0, 4).map((tag) => (\n <Badge key={tag} variant=\"outline\">\n {tag}\n </Badge>\n ))}\n {notebook.tags.length > 4 ? (\n <span className=\"text-xs text-text-secondary\">\n +{notebook.tags.length - 4}\n </span>\n ) : null}\n </div>\n ) : null}\n </button>\n <ActionMenu\n ariaLabel={`Actions for ${notebook.title}`}\n entityName={notebook.title}\n onView={() => navigate(`notebooks/${notebook.id}`)}\n onEdit={() => navigate(`notebooks/${notebook.id}/edit`)}\n onDelete={\n notebook.status === 'ARCHIVED' ? undefined : () => handleArchive(notebook)\n }\n deleteLabel=\"Archive notebook\"\n />\n </div>\n </li>\n ))}\n </ul>\n </>\n )}\n\n {!loading && !error && notebooks.length > 0 ? (\n <Pagination\n offset={pagination.offset}\n limit={pagination.limit}\n loadedCount={notebooks.length}\n hasMore={pageInfo?.hasMore ?? false}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel=\"notebooks\"\n loading={loading}\n />\n ) : null}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -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-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 +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-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 +1 @@
1
- {"version":3,"file":"PipelinesPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/pipelines/PipelinesPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { GitBranch, Play, Plus } from 'lucide-react';\nimport {\n Button,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useCosArchivePipelineMutation,\n useCosPipelinesQuery,\n useCosStartRunMutation,\n type CosPipeline,\n type CosPipelineStatus,\n} from '../../api/cosmicApi';\nimport {\n ActionMenu,\n EmptyState,\n ErrorState,\n LoadingState,\n PageLayout,\n Pagination,\n StatusBadge,\n formatEnumLabel,\n usePagination,\n} from '../../components';\nimport { useCosmicIntersection } from '../../context/CosmicIntersectionContext';\nimport { mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\nconst STATUS_OPTIONS: CosPipelineStatus[] = ['DRAFT', 'ACTIVE', 'PAUSED', 'ARCHIVED'];\nconst ALL = 'ALL';\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\nexport default function PipelinesPage() {\n const { navigate } = useCosmicIntersection();\n const [status, setStatus] = useState<CosPipelineStatus | typeof ALL>(ALL);\n const pagination = usePagination({ resetKey: status });\n\n const { data, loading, error, refetch } = useCosPipelinesQuery({\n status: status === ALL ? undefined : status,\n pagination: { limit: pagination.limit, offset: pagination.offset },\n });\n\n const [startRun] = useCosStartRunMutation();\n const [archivePipeline] = useCosArchivePipelineMutation();\n\n const pipelines = useMemo(() => data?.cosListPipelines.items ?? [], [data]);\n const pageInfo = data?.cosListPipelines.pageInfo;\n\n const handleStartRun = async (pipeline: CosPipeline) => {\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 for “${pipeline.name}” is queued.`);\n await refetch();\n if (runId) {\n navigate(`runs/${runId}`);\n }\n };\n\n const handleArchive = async (pipeline: CosPipeline) => {\n const response = await archivePipeline({ variables: { id: pipeline.id } });\n if (\n !mutationSucceeded(response, response.data?.cosArchivePipeline, 'Could not archive pipeline')\n ) {\n throw new Error('Archive not confirmed');\n }\n await refetch();\n notify.success('Pipeline archived', `“${pipeline.name}” was archived.`);\n };\n\n const renderBody = () => {\n if (loading) {\n return <LoadingState variant=\"rows\" rows={6} label=\"Loading pipelines…\" />;\n }\n if (error) {\n return (\n <ErrorState\n message=\"We could not load your pipelines. Please try again.\"\n onRetry={() => void refetch()}\n />\n );\n }\n if (pipelines.length === 0) {\n return (\n <EmptyState\n icon={GitBranch}\n title={status === ALL ? 'No pipelines yet' : 'No pipelines match this filter'}\n description={\n status === ALL\n ? 'Pipelines orchestrate ingest, calibration, transform and export steps over your datasets.'\n : 'Try a different status filter to see more pipelines.'\n }\n action={\n status === ALL ? (\n <Button type=\"button\" onClick={() => navigate('pipelines/create')}>\n <Plus className=\"size-4\" />\n New pipeline\n </Button>\n ) : undefined\n }\n />\n );\n }\n\n return (\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>Name</TableHead>\n <TableHead>Status</TableHead>\n <TableHead>Trigger</TableHead>\n <TableHead>Schedule</TableHead>\n <TableHead>Last run</TableHead>\n <TableHead className=\"text-right\">Version</TableHead>\n <TableHead>Tags</TableHead>\n <TableHead className=\"w-12 text-right\">\n <span className=\"sr-only\">Actions</span>\n </TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {pipelines.map((pipeline) => (\n <TableRow\n key={pipeline.id}\n className=\"cursor-pointer\"\n onClick={() => navigate(`pipelines/${pipeline.id}`)}\n >\n <TableCell className=\"font-medium text-text-primary\">\n <div className=\"flex flex-col\">\n <span className=\"truncate\">{pipeline.name}</span>\n {pipeline.description ? (\n <span className=\"mt-0.5 line-clamp-1 text-xs text-text-secondary\">\n {pipeline.description}\n </span>\n ) : null}\n </div>\n </TableCell>\n <TableCell>\n <StatusBadge status={pipeline.status} />\n </TableCell>\n <TableCell className=\"text-text-secondary\">\n {formatEnumLabel(pipeline.trigger)}\n </TableCell>\n <TableCell className=\"font-mono text-xs text-text-secondary\">\n {pipeline.schedule || '—'}\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-text-secondary\">\n {formatDate(pipeline.lastRunAt)}\n </TableCell>\n <TableCell className=\"text-right tabular-nums text-text-secondary\">\n v{pipeline.version}\n </TableCell>\n <TableCell>\n <div className=\"flex max-w-[12rem] flex-wrap gap-1\">\n {pipeline.tags.length === 0 ? (\n <span className=\"text-xs text-text-secondary\">—</span>\n ) : (\n pipeline.tags.slice(0, 3).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 )}\n {pipeline.tags.length > 3 ? (\n <span className=\"text-xs text-text-secondary\">\n +{pipeline.tags.length - 3}\n </span>\n ) : null}\n </div>\n </TableCell>\n <TableCell className=\"text-right\">\n <ActionMenu\n ariaLabel={`Actions for ${pipeline.name}`}\n entityName={pipeline.name}\n onView={() => navigate(`pipelines/${pipeline.id}`)}\n onEdit={() => navigate(`pipelines/${pipeline.id}/edit`)}\n onDelete={() => handleArchive(pipeline)}\n deleteLabel=\"Archive pipeline\"\n extraItems={[\n {\n key: 'start',\n label: 'Start run',\n icon: Play,\n onSelect: () => void handleStartRun(pipeline),\n disabled: pipeline.status === 'ARCHIVED',\n },\n ]}\n />\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 {pipelines.map((pipeline) => (\n <li key={pipeline.id} className=\"relative\">\n <button\n type=\"button\"\n onClick={() => navigate(`pipelines/${pipeline.id}`)}\n className=\"flex w-full flex-col gap-2 rounded-card border border-border-default bg-bg-elevated p-4 pr-12 text-left transition-colors hover:border-border-strong\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"min-w-0 font-medium text-text-primary\">\n <span className=\"block truncate\">{pipeline.name}</span>\n {pipeline.description ? (\n <span className=\"mt-0.5 line-clamp-1 text-xs font-normal text-text-secondary\">\n {pipeline.description}\n </span>\n ) : null}\n </span>\n <StatusBadge status={pipeline.status} className=\"shrink-0\" />\n </div>\n <dl className=\"grid grid-cols-2 gap-x-3 gap-y-1 text-xs\">\n <div className=\"flex flex-col\">\n <dt className=\"text-text-secondary\">Trigger</dt>\n <dd className=\"text-text-primary\">{formatEnumLabel(pipeline.trigger)}</dd>\n </div>\n <div className=\"flex flex-col\">\n <dt className=\"text-text-secondary\">Version</dt>\n <dd className=\"tabular-nums text-text-primary\">v{pipeline.version}</dd>\n </div>\n <div className=\"flex flex-col\">\n <dt className=\"text-text-secondary\">Schedule</dt>\n <dd className=\"font-mono text-text-primary\">{pipeline.schedule || '—'}</dd>\n </div>\n <div className=\"flex flex-col\">\n <dt className=\"text-text-secondary\">Last run</dt>\n <dd className=\"text-text-primary\">{formatDate(pipeline.lastRunAt)}</dd>\n </div>\n </dl>\n {pipeline.tags.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {pipeline.tags.slice(0, 4).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 {pipeline.tags.length > 4 ? (\n <span className=\"text-xs text-text-secondary\">\n +{pipeline.tags.length - 4}\n </span>\n ) : null}\n </div>\n ) : null}\n </button>\n <div className=\"absolute right-2 top-2\">\n <ActionMenu\n ariaLabel={`Actions for ${pipeline.name}`}\n entityName={pipeline.name}\n onView={() => navigate(`pipelines/${pipeline.id}`)}\n onEdit={() => navigate(`pipelines/${pipeline.id}/edit`)}\n onDelete={() => handleArchive(pipeline)}\n deleteLabel=\"Archive pipeline\"\n extraItems={[\n {\n key: 'start',\n label: 'Start run',\n icon: Play,\n onSelect: () => void handleStartRun(pipeline),\n disabled: pipeline.status === 'ARCHIVED',\n },\n ]}\n />\n </div>\n </li>\n ))}\n </ul>\n\n <Pagination\n offset={pagination.offset}\n limit={pagination.limit}\n loadedCount={pipelines.length}\n hasMore={pageInfo?.hasMore ?? false}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel=\"pipelines\"\n loading={loading}\n />\n </>\n );\n };\n\n return (\n <PageLayout\n title=\"Pipelines\"\n description=\"Orchestrate ingest, calibration, transform and export steps across your data.\"\n actions={\n <Button type=\"button\" onClick={() => navigate('pipelines/create')}>\n <Plus className=\"size-4\" />\n New pipeline\n </Button>\n }\n >\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <label className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <span>Status</span>\n <Select\n value={status}\n onValueChange={(value) => setStatus(value as CosPipelineStatus | typeof ALL)}\n >\n <SelectTrigger aria-label=\"Filter by status\" className=\"w-44\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>All statuses</SelectItem>\n {STATUS_OPTIONS.map((option) => (\n <SelectItem key={option} value={option}>\n {formatEnumLabel(option)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </label>\n </div>\n\n {renderBody()}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"PipelinesPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/pipelines/PipelinesPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { GitBranch, Play, Plus } from 'lucide-react';\nimport {\n Button,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n useCosArchivePipelineMutation,\n useCosPipelinesQuery,\n useCosStartRunMutation,\n type CosPipeline,\n type CosPipelineStatus,\n} from '../../api/cosmicApi';\nimport {\n ActionMenu,\n EmptyState,\n ErrorState,\n LoadingState,\n PageLayout,\n Pagination,\n StatusBadge,\n formatEnumLabel,\n usePagination,\n} from '../../components';\nimport { useCosmicIntersection } from '../../context/CosmicIntersectionContext';\nimport { mutationSucceeded } from '../../lib/mutationResult';\nimport { notify } from '../../lib/toast';\n\nconst STATUS_OPTIONS: CosPipelineStatus[] = ['DRAFT', 'ACTIVE', 'PAUSED', 'ARCHIVED'];\nconst ALL = 'ALL';\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\nexport default function PipelinesPage() {\n const { navigate } = useCosmicIntersection();\n const [status, setStatus] = useState<CosPipelineStatus | typeof ALL>(ALL);\n const pagination = usePagination({ resetKey: status });\n\n const { data, loading, error, refetch } = useCosPipelinesQuery({\n status: status === ALL ? undefined : status,\n pagination: { limit: pagination.limit, offset: pagination.offset },\n });\n\n const [startRun] = useCosStartRunMutation();\n const [archivePipeline] = useCosArchivePipelineMutation();\n\n const pipelines = useMemo(() => data?.cosListPipelines.items ?? [], [data]);\n const pageInfo = data?.cosListPipelines.pageInfo;\n\n const handleStartRun = async (pipeline: CosPipeline) => {\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 for “${pipeline.name}” is queued.`);\n await refetch();\n if (runId) {\n navigate(`runs/${runId}`);\n }\n };\n\n const handleArchive = async (pipeline: CosPipeline) => {\n const response = await archivePipeline({ variables: { id: pipeline.id } });\n if (\n !mutationSucceeded(response, response.data?.cosArchivePipeline, 'Could not archive pipeline')\n ) {\n throw new Error('Archive not confirmed');\n }\n await refetch();\n notify.success('Pipeline archived', `“${pipeline.name}” was archived.`);\n };\n\n const renderBody = () => {\n if (loading) {\n return <LoadingState variant=\"rows\" rows={6} label=\"Loading pipelines…\" />;\n }\n if (error) {\n return (\n <ErrorState\n message=\"We could not load your pipelines. Please try again.\"\n onRetry={() => void refetch()}\n />\n );\n }\n if (pipelines.length === 0) {\n return (\n <EmptyState\n icon={GitBranch}\n title={status === ALL ? 'No pipelines yet' : 'No pipelines match this filter'}\n description={\n status === ALL\n ? 'Pipelines orchestrate ingest, calibration, transform and export steps over your datasets.'\n : 'Try a different status filter to see more pipelines.'\n }\n action={\n status === ALL ? (\n <Button type=\"button\" onClick={() => navigate('pipelines/create')}>\n <Plus className=\"size-4\" />\n New pipeline\n </Button>\n ) : undefined\n }\n />\n );\n }\n\n return (\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>Name</TableHead>\n <TableHead>Status</TableHead>\n <TableHead>Trigger</TableHead>\n <TableHead>Schedule</TableHead>\n <TableHead>Last run</TableHead>\n <TableHead className=\"text-right\">Version</TableHead>\n <TableHead>Tags</TableHead>\n <TableHead className=\"w-12 text-right\">\n <span className=\"sr-only\">Actions</span>\n </TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {pipelines.map((pipeline) => (\n <TableRow\n key={pipeline.id}\n className=\"cursor-pointer\"\n onClick={() => navigate(`pipelines/${pipeline.id}`)}\n >\n <TableCell className=\"font-medium text-text-primary\">\n <div className=\"flex flex-col\">\n <span className=\"truncate\">{pipeline.name}</span>\n {pipeline.description ? (\n <span className=\"mt-0.5 line-clamp-1 text-xs text-text-secondary\">\n {pipeline.description}\n </span>\n ) : null}\n </div>\n </TableCell>\n <TableCell>\n <StatusBadge status={pipeline.status} />\n </TableCell>\n <TableCell className=\"text-text-secondary\">\n {formatEnumLabel(pipeline.trigger)}\n </TableCell>\n <TableCell className=\"font-mono text-xs text-text-secondary\">\n {pipeline.schedule || '—'}\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-text-secondary\">\n {formatDate(pipeline.lastRunAt)}\n </TableCell>\n <TableCell className=\"text-right tabular-nums text-text-secondary\">\n v{pipeline.version}\n </TableCell>\n <TableCell>\n <div className=\"flex max-w-[12rem] flex-wrap gap-1\">\n {pipeline.tags.length === 0 ? (\n <span className=\"text-xs text-text-secondary\">—</span>\n ) : (\n pipeline.tags.slice(0, 3).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 )}\n {pipeline.tags.length > 3 ? (\n <span className=\"text-xs text-text-secondary\">\n +{pipeline.tags.length - 3}\n </span>\n ) : null}\n </div>\n </TableCell>\n <TableCell className=\"text-right\">\n <ActionMenu\n ariaLabel={`Actions for ${pipeline.name}`}\n entityName={pipeline.name}\n onView={() => navigate(`pipelines/${pipeline.id}`)}\n onEdit={() => navigate(`pipelines/${pipeline.id}/edit`)}\n onDelete={() => handleArchive(pipeline)}\n deleteLabel=\"Archive pipeline\"\n extraItems={[\n {\n key: 'start',\n label: 'Start run',\n icon: Play,\n onSelect: () => void handleStartRun(pipeline),\n disabled: pipeline.status === 'ARCHIVED',\n },\n ]}\n />\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 {pipelines.map((pipeline) => (\n <li key={pipeline.id} className=\"relative\">\n <button\n type=\"button\"\n onClick={() => navigate(`pipelines/${pipeline.id}`)}\n className=\"flex w-full flex-col gap-2 rounded-card border border-border-default bg-bg-elevated p-4 pr-12 text-left transition-colors hover:border-border-strong\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"min-w-0 font-medium text-text-primary\">\n <span className=\"block truncate\">{pipeline.name}</span>\n {pipeline.description ? (\n <span className=\"mt-0.5 line-clamp-1 text-xs font-normal text-text-secondary\">\n {pipeline.description}\n </span>\n ) : null}\n </span>\n <StatusBadge status={pipeline.status} className=\"shrink-0\" />\n </div>\n <dl className=\"grid grid-cols-2 gap-x-3 gap-y-1 text-xs\">\n <div className=\"flex flex-col\">\n <dt className=\"text-text-secondary\">Trigger</dt>\n <dd className=\"text-text-primary\">{formatEnumLabel(pipeline.trigger)}</dd>\n </div>\n <div className=\"flex flex-col\">\n <dt className=\"text-text-secondary\">Version</dt>\n <dd className=\"tabular-nums text-text-primary\">v{pipeline.version}</dd>\n </div>\n <div className=\"flex flex-col\">\n <dt className=\"text-text-secondary\">Schedule</dt>\n <dd className=\"font-mono text-text-primary\">{pipeline.schedule || '—'}</dd>\n </div>\n <div className=\"flex flex-col\">\n <dt className=\"text-text-secondary\">Last run</dt>\n <dd className=\"text-text-primary\">{formatDate(pipeline.lastRunAt)}</dd>\n </div>\n </dl>\n {pipeline.tags.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {pipeline.tags.slice(0, 4).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 {pipeline.tags.length > 4 ? (\n <span className=\"text-xs text-text-secondary\">\n +{pipeline.tags.length - 4}\n </span>\n ) : null}\n </div>\n ) : null}\n </button>\n <div className=\"absolute right-2 top-2\">\n <ActionMenu\n ariaLabel={`Actions for ${pipeline.name}`}\n entityName={pipeline.name}\n onView={() => navigate(`pipelines/${pipeline.id}`)}\n onEdit={() => navigate(`pipelines/${pipeline.id}/edit`)}\n onDelete={() => handleArchive(pipeline)}\n deleteLabel=\"Archive pipeline\"\n extraItems={[\n {\n key: 'start',\n label: 'Start run',\n icon: Play,\n onSelect: () => void handleStartRun(pipeline),\n disabled: pipeline.status === 'ARCHIVED',\n },\n ]}\n />\n </div>\n </li>\n ))}\n </ul>\n\n <Pagination\n offset={pagination.offset}\n limit={pagination.limit}\n loadedCount={pipelines.length}\n hasMore={pageInfo?.hasMore ?? false}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel=\"pipelines\"\n loading={loading}\n />\n </>\n );\n };\n\n return (\n <PageLayout\n title=\"Pipelines\"\n description=\"Orchestrate ingest, calibration, transform and export steps across your data.\"\n actions={\n <Button type=\"button\" onClick={() => navigate('pipelines/create')}>\n <Plus className=\"size-4\" />\n New pipeline\n </Button>\n }\n >\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <label className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <span>Status</span>\n <Select\n value={status}\n onValueChange={(value) => setStatus(value as CosPipelineStatus | typeof ALL)}\n >\n <SelectTrigger aria-label=\"Filter by status\" className=\"w-44\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>All statuses</SelectItem>\n {STATUS_OPTIONS.map((option) => (\n <SelectItem key={option} value={option}>\n {formatEnumLabel(option)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </label>\n </div>\n\n {renderBody()}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}