@burdenoff/microfe-cosmicintersection 2026.830.1 → 2026.830.2

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 (35) hide show
  1. package/dist/cosmicintersection/components/ActionMenu.d.ts.map +1 -1
  2. package/dist/cosmicintersection/components/ActionMenu.js.map +1 -1
  3. package/dist/cosmicintersection/components/ErrorState.d.ts.map +1 -1
  4. package/dist/cosmicintersection/components/ErrorState.js.map +1 -1
  5. package/dist/cosmicintersection/components/FileUploadField.d.ts.map +1 -1
  6. package/dist/cosmicintersection/components/FileUploadField.js.map +1 -1
  7. package/dist/cosmicintersection/components/Pagination.d.ts.map +1 -1
  8. package/dist/cosmicintersection/components/Pagination.js.map +1 -1
  9. package/dist/cosmicintersection/components/StatusBadge.d.ts.map +1 -1
  10. package/dist/cosmicintersection/components/StatusBadge.js.map +1 -1
  11. package/dist/cosmicintersection/pages/access/AccessPage.js.map +1 -1
  12. package/dist/cosmicintersection/pages/analyses/AnalysesPage.d.ts.map +1 -1
  13. package/dist/cosmicintersection/pages/analyses/AnalysesPage.js.map +1 -1
  14. package/dist/cosmicintersection/pages/collections/CollectionsPage.d.ts.map +1 -1
  15. package/dist/cosmicintersection/pages/collections/CollectionsPage.js.map +1 -1
  16. package/dist/cosmicintersection/pages/dashboard/DashboardPage.d.ts.map +1 -1
  17. package/dist/cosmicintersection/pages/dashboard/DashboardPage.js.map +1 -1
  18. package/dist/cosmicintersection/pages/datasets/DatasetsPage.d.ts.map +1 -1
  19. package/dist/cosmicintersection/pages/datasets/DatasetsPage.js.map +1 -1
  20. package/dist/cosmicintersection/pages/discovery/DiscoveryPage.d.ts.map +1 -1
  21. package/dist/cosmicintersection/pages/discovery/DiscoveryPage.js.map +1 -1
  22. package/dist/cosmicintersection/pages/lineage/LineagePage.d.ts.map +1 -1
  23. package/dist/cosmicintersection/pages/lineage/LineagePage.js.map +1 -1
  24. package/dist/cosmicintersection/pages/notebooks/NotebooksPage.d.ts.map +1 -1
  25. package/dist/cosmicintersection/pages/notebooks/NotebooksPage.js.map +1 -1
  26. package/dist/cosmicintersection/pages/pipelines/PipelinesPage.d.ts.map +1 -1
  27. package/dist/cosmicintersection/pages/pipelines/PipelinesPage.js.map +1 -1
  28. package/dist/cosmicintersection/pages/releases/ReleasesPage.js.map +1 -1
  29. package/dist/cosmicintersection/pages/runs/RunsPage.d.ts.map +1 -1
  30. package/dist/cosmicintersection/pages/runs/RunsPage.js.map +1 -1
  31. package/dist/cosmicintersection/pages/sources/SourcesPage.d.ts.map +1 -1
  32. package/dist/cosmicintersection/pages/sources/SourcesPage.js.map +1 -1
  33. package/dist/shared/hooks/useTr.d.ts.map +1 -1
  34. package/dist/shared/hooks/useTr.js.map +1 -1
  35. package/package.json +1 -1
@@ -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';\nimport { useTr } from '../../../shared/hooks/useTr';\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 tr = useTr();\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, tr('cosmicintersection.pages.pipelines.startRunError', 'Could not start a run'))) {\n return;\n }\n notify.success(\n tr('cosmicintersection.pages.pipelines.startRunSuccess', 'Run started'),\n tr(\n 'cosmicintersection.pages.pipelines.startRunSuccessDescription',\n 'A new run for “{{name}}” is queued.',\n { name: pipeline.name },\n ),\n );\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(\n response,\n response.data?.cosArchivePipeline,\n tr('cosmicintersection.pages.pipelines.archiveError', 'Could not archive pipeline'),\n )\n ) {\n throw new Error('Archive not confirmed');\n }\n await refetch();\n notify.success(\n tr('cosmicintersection.pages.pipelines.archiveSuccess', 'Pipeline archived'),\n tr(\n 'cosmicintersection.pages.pipelines.archiveSuccessDescription',\n '“{{name}}” was archived.',\n { name: pipeline.name },\n ),\n );\n };\n\n const renderBody = () => {\n if (loading) {\n return <LoadingState variant=\"rows\" rows={6} label={tr('cosmicintersection.pages.pipelines.loading', 'Loading pipelines…')} />;\n }\n if (error) {\n return (\n <ErrorState\n message={tr('cosmicintersection.pages.pipelines.error', '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={\n status === ALL\n ? tr('cosmicintersection.pages.pipelines.emptyTitle', 'No pipelines yet')\n : tr('cosmicintersection.pages.pipelines.emptyFilterTitle', 'No pipelines match this filter')\n }\n description={\n status === ALL\n ? tr(\n 'cosmicintersection.pages.pipelines.emptyDescription',\n 'Pipelines orchestrate ingest, calibration, transform and export steps over your datasets.',\n )\n : tr(\n 'cosmicintersection.pages.pipelines.emptyFilterDescription',\n 'Try a different status filter to see more pipelines.',\n )\n }\n action={\n status === ALL ? (\n <Button type=\"button\" onClick={() => navigate('pipelines/create')}>\n <Plus className=\"size-4\" />\n {tr('cosmicintersection.pages.pipelines.create', '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>{tr('cosmicintersection.common.name', 'Name')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.status', 'Status')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.trigger', 'Trigger')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.schedule', 'Schedule')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.lastRun', 'Last run')}</TableHead>\n <TableHead className=\"text-right\">{tr('cosmicintersection.common.version', 'Version')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.tags', 'Tags')}</TableHead>\n <TableHead className=\"w-12 text-right\">\n <span className=\"sr-only\">{tr('cosmicintersection.common.actions', '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={tr('cosmicintersection.common.actionsFor', 'Actions for {{name}}', { name: 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={tr('cosmicintersection.pages.pipelines.archive', 'Archive pipeline')}\n extraItems={[\n {\n key: 'start',\n label: tr('cosmicintersection.pages.pipelines.startRun', '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\">{tr('cosmicintersection.common.trigger', '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\">{tr('cosmicintersection.common.version', '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\">{tr('cosmicintersection.common.schedule', '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\">{tr('cosmicintersection.common.lastRun', '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={tr('cosmicintersection.common.actionsFor', 'Actions for {{name}}', { name: 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={tr('cosmicintersection.pages.pipelines.archive', 'Archive pipeline')}\n extraItems={[\n {\n key: 'start',\n label: tr('cosmicintersection.pages.pipelines.startRun', '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={tr('cosmicintersection.pages.pipelines.itemLabel', 'pipelines')}\n loading={loading}\n />\n </>\n );\n };\n\n return (\n <PageLayout\n title={tr('cosmicintersection.pages.pipelines.title', 'Pipelines')}\n description={tr(\n 'cosmicintersection.pages.pipelines.description',\n 'Orchestrate ingest, calibration, transform and export steps across your data.',\n )}\n actions={\n <Button type=\"button\" onClick={() => navigate('pipelines/create')}>\n <Plus className=\"size-4\" />\n {tr('cosmicintersection.pages.pipelines.create', '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>{tr('cosmicintersection.common.status', 'Status')}</span>\n <Select\n value={status}\n onValueChange={(value) => setStatus(value as CosPipelineStatus | typeof ALL)}\n >\n <SelectTrigger aria-label={tr('cosmicintersection.common.filterByStatus', 'Filter by status')} className=\"w-44\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>{tr('cosmicintersection.common.allStatuses', '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":";;;;;;;;;;;;;;;;;;AAwCA,IAAM,IAAsC;CAAC;CAAS;CAAU;CAAU;AAAU,GAC9E,IAAM;AAEZ,SAAS,EAAW,GAA+B;CACjD,IAAI,CAAC,GACH,OAAO;CAET,IAAM,IAAO,IAAI,KAAK,CAAK;CAC3B,OAAO,OAAO,MAAM,EAAK,QAAQ,CAAC,IAAI,MAAM,EAAK,eAAe;AAClE;AAEA,SAAwB,IAAgB;CACtC,IAAM,EAAE,gBAAa,EAAsB,GACrC,IAAK,EAAM,GACX,CAAC,GAAQ,KAAa,EAAyC,CAAG,GAClE,IAAa,EAAc,EAAE,UAAU,EAAO,CAAC,GAE/C,EAAE,SAAM,YAAS,UAAO,eAAY,EAAqB;EAC7D,QAAQ,MAAW,IAAM,KAAA,IAAY;EACrC,YAAY;GAAE,OAAO,EAAW;GAAO,QAAQ,EAAW;EAAO;CACnE,CAAC,GAEK,CAAC,KAAY,EAAuB,GACpC,CAAC,KAAmB,EAA8B,GAElD,IAAY,QAAc,GAAM,iBAAiB,SAAS,CAAC,GAAG,CAAC,CAAI,CAAC,GACpE,IAAW,GAAM,iBAAiB,UAElC,IAAiB,OAAO,MAA0B;EACtD,IAAM,IAAW,MAAM,EAAS,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,EAAS,GAAG,EAAE,EAAE,CAAC,GAC/E,IAAQ,EAAS,MAAM,aAAa;EACrC,EAAkB,GAAU,GAAO,EAAG,oDAAoD,uBAAuB,CAAC,MAGvH,EAAO,QACL,EAAG,sDAAsD,aAAa,GACtE,EACE,iEACA,uCACA,EAAE,MAAM,EAAS,KAAK,CACxB,CACF,GACA,MAAM,EAAQ,GACV,KACF,EAAS,QAAQ,GAAO;CAE5B,GAEM,IAAgB,OAAO,MAA0B;EACrD,IAAM,IAAW,MAAM,EAAgB,EAAE,WAAW,EAAE,IAAI,EAAS,GAAG,EAAE,CAAC;EACzE,IACE,CAAC,EACC,GACA,EAAS,MAAM,oBACf,EAAG,mDAAmD,4BAA4B,CACpF,GAEA,MAAU,MAAM,uBAAuB;EAGzC,AADA,MAAM,EAAQ,GACd,EAAO,QACL,EAAG,qDAAqD,mBAAmB,GAC3E,EACE,gEACA,4BACA,EAAE,MAAM,EAAS,KAAK,CACxB,CACF;CACF;CA4OA,OACE,kBAAC,GAAD;EACE,OAAO,EAAG,4CAA4C,WAAW;EACjE,aAAa,EACX,kDACA,+EACF;EACA,SACE,kBAAC,GAAD;GAAQ,MAAK;GAAS,eAAe,EAAS,kBAAkB;aAAhE,CACE,kBAAC,GAAD,EAAM,WAAU,SAAU,CAAA,GACzB,EAAG,6CAA6C,cAAc,CACzD;;YAGV,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,SAAD;KAAO,WAAU;eAAjB,CACE,kBAAC,QAAD,EAAA,UAAO,EAAG,oCAAoC,QAAQ,EAAQ,CAAA,GAC9D,kBAAC,GAAD;MACE,OAAO;MACP,gBAAgB,MAAU,EAAU,CAAuC;gBAF7E,CAIE,kBAAC,GAAD;OAAe,cAAY,EAAG,4CAA4C,kBAAkB;OAAG,WAAU;iBACvG,kBAAC,GAAD,CAAc,CAAA;MACD,CAAA,GACf,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;OAAY,OAAO;iBAAM,EAAG,yCAAyC,cAAc;MAAc,CAAA,GAChG,EAAe,KAAK,MACnB,kBAAC,GAAD;OAAyB,OAAO;iBAC7B,EAAgB,CAAM;MACb,GAFK,CAEL,CACb,CACY,EAAA,CAAA,CACT;OACH;;GACJ,CAAA,GA5QL,IACK,kBAAC,GAAD;IAAc,SAAQ;IAAO,MAAM;IAAG,OAAO,EAAG,8CAA8C,oBAAoB;GAAI,CAAA,IAE3H,IAEA,kBAAC,GAAD;IACE,SAAS,EAAG,4CAA4C,qDAAqD;IAC7G,eAAe,KAAK,EAAQ;GAC7B,CAAA,IAGD,EAAU,WAAW,IAErB,kBAAC,GAAD;IACE,MAAM;IACN,OACE,MAAW,IACP,EAAG,iDAAiD,kBAAkB,IACtE,EAAG,uDAAuD,gCAAgC;IAEhG,aACE,MAAW,IACP,EACE,uDACA,2FACF,IACA,EACE,6DACA,sDACF;IAEN,QACE,MAAW,IACT,kBAAC,GAAD;KAAQ,MAAK;KAAS,eAAe,EAAS,kBAAkB;eAAhE,CACE,kBAAC,GAAD,EAAM,WAAU,SAAU,CAAA,GACzB,EAAG,6CAA6C,cAAc,CACzD;SACN,KAAA;GAEP,CAAA,IAKH,kBAAA,GAAA,EAAA,UAAA;IAEE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAA;MACE,kBAAC,GAAD,EAAA,UAAY,EAAG,kCAAkC,MAAM,EAAa,CAAA;MACpE,kBAAC,GAAD,EAAA,UAAY,EAAG,oCAAoC,QAAQ,EAAa,CAAA;MACxE,kBAAC,GAAD,EAAA,UAAY,EAAG,qCAAqC,SAAS,EAAa,CAAA;MAC1E,kBAAC,GAAD,EAAA,UAAY,EAAG,sCAAsC,UAAU,EAAa,CAAA;MAC5E,kBAAC,GAAD,EAAA,UAAY,EAAG,qCAAqC,UAAU,EAAa,CAAA;MAC3E,kBAAC,GAAD;OAAW,WAAU;iBAAc,EAAG,qCAAqC,SAAS;MAAa,CAAA;MACjG,kBAAC,GAAD,EAAA,UAAY,EAAG,kCAAkC,MAAM,EAAa,CAAA;MACpE,kBAAC,GAAD;OAAW,WAAU;iBACnB,kBAAC,QAAD;QAAM,WAAU;kBAAW,EAAG,qCAAqC,SAAS;OAAQ,CAAA;MAC3E,CAAA;KACH,EAAA,CAAA,EACC,CAAA,GACb,kBAAC,GAAD,EAAA,UACG,EAAU,KAAK,MACd,kBAAC,GAAD;MAEE,WAAU;MACV,eAAe,EAAS,aAAa,EAAS,IAAI;gBAHpD;OAKE,kBAAC,GAAD;QAAW,WAAU;kBACnB,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBAAY,EAAS;SAAW,CAAA,GAC/C,EAAS,cACR,kBAAC,QAAD;UAAM,WAAU;oBACb,EAAS;SACN,CAAA,IACJ,IACD;;OACI,CAAA;OACX,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAa,QAAQ,EAAS,OAAS,CAAA,EAC9B,CAAA;OACX,kBAAC,GAAD;QAAW,WAAU;kBAClB,EAAgB,EAAS,OAAO;OACxB,CAAA;OACX,kBAAC,GAAD;QAAW,WAAU;kBAClB,EAAS,YAAY;OACb,CAAA;OACX,kBAAC,GAAD;QAAW,WAAU;kBAClB,EAAW,EAAS,SAAS;OACrB,CAAA;OACX,kBAAC,GAAD;QAAW,WAAU;kBAArB,CAAmE,KAC/D,EAAS,OACF;;OACX,kBAAC,GAAD,EAAA,UACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACG,EAAS,KAAK,WAAW,IACxB,kBAAC,QAAD;SAAM,WAAU;mBAA8B;QAAO,CAAA,IAErD,EAAS,KAAK,MAAM,GAAG,CAAC,EAAE,KAAK,MAC7B,kBAAC,QAAD;SAEE,WAAU;mBAET;QACG,GAJC,CAID,CACP,GAEF,EAAS,KAAK,SAAS,IACtB,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CAA8C,KAC1C,EAAS,KAAK,SAAS,CACrB;aACJ,IACD;UACI,CAAA;OACX,kBAAC,GAAD;QAAW,WAAU;kBACnB,kBAAC,GAAD;SACE,WAAW,EAAG,wCAAwC,wBAAwB,EAAE,MAAM,EAAS,KAAK,CAAC;SACrG,YAAY,EAAS;SACrB,cAAc,EAAS,aAAa,EAAS,IAAI;SACjD,cAAc,EAAS,aAAa,EAAS,GAAG,MAAM;SACtD,gBAAgB,EAAc,CAAQ;SACtC,aAAa,EAAG,8CAA8C,kBAAkB;SAChF,YAAY,CACV;UACE,KAAK;UACL,OAAO,EAAG,+CAA+C,WAAW;UACpE,MAAM;UACN,gBAAgB,KAAK,EAAe,CAAQ;UAC5C,UAAU,EAAS,WAAW;SAChC,CACF;QACD,CAAA;OACQ,CAAA;MACH;QArEH,EAAS,EAqEN,CACX,EACQ,CAAA,CACN,EAAA,CAAA;IACJ,CAAA;IAGL,kBAAC,MAAD;KAAI,WAAU;eACX,EAAU,KAAK,MACd,kBAAC,MAAD;MAAsB,WAAU;gBAAhC,CACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAS,aAAa,EAAS,IAAI;OAClD,WAAU;iBAHZ;QAKE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,QAAD;UAAM,WAAU;oBAAhB,CACE,kBAAC,QAAD;WAAM,WAAU;qBAAkB,EAAS;UAAW,CAAA,GACrD,EAAS,cACR,kBAAC,QAAD;WAAM,WAAU;qBACb,EAAS;UACN,CAAA,IACJ,IACA;aACN,kBAAC,GAAD;UAAa,QAAQ,EAAS;UAAQ,WAAU;SAAY,CAAA,CACzD;;QACL,kBAAC,MAAD;SAAI,WAAU;mBAAd;UACE,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,MAAD;YAAI,WAAU;sBAAuB,EAAG,qCAAqC,SAAS;WAAM,CAAA,GAC5F,kBAAC,MAAD;YAAI,WAAU;sBAAqB,EAAgB,EAAS,OAAO;WAAM,CAAA,CACtE;;UACL,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,MAAD;YAAI,WAAU;sBAAuB,EAAG,qCAAqC,SAAS;WAAM,CAAA,GAC5F,kBAAC,MAAD;YAAI,WAAU;sBAAd,CAA+C,KAAE,EAAS,OAAY;aACnE;;UACL,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,MAAD;YAAI,WAAU;sBAAuB,EAAG,sCAAsC,UAAU;WAAM,CAAA,GAC9F,kBAAC,MAAD;YAAI,WAAU;sBAA+B,EAAS,YAAY;WAAQ,CAAA,CACvE;;UACL,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,MAAD;YAAI,WAAU;sBAAuB,EAAG,qCAAqC,UAAU;WAAM,CAAA,GAC7F,kBAAC,MAAD;YAAI,WAAU;sBAAqB,EAAW,EAAS,SAAS;WAAM,CAAA,CACnE;;SACH;;QACH,EAAS,KAAK,SAAS,IACtB,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACG,EAAS,KAAK,MAAM,GAAG,CAAC,EAAE,KAAK,MAC9B,kBAAC,QAAD;UAEE,WAAU;oBAET;SACG,GAJC,CAID,CACP,GACA,EAAS,KAAK,SAAS,IACtB,kBAAC,QAAD;UAAM,WAAU;oBAAhB,CAA8C,KAC1C,EAAS,KAAK,SAAS,CACrB;cACJ,IACD;aACH;OACE;UACR,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,GAAD;QACE,WAAW,EAAG,wCAAwC,wBAAwB,EAAE,MAAM,EAAS,KAAK,CAAC;QACrG,YAAY,EAAS;QACrB,cAAc,EAAS,aAAa,EAAS,IAAI;QACjD,cAAc,EAAS,aAAa,EAAS,GAAG,MAAM;QACtD,gBAAgB,EAAc,CAAQ;QACtC,aAAa,EAAG,8CAA8C,kBAAkB;QAChF,YAAY,CACV;SACE,KAAK;SACL,OAAO,EAAG,+CAA+C,WAAW;SACpE,MAAM;SACN,gBAAgB,KAAK,EAAe,CAAQ;SAC5C,UAAU,EAAS,WAAW;QAChC,CACF;OACD,CAAA;MACE,CAAA,CACH;QAxEK,EAAS,EAwEd,CACL;IACC,CAAA;IAEJ,kBAAC,GAAD;KACE,QAAQ,EAAW;KACnB,OAAO,EAAW;KAClB,aAAa,EAAU;KACvB,SAAS,GAAU,WAAW;KAC9B,QAAQ,EAAW;KACnB,QAAQ,EAAW;KACnB,WAAW,EAAG,gDAAgD,WAAW;KAChE;IACV,CAAA;GACD,EAAA,CAAA,CA0CG;;CACK,CAAA;AAEhB"}
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';\nimport { useTr } from '../../../shared/hooks/useTr';\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 tr = useTr();\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 (\n !mutationSucceeded(\n response,\n runId,\n tr('cosmicintersection.pages.pipelines.startRunError', 'Could not start a run')\n )\n ) {\n return;\n }\n notify.success(\n tr('cosmicintersection.pages.pipelines.startRunSuccess', 'Run started'),\n tr(\n 'cosmicintersection.pages.pipelines.startRunSuccessDescription',\n 'A new run for “{{name}}” is queued.',\n { name: pipeline.name }\n )\n );\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(\n response,\n response.data?.cosArchivePipeline,\n tr('cosmicintersection.pages.pipelines.archiveError', 'Could not archive pipeline')\n )\n ) {\n throw new Error('Archive not confirmed');\n }\n await refetch();\n notify.success(\n tr('cosmicintersection.pages.pipelines.archiveSuccess', 'Pipeline archived'),\n tr(\n 'cosmicintersection.pages.pipelines.archiveSuccessDescription',\n '“{{name}}” was archived.',\n { name: pipeline.name }\n )\n );\n };\n\n const renderBody = () => {\n if (loading) {\n return (\n <LoadingState\n variant=\"rows\"\n rows={6}\n label={tr('cosmicintersection.pages.pipelines.loading', 'Loading pipelines…')}\n />\n );\n }\n if (error) {\n return (\n <ErrorState\n message={tr(\n 'cosmicintersection.pages.pipelines.error',\n 'We could not load your pipelines. Please try again.'\n )}\n onRetry={() => void refetch()}\n />\n );\n }\n if (pipelines.length === 0) {\n return (\n <EmptyState\n icon={GitBranch}\n title={\n status === ALL\n ? tr('cosmicintersection.pages.pipelines.emptyTitle', 'No pipelines yet')\n : tr(\n 'cosmicintersection.pages.pipelines.emptyFilterTitle',\n 'No pipelines match this filter'\n )\n }\n description={\n status === ALL\n ? tr(\n 'cosmicintersection.pages.pipelines.emptyDescription',\n 'Pipelines orchestrate ingest, calibration, transform and export steps over your datasets.'\n )\n : tr(\n 'cosmicintersection.pages.pipelines.emptyFilterDescription',\n 'Try a different status filter to see more pipelines.'\n )\n }\n action={\n status === ALL ? (\n <Button type=\"button\" onClick={() => navigate('pipelines/create')}>\n <Plus className=\"size-4\" />\n {tr('cosmicintersection.pages.pipelines.create', '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>{tr('cosmicintersection.common.name', 'Name')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.status', 'Status')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.trigger', 'Trigger')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.schedule', 'Schedule')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.lastRun', 'Last run')}</TableHead>\n <TableHead className=\"text-right\">\n {tr('cosmicintersection.common.version', 'Version')}\n </TableHead>\n <TableHead>{tr('cosmicintersection.common.tags', 'Tags')}</TableHead>\n <TableHead className=\"w-12 text-right\">\n <span className=\"sr-only\">\n {tr('cosmicintersection.common.actions', 'Actions')}\n </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={tr(\n 'cosmicintersection.common.actionsFor',\n 'Actions for {{name}}',\n { name: pipeline.name }\n )}\n entityName={pipeline.name}\n onView={() => navigate(`pipelines/${pipeline.id}`)}\n onEdit={() => navigate(`pipelines/${pipeline.id}/edit`)}\n onDelete={() => handleArchive(pipeline)}\n deleteLabel={tr(\n 'cosmicintersection.pages.pipelines.archive',\n 'Archive pipeline'\n )}\n extraItems={[\n {\n key: 'start',\n label: tr('cosmicintersection.pages.pipelines.startRun', '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\">\n {tr('cosmicintersection.common.trigger', 'Trigger')}\n </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\">\n {tr('cosmicintersection.common.version', 'Version')}\n </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\">\n {tr('cosmicintersection.common.schedule', 'Schedule')}\n </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\">\n {tr('cosmicintersection.common.lastRun', 'Last run')}\n </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={tr('cosmicintersection.common.actionsFor', 'Actions for {{name}}', {\n name: pipeline.name,\n })}\n entityName={pipeline.name}\n onView={() => navigate(`pipelines/${pipeline.id}`)}\n onEdit={() => navigate(`pipelines/${pipeline.id}/edit`)}\n onDelete={() => handleArchive(pipeline)}\n deleteLabel={tr('cosmicintersection.pages.pipelines.archive', 'Archive pipeline')}\n extraItems={[\n {\n key: 'start',\n label: tr('cosmicintersection.pages.pipelines.startRun', '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={tr('cosmicintersection.pages.pipelines.itemLabel', 'pipelines')}\n loading={loading}\n />\n </>\n );\n };\n\n return (\n <PageLayout\n title={tr('cosmicintersection.pages.pipelines.title', 'Pipelines')}\n description={tr(\n 'cosmicintersection.pages.pipelines.description',\n 'Orchestrate ingest, calibration, transform and export steps across your data.'\n )}\n actions={\n <Button type=\"button\" onClick={() => navigate('pipelines/create')}>\n <Plus className=\"size-4\" />\n {tr('cosmicintersection.pages.pipelines.create', '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>{tr('cosmicintersection.common.status', 'Status')}</span>\n <Select\n value={status}\n onValueChange={(value) => setStatus(value as CosPipelineStatus | typeof ALL)}\n >\n <SelectTrigger\n aria-label={tr('cosmicintersection.common.filterByStatus', 'Filter by status')}\n className=\"w-44\"\n >\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>\n {tr('cosmicintersection.common.allStatuses', 'All statuses')}\n </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 +1 @@
1
- {"version":3,"file":"ReleasesPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/releases/ReleasesPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { BookOpen, Database, ExternalLink, NotebookPen, Plus, Rocket } from 'lucide-react';\nimport {\n Button,\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Label,\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 useCosDatasetsQuery,\n useCosNotebooksQuery,\n useCosPublishReleaseMutation,\n useCosReleasesQuery,\n type CosRelease,\n type CosReleaseStatus,\n} from '../../api/cosmicApi';\nimport {\n DiscussionPanel,\n EmptyState,\n ErrorState,\n LoadingState,\n PageLayout,\n Pagination,\n SafeLink,\n StatusBadge,\n formatEnumLabel,\n usePagination,\n} from '../../components';\nimport { ActionMenu } from '../../components/ActionMenu';\nimport { useCosmicIntersection } from '../../context/CosmicIntersectionContext';\nimport { notify } from '../../lib/toast';\nimport { useTr } from '../../../shared/hooks/useTr';\n\nconst ALL = 'ALL';\n\nconst RELEASE_STATUSES: CosReleaseStatus[] = ['DRAFT', 'UNDER_REVIEW', 'PUBLISHED', 'RETRACTED'];\n\n/** A release can be published only while it is still in a pre-publication state. */\nfunction canPublish(status: CosReleaseStatus): boolean {\n return status === 'DRAFT' || status === 'UNDER_REVIEW';\n}\n\nfunction formatDate(value?: string | null): string {\n if (!value) {\n return '—';\n }\n const parsed = new Date(value);\n if (Number.isNaN(parsed.getTime())) {\n return '—';\n }\n return parsed.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });\n}\n\nfunction doiUrl(doi: string): string {\n const trimmed = doi.trim();\n return /^https?:\\/\\//i.test(trimmed)\n ? trimmed\n : `https://doi.org/${trimmed.replace(/^doi:/i, '')}`;\n}\n\nexport default function ReleasesPage() {\n const { navigate } = useCosmicIntersection();\n const tr = useTr();\n const [statusFilter, setStatusFilter] = useState<string>(ALL);\n const pagination = usePagination({ pageSize: 20, resetKey: statusFilter });\n\n const { data, loading, error, refetch } = useCosReleasesQuery({\n status: statusFilter === ALL ? undefined : (statusFilter as CosReleaseStatus),\n pagination: { limit: pagination.limit, offset: pagination.offset },\n });\n\n const [publishRelease, { loading: publishing }] = useCosPublishReleaseMutation();\n\n const releases = useMemo(() => data?.cosListReleases.items ?? [], [data]);\n const pageInfo = data?.cosListReleases.pageInfo;\n\n // Resolve the release's linked dataset/notebook UUIDs to human names so the\n // detail dialog can render navigable buttons instead of raw ids.\n const datasetsQuery = useCosDatasetsQuery({ pagination: { limit: 100, offset: 0 } });\n const notebooksQuery = useCosNotebooksQuery({ pagination: { limit: 100, offset: 0 } });\n const datasetNameById = useMemo(() => {\n const map = new Map<string, string>();\n for (const dataset of datasetsQuery.data?.cosListDatasets.items ?? []) {\n map.set(dataset.id, dataset.name);\n }\n return map;\n }, [datasetsQuery.data]);\n const notebookTitleById = useMemo(() => {\n const map = new Map<string, string>();\n for (const notebook of notebooksQuery.data?.cosListNotebooks.items ?? []) {\n map.set(notebook.id, notebook.title);\n }\n return map;\n }, [notebooksQuery.data]);\n\n // Detail (View) dialog — rendered from row data, no extra fetch needed.\n const [detail, setDetail] = useState<CosRelease | null>(null);\n\n // Close the dialog before navigating to a linked entity.\n const goToLinked = (path: string) => {\n setDetail(null);\n navigate(path);\n };\n\n // Publish dialog — captures an optional DOI before transitioning the release.\n const [publishTarget, setPublishTarget] = useState<CosRelease | null>(null);\n const [doiDraft, setDoiDraft] = useState('');\n\n const openPublish = (release: CosRelease) => {\n setDoiDraft(release.doi ?? '');\n setPublishTarget(release);\n };\n\n const submitPublish = async () => {\n if (!publishTarget) {\n return;\n }\n const doi = doiDraft.trim();\n const response = await publishRelease({\n variables: { id: publishTarget.id, doi: doi || undefined },\n });\n if (response.data?.cosPublishRelease?.id) {\n notify.success(\n tr('cosmicintersection.pages.releases.publishSuccess', 'Release published'),\n tr(\n 'cosmicintersection.pages.releases.publishSuccessDescription',\n '“{{name}}” is now public.',\n { name: publishTarget.title },\n ),\n );\n setPublishTarget(null);\n setDoiDraft('');\n await refetch();\n }\n };\n\n const newButton = (\n <Button type=\"button\" onClick={() => navigate('releases/create')}>\n <Plus className=\"size-4\" />\n {tr('cosmicintersection.pages.releases.create', 'New release')}\n </Button>\n );\n\n const renderBody = () => {\n if (loading) {\n return (\n <LoadingState\n variant=\"rows\"\n rows={6}\n label={tr('cosmicintersection.pages.releases.loading', 'Loading releases…')}\n />\n );\n }\n if (error) {\n return <ErrorState message={error.message} onRetry={() => void refetch()} />;\n }\n if (releases.length === 0) {\n const filtered = statusFilter !== ALL;\n return (\n <EmptyState\n icon={Rocket}\n title={\n filtered\n ? tr(\n 'cosmicintersection.pages.releases.emptyFilterTitle',\n 'No releases match this filter',\n )\n : tr('cosmicintersection.pages.releases.emptyTitle', 'No releases yet')\n }\n description={\n filtered\n ? tr(\n 'cosmicintersection.pages.releases.emptyFilterDescription',\n 'Try clearing the status filter to see more releases.',\n )\n : tr(\n 'cosmicintersection.pages.releases.emptyDescription',\n 'Package datasets and notebooks into a citable, versioned release for publication.',\n )\n }\n action={filtered ? undefined : newButton}\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>Title</TableHead>\n <TableHead>Version</TableHead>\n <TableHead>Status</TableHead>\n <TableHead>DOI</TableHead>\n <TableHead>Authors</TableHead>\n <TableHead>Published</TableHead>\n <TableHead>License</TableHead>\n <TableHead className=\"text-right\">Actions</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {releases.map((release) => (\n <TableRow key={release.id}>\n <TableCell>\n <button\n type=\"button\"\n onClick={() => setDetail(release)}\n className=\"text-left font-medium text-text-primary underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-focus\"\n >\n {release.title}\n </button>\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-sm text-text-secondary\">\n {release.version || '—'}\n </TableCell>\n <TableCell>\n <StatusBadge status={release.status} />\n </TableCell>\n <TableCell className=\"max-w-[14rem]\">\n {release.doi ? (\n <SafeLink\n href={doiUrl(release.doi)}\n className=\"inline-flex items-center gap-1 truncate text-sm text-text-primary underline-offset-2 hover:underline\"\n fallbackClassName=\"block truncate text-sm text-text-secondary\"\n >\n <span className=\"truncate\">{release.doi}</span>\n <ExternalLink className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n </SafeLink>\n ) : (\n <span className=\"text-sm text-text-secondary\">—</span>\n )}\n </TableCell>\n <TableCell className=\"max-w-[14rem]\">\n {release.authors.length === 0 ? (\n <span className=\"text-sm text-text-secondary\">—</span>\n ) : (\n <span className=\"line-clamp-1 text-sm text-text-secondary\">\n {release.authors.join(', ')}\n </span>\n )}\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-sm text-text-secondary\">\n {formatDate(release.publishedAt)}\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-sm text-text-secondary\">\n {release.license || '—'}\n </TableCell>\n <TableCell className=\"text-right\">\n <ActionMenu\n ariaLabel={`Actions for ${release.title}`}\n onView={() => setDetail(release)}\n extraItems={\n canPublish(release.status)\n ? [\n {\n key: 'publish',\n label: tr('cosmicintersection.pages.releases.publish', 'Publish'),\n icon: Rocket,\n onSelect: () => openPublish(release),\n },\n ]\n : []\n }\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 {releases.map((release) => (\n <li\n key={release.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 <div className=\"min-w-0 flex-1 space-y-2\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setDetail(release)}\n className=\"truncate text-left font-medium text-text-primary underline-offset-2 hover:underline\"\n >\n {release.title}\n </button>\n <StatusBadge status={release.status} />\n </div>\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-secondary\">\n <span>{release.version || 'v—'}</span>\n {release.license ? <span>{release.license}</span> : null}\n <span>{formatDate(release.publishedAt)}</span>\n </div>\n {release.authors.length > 0 ? (\n <p className=\"line-clamp-1 text-xs text-text-secondary\">\n {release.authors.join(', ')}\n </p>\n ) : null}\n {release.doi ? (\n <SafeLink\n href={doiUrl(release.doi)}\n className=\"inline-flex items-center gap-1 text-xs text-text-primary underline-offset-2 hover:underline\"\n fallbackClassName=\"block truncate text-xs text-text-secondary\"\n >\n <span className=\"truncate\">DOI: {release.doi}</span>\n <ExternalLink className=\"size-3 shrink-0\" aria-hidden=\"true\" />\n </SafeLink>\n ) : null}\n </div>\n <ActionMenu\n ariaLabel={`Actions for ${release.title}`}\n onView={() => setDetail(release)}\n extraItems={\n canPublish(release.status)\n ? [\n {\n key: 'publish',\n label: tr('cosmicintersection.pages.releases.publish', 'Publish'),\n icon: Rocket,\n onSelect: () => openPublish(release),\n },\n ]\n : []\n }\n />\n </div>\n </li>\n ))}\n </ul>\n\n <Pagination\n offset={pagination.offset}\n limit={pagination.limit}\n loadedCount={releases.length}\n hasMore={pageInfo?.hasMore ?? false}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel={tr('cosmicintersection.pages.releases.itemLabel', 'releases')}\n loading={loading}\n />\n </>\n );\n };\n\n return (\n <PageLayout\n title={tr('cosmicintersection.pages.releases.title', 'Releases')}\n description={tr(\n 'cosmicintersection.pages.releases.description',\n 'Citable, versioned publications that bundle datasets and notebooks for the community.',\n )}\n actions={newButton}\n >\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"w-full max-w-[14rem]\">\n <Select value={statusFilter} onValueChange={setStatusFilter}>\n <SelectTrigger aria-label=\"Filter by status\">\n <SelectValue placeholder=\"All statuses\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>All statuses</SelectItem>\n {RELEASE_STATUSES.map((status) => (\n <SelectItem key={status} value={status}>\n {formatEnumLabel(status)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n {renderBody()}\n </div>\n\n {/* View / detail dialog */}\n <Dialog open={detail !== null} onOpenChange={(open) => (open ? undefined : setDetail(null))}>\n <DialogContent className=\"max-h-[85vh] max-w-lg overflow-y-auto\">\n {detail ? (\n <>\n <DialogHeader>\n <DialogTitle className=\"flex items-center gap-2\">\n <BookOpen className=\"size-5 text-text-secondary\" aria-hidden=\"true\" />\n {detail.title}\n </DialogTitle>\n <DialogDescription>\n Version {detail.version || '—'} · <StatusBadge status={detail.status} />\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-4 text-sm\">\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Abstract\n </p>\n <p className=\"mt-1 whitespace-pre-wrap text-text-primary\">\n {detail.abstract?.trim() || 'No abstract provided.'}\n </p>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Authors\n </p>\n <p className=\"mt-1 text-text-primary\">\n {detail.authors.length ? detail.authors.join(', ') : '—'}\n </p>\n </div>\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n License\n </p>\n <p className=\"mt-1 text-text-primary\">{detail.license || '—'}</p>\n </div>\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Published\n </p>\n <p className=\"mt-1 text-text-primary\">{formatDate(detail.publishedAt)}</p>\n </div>\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n DOI\n </p>\n <p className=\"mt-1\">\n {detail.doi ? (\n <SafeLink\n href={doiUrl(detail.doi)}\n className=\"inline-flex items-center gap-1 text-text-primary underline-offset-2 hover:underline\"\n fallbackClassName=\"text-text-secondary\"\n >\n {detail.doi}\n <ExternalLink className=\"size-3.5\" aria-hidden=\"true\" />\n </SafeLink>\n ) : (\n <span className=\"text-text-secondary\">Not assigned</span>\n )}\n </p>\n </div>\n </div>\n\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Datasets ({detail.datasetIds.length})\n </p>\n <div className=\"mt-1 flex flex-wrap gap-1.5\">\n {detail.datasetIds.length === 0 ? (\n <span className=\"text-text-secondary\">None linked.</span>\n ) : (\n detail.datasetIds.map((datasetId) => {\n const name = datasetNameById.get(datasetId);\n return (\n <Button\n key={datasetId}\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-auto gap-1 px-2 py-1\"\n onClick={() => goToLinked(`datasets/${datasetId}`)}\n >\n <Database className=\"size-3.5\" aria-hidden=\"true\" />\n <span className={name ? '' : 'font-mono text-xs'}>\n {name ?? datasetId}\n </span>\n </Button>\n );\n })\n )}\n </div>\n </div>\n\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Notebook\n </p>\n <div className=\"mt-1\">\n {detail.notebookId ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-auto gap-1 px-2 py-1\"\n onClick={() => goToLinked(`notebooks/${detail.notebookId}`)}\n >\n <NotebookPen className=\"size-3.5\" aria-hidden=\"true\" />\n <span\n className={\n notebookTitleById.get(detail.notebookId) ? '' : 'font-mono text-xs'\n }\n >\n {notebookTitleById.get(detail.notebookId) ?? detail.notebookId}\n </span>\n </Button>\n ) : (\n <span className=\"text-text-secondary\">None linked.</span>\n )}\n </div>\n </div>\n\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Discussion\n </p>\n <div className=\"mt-2\">\n <DiscussionPanel\n entityType=\"cosmicintersection-release\"\n entityId={detail.id}\n title={detail.title}\n widgetKey=\"release-discussion\"\n />\n </div>\n </div>\n </div>\n\n <DialogFooter>\n {canPublish(detail.status) ? (\n <Button\n type=\"button\"\n onClick={() => {\n const target = detail;\n setDetail(null);\n openPublish(target);\n }}\n >\n <Rocket className=\"size-4\" />\n Publish\n </Button>\n ) : null}\n <DialogClose asChild>\n <Button type=\"button\" variant=\"outline\">\n Close\n </Button>\n </DialogClose>\n </DialogFooter>\n </>\n ) : null}\n </DialogContent>\n </Dialog>\n\n {/* Publish dialog */}\n <Dialog\n open={publishTarget !== null}\n onOpenChange={(open) => (open || publishing ? undefined : setPublishTarget(null))}\n >\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>\n {tr('cosmicintersection.pages.releases.publishRelease', 'Publish release')}\n </DialogTitle>\n <DialogDescription>\n {publishTarget\n ? tr(\n 'cosmicintersection.pages.releases.publishDescription',\n 'Publish “{{name}}” to make it publicly citable. You can attach a DOI now or later.',\n { name: publishTarget.title },\n )\n : ''}\n </DialogDescription>\n </DialogHeader>\n\n {publishTarget ? (\n <dl className=\"grid grid-cols-3 gap-x-4 gap-y-3 rounded-card border border-border-default bg-bg-sunken p-3\">\n <div className=\"col-span-2 min-w-0\">\n <dt className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Authors\n </dt>\n <dd\n className=\"mt-0.5 truncate text-sm text-text-primary\"\n title={publishTarget.authors.join(', ')}\n >\n {publishTarget.authors.length ? publishTarget.authors.join(', ') : '—'}\n </dd>\n </div>\n <div>\n <dt className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Datasets\n </dt>\n <dd className=\"mt-0.5 text-sm tabular-nums text-text-primary\">\n {publishTarget.datasetIds.length}\n </dd>\n </div>\n <div className=\"col-span-3 min-w-0\">\n <dt className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n License\n </dt>\n <dd className=\"mt-0.5 truncate text-sm text-text-primary\">\n {publishTarget.license || '—'}\n </dd>\n </div>\n </dl>\n ) : null}\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"release-doi\">\n {tr('cosmicintersection.pages.releases.doiOptional', 'DOI (optional)')}\n </Label>\n <Input\n id=\"release-doi\"\n value={doiDraft}\n onChange={(event) => setDoiDraft(event.target.value)}\n placeholder=\"10.1234/cosmic.2026.0001\"\n />\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'cosmicintersection.pages.releases.doiHint',\n 'A Digital Object Identifier makes the release permanently referenceable.',\n )}\n </p>\n </div>\n\n <DialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => setPublishTarget(null)}\n disabled={publishing}\n >\n {tr('cosmicintersection.common.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" onClick={() => void submitPublish()} disabled={publishing}>\n <Rocket className=\"size-4\" />\n {publishing\n ? tr('cosmicintersection.pages.releases.publishing', 'Publishing…')\n : tr('cosmicintersection.pages.releases.publishRelease', 'Publish release')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ReleasesPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/releases/ReleasesPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { BookOpen, Database, ExternalLink, NotebookPen, Plus, Rocket } from 'lucide-react';\nimport {\n Button,\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Label,\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 useCosDatasetsQuery,\n useCosNotebooksQuery,\n useCosPublishReleaseMutation,\n useCosReleasesQuery,\n type CosRelease,\n type CosReleaseStatus,\n} from '../../api/cosmicApi';\nimport {\n DiscussionPanel,\n EmptyState,\n ErrorState,\n LoadingState,\n PageLayout,\n Pagination,\n SafeLink,\n StatusBadge,\n formatEnumLabel,\n usePagination,\n} from '../../components';\nimport { ActionMenu } from '../../components/ActionMenu';\nimport { useCosmicIntersection } from '../../context/CosmicIntersectionContext';\nimport { notify } from '../../lib/toast';\nimport { useTr } from '../../../shared/hooks/useTr';\n\nconst ALL = 'ALL';\n\nconst RELEASE_STATUSES: CosReleaseStatus[] = ['DRAFT', 'UNDER_REVIEW', 'PUBLISHED', 'RETRACTED'];\n\n/** A release can be published only while it is still in a pre-publication state. */\nfunction canPublish(status: CosReleaseStatus): boolean {\n return status === 'DRAFT' || status === 'UNDER_REVIEW';\n}\n\nfunction formatDate(value?: string | null): string {\n if (!value) {\n return '—';\n }\n const parsed = new Date(value);\n if (Number.isNaN(parsed.getTime())) {\n return '—';\n }\n return parsed.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });\n}\n\nfunction doiUrl(doi: string): string {\n const trimmed = doi.trim();\n return /^https?:\\/\\//i.test(trimmed)\n ? trimmed\n : `https://doi.org/${trimmed.replace(/^doi:/i, '')}`;\n}\n\nexport default function ReleasesPage() {\n const { navigate } = useCosmicIntersection();\n const tr = useTr();\n const [statusFilter, setStatusFilter] = useState<string>(ALL);\n const pagination = usePagination({ pageSize: 20, resetKey: statusFilter });\n\n const { data, loading, error, refetch } = useCosReleasesQuery({\n status: statusFilter === ALL ? undefined : (statusFilter as CosReleaseStatus),\n pagination: { limit: pagination.limit, offset: pagination.offset },\n });\n\n const [publishRelease, { loading: publishing }] = useCosPublishReleaseMutation();\n\n const releases = useMemo(() => data?.cosListReleases.items ?? [], [data]);\n const pageInfo = data?.cosListReleases.pageInfo;\n\n // Resolve the release's linked dataset/notebook UUIDs to human names so the\n // detail dialog can render navigable buttons instead of raw ids.\n const datasetsQuery = useCosDatasetsQuery({ pagination: { limit: 100, offset: 0 } });\n const notebooksQuery = useCosNotebooksQuery({ pagination: { limit: 100, offset: 0 } });\n const datasetNameById = useMemo(() => {\n const map = new Map<string, string>();\n for (const dataset of datasetsQuery.data?.cosListDatasets.items ?? []) {\n map.set(dataset.id, dataset.name);\n }\n return map;\n }, [datasetsQuery.data]);\n const notebookTitleById = useMemo(() => {\n const map = new Map<string, string>();\n for (const notebook of notebooksQuery.data?.cosListNotebooks.items ?? []) {\n map.set(notebook.id, notebook.title);\n }\n return map;\n }, [notebooksQuery.data]);\n\n // Detail (View) dialog — rendered from row data, no extra fetch needed.\n const [detail, setDetail] = useState<CosRelease | null>(null);\n\n // Close the dialog before navigating to a linked entity.\n const goToLinked = (path: string) => {\n setDetail(null);\n navigate(path);\n };\n\n // Publish dialog — captures an optional DOI before transitioning the release.\n const [publishTarget, setPublishTarget] = useState<CosRelease | null>(null);\n const [doiDraft, setDoiDraft] = useState('');\n\n const openPublish = (release: CosRelease) => {\n setDoiDraft(release.doi ?? '');\n setPublishTarget(release);\n };\n\n const submitPublish = async () => {\n if (!publishTarget) {\n return;\n }\n const doi = doiDraft.trim();\n const response = await publishRelease({\n variables: { id: publishTarget.id, doi: doi || undefined },\n });\n if (response.data?.cosPublishRelease?.id) {\n notify.success(\n tr('cosmicintersection.pages.releases.publishSuccess', 'Release published'),\n tr(\n 'cosmicintersection.pages.releases.publishSuccessDescription',\n '“{{name}}” is now public.',\n { name: publishTarget.title }\n )\n );\n setPublishTarget(null);\n setDoiDraft('');\n await refetch();\n }\n };\n\n const newButton = (\n <Button type=\"button\" onClick={() => navigate('releases/create')}>\n <Plus className=\"size-4\" />\n {tr('cosmicintersection.pages.releases.create', 'New release')}\n </Button>\n );\n\n const renderBody = () => {\n if (loading) {\n return (\n <LoadingState\n variant=\"rows\"\n rows={6}\n label={tr('cosmicintersection.pages.releases.loading', 'Loading releases…')}\n />\n );\n }\n if (error) {\n return <ErrorState message={error.message} onRetry={() => void refetch()} />;\n }\n if (releases.length === 0) {\n const filtered = statusFilter !== ALL;\n return (\n <EmptyState\n icon={Rocket}\n title={\n filtered\n ? tr(\n 'cosmicintersection.pages.releases.emptyFilterTitle',\n 'No releases match this filter'\n )\n : tr('cosmicintersection.pages.releases.emptyTitle', 'No releases yet')\n }\n description={\n filtered\n ? tr(\n 'cosmicintersection.pages.releases.emptyFilterDescription',\n 'Try clearing the status filter to see more releases.'\n )\n : tr(\n 'cosmicintersection.pages.releases.emptyDescription',\n 'Package datasets and notebooks into a citable, versioned release for publication.'\n )\n }\n action={filtered ? undefined : newButton}\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>Title</TableHead>\n <TableHead>Version</TableHead>\n <TableHead>Status</TableHead>\n <TableHead>DOI</TableHead>\n <TableHead>Authors</TableHead>\n <TableHead>Published</TableHead>\n <TableHead>License</TableHead>\n <TableHead className=\"text-right\">Actions</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {releases.map((release) => (\n <TableRow key={release.id}>\n <TableCell>\n <button\n type=\"button\"\n onClick={() => setDetail(release)}\n className=\"text-left font-medium text-text-primary underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-focus\"\n >\n {release.title}\n </button>\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-sm text-text-secondary\">\n {release.version || '—'}\n </TableCell>\n <TableCell>\n <StatusBadge status={release.status} />\n </TableCell>\n <TableCell className=\"max-w-[14rem]\">\n {release.doi ? (\n <SafeLink\n href={doiUrl(release.doi)}\n className=\"inline-flex items-center gap-1 truncate text-sm text-text-primary underline-offset-2 hover:underline\"\n fallbackClassName=\"block truncate text-sm text-text-secondary\"\n >\n <span className=\"truncate\">{release.doi}</span>\n <ExternalLink className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n </SafeLink>\n ) : (\n <span className=\"text-sm text-text-secondary\">—</span>\n )}\n </TableCell>\n <TableCell className=\"max-w-[14rem]\">\n {release.authors.length === 0 ? (\n <span className=\"text-sm text-text-secondary\">—</span>\n ) : (\n <span className=\"line-clamp-1 text-sm text-text-secondary\">\n {release.authors.join(', ')}\n </span>\n )}\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-sm text-text-secondary\">\n {formatDate(release.publishedAt)}\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-sm text-text-secondary\">\n {release.license || '—'}\n </TableCell>\n <TableCell className=\"text-right\">\n <ActionMenu\n ariaLabel={`Actions for ${release.title}`}\n onView={() => setDetail(release)}\n extraItems={\n canPublish(release.status)\n ? [\n {\n key: 'publish',\n label: tr('cosmicintersection.pages.releases.publish', 'Publish'),\n icon: Rocket,\n onSelect: () => openPublish(release),\n },\n ]\n : []\n }\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 {releases.map((release) => (\n <li\n key={release.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 <div className=\"min-w-0 flex-1 space-y-2\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setDetail(release)}\n className=\"truncate text-left font-medium text-text-primary underline-offset-2 hover:underline\"\n >\n {release.title}\n </button>\n <StatusBadge status={release.status} />\n </div>\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-secondary\">\n <span>{release.version || 'v—'}</span>\n {release.license ? <span>{release.license}</span> : null}\n <span>{formatDate(release.publishedAt)}</span>\n </div>\n {release.authors.length > 0 ? (\n <p className=\"line-clamp-1 text-xs text-text-secondary\">\n {release.authors.join(', ')}\n </p>\n ) : null}\n {release.doi ? (\n <SafeLink\n href={doiUrl(release.doi)}\n className=\"inline-flex items-center gap-1 text-xs text-text-primary underline-offset-2 hover:underline\"\n fallbackClassName=\"block truncate text-xs text-text-secondary\"\n >\n <span className=\"truncate\">DOI: {release.doi}</span>\n <ExternalLink className=\"size-3 shrink-0\" aria-hidden=\"true\" />\n </SafeLink>\n ) : null}\n </div>\n <ActionMenu\n ariaLabel={`Actions for ${release.title}`}\n onView={() => setDetail(release)}\n extraItems={\n canPublish(release.status)\n ? [\n {\n key: 'publish',\n label: tr('cosmicintersection.pages.releases.publish', 'Publish'),\n icon: Rocket,\n onSelect: () => openPublish(release),\n },\n ]\n : []\n }\n />\n </div>\n </li>\n ))}\n </ul>\n\n <Pagination\n offset={pagination.offset}\n limit={pagination.limit}\n loadedCount={releases.length}\n hasMore={pageInfo?.hasMore ?? false}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel={tr('cosmicintersection.pages.releases.itemLabel', 'releases')}\n loading={loading}\n />\n </>\n );\n };\n\n return (\n <PageLayout\n title={tr('cosmicintersection.pages.releases.title', 'Releases')}\n description={tr(\n 'cosmicintersection.pages.releases.description',\n 'Citable, versioned publications that bundle datasets and notebooks for the community.'\n )}\n actions={newButton}\n >\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"w-full max-w-[14rem]\">\n <Select value={statusFilter} onValueChange={setStatusFilter}>\n <SelectTrigger aria-label=\"Filter by status\">\n <SelectValue placeholder=\"All statuses\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>All statuses</SelectItem>\n {RELEASE_STATUSES.map((status) => (\n <SelectItem key={status} value={status}>\n {formatEnumLabel(status)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n {renderBody()}\n </div>\n\n {/* View / detail dialog */}\n <Dialog open={detail !== null} onOpenChange={(open) => (open ? undefined : setDetail(null))}>\n <DialogContent className=\"max-h-[85vh] max-w-lg overflow-y-auto\">\n {detail ? (\n <>\n <DialogHeader>\n <DialogTitle className=\"flex items-center gap-2\">\n <BookOpen className=\"size-5 text-text-secondary\" aria-hidden=\"true\" />\n {detail.title}\n </DialogTitle>\n <DialogDescription>\n Version {detail.version || '—'} · <StatusBadge status={detail.status} />\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"space-y-4 text-sm\">\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Abstract\n </p>\n <p className=\"mt-1 whitespace-pre-wrap text-text-primary\">\n {detail.abstract?.trim() || 'No abstract provided.'}\n </p>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Authors\n </p>\n <p className=\"mt-1 text-text-primary\">\n {detail.authors.length ? detail.authors.join(', ') : '—'}\n </p>\n </div>\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n License\n </p>\n <p className=\"mt-1 text-text-primary\">{detail.license || '—'}</p>\n </div>\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Published\n </p>\n <p className=\"mt-1 text-text-primary\">{formatDate(detail.publishedAt)}</p>\n </div>\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n DOI\n </p>\n <p className=\"mt-1\">\n {detail.doi ? (\n <SafeLink\n href={doiUrl(detail.doi)}\n className=\"inline-flex items-center gap-1 text-text-primary underline-offset-2 hover:underline\"\n fallbackClassName=\"text-text-secondary\"\n >\n {detail.doi}\n <ExternalLink className=\"size-3.5\" aria-hidden=\"true\" />\n </SafeLink>\n ) : (\n <span className=\"text-text-secondary\">Not assigned</span>\n )}\n </p>\n </div>\n </div>\n\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Datasets ({detail.datasetIds.length})\n </p>\n <div className=\"mt-1 flex flex-wrap gap-1.5\">\n {detail.datasetIds.length === 0 ? (\n <span className=\"text-text-secondary\">None linked.</span>\n ) : (\n detail.datasetIds.map((datasetId) => {\n const name = datasetNameById.get(datasetId);\n return (\n <Button\n key={datasetId}\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-auto gap-1 px-2 py-1\"\n onClick={() => goToLinked(`datasets/${datasetId}`)}\n >\n <Database className=\"size-3.5\" aria-hidden=\"true\" />\n <span className={name ? '' : 'font-mono text-xs'}>\n {name ?? datasetId}\n </span>\n </Button>\n );\n })\n )}\n </div>\n </div>\n\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Notebook\n </p>\n <div className=\"mt-1\">\n {detail.notebookId ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-auto gap-1 px-2 py-1\"\n onClick={() => goToLinked(`notebooks/${detail.notebookId}`)}\n >\n <NotebookPen className=\"size-3.5\" aria-hidden=\"true\" />\n <span\n className={\n notebookTitleById.get(detail.notebookId) ? '' : 'font-mono text-xs'\n }\n >\n {notebookTitleById.get(detail.notebookId) ?? detail.notebookId}\n </span>\n </Button>\n ) : (\n <span className=\"text-text-secondary\">None linked.</span>\n )}\n </div>\n </div>\n\n <div>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Discussion\n </p>\n <div className=\"mt-2\">\n <DiscussionPanel\n entityType=\"cosmicintersection-release\"\n entityId={detail.id}\n title={detail.title}\n widgetKey=\"release-discussion\"\n />\n </div>\n </div>\n </div>\n\n <DialogFooter>\n {canPublish(detail.status) ? (\n <Button\n type=\"button\"\n onClick={() => {\n const target = detail;\n setDetail(null);\n openPublish(target);\n }}\n >\n <Rocket className=\"size-4\" />\n Publish\n </Button>\n ) : null}\n <DialogClose asChild>\n <Button type=\"button\" variant=\"outline\">\n Close\n </Button>\n </DialogClose>\n </DialogFooter>\n </>\n ) : null}\n </DialogContent>\n </Dialog>\n\n {/* Publish dialog */}\n <Dialog\n open={publishTarget !== null}\n onOpenChange={(open) => (open || publishing ? undefined : setPublishTarget(null))}\n >\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>\n {tr('cosmicintersection.pages.releases.publishRelease', 'Publish release')}\n </DialogTitle>\n <DialogDescription>\n {publishTarget\n ? tr(\n 'cosmicintersection.pages.releases.publishDescription',\n 'Publish “{{name}}” to make it publicly citable. You can attach a DOI now or later.',\n { name: publishTarget.title }\n )\n : ''}\n </DialogDescription>\n </DialogHeader>\n\n {publishTarget ? (\n <dl className=\"grid grid-cols-3 gap-x-4 gap-y-3 rounded-card border border-border-default bg-bg-sunken p-3\">\n <div className=\"col-span-2 min-w-0\">\n <dt className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Authors\n </dt>\n <dd\n className=\"mt-0.5 truncate text-sm text-text-primary\"\n title={publishTarget.authors.join(', ')}\n >\n {publishTarget.authors.length ? publishTarget.authors.join(', ') : '—'}\n </dd>\n </div>\n <div>\n <dt className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Datasets\n </dt>\n <dd className=\"mt-0.5 text-sm tabular-nums text-text-primary\">\n {publishTarget.datasetIds.length}\n </dd>\n </div>\n <div className=\"col-span-3 min-w-0\">\n <dt className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n License\n </dt>\n <dd className=\"mt-0.5 truncate text-sm text-text-primary\">\n {publishTarget.license || '—'}\n </dd>\n </div>\n </dl>\n ) : null}\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"release-doi\">\n {tr('cosmicintersection.pages.releases.doiOptional', 'DOI (optional)')}\n </Label>\n <Input\n id=\"release-doi\"\n value={doiDraft}\n onChange={(event) => setDoiDraft(event.target.value)}\n placeholder=\"10.1234/cosmic.2026.0001\"\n />\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'cosmicintersection.pages.releases.doiHint',\n 'A Digital Object Identifier makes the release permanently referenceable.'\n )}\n </p>\n </div>\n\n <DialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => setPublishTarget(null)}\n disabled={publishing}\n >\n {tr('cosmicintersection.common.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" onClick={() => void submitPublish()} disabled={publishing}>\n <Rocket className=\"size-4\" />\n {publishing\n ? tr('cosmicintersection.pages.releases.publishing', 'Publishing…')\n : tr('cosmicintersection.pages.releases.publishRelease', 'Publish release')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmDA,IAAM,IAAM,OAEN,IAAuC;CAAC;CAAS;CAAgB;CAAa;AAAW;AAG/F,SAAS,EAAW,GAAmC;CACrD,OAAO,MAAW,WAAW,MAAW;AAC1C;AAEA,SAAS,EAAW,GAA+B;CACjD,IAAI,CAAC,GACH,OAAO;CAET,IAAM,IAAS,IAAI,KAAK,CAAK;CAI7B,OAHI,OAAO,MAAM,EAAO,QAAQ,CAAC,IACxB,MAEF,EAAO,mBAAmB,KAAA,GAAW;EAAE,MAAM;EAAW,OAAO;EAAS,KAAK;CAAU,CAAC;AACjG;AAEA,SAAS,EAAO,GAAqB;CACnC,IAAM,IAAU,EAAI,KAAK;CACzB,OAAO,gBAAgB,KAAK,CAAO,IAC/B,IACA,mBAAmB,EAAQ,QAAQ,UAAU,EAAE;AACrD;AAEA,SAAwB,IAAe;CACrC,IAAM,EAAE,gBAAa,EAAsB,GACrC,IAAK,GAAM,GACX,CAAC,GAAc,MAAmB,EAAiB,CAAG,GACtD,IAAa,EAAc;EAAE,UAAU;EAAI,UAAU;CAAa,CAAC,GAEnE,EAAE,SAAM,YAAS,UAAO,eAAY,EAAoB;EAC5D,QAAQ,MAAiB,IAAM,KAAA,IAAa;EAC5C,YAAY;GAAE,OAAO,EAAW;GAAO,QAAQ,EAAW;EAAO;CACnE,CAAC,GAEK,CAAC,IAAgB,EAAE,SAAS,OAAgB,EAA6B,GAEzE,IAAW,QAAc,GAAM,gBAAgB,SAAS,CAAC,GAAG,CAAC,CAAI,CAAC,GAClE,KAAW,GAAM,gBAAgB,UAIjC,IAAgB,GAAoB,EAAE,YAAY;EAAE,OAAO;EAAK,QAAQ;CAAE,EAAE,CAAC,GAC7E,IAAiB,GAAqB,EAAE,YAAY;EAAE,OAAO;EAAK,QAAQ;CAAE,EAAE,CAAC,GAC/E,KAAkB,QAAc;EACpC,IAAM,oBAAM,IAAI,IAAoB;EACpC,KAAK,IAAM,KAAW,EAAc,MAAM,gBAAgB,SAAS,CAAC,GAClE,EAAI,IAAI,EAAQ,IAAI,EAAQ,IAAI;EAElC,OAAO;CACT,GAAG,CAAC,EAAc,IAAI,CAAC,GACjB,IAAoB,QAAc;EACtC,IAAM,oBAAM,IAAI,IAAoB;EACpC,KAAK,IAAM,KAAY,EAAe,MAAM,iBAAiB,SAAS,CAAC,GACrE,EAAI,IAAI,EAAS,IAAI,EAAS,KAAK;EAErC,OAAO;CACT,GAAG,CAAC,EAAe,IAAI,CAAC,GAGlB,CAAC,GAAQ,KAAa,EAA4B,IAAI,GAGtD,KAAc,MAAiB;EAEnC,AADA,EAAU,IAAI,GACd,EAAS,CAAI;CACf,GAGM,CAAC,GAAe,KAAoB,EAA4B,IAAI,GACpE,CAAC,GAAU,KAAe,EAAS,EAAE,GAErC,KAAe,MAAwB;EAE3C,AADA,EAAY,EAAQ,OAAO,EAAE,GAC7B,EAAiB,CAAO;CAC1B,GAEM,KAAgB,YAAY;EAChC,IAAI,CAAC,GACH;EAEF,IAAM,IAAM,EAAS,KAAK;EAI1B,CAAI,MAHmB,GAAe,EACpC,WAAW;GAAE,IAAI,EAAc;GAAI,KAAK,KAAO,KAAA;EAAU,EAC3D,CAAC,GACY,MAAM,mBAAmB,OACpC,EAAO,QACL,EAAG,oDAAoD,mBAAmB,GAC1E,EACE,+DACA,6BACA,EAAE,MAAM,EAAc,MAAM,CAC9B,CACF,GACA,EAAiB,IAAI,GACrB,EAAY,EAAE,GACd,MAAM,EAAQ;CAElB,GAEM,IACJ,kBAAC,GAAD;EAAQ,MAAK;EAAS,eAAe,EAAS,iBAAiB;YAA/D,CACE,kBAAC,IAAD,EAAM,WAAU,SAAU,CAAA,GACzB,EAAG,4CAA4C,aAAa,CACvD;;CAgNV,OACE,kBAAC,IAAD;EACE,OAAO,EAAG,2CAA2C,UAAU;EAC/D,aAAa,EACX,iDACA,uFACF;EACA,SAAS;YANX;GAQE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,IAAD;OAAQ,OAAO;OAAc,eAAe;iBAA5C,CACE,kBAAC,IAAD;QAAe,cAAW;kBACxB,kBAAC,IAAD,EAAa,aAAY,eAAgB,CAAA;OAC5B,CAAA,GACf,kBAAC,IAAD,EAAA,UAAA,CACE,kBAAC,GAAD;QAAY,OAAO;kBAAK;OAAwB,CAAA,GAC/C,EAAiB,KAAK,MACrB,kBAAC,GAAD;QAAyB,OAAO;kBAC7B,GAAgB,CAAM;OACb,GAFK,CAEL,CACb,CACY,EAAA,CAAA,CACT;;KACL,CAAA;IACF,CAAA,UAvOc;KACvB,IAAI,GACF,OACE,kBAAC,IAAD;MACE,SAAQ;MACR,MAAM;MACN,OAAO,EAAG,6CAA6C,mBAAmB;KAC3E,CAAA;KAGL,IAAI,GACF,OAAO,kBAAC,IAAD;MAAY,SAAS,EAAM;MAAS,eAAe,KAAK,EAAQ;KAAI,CAAA;KAE7E,IAAI,EAAS,WAAW,GAAG;MACzB,IAAM,IAAW,MAAiB;MAClC,OACE,kBAAC,IAAD;OACE,MAAM;OACN,OACE,IACI,EACE,sDACA,+BACF,IACA,EAAG,gDAAgD,iBAAiB;OAE1E,aACE,IACI,EACE,4DACA,sDACF,IACA,EACE,sDACA,mFACF;OAEN,QAAQ,IAAW,KAAA,IAAY;MAChC,CAAA;KAEL;KAEA,OACE,kBAAA,GAAA,EAAA,UAAA;MAEE,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,IAAD,EAAA,UAAA,CACE,kBAAC,IAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAA;QACE,kBAAC,GAAD,EAAA,UAAW,QAAgB,CAAA;QAC3B,kBAAC,GAAD,EAAA,UAAW,UAAkB,CAAA;QAC7B,kBAAC,GAAD,EAAA,UAAW,SAAiB,CAAA;QAC5B,kBAAC,GAAD,EAAA,UAAW,MAAc,CAAA;QACzB,kBAAC,GAAD,EAAA,UAAW,UAAkB,CAAA;QAC7B,kBAAC,GAAD,EAAA,UAAW,YAAoB,CAAA;QAC/B,kBAAC,GAAD,EAAA,UAAW,UAAkB,CAAA;QAC7B,kBAAC,GAAD;SAAW,WAAU;mBAAa;QAAkB,CAAA;OAC5C,EAAA,CAAA,EACC,CAAA,GACb,kBAAC,IAAD,EAAA,UACG,EAAS,KAAK,MACb,kBAAC,GAAD,EAAA,UAAA;QACE,kBAAC,GAAD,EAAA,UACE,kBAAC,UAAD;SACE,MAAK;SACL,eAAe,EAAU,CAAO;SAChC,WAAU;mBAET,EAAQ;QACH,CAAA,EACC,CAAA;QACX,kBAAC,GAAD;SAAW,WAAU;mBAClB,EAAQ,WAAW;QACX,CAAA;QACX,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAa,QAAQ,EAAQ,OAAS,CAAA,EAC7B,CAAA;QACX,kBAAC,GAAD;SAAW,WAAU;mBAClB,EAAQ,MACP,kBAAC,GAAD;UACE,MAAM,EAAO,EAAQ,GAAG;UACxB,WAAU;UACV,mBAAkB;oBAHpB,CAKE,kBAAC,QAAD;WAAM,WAAU;qBAAY,EAAQ;UAAU,CAAA,GAC9C,kBAAC,GAAD;WAAc,WAAU;WAAoB,eAAY;UAAQ,CAAA,CACxD;cAEV,kBAAC,QAAD;UAAM,WAAU;oBAA8B;SAAO,CAAA;QAE9C,CAAA;QACX,kBAAC,GAAD;SAAW,WAAU;mBAClB,EAAQ,QAAQ,WAAW,IAC1B,kBAAC,QAAD;UAAM,WAAU;oBAA8B;SAAO,CAAA,IAErD,kBAAC,QAAD;UAAM,WAAU;oBACb,EAAQ,QAAQ,KAAK,IAAI;SACtB,CAAA;QAEC,CAAA;QACX,kBAAC,GAAD;SAAW,WAAU;mBAClB,EAAW,EAAQ,WAAW;QACtB,CAAA;QACX,kBAAC,GAAD;SAAW,WAAU;mBAClB,EAAQ,WAAW;QACX,CAAA;QACX,kBAAC,GAAD;SAAW,WAAU;mBACnB,kBAAC,GAAD;UACE,WAAW,eAAe,EAAQ;UAClC,cAAc,EAAU,CAAO;UAC/B,YACE,EAAW,EAAQ,MAAM,IACrB,CACE;WACE,KAAK;WACL,OAAO,EAAG,6CAA6C,SAAS;WAChE,MAAM;WACN,gBAAgB,EAAY,CAAO;UACrC,CACF,IACA,CAAC;SAER,CAAA;QACQ,CAAA;OACH,EAAA,GA/DK,EAAQ,EA+Db,CACX,EACQ,CAAA,CACN,EAAA,CAAA;MACJ,CAAA;MAGL,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAS,KAAK,MACb,kBAAC,MAAD;QAEE,WAAU;kBAEV,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf;WACE,kBAAC,OAAD;YAAK,WAAU;sBAAf,CACE,kBAAC,UAAD;aACE,MAAK;aACL,eAAe,EAAU,CAAO;aAChC,WAAU;uBAET,EAAQ;YACH,CAAA,GACR,kBAAC,GAAD,EAAa,QAAQ,EAAQ,OAAS,CAAA,CACnC;;WACL,kBAAC,OAAD;YAAK,WAAU;sBAAf;aACE,kBAAC,QAAD,EAAA,UAAO,EAAQ,WAAW,KAAW,CAAA;aACpC,EAAQ,UAAU,kBAAC,QAAD,EAAA,UAAO,EAAQ,QAAc,CAAA,IAAI;aACpD,kBAAC,QAAD,EAAA,UAAO,EAAW,EAAQ,WAAW,EAAQ,CAAA;YAC1C;;WACJ,EAAQ,QAAQ,SAAS,IACxB,kBAAC,KAAD;YAAG,WAAU;sBACV,EAAQ,QAAQ,KAAK,IAAI;WACzB,CAAA,IACD;WACH,EAAQ,MACP,kBAAC,GAAD;YACE,MAAM,EAAO,EAAQ,GAAG;YACxB,WAAU;YACV,mBAAkB;sBAHpB,CAKE,kBAAC,QAAD;aAAM,WAAU;uBAAhB,CAA2B,SAAM,EAAQ,GAAU;gBACnD,kBAAC,GAAD;aAAc,WAAU;aAAkB,eAAY;YAAQ,CAAA,CACtD;gBACR;UACD;aACL,kBAAC,GAAD;UACE,WAAW,eAAe,EAAQ;UAClC,cAAc,EAAU,CAAO;UAC/B,YACE,EAAW,EAAQ,MAAM,IACrB,CACE;WACE,KAAK;WACL,OAAO,EAAG,6CAA6C,SAAS;WAChE,MAAM;WACN,gBAAgB,EAAY,CAAO;UACrC,CACF,IACA,CAAC;SAER,CAAA,CACE;;OACH,GArDG,EAAQ,EAqDX,CACL;MACC,CAAA;MAEJ,kBAAC,GAAD;OACE,QAAQ,EAAW;OACnB,OAAO,EAAW;OAClB,aAAa,EAAS;OACtB,SAAS,IAAU,WAAW;OAC9B,QAAQ,EAAW;OACnB,QAAQ,EAAW;OACnB,WAAW,EAAG,+CAA+C,UAAU;OAC9D;MACV,CAAA;KACD,EAAA,CAAA;IAEN,GA8BkB,CACT;;GAGL,kBAAC,GAAD;IAAQ,MAAM,MAAW;IAAM,eAAe,MAAU,IAAO,KAAA,IAAY,EAAU,IAAI;cACvF,kBAAC,GAAD;KAAe,WAAU;eACtB,IACC,kBAAA,GAAA,EAAA,UAAA;MACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;OAAa,WAAU;iBAAvB,CACE,kBAAC,IAAD;QAAU,WAAU;QAA6B,eAAY;OAAQ,CAAA,GACpE,EAAO,KACG;UACb,kBAAC,GAAD,EAAA,UAAA;OAAmB;OACR,EAAO,WAAW;OAAI;OAAG,kBAAC,GAAD,EAAa,QAAQ,EAAO,OAAS,CAAA;MACtD,EAAA,CAAA,CACP,EAAA,CAAA;MAEd,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAkE;QAE5E,CAAA,GACH,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAO,UAAU,KAAK,KAAK;QAC3B,CAAA,CACA,EAAA,CAAA;QAEL,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAkE;UAE5E,CAAA,GACH,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAO,QAAQ,SAAS,EAAO,QAAQ,KAAK,IAAI,IAAI;UACpD,CAAA,CACA,EAAA,CAAA;UACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAkE;UAE5E,CAAA,GACH,kBAAC,KAAD;WAAG,WAAU;qBAA0B,EAAO,WAAW;UAAO,CAAA,CAC7D,EAAA,CAAA;UACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAkE;UAE5E,CAAA,GACH,kBAAC,KAAD;WAAG,WAAU;qBAA0B,EAAW,EAAO,WAAW;UAAK,CAAA,CACtE,EAAA,CAAA;UACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAkE;UAE5E,CAAA,GACH,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAO,MACN,kBAAC,GAAD;YACE,MAAM,EAAO,EAAO,GAAG;YACvB,WAAU;YACV,mBAAkB;sBAHpB,CAKG,EAAO,KACR,kBAAC,GAAD;aAAc,WAAU;aAAW,eAAY;YAAQ,CAAA,CAC/C;gBAEV,kBAAC,QAAD;YAAM,WAAU;sBAAsB;WAAkB,CAAA;UAEzD,CAAA,CACA,EAAA,CAAA;SACF;;QAEL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAb;UAA+E;UAClE,EAAO,WAAW;UAAO;SACnC;YACH,kBAAC,OAAD;SAAK,WAAU;mBACZ,EAAO,WAAW,WAAW,IAC5B,kBAAC,QAAD;UAAM,WAAU;oBAAsB;SAAkB,CAAA,IAExD,EAAO,WAAW,KAAK,MAAc;UACnC,IAAM,IAAO,GAAgB,IAAI,CAAS;UAC1C,OACE,kBAAC,GAAD;WAEE,MAAK;WACL,SAAQ;WACR,MAAK;WACL,WAAU;WACV,eAAe,EAAW,YAAY,GAAW;qBANnD,CAQE,kBAAC,IAAD;YAAU,WAAU;YAAW,eAAY;WAAQ,CAAA,GACnD,kBAAC,QAAD;YAAM,WAAW,IAAO,KAAK;sBAC1B,KAAQ;WACL,CAAA,CACA;aAXD,CAWC;SAEZ,CAAC;QAEA,CAAA,CACF,EAAA,CAAA;QAEL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAkE;QAE5E,CAAA,GACH,kBAAC,OAAD;SAAK,WAAU;mBACZ,EAAO,aACN,kBAAC,GAAD;UACE,MAAK;UACL,SAAQ;UACR,MAAK;UACL,WAAU;UACV,eAAe,EAAW,aAAa,EAAO,YAAY;oBAL5D,CAOE,kBAAC,IAAD;WAAa,WAAU;WAAW,eAAY;UAAQ,CAAA,GACtD,kBAAC,QAAD;WACE,WACE,EAAkB,IAAI,EAAO,UAAU,IAAI,KAAK;qBAGjD,EAAkB,IAAI,EAAO,UAAU,KAAK,EAAO;UAChD,CAAA,CACA;cAER,kBAAC,QAAD;UAAM,WAAU;oBAAsB;SAAkB,CAAA;QAEvD,CAAA,CACF,EAAA,CAAA;QAEL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAkE;QAE5E,CAAA,GACH,kBAAC,OAAD;SAAK,WAAU;mBACb,kBAAC,IAAD;UACE,YAAW;UACX,UAAU,EAAO;UACjB,OAAO,EAAO;UACd,WAAU;SACX,CAAA;QACE,CAAA,CACF,EAAA,CAAA;OACF;;MAEL,kBAAC,GAAD,EAAA,UAAA,CACG,EAAW,EAAO,MAAM,IACvB,kBAAC,GAAD;OACE,MAAK;OACL,eAAe;QACb,IAAM,IAAS;QAEf,AADA,EAAU,IAAI,GACd,EAAY,CAAM;OACpB;iBANF,CAQE,kBAAC,GAAD,EAAQ,WAAU,SAAU,CAAA,GAAC,SAEvB;WACN,MACJ,kBAAC,IAAD;OAAa,SAAA;iBACX,kBAAC,GAAD;QAAQ,MAAK;QAAS,SAAQ;kBAAU;OAEhC,CAAA;MACG,CAAA,CACD,EAAA,CAAA;KACd,EAAA,CAAA,IACA;IACS,CAAA;GACT,CAAA;GAGR,kBAAC,GAAD;IACE,MAAM,MAAkB;IACxB,eAAe,MAAU,KAAQ,IAAa,KAAA,IAAY,EAAiB,IAAI;cAE/E,kBAAC,GAAD;KAAe,WAAU;eAAzB;MACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACG,EAAG,oDAAoD,iBAAiB,EAC9D,CAAA,GACb,kBAAC,GAAD,EAAA,UACG,IACG,EACE,wDACA,sFACA,EAAE,MAAM,EAAc,MAAM,CAC9B,IACA,GACa,CAAA,CACP,EAAA,CAAA;MAEb,IACC,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,MAAD;UAAI,WAAU;oBAAkE;SAE5E,CAAA,GACJ,kBAAC,MAAD;UACE,WAAU;UACV,OAAO,EAAc,QAAQ,KAAK,IAAI;oBAErC,EAAc,QAAQ,SAAS,EAAc,QAAQ,KAAK,IAAI,IAAI;SACjE,CAAA,CACD;;QACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;SAAI,WAAU;mBAAkE;QAE5E,CAAA,GACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAc,WAAW;QACxB,CAAA,CACD,EAAA,CAAA;QACL,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,MAAD;UAAI,WAAU;oBAAkE;SAE5E,CAAA,GACJ,kBAAC,MAAD;UAAI,WAAU;oBACX,EAAc,WAAW;SACxB,CAAA,CACD;;OACH;WACF;MAEJ,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,IAAD;SAAO,SAAQ;mBACZ,EAAG,iDAAiD,gBAAgB;QAChE,CAAA;QACP,kBAAC,IAAD;SACE,IAAG;SACH,OAAO;SACP,WAAW,MAAU,EAAY,EAAM,OAAO,KAAK;SACnD,aAAY;QACb,CAAA;QACD,kBAAC,KAAD;SAAG,WAAU;mBACV,EACC,6CACA,0EACF;QACC,CAAA;OACA;;MAEL,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;OACE,MAAK;OACL,SAAQ;OACR,eAAe,EAAiB,IAAI;OACpC,UAAU;iBAET,EAAG,oCAAoC,QAAQ;MAC1C,CAAA,GACR,kBAAC,GAAD;OAAQ,MAAK;OAAS,eAAe,KAAK,GAAc;OAAG,UAAU;iBAArE,CACE,kBAAC,GAAD,EAAQ,WAAU,SAAU,CAAA,GAC3B,IACG,EAAG,gDAAgD,aAAa,IAChE,EAAG,oDAAoD,iBAAiB,CACtE;QACI,EAAA,CAAA;KACD;;GACT,CAAA;EACE;;AAEhB"}
@@ -1 +1 @@
1
- {"version":3,"file":"RunsPage.d.ts","sourceRoot":"","sources":["../../../../src/cosmicintersection/pages/runs/RunsPage.tsx"],"names":[],"mappings":"AA0DA,MAAM,CAAC,OAAO,UAAU,QAAQ,4CAsO/B"}
1
+ {"version":3,"file":"RunsPage.d.ts","sourceRoot":"","sources":["../../../../src/cosmicintersection/pages/runs/RunsPage.tsx"],"names":[],"mappings":"AA0DA,MAAM,CAAC,OAAO,UAAU,QAAQ,4CA+O/B"}
@@ -1 +1 @@
1
- {"version":3,"file":"RunsPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/runs/RunsPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { Activity, Eye } 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 { useCosPipelinesQuery, useCosRunsQuery, type CosRunStatus } from '../../api/cosmicApi';\nimport {\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 { useTr } from '../../../shared/hooks/useTr';\n\nconst STATUS_OPTIONS: CosRunStatus[] = ['QUEUED', 'RUNNING', 'SUCCEEDED', 'FAILED', 'CANCELLED'];\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\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\nexport default function RunsPage() {\n const { navigate } = useCosmicIntersection();\n const tr = useTr();\n const [status, setStatus] = useState<CosRunStatus | typeof ALL>(ALL);\n const [pipelineId, setPipelineId] = useState<string | typeof ALL>(ALL);\n const pagination = usePagination({ resetKey: `${status}:${pipelineId}` });\n\n // Load pipelines to populate the filter and to resolve run -> pipeline names.\n const { data: pipelinesData } = useCosPipelinesQuery({ pagination: { limit: 100, offset: 0 } });\n const pipelines = useMemo(() => pipelinesData?.cosListPipelines.items ?? [], [pipelinesData]);\n const pipelineNameById = useMemo(() => {\n const map = new Map<string, string>();\n pipelines.forEach((pipeline) => map.set(pipeline.id, pipeline.name));\n return map;\n }, [pipelines]);\n\n const { data, loading, error, refetch } = useCosRunsQuery({\n status: status === ALL ? undefined : status,\n pipelineId: pipelineId === ALL ? undefined : pipelineId,\n pagination: { limit: pagination.limit, offset: pagination.offset },\n });\n\n const runs = useMemo(() => data?.cosListRuns.items ?? [], [data]);\n const pageInfo = data?.cosListRuns.pageInfo;\n\n const renderBody = () => {\n if (loading) {\n return <LoadingState variant=\"rows\" rows={6} label={tr('cosmicintersection.pages.runs.loading', 'Loading runs…')} />;\n }\n if (error) {\n return (\n <ErrorState\n message={tr('cosmicintersection.pages.runs.error', 'We could not load pipeline runs. Please try again.')}\n onRetry={() => void refetch()}\n />\n );\n }\n if (runs.length === 0) {\n const noFilters = status === ALL && pipelineId === ALL;\n return (\n <EmptyState\n icon={Activity}\n title={\n noFilters\n ? tr('cosmicintersection.pages.runs.emptyTitle', 'No runs yet')\n : tr('cosmicintersection.pages.runs.emptyFilterTitle', 'No runs match these filters')\n }\n description={\n noFilters\n ? tr(\n 'cosmicintersection.pages.runs.emptyDescription',\n 'Runs appear here once you start a pipeline. Each run tracks its steps, timings and metrics.',\n )\n : tr(\n 'cosmicintersection.pages.runs.emptyFilterDescription',\n 'Try a different status or pipeline filter.',\n )\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>Run</TableHead>\n <TableHead>Pipeline</TableHead>\n <TableHead>Status</TableHead>\n <TableHead>Started</TableHead>\n <TableHead>Finished</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\">#{run.runNumber}</TableCell>\n <TableCell className=\"text-text-secondary\">\n <span className=\"truncate\">\n {pipelineNameById.get(run.pipelineId) ?? (\n <span className=\"font-mono text-xs\">{run.pipelineId}</span>\n )}\n </span>\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=\"whitespace-nowrap text-text-secondary\">\n {formatDate(run.finishedAt)}\n </TableCell>\n <TableCell className=\"tabular-nums text-text-secondary\">\n {formatDuration(run.durationMs)}\n </TableCell>\n <TableCell className=\"text-text-secondary\">{run.triggeredBy || '—'}</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 <p className=\"truncate text-sm text-text-secondary\">\n {pipelineNameById.get(run.pipelineId) ?? (\n <span className=\"font-mono text-xs\">{run.pipelineId}</span>\n )}\n </p>\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\">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=\"flex flex-col\">\n <dt className=\"text-text-secondary\">Finished</dt>\n <dd className=\"text-text-primary\">{formatDate(run.finishedAt)}</dd>\n </div>\n <div className=\"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={pagination.offset}\n limit={pagination.limit}\n loadedCount={runs.length}\n hasMore={pageInfo?.hasMore ?? false}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel={tr('cosmicintersection.pages.runs.itemLabel', 'runs')}\n loading={loading}\n />\n </>\n );\n };\n\n return (\n <PageLayout\n title={tr('cosmicintersection.pages.runs.title', 'Runs')}\n description={tr(\n 'cosmicintersection.pages.runs.description',\n 'Every pipeline execution, with status, timings and outcomes.',\n )}\n actions={\n <Button type=\"button\" variant=\"outline\" onClick={() => navigate('pipelines')}>\n <Eye className=\"size-4\" />\n {tr('cosmicintersection.pages.runs.viewPipelines', 'View pipelines')}\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 CosRunStatus | typeof ALL)}\n >\n <SelectTrigger aria-label=\"Filter by status\" className=\"w-40\">\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\n <label className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <span>Pipeline</span>\n <Select\n value={pipelineId}\n onValueChange={(value) => setPipelineId(value as string | typeof ALL)}\n >\n <SelectTrigger aria-label=\"Filter by pipeline\" className=\"w-56\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>All pipelines</SelectItem>\n {pipelines.map((pipeline) => (\n <SelectItem key={pipeline.id} value={pipeline.id}>\n {pipeline.name}\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":"RunsPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/runs/RunsPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { Activity, Eye } 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 { useCosPipelinesQuery, useCosRunsQuery, type CosRunStatus } from '../../api/cosmicApi';\nimport {\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 { useTr } from '../../../shared/hooks/useTr';\n\nconst STATUS_OPTIONS: CosRunStatus[] = ['QUEUED', 'RUNNING', 'SUCCEEDED', 'FAILED', 'CANCELLED'];\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\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\nexport default function RunsPage() {\n const { navigate } = useCosmicIntersection();\n const tr = useTr();\n const [status, setStatus] = useState<CosRunStatus | typeof ALL>(ALL);\n const [pipelineId, setPipelineId] = useState<string | typeof ALL>(ALL);\n const pagination = usePagination({ resetKey: `${status}:${pipelineId}` });\n\n // Load pipelines to populate the filter and to resolve run -> pipeline names.\n const { data: pipelinesData } = useCosPipelinesQuery({ pagination: { limit: 100, offset: 0 } });\n const pipelines = useMemo(() => pipelinesData?.cosListPipelines.items ?? [], [pipelinesData]);\n const pipelineNameById = useMemo(() => {\n const map = new Map<string, string>();\n pipelines.forEach((pipeline) => map.set(pipeline.id, pipeline.name));\n return map;\n }, [pipelines]);\n\n const { data, loading, error, refetch } = useCosRunsQuery({\n status: status === ALL ? undefined : status,\n pipelineId: pipelineId === ALL ? undefined : pipelineId,\n pagination: { limit: pagination.limit, offset: pagination.offset },\n });\n\n const runs = useMemo(() => data?.cosListRuns.items ?? [], [data]);\n const pageInfo = data?.cosListRuns.pageInfo;\n\n const renderBody = () => {\n if (loading) {\n return (\n <LoadingState\n variant=\"rows\"\n rows={6}\n label={tr('cosmicintersection.pages.runs.loading', 'Loading runs…')}\n />\n );\n }\n if (error) {\n return (\n <ErrorState\n message={tr(\n 'cosmicintersection.pages.runs.error',\n 'We could not load pipeline runs. Please try again.'\n )}\n onRetry={() => void refetch()}\n />\n );\n }\n if (runs.length === 0) {\n const noFilters = status === ALL && pipelineId === ALL;\n return (\n <EmptyState\n icon={Activity}\n title={\n noFilters\n ? tr('cosmicintersection.pages.runs.emptyTitle', 'No runs yet')\n : tr('cosmicintersection.pages.runs.emptyFilterTitle', 'No runs match these filters')\n }\n description={\n noFilters\n ? tr(\n 'cosmicintersection.pages.runs.emptyDescription',\n 'Runs appear here once you start a pipeline. Each run tracks its steps, timings and metrics.'\n )\n : tr(\n 'cosmicintersection.pages.runs.emptyFilterDescription',\n 'Try a different status or pipeline filter.'\n )\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>Run</TableHead>\n <TableHead>Pipeline</TableHead>\n <TableHead>Status</TableHead>\n <TableHead>Started</TableHead>\n <TableHead>Finished</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\">#{run.runNumber}</TableCell>\n <TableCell className=\"text-text-secondary\">\n <span className=\"truncate\">\n {pipelineNameById.get(run.pipelineId) ?? (\n <span className=\"font-mono text-xs\">{run.pipelineId}</span>\n )}\n </span>\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=\"whitespace-nowrap text-text-secondary\">\n {formatDate(run.finishedAt)}\n </TableCell>\n <TableCell className=\"tabular-nums text-text-secondary\">\n {formatDuration(run.durationMs)}\n </TableCell>\n <TableCell className=\"text-text-secondary\">{run.triggeredBy || '—'}</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 <p className=\"truncate text-sm text-text-secondary\">\n {pipelineNameById.get(run.pipelineId) ?? (\n <span className=\"font-mono text-xs\">{run.pipelineId}</span>\n )}\n </p>\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\">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=\"flex flex-col\">\n <dt className=\"text-text-secondary\">Finished</dt>\n <dd className=\"text-text-primary\">{formatDate(run.finishedAt)}</dd>\n </div>\n <div className=\"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={pagination.offset}\n limit={pagination.limit}\n loadedCount={runs.length}\n hasMore={pageInfo?.hasMore ?? false}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel={tr('cosmicintersection.pages.runs.itemLabel', 'runs')}\n loading={loading}\n />\n </>\n );\n };\n\n return (\n <PageLayout\n title={tr('cosmicintersection.pages.runs.title', 'Runs')}\n description={tr(\n 'cosmicintersection.pages.runs.description',\n 'Every pipeline execution, with status, timings and outcomes.'\n )}\n actions={\n <Button type=\"button\" variant=\"outline\" onClick={() => navigate('pipelines')}>\n <Eye className=\"size-4\" />\n {tr('cosmicintersection.pages.runs.viewPipelines', 'View pipelines')}\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 CosRunStatus | typeof ALL)}\n >\n <SelectTrigger aria-label=\"Filter by status\" className=\"w-40\">\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\n <label className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <span>Pipeline</span>\n <Select\n value={pipelineId}\n onValueChange={(value) => setPipelineId(value as string | typeof ALL)}\n >\n <SelectTrigger aria-label=\"Filter by pipeline\" className=\"w-56\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>All pipelines</SelectItem>\n {pipelines.map((pipeline) => (\n <SelectItem key={pipeline.id} value={pipeline.id}>\n {pipeline.name}\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 +1 @@
1
- {"version":3,"file":"SourcesPage.d.ts","sourceRoot":"","sources":["../../../../src/cosmicintersection/pages/sources/SourcesPage.tsx"],"names":[],"mappings":"AAmGA,MAAM,CAAC,OAAO,UAAU,WAAW,4CAkWlC"}
1
+ {"version":3,"file":"SourcesPage.d.ts","sourceRoot":"","sources":["../../../../src/cosmicintersection/pages/sources/SourcesPage.tsx"],"names":[],"mappings":"AAmGA,MAAM,CAAC,OAAO,UAAU,WAAW,4CA0YlC"}
@@ -1 +1 @@
1
- {"version":3,"file":"SourcesPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/sources/SourcesPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Archive,\n FileInput,\n FlaskConical,\n Plus,\n Radio,\n RadioTower,\n RefreshCw,\n Satellite,\n Telescope,\n Webhook,\n type LucideIcon,\n} 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 useCosArchiveSourceMutation,\n useCosMarkSourceSyncedMutation,\n useCosSourcesQuery,\n type CosSource,\n type CosSourceStatus,\n type CosSourceType,\n} from '../../api/cosmicApi';\nimport {\n ActionMenu,\n EmptyState,\n ErrorState,\n LoadingState,\n PageLayout,\n Pagination,\n SafeLink,\n StatusBadge,\n formatEnumLabel,\n usePagination,\n} from '../../components';\nimport { useCosmicIntersection } from '../../context/CosmicIntersectionContext';\nimport { notify } from '../../lib/toast';\nimport { useTr } from '../../../shared/hooks/useTr';\n\nconst ALL = 'ALL';\n\nconst SOURCE_TYPES: CosSourceType[] = [\n 'TELESCOPE',\n 'SATELLITE',\n 'GROUND_SENSOR',\n 'ARCHIVE',\n 'API',\n 'STREAM',\n 'FILE_DROP',\n 'SIMULATION',\n];\n\nconst SOURCE_STATUSES: CosSourceStatus[] = [\n 'CONFIGURING',\n 'CONNECTED',\n 'SYNCING',\n 'DEGRADED',\n 'ERROR',\n 'DISABLED',\n];\n\n/** Per-type glyph to aid scannability (mockup 06). */\nconst SOURCE_TYPE_ICON: Record<CosSourceType, LucideIcon> = {\n TELESCOPE: Telescope,\n SATELLITE: Satellite,\n GROUND_SENSOR: RadioTower,\n ARCHIVE: Archive,\n API: Webhook,\n STREAM: Radio,\n FILE_DROP: FileInput,\n SIMULATION: FlaskConical,\n};\n\nfunction formatDate(value?: string | null): string {\n if (!value) {\n return '—';\n }\n const parsed = new Date(value);\n if (Number.isNaN(parsed.getTime())) {\n return '—';\n }\n return parsed.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });\n}\n\nexport default function SourcesPage() {\n const { navigate } = useCosmicIntersection();\n const tr = useTr();\n const [typeFilter, setTypeFilter] = useState<string>(ALL);\n const [statusFilter, setStatusFilter] = useState<string>(ALL);\n const pagination = usePagination({ pageSize: 20, resetKey: `${typeFilter}|${statusFilter}` });\n\n const { data, loading, error, refetch } = useCosSourcesQuery({\n type: typeFilter === ALL ? undefined : (typeFilter as CosSourceType),\n status: statusFilter === ALL ? undefined : (statusFilter as CosSourceStatus),\n pagination: { limit: pagination.limit, offset: pagination.offset },\n });\n\n const [markSynced] = useCosMarkSourceSyncedMutation();\n const [archiveSource] = useCosArchiveSourceMutation();\n\n const sources = useMemo(() => data?.cosListSources.items ?? [], [data]);\n const pageInfo = data?.cosListSources.pageInfo;\n\n const handleMarkSynced = async (source: CosSource) => {\n const response = await markSynced({ variables: { id: source.id } });\n if (response.data?.cosMarkSourceSynced) {\n notify.success(\n tr('cosmicintersection.pages.sources.syncSuccess', 'Source marked synced'),\n tr(\n 'cosmicintersection.pages.sources.syncSuccessDescription',\n '“{{name}}” is now up to date.',\n { name: source.name },\n ),\n );\n await refetch();\n }\n };\n\n const handleArchive = async (source: CosSource) => {\n const response = await archiveSource({ variables: { id: source.id } });\n if (response.error) {\n throw new Error(response.error.message);\n }\n if (!response.data?.cosArchiveSource) {\n notify.error(\n tr('cosmicintersection.pages.sources.archiveError', 'Could not archive source'),\n tr('cosmicintersection.pages.sources.archiveErrorDescription', 'The server did not confirm the change.'),\n );\n throw new Error('Archive not confirmed');\n }\n notify.success(tr('cosmicintersection.pages.sources.archiveSuccess', 'Source archived'));\n await refetch();\n };\n\n const newButton = (\n <Button type=\"button\" onClick={() => navigate('sources/create')}>\n <Plus className=\"size-4\" />\n {tr('cosmicintersection.pages.sources.create', 'New source')}\n </Button>\n );\n\n const renderBody = () => {\n if (loading) {\n return <LoadingState variant=\"rows\" rows={6} label={tr('cosmicintersection.pages.sources.loading', 'Loading sources…')} />;\n }\n if (error) {\n return <ErrorState message={error.message} onRetry={() => void refetch()} />;\n }\n if (sources.length === 0) {\n const filtered = typeFilter !== ALL || statusFilter !== ALL;\n return (\n <EmptyState\n icon={Radio}\n title={\n filtered\n ? tr('cosmicintersection.pages.sources.emptyFilterTitle', 'No sources match these filters')\n : tr('cosmicintersection.pages.sources.emptyTitle', 'No data sources yet')\n }\n description={\n filtered\n ? tr(\n 'cosmicintersection.pages.sources.emptyFilterDescription',\n 'Try clearing the type or status filter to see more sources.',\n )\n : tr(\n 'cosmicintersection.pages.sources.emptyDescription',\n 'Connect a telescope, satellite feed, archive or API to start ingesting scientific data.',\n )\n }\n action={filtered ? undefined : newButton}\n />\n );\n }\n\n return (\n <>\n {/* Desktop / tablet: table */}\n <div className=\"hidden overflow-hidden rounded-card border border-border-default bg-bg-elevated md:block\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{tr('cosmicintersection.common.name', 'Name')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.type', 'Type')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.status', 'Status')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.endpoint', 'Endpoint')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.lastSynced', 'Last synced')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.tags', 'Tags')}</TableHead>\n <TableHead className=\"text-right\">{tr('cosmicintersection.common.actions', 'Actions')}</TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {sources.map((source) => {\n const TypeIcon = SOURCE_TYPE_ICON[source.type] ?? Radio;\n return (\n <TableRow\n key={source.id}\n className=\"cursor-pointer\"\n onClick={() => navigate(`sources/${source.id}`)}\n >\n <TableCell>\n <div className=\"flex items-start gap-2.5\">\n <span className=\"mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-button bg-bg-sunken text-text-secondary\">\n <TypeIcon className=\"size-4\" aria-hidden=\"true\" />\n </span>\n <div className=\"min-w-0\">\n <div className=\"font-medium text-text-primary\">{source.name}</div>\n {source.description ? (\n <div className=\"mt-0.5 line-clamp-1 max-w-xs text-xs text-text-secondary\">\n {source.description}\n </div>\n ) : null}\n </div>\n </div>\n </TableCell>\n <TableCell>\n <Badge variant=\"secondary\">{formatEnumLabel(source.type)}</Badge>\n </TableCell>\n <TableCell>\n <StatusBadge status={source.status} />\n </TableCell>\n <TableCell\n className=\"max-w-[16rem]\"\n onClick={(event) => event.stopPropagation()}\n >\n {source.endpoint ? (\n <SafeLink\n href={source.endpoint}\n className=\"block truncate text-sm text-text-primary underline-offset-2 hover:underline\"\n fallbackClassName=\"block truncate text-sm text-text-secondary\"\n >\n {source.endpoint}\n </SafeLink>\n ) : (\n <span className=\"text-sm text-text-secondary\">—</span>\n )}\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-sm text-text-secondary\">\n {formatDate(source.lastSyncedAt)}\n </TableCell>\n <TableCell>\n <div className=\"flex max-w-[12rem] flex-wrap gap-1\">\n {source.tags.length === 0 ? (\n <span className=\"text-xs text-text-secondary\">—</span>\n ) : (\n source.tags.slice(0, 3).map((tag) => (\n <Badge key={tag} variant=\"outline\" className=\"text-xs\">\n {tag}\n </Badge>\n ))\n )}\n {source.tags.length > 3 ? (\n <span className=\"text-xs text-text-secondary\">\n +{source.tags.length - 3}\n </span>\n ) : null}\n </div>\n </TableCell>\n <TableCell className=\"text-right\">\n <ActionMenu\n ariaLabel={tr('cosmicintersection.common.actionsFor', 'Actions for {{name}}', { name: source.name })}\n entityName={source.name}\n onView={() => navigate(`sources/${source.id}`)}\n onEdit={() => navigate(`sources/${source.id}/edit`)}\n onDelete={() => handleArchive(source)}\n deleteLabel={tr('cosmicintersection.pages.sources.archive', 'Archive source')}\n extraItems={[\n {\n key: 'mark-synced',\n label: tr('cosmicintersection.pages.sources.markSynced', 'Mark synced'),\n icon: RefreshCw,\n onSelect: () => void handleMarkSynced(source),\n },\n ]}\n />\n </TableCell>\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </div>\n\n {/* Mobile: stacked cards */}\n <ul className=\"space-y-3 md:hidden\">\n {sources.map((source) => {\n const TypeIcon = SOURCE_TYPE_ICON[source.type] ?? Radio;\n return (\n <li key={source.id}>\n <div\n role=\"button\"\n tabIndex={0}\n aria-label={tr('cosmicintersection.common.open', 'Open {{name}}', { name: source.name })}\n onClick={() => navigate(`sources/${source.id}`)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n navigate(`sources/${source.id}`);\n }\n }}\n className=\"flex flex-col gap-3 rounded-card border border-border-default bg-bg-elevated p-4 outline-none transition-colors hover:bg-bg-sunken focus-visible:ring-2 focus-visible:ring-border-focus\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-start gap-2.5\">\n <span className=\"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-button bg-bg-sunken text-text-secondary\">\n <TypeIcon className=\"size-4\" aria-hidden=\"true\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"font-medium text-text-primary\">{source.name}</p>\n {source.description ? (\n <p className=\"mt-0.5 line-clamp-2 text-xs text-text-secondary\">\n {source.description}\n </p>\n ) : null}\n </div>\n </div>\n <div onClick={(event) => event.stopPropagation()}>\n <ActionMenu\n ariaLabel={tr('cosmicintersection.common.actionsFor', 'Actions for {{name}}', { name: source.name })}\n entityName={source.name}\n onView={() => navigate(`sources/${source.id}`)}\n onEdit={() => navigate(`sources/${source.id}/edit`)}\n onDelete={() => handleArchive(source)}\n deleteLabel={tr('cosmicintersection.pages.sources.archive', 'Archive source')}\n extraItems={[\n {\n key: 'mark-synced',\n label: tr('cosmicintersection.pages.sources.markSynced', 'Mark synced'),\n icon: RefreshCw,\n onSelect: () => void handleMarkSynced(source),\n },\n ]}\n />\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <Badge variant=\"secondary\">{formatEnumLabel(source.type)}</Badge>\n <StatusBadge status={source.status} />\n </div>\n\n {source.endpoint ? (\n <div onClick={(event) => event.stopPropagation()}>\n <SafeLink\n href={source.endpoint}\n className=\"block truncate text-sm text-text-primary underline-offset-2 hover:underline\"\n fallbackClassName=\"block truncate text-sm text-text-secondary\"\n >\n {source.endpoint}\n </SafeLink>\n </div>\n ) : null}\n\n <div className=\"flex items-center justify-between gap-2 text-xs text-text-secondary\">\n <span>{tr('cosmicintersection.common.lastSynced', 'Last synced')}</span>\n <span>{formatDate(source.lastSyncedAt)}</span>\n </div>\n\n {source.tags.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {source.tags.slice(0, 4).map((tag) => (\n <Badge key={tag} variant=\"outline\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n {source.tags.length > 4 ? (\n <span className=\"text-xs text-text-secondary\">\n +{source.tags.length - 4}\n </span>\n ) : null}\n </div>\n ) : null}\n </div>\n </li>\n );\n })}\n </ul>\n\n <Pagination\n offset={pagination.offset}\n limit={pagination.limit}\n loadedCount={sources.length}\n hasMore={pageInfo?.hasMore ?? false}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel={tr('cosmicintersection.pages.sources.itemLabel', 'sources')}\n loading={loading}\n />\n </>\n );\n };\n\n return (\n <PageLayout\n title={tr('cosmicintersection.pages.sources.title', 'Data sources')}\n description={tr(\n 'cosmicintersection.pages.sources.description',\n 'Instruments, archives and feeds that supply raw data into your federation.',\n )}\n actions={newButton}\n >\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"w-full max-w-[14rem]\">\n <Select value={typeFilter} onValueChange={setTypeFilter}>\n <SelectTrigger aria-label={tr('cosmicintersection.common.filterByType', 'Filter by type')}>\n <SelectValue placeholder={tr('cosmicintersection.common.allTypes', 'All types')} />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>{tr('cosmicintersection.common.allTypes', 'All types')}</SelectItem>\n {SOURCE_TYPES.map((type) => (\n <SelectItem key={type} value={type}>\n {formatEnumLabel(type)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"w-full max-w-[14rem]\">\n <Select value={statusFilter} onValueChange={setStatusFilter}>\n <SelectTrigger aria-label={tr('cosmicintersection.common.filterByStatus', 'Filter by status')}>\n <SelectValue placeholder={tr('cosmicintersection.common.allStatuses', 'All statuses')} />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>{tr('cosmicintersection.common.allStatuses', 'All statuses')}</SelectItem>\n {SOURCE_STATUSES.map((status) => (\n <SelectItem key={status} value={status}>\n {formatEnumLabel(status)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n {renderBody()}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SourcesPage.js","names":[],"sources":["../../../../src/cosmicintersection/pages/sources/SourcesPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Archive,\n FileInput,\n FlaskConical,\n Plus,\n Radio,\n RadioTower,\n RefreshCw,\n Satellite,\n Telescope,\n Webhook,\n type LucideIcon,\n} 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 useCosArchiveSourceMutation,\n useCosMarkSourceSyncedMutation,\n useCosSourcesQuery,\n type CosSource,\n type CosSourceStatus,\n type CosSourceType,\n} from '../../api/cosmicApi';\nimport {\n ActionMenu,\n EmptyState,\n ErrorState,\n LoadingState,\n PageLayout,\n Pagination,\n SafeLink,\n StatusBadge,\n formatEnumLabel,\n usePagination,\n} from '../../components';\nimport { useCosmicIntersection } from '../../context/CosmicIntersectionContext';\nimport { notify } from '../../lib/toast';\nimport { useTr } from '../../../shared/hooks/useTr';\n\nconst ALL = 'ALL';\n\nconst SOURCE_TYPES: CosSourceType[] = [\n 'TELESCOPE',\n 'SATELLITE',\n 'GROUND_SENSOR',\n 'ARCHIVE',\n 'API',\n 'STREAM',\n 'FILE_DROP',\n 'SIMULATION',\n];\n\nconst SOURCE_STATUSES: CosSourceStatus[] = [\n 'CONFIGURING',\n 'CONNECTED',\n 'SYNCING',\n 'DEGRADED',\n 'ERROR',\n 'DISABLED',\n];\n\n/** Per-type glyph to aid scannability (mockup 06). */\nconst SOURCE_TYPE_ICON: Record<CosSourceType, LucideIcon> = {\n TELESCOPE: Telescope,\n SATELLITE: Satellite,\n GROUND_SENSOR: RadioTower,\n ARCHIVE: Archive,\n API: Webhook,\n STREAM: Radio,\n FILE_DROP: FileInput,\n SIMULATION: FlaskConical,\n};\n\nfunction formatDate(value?: string | null): string {\n if (!value) {\n return '—';\n }\n const parsed = new Date(value);\n if (Number.isNaN(parsed.getTime())) {\n return '—';\n }\n return parsed.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });\n}\n\nexport default function SourcesPage() {\n const { navigate } = useCosmicIntersection();\n const tr = useTr();\n const [typeFilter, setTypeFilter] = useState<string>(ALL);\n const [statusFilter, setStatusFilter] = useState<string>(ALL);\n const pagination = usePagination({ pageSize: 20, resetKey: `${typeFilter}|${statusFilter}` });\n\n const { data, loading, error, refetch } = useCosSourcesQuery({\n type: typeFilter === ALL ? undefined : (typeFilter as CosSourceType),\n status: statusFilter === ALL ? undefined : (statusFilter as CosSourceStatus),\n pagination: { limit: pagination.limit, offset: pagination.offset },\n });\n\n const [markSynced] = useCosMarkSourceSyncedMutation();\n const [archiveSource] = useCosArchiveSourceMutation();\n\n const sources = useMemo(() => data?.cosListSources.items ?? [], [data]);\n const pageInfo = data?.cosListSources.pageInfo;\n\n const handleMarkSynced = async (source: CosSource) => {\n const response = await markSynced({ variables: { id: source.id } });\n if (response.data?.cosMarkSourceSynced) {\n notify.success(\n tr('cosmicintersection.pages.sources.syncSuccess', 'Source marked synced'),\n tr(\n 'cosmicintersection.pages.sources.syncSuccessDescription',\n '“{{name}}” is now up to date.',\n { name: source.name }\n )\n );\n await refetch();\n }\n };\n\n const handleArchive = async (source: CosSource) => {\n const response = await archiveSource({ variables: { id: source.id } });\n if (response.error) {\n throw new Error(response.error.message);\n }\n if (!response.data?.cosArchiveSource) {\n notify.error(\n tr('cosmicintersection.pages.sources.archiveError', 'Could not archive source'),\n tr(\n 'cosmicintersection.pages.sources.archiveErrorDescription',\n 'The server did not confirm the change.'\n )\n );\n throw new Error('Archive not confirmed');\n }\n notify.success(tr('cosmicintersection.pages.sources.archiveSuccess', 'Source archived'));\n await refetch();\n };\n\n const newButton = (\n <Button type=\"button\" onClick={() => navigate('sources/create')}>\n <Plus className=\"size-4\" />\n {tr('cosmicintersection.pages.sources.create', 'New source')}\n </Button>\n );\n\n const renderBody = () => {\n if (loading) {\n return (\n <LoadingState\n variant=\"rows\"\n rows={6}\n label={tr('cosmicintersection.pages.sources.loading', 'Loading sources…')}\n />\n );\n }\n if (error) {\n return <ErrorState message={error.message} onRetry={() => void refetch()} />;\n }\n if (sources.length === 0) {\n const filtered = typeFilter !== ALL || statusFilter !== ALL;\n return (\n <EmptyState\n icon={Radio}\n title={\n filtered\n ? tr(\n 'cosmicintersection.pages.sources.emptyFilterTitle',\n 'No sources match these filters'\n )\n : tr('cosmicintersection.pages.sources.emptyTitle', 'No data sources yet')\n }\n description={\n filtered\n ? tr(\n 'cosmicintersection.pages.sources.emptyFilterDescription',\n 'Try clearing the type or status filter to see more sources.'\n )\n : tr(\n 'cosmicintersection.pages.sources.emptyDescription',\n 'Connect a telescope, satellite feed, archive or API to start ingesting scientific data.'\n )\n }\n action={filtered ? undefined : newButton}\n />\n );\n }\n\n return (\n <>\n {/* Desktop / tablet: table */}\n <div className=\"hidden overflow-hidden rounded-card border border-border-default bg-bg-elevated md:block\">\n <Table>\n <TableHeader>\n <TableRow>\n <TableHead>{tr('cosmicintersection.common.name', 'Name')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.type', 'Type')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.status', 'Status')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.endpoint', 'Endpoint')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.lastSynced', 'Last synced')}</TableHead>\n <TableHead>{tr('cosmicintersection.common.tags', 'Tags')}</TableHead>\n <TableHead className=\"text-right\">\n {tr('cosmicintersection.common.actions', 'Actions')}\n </TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {sources.map((source) => {\n const TypeIcon = SOURCE_TYPE_ICON[source.type] ?? Radio;\n return (\n <TableRow\n key={source.id}\n className=\"cursor-pointer\"\n onClick={() => navigate(`sources/${source.id}`)}\n >\n <TableCell>\n <div className=\"flex items-start gap-2.5\">\n <span className=\"mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-button bg-bg-sunken text-text-secondary\">\n <TypeIcon className=\"size-4\" aria-hidden=\"true\" />\n </span>\n <div className=\"min-w-0\">\n <div className=\"font-medium text-text-primary\">{source.name}</div>\n {source.description ? (\n <div className=\"mt-0.5 line-clamp-1 max-w-xs text-xs text-text-secondary\">\n {source.description}\n </div>\n ) : null}\n </div>\n </div>\n </TableCell>\n <TableCell>\n <Badge variant=\"secondary\">{formatEnumLabel(source.type)}</Badge>\n </TableCell>\n <TableCell>\n <StatusBadge status={source.status} />\n </TableCell>\n <TableCell\n className=\"max-w-[16rem]\"\n onClick={(event) => event.stopPropagation()}\n >\n {source.endpoint ? (\n <SafeLink\n href={source.endpoint}\n className=\"block truncate text-sm text-text-primary underline-offset-2 hover:underline\"\n fallbackClassName=\"block truncate text-sm text-text-secondary\"\n >\n {source.endpoint}\n </SafeLink>\n ) : (\n <span className=\"text-sm text-text-secondary\">—</span>\n )}\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-sm text-text-secondary\">\n {formatDate(source.lastSyncedAt)}\n </TableCell>\n <TableCell>\n <div className=\"flex max-w-[12rem] flex-wrap gap-1\">\n {source.tags.length === 0 ? (\n <span className=\"text-xs text-text-secondary\">—</span>\n ) : (\n source.tags.slice(0, 3).map((tag) => (\n <Badge key={tag} variant=\"outline\" className=\"text-xs\">\n {tag}\n </Badge>\n ))\n )}\n {source.tags.length > 3 ? (\n <span className=\"text-xs text-text-secondary\">\n +{source.tags.length - 3}\n </span>\n ) : null}\n </div>\n </TableCell>\n <TableCell className=\"text-right\">\n <ActionMenu\n ariaLabel={tr(\n 'cosmicintersection.common.actionsFor',\n 'Actions for {{name}}',\n { name: source.name }\n )}\n entityName={source.name}\n onView={() => navigate(`sources/${source.id}`)}\n onEdit={() => navigate(`sources/${source.id}/edit`)}\n onDelete={() => handleArchive(source)}\n deleteLabel={tr(\n 'cosmicintersection.pages.sources.archive',\n 'Archive source'\n )}\n extraItems={[\n {\n key: 'mark-synced',\n label: tr('cosmicintersection.pages.sources.markSynced', 'Mark synced'),\n icon: RefreshCw,\n onSelect: () => void handleMarkSynced(source),\n },\n ]}\n />\n </TableCell>\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </div>\n\n {/* Mobile: stacked cards */}\n <ul className=\"space-y-3 md:hidden\">\n {sources.map((source) => {\n const TypeIcon = SOURCE_TYPE_ICON[source.type] ?? Radio;\n return (\n <li key={source.id}>\n <div\n role=\"button\"\n tabIndex={0}\n aria-label={tr('cosmicintersection.common.open', 'Open {{name}}', {\n name: source.name,\n })}\n onClick={() => navigate(`sources/${source.id}`)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n navigate(`sources/${source.id}`);\n }\n }}\n className=\"flex flex-col gap-3 rounded-card border border-border-default bg-bg-elevated p-4 outline-none transition-colors hover:bg-bg-sunken focus-visible:ring-2 focus-visible:ring-border-focus\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-start gap-2.5\">\n <span className=\"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-button bg-bg-sunken text-text-secondary\">\n <TypeIcon className=\"size-4\" aria-hidden=\"true\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"font-medium text-text-primary\">{source.name}</p>\n {source.description ? (\n <p className=\"mt-0.5 line-clamp-2 text-xs text-text-secondary\">\n {source.description}\n </p>\n ) : null}\n </div>\n </div>\n <div onClick={(event) => event.stopPropagation()}>\n <ActionMenu\n ariaLabel={tr(\n 'cosmicintersection.common.actionsFor',\n 'Actions for {{name}}',\n { name: source.name }\n )}\n entityName={source.name}\n onView={() => navigate(`sources/${source.id}`)}\n onEdit={() => navigate(`sources/${source.id}/edit`)}\n onDelete={() => handleArchive(source)}\n deleteLabel={tr(\n 'cosmicintersection.pages.sources.archive',\n 'Archive source'\n )}\n extraItems={[\n {\n key: 'mark-synced',\n label: tr('cosmicintersection.pages.sources.markSynced', 'Mark synced'),\n icon: RefreshCw,\n onSelect: () => void handleMarkSynced(source),\n },\n ]}\n />\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <Badge variant=\"secondary\">{formatEnumLabel(source.type)}</Badge>\n <StatusBadge status={source.status} />\n </div>\n\n {source.endpoint ? (\n <div onClick={(event) => event.stopPropagation()}>\n <SafeLink\n href={source.endpoint}\n className=\"block truncate text-sm text-text-primary underline-offset-2 hover:underline\"\n fallbackClassName=\"block truncate text-sm text-text-secondary\"\n >\n {source.endpoint}\n </SafeLink>\n </div>\n ) : null}\n\n <div className=\"flex items-center justify-between gap-2 text-xs text-text-secondary\">\n <span>{tr('cosmicintersection.common.lastSynced', 'Last synced')}</span>\n <span>{formatDate(source.lastSyncedAt)}</span>\n </div>\n\n {source.tags.length > 0 ? (\n <div className=\"flex flex-wrap gap-1\">\n {source.tags.slice(0, 4).map((tag) => (\n <Badge key={tag} variant=\"outline\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n {source.tags.length > 4 ? (\n <span className=\"text-xs text-text-secondary\">\n +{source.tags.length - 4}\n </span>\n ) : null}\n </div>\n ) : null}\n </div>\n </li>\n );\n })}\n </ul>\n\n <Pagination\n offset={pagination.offset}\n limit={pagination.limit}\n loadedCount={sources.length}\n hasMore={pageInfo?.hasMore ?? false}\n onPrev={pagination.prevPage}\n onNext={pagination.nextPage}\n itemLabel={tr('cosmicintersection.pages.sources.itemLabel', 'sources')}\n loading={loading}\n />\n </>\n );\n };\n\n return (\n <PageLayout\n title={tr('cosmicintersection.pages.sources.title', 'Data sources')}\n description={tr(\n 'cosmicintersection.pages.sources.description',\n 'Instruments, archives and feeds that supply raw data into your federation.'\n )}\n actions={newButton}\n >\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <div className=\"w-full max-w-[14rem]\">\n <Select value={typeFilter} onValueChange={setTypeFilter}>\n <SelectTrigger\n aria-label={tr('cosmicintersection.common.filterByType', 'Filter by type')}\n >\n <SelectValue placeholder={tr('cosmicintersection.common.allTypes', 'All types')} />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>\n {tr('cosmicintersection.common.allTypes', 'All types')}\n </SelectItem>\n {SOURCE_TYPES.map((type) => (\n <SelectItem key={type} value={type}>\n {formatEnumLabel(type)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"w-full max-w-[14rem]\">\n <Select value={statusFilter} onValueChange={setStatusFilter}>\n <SelectTrigger\n aria-label={tr('cosmicintersection.common.filterByStatus', 'Filter by status')}\n >\n <SelectValue\n placeholder={tr('cosmicintersection.common.allStatuses', 'All statuses')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL}>\n {tr('cosmicintersection.common.allStatuses', 'All statuses')}\n </SelectItem>\n {SOURCE_STATUSES.map((status) => (\n <SelectItem key={status} value={status}>\n {formatEnumLabel(status)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n {renderBody()}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useTr.d.ts","sourceRoot":"","sources":["../../../src/shared/hooks/useTr.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,KAAK,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;AAEzD,wBAAgB,KAAK,KAIjB,KAAK,MAAM,EACX,UAAU,MAAM,EAChB,SAAS,iBAAiB,KACzB,MAAM,CAaV"}
1
+ {"version":3,"file":"useTr.d.ts","sourceRoot":"","sources":["../../../src/shared/hooks/useTr.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,KAAK,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;AAEzD,wBAAgB,KAAK,KAGX,KAAK,MAAM,EAAE,UAAU,MAAM,EAAE,SAAS,iBAAiB,KAAG,MAAM,CAa3E"}
@@ -1 +1 @@
1
- {"version":3,"file":"useTr.js","names":[],"sources":["../../../src/shared/hooks/useTr.ts"],"sourcesContent":["import { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\n\n/**\n * CosmicIntersection MFE translation hook.\n *\n * The app shell owns the real I18nProvider and translation bundles.\n * This hook just wraps `useI18n` so callers can safely fall back to English\n * when a key is missing. The local dev harness wraps the MFE root with a\n * minimal I18nProvider so this hook never throws during isolated development.\n */\ntype TranslationParams = Record<string, string | number>;\n\nexport function useTr() {\n const { t } = useI18n();\n\n return (\n key: string,\n fallback: string,\n params?: TranslationParams,\n ): string => {\n const translated = t(key, params);\n // When the key is missing, the provider returns the key itself.\n if (translated === key) {\n // Apply interpolation to the fallback so callers still get params filled.\n if (!params) return fallback;\n return fallback.replace(/\\{\\{\\s*(\\w+)\\s*\\}\\}/g, (_, paramKey) => {\n const value = params[paramKey];\n return value === undefined ? `{{${paramKey}}}` : String(value);\n });\n }\n return translated;\n };\n}\n"],"mappings":";;AAYA,SAAgB,IAAQ;CACtB,IAAM,EAAE,MAAM,EAAQ;CAEtB,QACE,GACA,GACA,MACW;EACX,IAAM,IAAa,EAAE,GAAK,CAAM;EAUhC,OARI,MAAe,IAEZ,IACE,EAAS,QAAQ,yBAAyB,GAAG,MAAa;GAC/D,IAAM,IAAQ,EAAO;GACrB,OAAO,MAAU,KAAA,IAAY,KAAK,EAAS,MAAM,OAAO,CAAK;EAC/D,CAAC,IAJmB,IAMf;CACT;AACF"}
1
+ {"version":3,"file":"useTr.js","names":[],"sources":["../../../src/shared/hooks/useTr.ts"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\n/**\n * CosmicIntersection MFE translation hook.\n *\n * The app shell owns the real I18nProvider and translation bundles.\n * This hook just wraps `useI18n` so callers can safely fall back to English\n * when a key is missing. The local dev harness wraps the MFE root with a\n * minimal I18nProvider so this hook never throws during isolated development.\n */\ntype TranslationParams = Record<string, string | number>;\n\nexport function useTr() {\n const { t } = useI18n();\n\n return (key: string, fallback: string, params?: TranslationParams): string => {\n const translated = t(key, params);\n // When the key is missing, the provider returns the key itself.\n if (translated === key) {\n // Apply interpolation to the fallback so callers still get params filled.\n if (!params) return fallback;\n return fallback.replace(/\\{\\{\\s*(\\w+)\\s*\\}\\}/g, (_, paramKey) => {\n const value = params[paramKey];\n return value === undefined ? `{{${paramKey}}}` : String(value);\n });\n }\n return translated;\n };\n}\n"],"mappings":";;AAYA,SAAgB,IAAQ;CACtB,IAAM,EAAE,MAAM,EAAQ;CAEtB,QAAQ,GAAa,GAAkB,MAAuC;EAC5E,IAAM,IAAa,EAAE,GAAK,CAAM;EAUhC,OARI,MAAe,IAEZ,IACE,EAAS,QAAQ,yBAAyB,GAAG,MAAa;GAC/D,IAAM,IAAQ,EAAO;GACrB,OAAO,MAAU,KAAA,IAAY,KAAK,EAAS,MAAM,OAAO,CAAK;EAC/D,CAAC,IAJmB,IAMf;CACT;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-cosmicintersection",
3
- "version": "2026.830.1",
3
+ "version": "2026.830.2",
4
4
  "description": "Burdenoff CosmicIntersection Microfrontend",
5
5
  "type": "module",
6
6
  "files": [