@flanksource/clicky-ui 0.3.53 → 0.3.54
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.
- package/dist/data/TaskManager.cjs +39 -32
- package/dist/data/TaskManager.cjs.map +1 -1
- package/dist/data/TaskManager.d.ts +6 -1
- package/dist/data/TaskManager.d.ts.map +1 -1
- package/dist/data/TaskManager.js +39 -32
- package/dist/data/TaskManager.js.map +1 -1
- package/dist/data/clicky-export/ClickyExportDialog.cjs +4 -3
- package/dist/data/clicky-export/ClickyExportDialog.cjs.map +1 -1
- package/dist/data/clicky-export/ClickyExportDialog.d.ts +4 -1
- package/dist/data/clicky-export/ClickyExportDialog.d.ts.map +1 -1
- package/dist/data/clicky-export/ClickyExportDialog.js +4 -3
- package/dist/data/clicky-export/ClickyExportDialog.js.map +1 -1
- package/dist/data/version-info.cjs +3 -3
- package/dist/data/version-info.js +3 -3
- package/dist/hooks/use-task-run.cjs +22 -9
- package/dist/hooks/use-task-run.cjs.map +1 -1
- package/dist/hooks/use-task-run.d.ts +2 -0
- package/dist/hooks/use-task-run.d.ts.map +1 -1
- package/dist/hooks/use-task-run.js +22 -9
- package/dist/hooks/use-task-run.js.map +1 -1
- package/package.json +1 -1
|
@@ -110,7 +110,7 @@ var compactRunColumns = [
|
|
|
110
110
|
progressColumn,
|
|
111
111
|
startedColumn
|
|
112
112
|
];
|
|
113
|
-
function TaskManager({ basePath, kind, labels, pollMs, className, compact, selectedId, onSelectRun, extraTabs, headerExtra }) {
|
|
113
|
+
function TaskManager({ basePath, kind, labels, pollMs, className, compact, selectedId, onSelectRun, extraTabs, headerExtra, renderRunDetails, controlUrl, showTaskControls }) {
|
|
114
114
|
const [kindFilter, setKindFilter] = (0, react.useState)(kind ?? "");
|
|
115
115
|
const [statusFilter, setStatusFilter] = (0, react.useState)("");
|
|
116
116
|
const [labelFilter, setLabelFilter] = (0, react.useState)("");
|
|
@@ -263,7 +263,10 @@ function TaskManager({ basePath, kind, labels, pollMs, className, compact, selec
|
|
|
263
263
|
pollMs,
|
|
264
264
|
runsQueryKey,
|
|
265
265
|
extraTabs,
|
|
266
|
-
headerExtra
|
|
266
|
+
headerExtra,
|
|
267
|
+
renderRunDetails,
|
|
268
|
+
controlUrl,
|
|
269
|
+
showTaskControls
|
|
267
270
|
})
|
|
268
271
|
});
|
|
269
272
|
}
|
|
@@ -287,7 +290,7 @@ function applicationLabel(kind) {
|
|
|
287
290
|
other: "Other"
|
|
288
291
|
}[kind] ?? kind;
|
|
289
292
|
}
|
|
290
|
-
function ExpandedRun({ run, basePath, pollMs, runsQueryKey, extraTabs, headerExtra }) {
|
|
293
|
+
function ExpandedRun({ run, basePath, pollMs, runsQueryKey, extraTabs, headerExtra, renderRunDetails, controlUrl, showTaskControls }) {
|
|
291
294
|
const runId = run.id;
|
|
292
295
|
const { snapshots, status } = require_use_task_run.useTaskRun({
|
|
293
296
|
id: runId,
|
|
@@ -342,45 +345,49 @@ function ExpandedRun({ run, basePath, pollMs, runsQueryKey, extraTabs, headerExt
|
|
|
342
345
|
return mutation;
|
|
343
346
|
}, [controlMutation.mutateAsync, controlMutation.reset]);
|
|
344
347
|
const control = (action, group) => mutateControl({
|
|
345
|
-
url: `${apiBase}/tasks/${encodeURIComponent(runId)}/control`,
|
|
348
|
+
url: controlUrl?.(run, action) ?? `${apiBase}/tasks/${encodeURIComponent(runId)}/control`,
|
|
346
349
|
action,
|
|
347
350
|
target: group
|
|
348
351
|
});
|
|
349
352
|
const controlTask = (action, task) => mutateControl({
|
|
350
|
-
url: `${apiBase}/tasks/${encodeURIComponent(runId)}/tasks/${encodeURIComponent(task.id)}/control`,
|
|
353
|
+
url: controlUrl?.(run, action) ?? `${apiBase}/tasks/${encodeURIComponent(runId)}/tasks/${encodeURIComponent(task.id)}/control`,
|
|
351
354
|
action,
|
|
352
355
|
target: task
|
|
353
356
|
});
|
|
354
357
|
const progress = require_task_work_progress.taskWorkProgress(run);
|
|
355
358
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
356
359
|
className: "space-y-3 bg-muted/20 px-4 py-4",
|
|
357
|
-
children: [
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
360
|
+
children: [
|
|
361
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
362
|
+
className: "flex flex-wrap gap-x-5 gap-y-1 text-xs text-muted-foreground",
|
|
363
|
+
children: [
|
|
364
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [progress.completed, " completed"] }),
|
|
365
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [progress.running, " running"] }),
|
|
366
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [progress.failed, " failed"] }),
|
|
367
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [progress.counts.pending + progress.unstarted, " waiting"] }),
|
|
368
|
+
Object.entries(run.labels ?? {}).map(([key, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
369
|
+
className: "font-mono",
|
|
370
|
+
children: [
|
|
371
|
+
key,
|
|
372
|
+
"=",
|
|
373
|
+
value
|
|
374
|
+
]
|
|
375
|
+
}, key))
|
|
376
|
+
]
|
|
377
|
+
}),
|
|
378
|
+
renderRunDetails?.(run, snapshots),
|
|
379
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_TaskProgress.TaskProgress, {
|
|
380
|
+
snapshots,
|
|
381
|
+
compact: true,
|
|
382
|
+
connectionStatus: status,
|
|
383
|
+
initialShowAllTasks: true,
|
|
384
|
+
onControl: control,
|
|
385
|
+
...showTaskControls?.(run) !== false ? { onTaskControl: controlTask } : {},
|
|
386
|
+
metricsBaseUrl: `${apiBase}/tasks/metrics/`,
|
|
387
|
+
...extraTabs ? { extraTabs } : {},
|
|
388
|
+
...headerExtra ? { headerExtra } : {}
|
|
389
|
+
})
|
|
390
|
+
]
|
|
384
391
|
});
|
|
385
392
|
}
|
|
386
393
|
async function postTaskControl({ url, action, target }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TaskManager.cjs","names":["taskWorkProgress","ProgressBar","taskSegments","cn","taskStatusColor","Icon","taskStatusIcon","useState","useQueryClient","useMemo","splitTagToken","useTaskRuns","taskQueryKeys","hashKey","DataTable","useResourceClock","formatDuration","useTaskRun","useRef","useMutation","useCallback","TaskProgress"],"sources":["../../src/data/TaskManager.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { hashKey, useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport type { QueryKey } from \"@tanstack/react-query\";\nimport { cn } from \"../lib/utils\";\nimport { formatDuration } from \"../lib/format\";\nimport { useResourceClock } from \"../hooks/use-resource-clock\";\nimport { useTaskRun, useTaskRuns } from \"../hooks/use-task-run\";\nimport type { FilterBarFilter } from \"../components/FilterBar\";\nimport { Icon } from \"./Icon\";\nimport { DataTable, type DataTableColumn } from \"./DataTable\";\nimport { splitTagToken } from \"./cells/tag-utils\";\nimport { ProgressBar } from \"./ProgressBar\";\nimport { TaskProgress } from \"./TaskProgress\";\nimport { taskWorkProgress } from \"./task-work-progress\";\nimport type { TaskControlAction, TaskRunMeta, TaskSnapshot } from \"./TaskSnapshot\";\nimport type { TaskHeaderExtra } from \"./TaskHeaderExtras\";\nimport type { TaskExtraTabs } from \"./TaskStreamTabs\";\nimport { taskQueryKeys } from \"./task-query-keys\";\nimport { taskSegments, taskStatusColor, taskStatusIcon } from \"./task-status\";\n\n// TaskManager is the generic clicky-ui task-manager view: it lists every run\n// (TaskRunMeta) from GET {basePath}/tasks with kind/status filters, and expands\n// each row into a live <TaskProgress> for that run id.\n//\n// Requires a react-query QueryClientProvider in the host app (like\n// TimeseriesPanel and CacheBrowser): control actions run through useMutation,\n// and the listing is mirrored into the query cache so sibling views can read it.\n\nexport interface TaskManagerProps {\n /** Base path of the clicky task API, e.g. \"/api/v1\". */\n basePath?: string;\n /** Restrict the listing to a single kind (also hides the kind filter). */\n kind?: string;\n /** Restrict the listing to runs whose labels contain every key/value pair. */\n labels?: Record<string, string>;\n pollMs?: number;\n className?: string;\n /** Keep the run columns usable in a narrow panel. */\n compact?: boolean;\n /**\n * Currently-selected (expanded) run id, e.g. driven from the URL. When set,\n * the matching run renders expanded and selection is controlled — otherwise\n * each row manages its own expand/collapse state.\n */\n selectedId?: string;\n /**\n * Called when a run row is toggled: the new selection id, or null when the\n * open row is collapsed. Pair with selectedId for URL-driven deep links.\n */\n onSelectRun?: (id: string | null) => void;\n /**\n * Contributes host-owned output panes to each task, alongside its own\n * stdout/stderr — an agent's session transcript, say.\n */\n extraTabs?: TaskExtraTabs;\n /**\n * Replaces the content beside a task's name in its header row. Returning null\n * for a task falls back to the default metadata chips.\n */\n headerExtra?: TaskHeaderExtra;\n}\n\ntype RunTableRow = Record<string, unknown> & {\n id: string;\n name: string;\n kind: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n labels?: Record<string, string>;\n run: TaskRunMeta;\n};\n\nconst progressColumn: DataTableColumn<RunTableRow> = {\n key: \"progress\", label: \"Progress\", minWidth: 180,\n accessor: (row) => taskWorkProgress(row.run).done,\n render: (_, row) => {\n const progress = taskWorkProgress(row.run);\n return (\n <div className=\"min-w-36 space-y-1\">\n <span className=\"text-xs tabular-nums text-muted-foreground\">{progress.label}</span>\n {progress.total > 0 && <ProgressBar segments={taskSegments(progress.counts)} total={progress.total} height=\"h-1.5\" />}\n </div>\n );\n },\n};\nconst startedColumn: DataTableColumn<RunTableRow> = { key: \"startedAt\", label: \"Started\", kind: \"timestamp\", sortable: true };\nconst durationColumn: DataTableColumn<RunTableRow> = {\n key: \"duration\", label: \"Duration\", sortable: true,\n accessor: (row) => row.startedAt ? Date.parse(row.finishedAt ?? new Date().toISOString()) - Date.parse(row.startedAt) : null,\n render: (_, row) => <RunDuration startedAt={row.startedAt} finishedAt={row.finishedAt} />,\n};\nconst runColumns: DataTableColumn<RunTableRow>[] = [\n { key: \"name\", label: \"Run\", sortable: true, grow: true, minWidth: 220 },\n {\n key: \"status\", label: \"Status\", sortable: true,\n render: (_, row) => (\n <span className={cn(\"inline-flex items-center gap-1.5 capitalize\", taskStatusColor(row.status))}>\n <Icon icon={taskStatusIcon(row.status)} {...(row.status === \"running\" ? { className: \"animate-spin\" } : {})} />\n {row.status}\n </span>\n ),\n },\n durationColumn,\n progressColumn,\n { key: \"labels\", label: \"Labels\", kind: \"tags\", tags: { maxVisible: 2 }, minWidth: 160 },\n startedColumn,\n { key: \"finishedAt\", label: \"Finished\", kind: \"timestamp\", sortable: true },\n];\nconst compactRunColumns: DataTableColumn<RunTableRow>[] = [\n {\n key: \"name\", label: \"Run\", sortable: true, grow: true, minWidth: 220,\n render: (_, row) => (\n <div className=\"space-y-0.5\">\n <div className=\"font-medium\">{row.name}</div>\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span>{applicationLabel(row.kind)}</span>\n <span className={cn(\"inline-flex items-center gap-1 capitalize\", taskStatusColor(row.status))}>\n <Icon icon={taskStatusIcon(row.status)} />{row.status}\n </span>\n </div>\n </div>\n ),\n },\n progressColumn,\n startedColumn,\n];\n\nexport function TaskManager({\n basePath,\n kind,\n labels,\n pollMs,\n className,\n compact,\n selectedId,\n onSelectRun,\n extraTabs,\n headerExtra,\n}: TaskManagerProps) {\n const [kindFilter, setKindFilter] = useState<string>(kind ?? \"\");\n const [statusFilter, setStatusFilter] = useState<string>(\"\");\n const [labelFilter, setLabelFilter] = useState(\"\");\n const [filterOptions, setFilterOptions] = useState({ kinds: [] as string[], labels: [] as string[] });\n const [localSelectedId, setLocalSelectedId] = useState<string | null>(null);\n const queryClient = useQueryClient();\n const apiBase = basePath ?? \"/api/v1\";\n const activeKind = kind ?? (kindFilter || undefined);\n const activeLabels = useMemo(() => {\n if (!labelFilter) return labels;\n const selected = splitTagToken(labelFilter);\n return { ...labels, [selected.key]: selected.value };\n }, [labels, labelFilter]);\n const { runs, runsKey, status } = useTaskRuns({\n basePath: apiBase,\n kind: activeKind,\n labels: activeLabels,\n status: statusFilter || undefined,\n pollMs,\n });\n const runsQueryKey = useMemo(\n () => taskQueryKeys.runs({\n basePath: apiBase,\n kind: activeKind,\n status: statusFilter || undefined,\n labels: activeLabels,\n }),\n [activeKind, apiBase, activeLabels, statusFilter],\n );\n\n // Changing a filter re-subscribes the transport but leaves the previous\n // listing on screen until the next SSE/poll frame. Publishing that listing\n // under the new key would hand cache consumers rows that do not match their\n // filters, so mirror only once the delivered listing's own key agrees.\n useEffect(() => {\n if (!runsKey || hashKey(runsKey) !== hashKey(runsQueryKey)) return;\n queryClient.setQueryData(runsQueryKey, runs);\n }, [queryClient, runs, runsKey, runsQueryKey]);\n\n useEffect(() => {\n setFilterOptions((previous) => {\n const kinds = [...new Set([...previous.kinds, ...runs.flatMap((run) => run.kind ? [run.kind] : [])])].sort();\n const labels = [...new Set([...previous.labels, ...runs.flatMap((run) => Object.entries(run.labels ?? {}).map(([key, value]) => `${key}=${value}`))])].sort();\n if (kinds.length === previous.kinds.length && labels.length === previous.labels.length) return previous;\n return { kinds, labels };\n });\n }, [runs]);\n const filters: FilterBarFilter[] = [\n ...(!kind ? [{ key: \"kind\", kind: \"enum\" as const, label: \"Kind\", placeholder: \"All kinds\", value: kindFilter, options: filterOptions.kinds.map((value) => ({ value, label: applicationLabel(value) })), onChange: setKindFilter }] : []),\n { key: \"status\", kind: \"enum\", label: \"Status\", placeholder: \"Any status\", value: statusFilter, options: [\n { value: \"running\", label: \"Running\" },\n { value: \"pending\", label: \"Pending\" },\n { value: \"success\", label: \"Success\" },\n { value: \"failed\", label: \"Failed\" },\n { value: \"warning\", label: \"Warning\" },\n { value: \"canceled\", label: \"Canceled\" },\n ], onChange: setStatusFilter },\n { key: \"labels\", kind: \"enum\", label: \"Labels\", placeholder: \"Any label\", value: labelFilter,\n options: filterOptions.labels.filter((token) => !(splitTagToken(token).key in (labels ?? {}))).map((value) => ({ value })),\n onChange: setLabelFilter },\n ];\n const rows = useMemo<RunTableRow[]>(() => runs.map((run) => ({\n id: run.id,\n name: run.name,\n kind: run.kind || \"other\",\n status: run.status,\n ...(run.startedAt ? { startedAt: run.startedAt } : {}),\n ...(run.finishedAt ? { finishedAt: run.finishedAt } : {}),\n ...(run.labels ? { labels: run.labels } : {}),\n run,\n })), [runs]);\n const openId = onSelectRun ? selectedId : localSelectedId;\n\n return (\n <DataTable\n {...(className ? { className } : {})}\n data={rows}\n columns={compact ? compactRunColumns : runColumns}\n emptyMessage=\"No background tasks.\"\n externalFilters={filters}\n filterBarProps={{ trailing: <span className=\"text-xs text-muted-foreground\">{status}</span> }}\n getRowId={(row) => row.id}\n defaultSort={{ key: \"startedAt\", dir: \"desc\" }}\n expandedRowIds={openId ? [openId] : []}\n onExpandedRowIdsChange={(ids) => {\n const next = ids.find((id) => id !== openId) ?? null;\n if (onSelectRun) onSelectRun(next);\n else setLocalSelectedId(next);\n }}\n renderExpandedRow={(row) => (\n <ExpandedRun\n run={row.run}\n basePath={basePath}\n pollMs={pollMs}\n runsQueryKey={runsQueryKey}\n extraTabs={extraTabs}\n headerExtra={headerExtra}\n />\n )}\n />\n );\n}\n\nfunction RunDuration({ startedAt, finishedAt }: { startedAt: string | undefined; finishedAt: string | undefined }) {\n const now = useResourceClock(!!startedAt && !finishedAt);\n if (!startedAt) return <span className=\"text-muted-foreground\">—</span>;\n return <span className=\"tabular-nums\">{formatDuration(Math.max(0, (finishedAt ? Date.parse(finishedAt) : now) - Date.parse(startedAt)))}</span>;\n}\n\nfunction applicationLabel(kind: string): string {\n const names: Record<string, string> = {\n makerprint: \"MakerPrint\",\n openscad: \"OpenSCAD\",\n orca: \"Orca\",\n snapmaker: \"Snapmaker\",\n other: \"Other\",\n };\n return names[kind] ?? kind;\n}\n\nfunction ExpandedRun({\n run,\n basePath,\n pollMs,\n runsQueryKey,\n extraTabs,\n headerExtra,\n}: {\n run: TaskRunMeta;\n basePath: string | undefined;\n pollMs: number | undefined;\n runsQueryKey: QueryKey;\n extraTabs: TaskExtraTabs | undefined;\n headerExtra: TaskHeaderExtra | undefined;\n}) {\n const runId = run.id;\n const { snapshots, status } = useTaskRun({ id: runId, basePath, pollMs });\n const apiBase = basePath ?? \"/api/v1\";\n const queryClient = useQueryClient();\n const pendingControls = useRef(new Map<string, Promise<void>>());\n const cachedSnapshots = useRef<TaskSnapshot[] | undefined>(undefined);\n const runQueryKey = useMemo(\n () => taskQueryKeys.run({ basePath: apiBase, runId }),\n [apiBase, runId],\n );\n const controlMutation = useMutation({\n mutationKey: taskQueryKeys.control({ basePath: apiBase, runId }),\n mutationFn: postTaskControl,\n onSuccess: async () => {\n await Promise.all([\n queryClient.invalidateQueries({ queryKey: runsQueryKey, exact: true, refetchType: \"none\" }),\n queryClient.invalidateQueries({ queryKey: runQueryKey, exact: true, refetchType: \"none\" }),\n ]);\n },\n });\n\n useEffect(() => {\n const previous = cachedSnapshots.current;\n if (previous?.length === snapshots.length && previous.every((snapshot, index) => snapshot === snapshots[index])) {\n return;\n }\n cachedSnapshots.current = snapshots;\n queryClient.setQueryData(runQueryKey, snapshots);\n }, [queryClient, runQueryKey, snapshots]);\n\n const mutateControl = useCallback((request: TaskControlRequest) => {\n const key = `${request.url}:${request.action}`;\n const pending = pendingControls.current.get(key);\n if (pending) return pending;\n controlMutation.reset();\n const mutation = controlMutation.mutateAsync(request).finally(() => {\n if (pendingControls.current.get(key) === mutation) pendingControls.current.delete(key);\n });\n pendingControls.current.set(key, mutation);\n return mutation;\n }, [controlMutation.mutateAsync, controlMutation.reset]);\n const control = (action: TaskControlAction, group: TaskSnapshot) =>\n mutateControl({\n url: `${apiBase}/tasks/${encodeURIComponent(runId)}/control`,\n action,\n target: group,\n });\n const controlTask = (action: TaskControlAction, task: TaskSnapshot) =>\n mutateControl({\n url: `${apiBase}/tasks/${encodeURIComponent(runId)}/tasks/${encodeURIComponent(task.id)}/control`,\n action,\n target: task,\n });\n const progress = taskWorkProgress(run);\n return (\n <div className=\"space-y-3 bg-muted/20 px-4 py-4\">\n <div className=\"flex flex-wrap gap-x-5 gap-y-1 text-xs text-muted-foreground\">\n <span>{progress.completed} completed</span>\n <span>{progress.running} running</span>\n <span>{progress.failed} failed</span>\n <span>{progress.counts.pending + progress.unstarted} waiting</span>\n {Object.entries(run.labels ?? {}).map(([key, value]) => (\n <span key={key} className=\"font-mono\">{key}={value}</span>\n ))}\n </div>\n <TaskProgress\n snapshots={snapshots}\n compact\n connectionStatus={status}\n initialShowAllTasks\n onControl={control}\n onTaskControl={controlTask}\n metricsBaseUrl={`${apiBase}/tasks/metrics/`}\n {...(extraTabs ? { extraTabs } : {})}\n {...(headerExtra ? { headerExtra } : {})}\n />\n </div>\n );\n}\n\ninterface TaskControlRequest {\n url: string;\n action: TaskControlAction;\n target: TaskSnapshot;\n}\n\nasync function postTaskControl({ url, action, target }: TaskControlRequest) {\n const context = `Failed to ${action} ${target.type} \"${target.name}\"`;\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ action }),\n });\n } catch (cause) {\n throw new Error(`${context}: ${cause instanceof Error ? cause.message : \"request failed\"}`, { cause });\n }\n if (!response.ok) {\n const detail = (await response.text()).trim() || `HTTP ${response.status}`;\n throw new Error(`${context}: ${detail}`);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TaskManager.cjs","names":["taskWorkProgress","ProgressBar","taskSegments","cn","taskStatusColor","Icon","taskStatusIcon","useState","useQueryClient","useMemo","splitTagToken","useTaskRuns","taskQueryKeys","hashKey","DataTable","useResourceClock","formatDuration","useTaskRun","useRef","useMutation","useCallback","TaskProgress"],"sources":["../../src/data/TaskManager.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { hashKey, useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport type { QueryKey } from \"@tanstack/react-query\";\nimport { cn } from \"../lib/utils\";\nimport { formatDuration } from \"../lib/format\";\nimport { useResourceClock } from \"../hooks/use-resource-clock\";\nimport { useTaskRun, useTaskRuns } from \"../hooks/use-task-run\";\nimport type { FilterBarFilter } from \"../components/FilterBar\";\nimport { Icon } from \"./Icon\";\nimport { DataTable, type DataTableColumn } from \"./DataTable\";\nimport { splitTagToken } from \"./cells/tag-utils\";\nimport { ProgressBar } from \"./ProgressBar\";\nimport { TaskProgress } from \"./TaskProgress\";\nimport { taskWorkProgress } from \"./task-work-progress\";\nimport type { TaskControlAction, TaskRunMeta, TaskSnapshot } from \"./TaskSnapshot\";\nimport type { TaskHeaderExtra } from \"./TaskHeaderExtras\";\nimport type { TaskExtraTabs } from \"./TaskStreamTabs\";\nimport { taskQueryKeys } from \"./task-query-keys\";\nimport { taskSegments, taskStatusColor, taskStatusIcon } from \"./task-status\";\n\n// TaskManager is the generic clicky-ui task-manager view: it lists every run\n// (TaskRunMeta) from GET {basePath}/tasks with kind/status filters, and expands\n// each row into a live <TaskProgress> for that run id.\n//\n// Requires a react-query QueryClientProvider in the host app (like\n// TimeseriesPanel and CacheBrowser): control actions run through useMutation,\n// and the listing is mirrored into the query cache so sibling views can read it.\n\nexport interface TaskManagerProps {\n /** Base path of the clicky task API, e.g. \"/api/v1\". */\n basePath?: string;\n /** Restrict the listing to a single kind (also hides the kind filter). */\n kind?: string;\n /** Restrict the listing to runs whose labels contain every key/value pair. */\n labels?: Record<string, string>;\n pollMs?: number;\n className?: string;\n /** Keep the run columns usable in a narrow panel. */\n compact?: boolean;\n /**\n * Currently-selected (expanded) run id, e.g. driven from the URL. When set,\n * the matching run renders expanded and selection is controlled — otherwise\n * each row manages its own expand/collapse state.\n */\n selectedId?: string;\n /**\n * Called when a run row is toggled: the new selection id, or null when the\n * open row is collapsed. Pair with selectedId for URL-driven deep links.\n */\n onSelectRun?: (id: string | null) => void;\n /**\n * Contributes host-owned output panes to each task, alongside its own\n * stdout/stderr — an agent's session transcript, say.\n */\n extraTabs?: TaskExtraTabs;\n /**\n * Replaces the content beside a task's name in its header row. Returning null\n * for a task falls back to the default metadata chips.\n */\n headerExtra?: TaskHeaderExtra;\n renderRunDetails?: (run: TaskRunMeta, snapshots: TaskSnapshot[]) => ReactNode;\n controlUrl?: (run: TaskRunMeta, action: TaskControlAction) => string | undefined;\n showTaskControls?: (run: TaskRunMeta) => boolean;\n}\n\ntype RunTableRow = Record<string, unknown> & {\n id: string;\n name: string;\n kind: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n labels?: Record<string, string>;\n run: TaskRunMeta;\n};\n\nconst progressColumn: DataTableColumn<RunTableRow> = {\n key: \"progress\", label: \"Progress\", minWidth: 180,\n accessor: (row) => taskWorkProgress(row.run).done,\n render: (_, row) => {\n const progress = taskWorkProgress(row.run);\n return (\n <div className=\"min-w-36 space-y-1\">\n <span className=\"text-xs tabular-nums text-muted-foreground\">{progress.label}</span>\n {progress.total > 0 && <ProgressBar segments={taskSegments(progress.counts)} total={progress.total} height=\"h-1.5\" />}\n </div>\n );\n },\n};\nconst startedColumn: DataTableColumn<RunTableRow> = { key: \"startedAt\", label: \"Started\", kind: \"timestamp\", sortable: true };\nconst durationColumn: DataTableColumn<RunTableRow> = {\n key: \"duration\", label: \"Duration\", sortable: true,\n accessor: (row) => row.startedAt ? Date.parse(row.finishedAt ?? new Date().toISOString()) - Date.parse(row.startedAt) : null,\n render: (_, row) => <RunDuration startedAt={row.startedAt} finishedAt={row.finishedAt} />,\n};\nconst runColumns: DataTableColumn<RunTableRow>[] = [\n { key: \"name\", label: \"Run\", sortable: true, grow: true, minWidth: 220 },\n {\n key: \"status\", label: \"Status\", sortable: true,\n render: (_, row) => (\n <span className={cn(\"inline-flex items-center gap-1.5 capitalize\", taskStatusColor(row.status))}>\n <Icon icon={taskStatusIcon(row.status)} {...(row.status === \"running\" ? { className: \"animate-spin\" } : {})} />\n {row.status}\n </span>\n ),\n },\n durationColumn,\n progressColumn,\n { key: \"labels\", label: \"Labels\", kind: \"tags\", tags: { maxVisible: 2 }, minWidth: 160 },\n startedColumn,\n { key: \"finishedAt\", label: \"Finished\", kind: \"timestamp\", sortable: true },\n];\nconst compactRunColumns: DataTableColumn<RunTableRow>[] = [\n {\n key: \"name\", label: \"Run\", sortable: true, grow: true, minWidth: 220,\n render: (_, row) => (\n <div className=\"space-y-0.5\">\n <div className=\"font-medium\">{row.name}</div>\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span>{applicationLabel(row.kind)}</span>\n <span className={cn(\"inline-flex items-center gap-1 capitalize\", taskStatusColor(row.status))}>\n <Icon icon={taskStatusIcon(row.status)} />{row.status}\n </span>\n </div>\n </div>\n ),\n },\n progressColumn,\n startedColumn,\n];\n\nexport function TaskManager({\n basePath,\n kind,\n labels,\n pollMs,\n className,\n compact,\n selectedId,\n onSelectRun,\n extraTabs,\n headerExtra,\n renderRunDetails,\n controlUrl,\n showTaskControls,\n}: TaskManagerProps) {\n const [kindFilter, setKindFilter] = useState<string>(kind ?? \"\");\n const [statusFilter, setStatusFilter] = useState<string>(\"\");\n const [labelFilter, setLabelFilter] = useState(\"\");\n const [filterOptions, setFilterOptions] = useState({ kinds: [] as string[], labels: [] as string[] });\n const [localSelectedId, setLocalSelectedId] = useState<string | null>(null);\n const queryClient = useQueryClient();\n const apiBase = basePath ?? \"/api/v1\";\n const activeKind = kind ?? (kindFilter || undefined);\n const activeLabels = useMemo(() => {\n if (!labelFilter) return labels;\n const selected = splitTagToken(labelFilter);\n return { ...labels, [selected.key]: selected.value };\n }, [labels, labelFilter]);\n const { runs, runsKey, status } = useTaskRuns({\n basePath: apiBase,\n kind: activeKind,\n labels: activeLabels,\n status: statusFilter || undefined,\n pollMs,\n });\n const runsQueryKey = useMemo(\n () => taskQueryKeys.runs({\n basePath: apiBase,\n kind: activeKind,\n status: statusFilter || undefined,\n labels: activeLabels,\n }),\n [activeKind, apiBase, activeLabels, statusFilter],\n );\n\n // Changing a filter re-subscribes the transport but leaves the previous\n // listing on screen until the next SSE/poll frame. Publishing that listing\n // under the new key would hand cache consumers rows that do not match their\n // filters, so mirror only once the delivered listing's own key agrees.\n useEffect(() => {\n if (!runsKey || hashKey(runsKey) !== hashKey(runsQueryKey)) return;\n queryClient.setQueryData(runsQueryKey, runs);\n }, [queryClient, runs, runsKey, runsQueryKey]);\n\n useEffect(() => {\n setFilterOptions((previous) => {\n const kinds = [...new Set([...previous.kinds, ...runs.flatMap((run) => run.kind ? [run.kind] : [])])].sort();\n const labels = [...new Set([...previous.labels, ...runs.flatMap((run) => Object.entries(run.labels ?? {}).map(([key, value]) => `${key}=${value}`))])].sort();\n if (kinds.length === previous.kinds.length && labels.length === previous.labels.length) return previous;\n return { kinds, labels };\n });\n }, [runs]);\n const filters: FilterBarFilter[] = [\n ...(!kind ? [{ key: \"kind\", kind: \"enum\" as const, label: \"Kind\", placeholder: \"All kinds\", value: kindFilter, options: filterOptions.kinds.map((value) => ({ value, label: applicationLabel(value) })), onChange: setKindFilter }] : []),\n { key: \"status\", kind: \"enum\", label: \"Status\", placeholder: \"Any status\", value: statusFilter, options: [\n { value: \"running\", label: \"Running\" },\n { value: \"pending\", label: \"Pending\" },\n { value: \"success\", label: \"Success\" },\n { value: \"failed\", label: \"Failed\" },\n { value: \"warning\", label: \"Warning\" },\n { value: \"canceled\", label: \"Canceled\" },\n ], onChange: setStatusFilter },\n { key: \"labels\", kind: \"enum\", label: \"Labels\", placeholder: \"Any label\", value: labelFilter,\n options: filterOptions.labels.filter((token) => !(splitTagToken(token).key in (labels ?? {}))).map((value) => ({ value })),\n onChange: setLabelFilter },\n ];\n const rows = useMemo<RunTableRow[]>(() => runs.map((run) => ({\n id: run.id,\n name: run.name,\n kind: run.kind || \"other\",\n status: run.status,\n ...(run.startedAt ? { startedAt: run.startedAt } : {}),\n ...(run.finishedAt ? { finishedAt: run.finishedAt } : {}),\n ...(run.labels ? { labels: run.labels } : {}),\n run,\n })), [runs]);\n const openId = onSelectRun ? selectedId : localSelectedId;\n\n return (\n <DataTable\n {...(className ? { className } : {})}\n data={rows}\n columns={compact ? compactRunColumns : runColumns}\n emptyMessage=\"No background tasks.\"\n externalFilters={filters}\n filterBarProps={{ trailing: <span className=\"text-xs text-muted-foreground\">{status}</span> }}\n getRowId={(row) => row.id}\n defaultSort={{ key: \"startedAt\", dir: \"desc\" }}\n expandedRowIds={openId ? [openId] : []}\n onExpandedRowIdsChange={(ids) => {\n const next = ids.find((id) => id !== openId) ?? null;\n if (onSelectRun) onSelectRun(next);\n else setLocalSelectedId(next);\n }}\n renderExpandedRow={(row) => (\n <ExpandedRun\n run={row.run}\n basePath={basePath}\n pollMs={pollMs}\n runsQueryKey={runsQueryKey}\n extraTabs={extraTabs}\n headerExtra={headerExtra}\n renderRunDetails={renderRunDetails}\n controlUrl={controlUrl}\n showTaskControls={showTaskControls}\n />\n )}\n />\n );\n}\n\nfunction RunDuration({ startedAt, finishedAt }: { startedAt: string | undefined; finishedAt: string | undefined }) {\n const now = useResourceClock(!!startedAt && !finishedAt);\n if (!startedAt) return <span className=\"text-muted-foreground\">—</span>;\n return <span className=\"tabular-nums\">{formatDuration(Math.max(0, (finishedAt ? Date.parse(finishedAt) : now) - Date.parse(startedAt)))}</span>;\n}\n\nfunction applicationLabel(kind: string): string {\n const names: Record<string, string> = {\n makerprint: \"MakerPrint\",\n openscad: \"OpenSCAD\",\n orca: \"Orca\",\n snapmaker: \"Snapmaker\",\n other: \"Other\",\n };\n return names[kind] ?? kind;\n}\n\nfunction ExpandedRun({\n run,\n basePath,\n pollMs,\n runsQueryKey,\n extraTabs,\n headerExtra,\n renderRunDetails,\n controlUrl,\n showTaskControls,\n}: {\n run: TaskRunMeta;\n basePath: string | undefined;\n pollMs: number | undefined;\n runsQueryKey: QueryKey;\n extraTabs: TaskExtraTabs | undefined;\n headerExtra: TaskHeaderExtra | undefined;\n renderRunDetails: TaskManagerProps[\"renderRunDetails\"];\n controlUrl: TaskManagerProps[\"controlUrl\"];\n showTaskControls: TaskManagerProps[\"showTaskControls\"];\n}) {\n const runId = run.id;\n const { snapshots, status } = useTaskRun({ id: runId, basePath, pollMs });\n const apiBase = basePath ?? \"/api/v1\";\n const queryClient = useQueryClient();\n const pendingControls = useRef(new Map<string, Promise<void>>());\n const cachedSnapshots = useRef<TaskSnapshot[] | undefined>(undefined);\n const runQueryKey = useMemo(\n () => taskQueryKeys.run({ basePath: apiBase, runId }),\n [apiBase, runId],\n );\n const controlMutation = useMutation({\n mutationKey: taskQueryKeys.control({ basePath: apiBase, runId }),\n mutationFn: postTaskControl,\n onSuccess: async () => {\n await Promise.all([\n queryClient.invalidateQueries({ queryKey: runsQueryKey, exact: true, refetchType: \"none\" }),\n queryClient.invalidateQueries({ queryKey: runQueryKey, exact: true, refetchType: \"none\" }),\n ]);\n },\n });\n\n useEffect(() => {\n const previous = cachedSnapshots.current;\n if (previous?.length === snapshots.length && previous.every((snapshot, index) => snapshot === snapshots[index])) {\n return;\n }\n cachedSnapshots.current = snapshots;\n queryClient.setQueryData(runQueryKey, snapshots);\n }, [queryClient, runQueryKey, snapshots]);\n\n const mutateControl = useCallback((request: TaskControlRequest) => {\n const key = `${request.url}:${request.action}`;\n const pending = pendingControls.current.get(key);\n if (pending) return pending;\n controlMutation.reset();\n const mutation = controlMutation.mutateAsync(request).finally(() => {\n if (pendingControls.current.get(key) === mutation) pendingControls.current.delete(key);\n });\n pendingControls.current.set(key, mutation);\n return mutation;\n }, [controlMutation.mutateAsync, controlMutation.reset]);\n const control = (action: TaskControlAction, group: TaskSnapshot) =>\n mutateControl({\n url: controlUrl?.(run, action) ?? `${apiBase}/tasks/${encodeURIComponent(runId)}/control`,\n action,\n target: group,\n });\n const controlTask = (action: TaskControlAction, task: TaskSnapshot) =>\n mutateControl({\n url: controlUrl?.(run, action) ?? `${apiBase}/tasks/${encodeURIComponent(runId)}/tasks/${encodeURIComponent(task.id)}/control`,\n action,\n target: task,\n });\n const progress = taskWorkProgress(run);\n return (\n <div className=\"space-y-3 bg-muted/20 px-4 py-4\">\n <div className=\"flex flex-wrap gap-x-5 gap-y-1 text-xs text-muted-foreground\">\n <span>{progress.completed} completed</span>\n <span>{progress.running} running</span>\n <span>{progress.failed} failed</span>\n <span>{progress.counts.pending + progress.unstarted} waiting</span>\n {Object.entries(run.labels ?? {}).map(([key, value]) => (\n <span key={key} className=\"font-mono\">{key}={value}</span>\n ))}\n </div>\n {renderRunDetails?.(run, snapshots)}\n <TaskProgress\n snapshots={snapshots}\n compact\n connectionStatus={status}\n initialShowAllTasks\n onControl={control}\n {...(showTaskControls?.(run) !== false ? { onTaskControl: controlTask } : {})}\n metricsBaseUrl={`${apiBase}/tasks/metrics/`}\n {...(extraTabs ? { extraTabs } : {})}\n {...(headerExtra ? { headerExtra } : {})}\n />\n </div>\n );\n}\n\ninterface TaskControlRequest {\n url: string;\n action: TaskControlAction;\n target: TaskSnapshot;\n}\n\nasync function postTaskControl({ url, action, target }: TaskControlRequest) {\n const context = `Failed to ${action} ${target.type} \"${target.name}\"`;\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ action }),\n });\n } catch (cause) {\n throw new Error(`${context}: ${cause instanceof Error ? cause.message : \"request failed\"}`, { cause });\n }\n if (!response.ok) {\n const detail = (await response.text()).trim() || `HTTP ${response.status}`;\n throw new Error(`${context}: ${detail}`);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA6EA,IAAM,iBAA+C;CACnD,KAAK;CAAY,OAAO;CAAY,UAAU;CAC9C,WAAW,QAAQA,2BAAAA,iBAAiB,IAAI,GAAG,CAAC,CAAC;CAC7C,SAAS,GAAG,QAAQ;EAClB,MAAM,WAAWA,2BAAAA,iBAAiB,IAAI,GAAG;EACzC,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;IAAM,WAAU;IAA8C,UAAA,SAAS;GAAY,CAAA,GAClF,SAAS,QAAQ,KAAK,iBAAA,GAAA,kBAAA,IAAA,CAACC,oBAAAA,aAAD;IAAa,UAAUC,oBAAAA,aAAa,SAAS,MAAM;IAAG,OAAO,SAAS;IAAO,QAAO;GAAS,CAAA,CACjH;;CAET;AACF;AACA,IAAM,gBAA8C;CAAE,KAAK;CAAa,OAAO;CAAW,MAAM;CAAa,UAAU;AAAK;AAM5H,IAAM,aAA6C;CACjD;EAAE,KAAK;EAAQ,OAAO;EAAO,UAAU;EAAM,MAAM;EAAM,UAAU;CAAI;CACvE;EACE,KAAK;EAAU,OAAO;EAAU,UAAU;EAC1C,SAAS,GAAG,QACV,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;GAAM,WAAWC,cAAAA,GAAG,+CAA+CC,oBAAAA,gBAAgB,IAAI,MAAM,CAAC;GAA9F,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAACC,aAAAA,MAAD;IAAM,MAAMC,oBAAAA,eAAe,IAAI,MAAM;IAAG,GAAK,IAAI,WAAW,YAAY,EAAE,WAAW,eAAe,IAAI,CAAC;GAAK,CAAA,GAC7G,IAAI,MACD;;CAEV;CACA;EAfA,KAAK;EAAY,OAAO;EAAY,UAAU;EAC9C,WAAW,QAAQ,IAAI,YAAY,KAAK,MAAM,IAAI,+BAAc,IAAI,KAAK,EAAA,CAAE,YAAY,CAAC,IAAI,KAAK,MAAM,IAAI,SAAS,IAAI;EACxH,SAAS,GAAG,QAAQ,iBAAA,GAAA,kBAAA,IAAA,CAAC,aAAD;GAAa,WAAW,IAAI;GAAW,YAAY,IAAI;EAAa,CAAA;CAaxF;CACA;CACA;EAAE,KAAK;EAAU,OAAO;EAAU,MAAM;EAAQ,MAAM,EAAE,YAAY,EAAE;EAAG,UAAU;CAAI;CACvF;CACA;EAAE,KAAK;EAAc,OAAO;EAAY,MAAM;EAAa,UAAU;CAAK;AAC5E;AACA,IAAM,oBAAoD;CACxD;EACE,KAAK;EAAQ,OAAO;EAAO,UAAU;EAAM,MAAM;EAAM,UAAU;EACjE,SAAS,GAAG,QACV,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;IAAK,WAAU;IAAe,UAAA,IAAI;GAAU,CAAA,GAC5C,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAO,iBAAiB,IAAI,IAAI,EAAQ,CAAA,GACxC,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;KAAM,WAAWH,cAAAA,GAAG,6CAA6CC,oBAAAA,gBAAgB,IAAI,MAAM,CAAC;KAA5F,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAACC,aAAAA,MAAD,EAAM,MAAMC,oBAAAA,eAAe,IAAI,MAAM,EAAI,CAAA,GAAE,IAAI,MAC3C;IACH,CAAA,CAAA;GACF,CAAA,CAAA;;CAET;CACA;CACA;AACF;AAEA,SAAgB,YAAY,EAC1B,UACA,MACA,QACA,QACA,WACA,SACA,YACA,aACA,WACA,aACA,kBACA,YACA,oBACmB;CACnB,MAAM,CAAC,YAAY,kBAAA,GAAiBC,MAAAA,SAAAA,CAAiB,QAAQ,EAAE;CAC/D,MAAM,CAAC,cAAc,oBAAA,GAAmBA,MAAAA,SAAAA,CAAiB,EAAE;CAC3D,MAAM,CAAC,aAAa,mBAAA,GAAkBA,MAAAA,SAAAA,CAAS,EAAE;CACjD,MAAM,CAAC,eAAe,qBAAA,GAAoBA,MAAAA,SAAAA,CAAS;EAAE,OAAO,CAAC;EAAe,QAAQ,CAAC;CAAc,CAAC;CACpG,MAAM,CAAC,iBAAiB,uBAAA,GAAsBA,MAAAA,SAAAA,CAAwB,IAAI;CAC1E,MAAM,eAAA,GAAcC,sBAAAA,eAAAA,CAAe;CACnC,MAAM,UAAU,YAAY;CAC5B,MAAM,aAAa,SAAS,cAAc,KAAA;CAC1C,MAAM,gBAAA,GAAeC,MAAAA,QAAAA,OAAc;EACjC,IAAI,CAAC,aAAa,OAAO;EACzB,MAAM,WAAWC,kBAAAA,cAAc,WAAW;EAC1C,OAAO;GAAE,GAAG;IAAS,SAAS,MAAM,SAAS;EAAM;CACrD,GAAG,CAAC,QAAQ,WAAW,CAAC;CACxB,MAAM,EAAE,MAAM,SAAS,WAAWC,qBAAAA,YAAY;EAC5C,UAAU;EACV,MAAM;EACN,QAAQ;EACR,QAAQ,gBAAgB,KAAA;EACxB;CACF,CAAC;CACD,MAAM,gBAAA,GAAeF,MAAAA,QAAAA,OACbG,wBAAAA,cAAc,KAAK;EACvB,UAAU;EACV,MAAM;EACN,QAAQ,gBAAgB,KAAA;EACxB,QAAQ;CACV,CAAC,GACD;EAAC;EAAY;EAAS;EAAc;CAAY,CAClD;CAMA,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,IAAI,CAAC,YAAA,GAAWC,sBAAAA,QAAAA,CAAQ,OAAO,OAAA,GAAMA,sBAAAA,QAAAA,CAAQ,YAAY,GAAG;EAC5D,YAAY,aAAa,cAAc,IAAI;CAC7C,GAAG;EAAC;EAAa;EAAM;EAAS;CAAY,CAAC;CAE7C,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,kBAAkB,aAAa;GAC7B,MAAM,QAAQ,CAAC,mBAAG,IAAI,IAAI,CAAC,GAAG,SAAS,OAAO,GAAG,KAAK,SAAS,QAAQ,IAAI,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK;GAC3G,MAAM,SAAS,CAAC,mBAAG,IAAI,IAAI,CAAC,GAAG,SAAS,QAAQ,GAAG,KAAK,SAAS,QAAQ,OAAO,QAAQ,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK;GAC5J,IAAI,MAAM,WAAW,SAAS,MAAM,UAAU,OAAO,WAAW,SAAS,OAAO,QAAQ,OAAO;GAC/F,OAAO;IAAE;IAAO;GAAO;EACzB,CAAC;CACH,GAAG,CAAC,IAAI,CAAC;CACT,MAAM,UAA6B;EACjC,GAAI,CAAC,OAAO,CAAC;GAAE,KAAK;GAAQ,MAAM;GAAiB,OAAO;GAAQ,aAAa;GAAa,OAAO;GAAY,SAAS,cAAc,MAAM,KAAK,WAAW;IAAE;IAAO,OAAO,iBAAiB,KAAK;GAAE,EAAE;GAAG,UAAU;EAAc,CAAC,IAAI,CAAC;EACvO;GAAE,KAAK;GAAU,MAAM;GAAQ,OAAO;GAAU,aAAa;GAAc,OAAO;GAAc,SAAS;IACvG;KAAE,OAAO;KAAW,OAAO;IAAU;IACrC;KAAE,OAAO;KAAW,OAAO;IAAU;IACrC;KAAE,OAAO;KAAW,OAAO;IAAU;IACrC;KAAE,OAAO;KAAU,OAAO;IAAS;IACnC;KAAE,OAAO;KAAW,OAAO;IAAU;IACrC;KAAE,OAAO;KAAY,OAAO;IAAW;GACzC;GAAG,UAAU;EAAgB;EAC7B;GAAE,KAAK;GAAU,MAAM;GAAQ,OAAO;GAAU,aAAa;GAAa,OAAO;GAC/E,SAAS,cAAc,OAAO,QAAQ,UAAU,EAAEH,kBAAAA,cAAc,KAAK,CAAC,CAAC,QAAQ,UAAU,CAAC,GAAG,CAAC,CAAC,KAAK,WAAW,EAAE,MAAM,EAAE;GACzH,UAAU;EAAe;CAC7B;CACA,MAAM,QAAA,GAAOD,MAAAA,QAAAA,OAA6B,KAAK,KAAK,SAAS;EAC3D,IAAI,IAAI;EACR,MAAM,IAAI;EACV,MAAM,IAAI,QAAQ;EAClB,QAAQ,IAAI;EACZ,GAAI,IAAI,YAAY,EAAE,WAAW,IAAI,UAAU,IAAI,CAAC;EACpD,GAAI,IAAI,aAAa,EAAE,YAAY,IAAI,WAAW,IAAI,CAAC;EACvD,GAAI,IAAI,SAAS,EAAE,QAAQ,IAAI,OAAO,IAAI,CAAC;EAC3C;CACF,EAAE,GAAG,CAAC,IAAI,CAAC;CACX,MAAM,SAAS,cAAc,aAAa;CAE1C,OACE,iBAAA,GAAA,kBAAA,IAAA,CAACK,kBAAAA,WAAD;EACE,GAAK,YAAY,EAAE,UAAU,IAAI,CAAC;EAClC,MAAM;EACN,SAAS,UAAU,oBAAoB;EACvC,cAAa;EACb,iBAAiB;EACjB,gBAAgB,EAAE,UAAU,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;GAAM,WAAU;GAAiC,UAAA;EAAa,CAAA,EAAE;EAC5F,WAAW,QAAQ,IAAI;EACvB,aAAa;GAAE,KAAK;GAAa,KAAK;EAAO;EAC7C,gBAAgB,SAAS,CAAC,MAAM,IAAI,CAAC;EACrC,yBAAyB,QAAQ;GAC/B,MAAM,OAAO,IAAI,MAAM,OAAO,OAAO,MAAM,KAAK;GAChD,IAAI,aAAa,YAAY,IAAI;QAC5B,mBAAmB,IAAI;EAC9B;EACA,oBAAoB,QAClB,iBAAA,GAAA,kBAAA,IAAA,CAAC,aAAD;GACE,KAAK,IAAI;GACC;GACF;GACM;GACH;GACE;GACK;GACN;GACM;EACnB,CAAA;CAEJ,CAAA;AAEL;AAEA,SAAS,YAAY,EAAE,WAAW,cAAiF;CACjH,MAAM,MAAMC,2BAAAA,iBAAiB,CAAC,CAAC,aAAa,CAAC,UAAU;CACvD,IAAI,CAAC,WAAW,OAAO,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;EAAM,WAAU;EAAwB,UAAA;CAAO,CAAA;CACtE,OAAO,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;EAAM,WAAU;EAAgB,UAAAC,eAAAA,eAAe,KAAK,IAAI,IAAI,aAAa,KAAK,MAAM,UAAU,IAAI,OAAO,KAAK,MAAM,SAAS,CAAC,CAAC;CAAQ,CAAA;AAChJ;AAEA,SAAS,iBAAiB,MAAsB;CAQ9C,OAAO;EANL,YAAY;EACZ,UAAU;EACV,MAAM;EACN,WAAW;EACX,OAAO;CAEF,EAAM,SAAS;AACxB;AAEA,SAAS,YAAY,EACnB,KACA,UACA,QACA,cACA,WACA,aACA,kBACA,YACA,oBAWC;CACD,MAAM,QAAQ,IAAI;CAClB,MAAM,EAAE,WAAW,WAAWC,qBAAAA,WAAW;EAAE,IAAI;EAAO;EAAU;CAAO,CAAC;CACxE,MAAM,UAAU,YAAY;CAC5B,MAAM,eAAA,GAAcT,sBAAAA,eAAAA,CAAe;CACnC,MAAM,mBAAA,GAAkBU,MAAAA,OAAAA,iBAAO,IAAI,IAA2B,CAAC;CAC/D,MAAM,mBAAA,GAAkBA,MAAAA,OAAAA,CAAmC,KAAA,CAAS;CACpE,MAAM,eAAA,GAAcT,MAAAA,QAAAA,OACZG,wBAAAA,cAAc,IAAI;EAAE,UAAU;EAAS;CAAM,CAAC,GACpD,CAAC,SAAS,KAAK,CACjB;CACA,MAAM,mBAAA,GAAkBO,sBAAAA,YAAAA,CAAY;EAClC,aAAaP,wBAAAA,cAAc,QAAQ;GAAE,UAAU;GAAS;EAAM,CAAC;EAC/D,YAAY;EACZ,WAAW,YAAY;GACrB,MAAM,QAAQ,IAAI,CAChB,YAAY,kBAAkB;IAAE,UAAU;IAAc,OAAO;IAAM,aAAa;GAAO,CAAC,GAC1F,YAAY,kBAAkB;IAAE,UAAU;IAAa,OAAO;IAAM,aAAa;GAAO,CAAC,CAC3F,CAAC;EACH;CACF,CAAC;CAED,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,MAAM,WAAW,gBAAgB;EACjC,IAAI,UAAU,WAAW,UAAU,UAAU,SAAS,OAAO,UAAU,UAAU,aAAa,UAAU,MAAM,GAC5G;EAEF,gBAAgB,UAAU;EAC1B,YAAY,aAAa,aAAa,SAAS;CACjD,GAAG;EAAC;EAAa;EAAa;CAAS,CAAC;CAExC,MAAM,iBAAA,GAAgBQ,MAAAA,YAAAA,EAAa,YAAgC;EACjE,MAAM,MAAM,GAAG,QAAQ,IAAI,GAAG,QAAQ;EACtC,MAAM,UAAU,gBAAgB,QAAQ,IAAI,GAAG;EAC/C,IAAI,SAAS,OAAO;EACpB,gBAAgB,MAAM;EACtB,MAAM,WAAW,gBAAgB,YAAY,OAAO,CAAC,CAAC,cAAc;GAClE,IAAI,gBAAgB,QAAQ,IAAI,GAAG,MAAM,UAAU,gBAAgB,QAAQ,OAAO,GAAG;EACvF,CAAC;EACD,gBAAgB,QAAQ,IAAI,KAAK,QAAQ;EACzC,OAAO;CACT,GAAG,CAAC,gBAAgB,aAAa,gBAAgB,KAAK,CAAC;CACvD,MAAM,WAAW,QAA2B,UAC1C,cAAc;EACZ,KAAK,aAAa,KAAK,MAAM,KAAK,GAAG,QAAQ,SAAS,mBAAmB,KAAK,EAAE;EAChF;EACA,QAAQ;CACV,CAAC;CACH,MAAM,eAAe,QAA2B,SAC9C,cAAc;EACZ,KAAK,aAAa,KAAK,MAAM,KAAK,GAAG,QAAQ,SAAS,mBAAmB,KAAK,EAAE,SAAS,mBAAmB,KAAK,EAAE,EAAE;EACrH;EACA,QAAQ;CACV,CAAC;CACH,MAAM,WAAWpB,2BAAAA,iBAAiB,GAAG;CACrC,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;EAAK,WAAU;EAAf,UAAA;GACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,WAAU;IAAf,UAAA;KACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD,EAAA,UAAA,CAAO,SAAS,WAAU,YAAgB,EAAA,CAAA;KAC1C,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD,EAAA,UAAA,CAAO,SAAS,SAAQ,UAAc,EAAA,CAAA;KACtC,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD,EAAA,UAAA,CAAO,SAAS,QAAO,SAAa,EAAA,CAAA;KACpC,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD,EAAA,UAAA,CAAO,SAAS,OAAO,UAAU,SAAS,WAAU,UAAc,EAAA,CAAA;KACjE,OAAO,QAAQ,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,WAC3C,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;MAAgB,WAAU;MAA1B,UAAA;OAAuC;OAAI;OAAE;MAAY;KAA9C,GAAA,GAA8C,CAC1D;IACE;;GACJ,mBAAmB,KAAK,SAAS;GAClC,iBAAA,GAAA,kBAAA,IAAA,CAACqB,qBAAAA,cAAD;IACa;IACX,SAAA;IACA,kBAAkB;IAClB,qBAAA;IACA,WAAW;IACX,GAAK,mBAAmB,GAAG,MAAM,QAAQ,EAAE,eAAe,YAAY,IAAI,CAAC;IAC3E,gBAAgB,GAAG,QAAQ;IAC3B,GAAK,YAAY,EAAE,UAAU,IAAI,CAAC;IAClC,GAAK,cAAc,EAAE,YAAY,IAAI,CAAC;GACvC,CAAA;EACE;;AAET;AAQA,eAAe,gBAAgB,EAAE,KAAK,QAAQ,UAA8B;CAC1E,MAAM,UAAU,aAAa,OAAO,GAAG,OAAO,KAAK,IAAI,OAAO,KAAK;CACnE,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,MAAM,KAAK;GAC1B,QAAQ;GACR,SAAS,EAAE,gBAAgB,mBAAmB;GAC9C,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC;EACjC,CAAC;CACH,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,GAAG,QAAQ,IAAI,iBAAiB,QAAQ,MAAM,UAAU,oBAAoB,EAAE,MAAM,CAAC;CACvG;CACA,IAAI,CAAC,SAAS,IAAI;EAChB,MAAM,UAAU,MAAM,SAAS,KAAK,EAAA,CAAG,KAAK,KAAK,QAAQ,SAAS;EAClE,MAAM,IAAI,MAAM,GAAG,QAAQ,IAAI,QAAQ;CACzC;AACF"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { TaskControlAction, TaskRunMeta, TaskSnapshot } from './TaskSnapshot.js';
|
|
1
3
|
import { TaskHeaderExtra } from './TaskHeaderExtras.js';
|
|
2
4
|
import { TaskExtraTabs } from './TaskStreamTabs.js';
|
|
3
5
|
export interface TaskManagerProps {
|
|
@@ -32,6 +34,9 @@ export interface TaskManagerProps {
|
|
|
32
34
|
* for a task falls back to the default metadata chips.
|
|
33
35
|
*/
|
|
34
36
|
headerExtra?: TaskHeaderExtra;
|
|
37
|
+
renderRunDetails?: (run: TaskRunMeta, snapshots: TaskSnapshot[]) => ReactNode;
|
|
38
|
+
controlUrl?: (run: TaskRunMeta, action: TaskControlAction) => string | undefined;
|
|
39
|
+
showTaskControls?: (run: TaskRunMeta) => boolean;
|
|
35
40
|
}
|
|
36
|
-
export declare function TaskManager({ basePath, kind, labels, pollMs, className, compact, selectedId, onSelectRun, extraTabs, headerExtra, }: TaskManagerProps): import("react").JSX.Element;
|
|
41
|
+
export declare function TaskManager({ basePath, kind, labels, pollMs, className, compact, selectedId, onSelectRun, extraTabs, headerExtra, renderRunDetails, controlUrl, showTaskControls, }: TaskManagerProps): import("react").JSX.Element;
|
|
37
42
|
//# sourceMappingURL=TaskManager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TaskManager.d.ts","sourceRoot":"","sources":["../../src/data/TaskManager.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"TaskManager.d.ts","sourceRoot":"","sources":["../../src/data/TaskManager.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAcvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAYtD,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC1C;;;OAGG;IACH,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B;;;OAGG;IACH,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,SAAS,CAAC;IAC9E,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,KAAK,MAAM,GAAG,SAAS,CAAC;IACjF,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC;CAClD;AAoED,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,IAAI,EACJ,MAAM,EACN,MAAM,EACN,SAAS,EACT,OAAO,EACP,UAAU,EACV,WAAW,EACX,SAAS,EACT,WAAW,EACX,gBAAgB,EAChB,UAAU,EACV,gBAAgB,GACjB,EAAE,gBAAgB,+BAyGlB"}
|
package/dist/data/TaskManager.js
CHANGED
|
@@ -110,7 +110,7 @@ var compactRunColumns = [
|
|
|
110
110
|
progressColumn,
|
|
111
111
|
startedColumn
|
|
112
112
|
];
|
|
113
|
-
function TaskManager({ basePath, kind, labels, pollMs, className, compact, selectedId, onSelectRun, extraTabs, headerExtra }) {
|
|
113
|
+
function TaskManager({ basePath, kind, labels, pollMs, className, compact, selectedId, onSelectRun, extraTabs, headerExtra, renderRunDetails, controlUrl, showTaskControls }) {
|
|
114
114
|
const [kindFilter, setKindFilter] = useState(kind ?? "");
|
|
115
115
|
const [statusFilter, setStatusFilter] = useState("");
|
|
116
116
|
const [labelFilter, setLabelFilter] = useState("");
|
|
@@ -263,7 +263,10 @@ function TaskManager({ basePath, kind, labels, pollMs, className, compact, selec
|
|
|
263
263
|
pollMs,
|
|
264
264
|
runsQueryKey,
|
|
265
265
|
extraTabs,
|
|
266
|
-
headerExtra
|
|
266
|
+
headerExtra,
|
|
267
|
+
renderRunDetails,
|
|
268
|
+
controlUrl,
|
|
269
|
+
showTaskControls
|
|
267
270
|
})
|
|
268
271
|
});
|
|
269
272
|
}
|
|
@@ -287,7 +290,7 @@ function applicationLabel(kind) {
|
|
|
287
290
|
other: "Other"
|
|
288
291
|
}[kind] ?? kind;
|
|
289
292
|
}
|
|
290
|
-
function ExpandedRun({ run, basePath, pollMs, runsQueryKey, extraTabs, headerExtra }) {
|
|
293
|
+
function ExpandedRun({ run, basePath, pollMs, runsQueryKey, extraTabs, headerExtra, renderRunDetails, controlUrl, showTaskControls }) {
|
|
291
294
|
const runId = run.id;
|
|
292
295
|
const { snapshots, status } = useTaskRun({
|
|
293
296
|
id: runId,
|
|
@@ -342,45 +345,49 @@ function ExpandedRun({ run, basePath, pollMs, runsQueryKey, extraTabs, headerExt
|
|
|
342
345
|
return mutation;
|
|
343
346
|
}, [controlMutation.mutateAsync, controlMutation.reset]);
|
|
344
347
|
const control = (action, group) => mutateControl({
|
|
345
|
-
url: `${apiBase}/tasks/${encodeURIComponent(runId)}/control`,
|
|
348
|
+
url: controlUrl?.(run, action) ?? `${apiBase}/tasks/${encodeURIComponent(runId)}/control`,
|
|
346
349
|
action,
|
|
347
350
|
target: group
|
|
348
351
|
});
|
|
349
352
|
const controlTask = (action, task) => mutateControl({
|
|
350
|
-
url: `${apiBase}/tasks/${encodeURIComponent(runId)}/tasks/${encodeURIComponent(task.id)}/control`,
|
|
353
|
+
url: controlUrl?.(run, action) ?? `${apiBase}/tasks/${encodeURIComponent(runId)}/tasks/${encodeURIComponent(task.id)}/control`,
|
|
351
354
|
action,
|
|
352
355
|
target: task
|
|
353
356
|
});
|
|
354
357
|
const progress = taskWorkProgress(run);
|
|
355
358
|
return /* @__PURE__ */ jsxs("div", {
|
|
356
359
|
className: "space-y-3 bg-muted/20 px-4 py-4",
|
|
357
|
-
children: [
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
360
|
+
children: [
|
|
361
|
+
/* @__PURE__ */ jsxs("div", {
|
|
362
|
+
className: "flex flex-wrap gap-x-5 gap-y-1 text-xs text-muted-foreground",
|
|
363
|
+
children: [
|
|
364
|
+
/* @__PURE__ */ jsxs("span", { children: [progress.completed, " completed"] }),
|
|
365
|
+
/* @__PURE__ */ jsxs("span", { children: [progress.running, " running"] }),
|
|
366
|
+
/* @__PURE__ */ jsxs("span", { children: [progress.failed, " failed"] }),
|
|
367
|
+
/* @__PURE__ */ jsxs("span", { children: [progress.counts.pending + progress.unstarted, " waiting"] }),
|
|
368
|
+
Object.entries(run.labels ?? {}).map(([key, value]) => /* @__PURE__ */ jsxs("span", {
|
|
369
|
+
className: "font-mono",
|
|
370
|
+
children: [
|
|
371
|
+
key,
|
|
372
|
+
"=",
|
|
373
|
+
value
|
|
374
|
+
]
|
|
375
|
+
}, key))
|
|
376
|
+
]
|
|
377
|
+
}),
|
|
378
|
+
renderRunDetails?.(run, snapshots),
|
|
379
|
+
/* @__PURE__ */ jsx(TaskProgress, {
|
|
380
|
+
snapshots,
|
|
381
|
+
compact: true,
|
|
382
|
+
connectionStatus: status,
|
|
383
|
+
initialShowAllTasks: true,
|
|
384
|
+
onControl: control,
|
|
385
|
+
...showTaskControls?.(run) !== false ? { onTaskControl: controlTask } : {},
|
|
386
|
+
metricsBaseUrl: `${apiBase}/tasks/metrics/`,
|
|
387
|
+
...extraTabs ? { extraTabs } : {},
|
|
388
|
+
...headerExtra ? { headerExtra } : {}
|
|
389
|
+
})
|
|
390
|
+
]
|
|
384
391
|
});
|
|
385
392
|
}
|
|
386
393
|
async function postTaskControl({ url, action, target }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TaskManager.js","names":[],"sources":["../../src/data/TaskManager.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { hashKey, useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport type { QueryKey } from \"@tanstack/react-query\";\nimport { cn } from \"../lib/utils\";\nimport { formatDuration } from \"../lib/format\";\nimport { useResourceClock } from \"../hooks/use-resource-clock\";\nimport { useTaskRun, useTaskRuns } from \"../hooks/use-task-run\";\nimport type { FilterBarFilter } from \"../components/FilterBar\";\nimport { Icon } from \"./Icon\";\nimport { DataTable, type DataTableColumn } from \"./DataTable\";\nimport { splitTagToken } from \"./cells/tag-utils\";\nimport { ProgressBar } from \"./ProgressBar\";\nimport { TaskProgress } from \"./TaskProgress\";\nimport { taskWorkProgress } from \"./task-work-progress\";\nimport type { TaskControlAction, TaskRunMeta, TaskSnapshot } from \"./TaskSnapshot\";\nimport type { TaskHeaderExtra } from \"./TaskHeaderExtras\";\nimport type { TaskExtraTabs } from \"./TaskStreamTabs\";\nimport { taskQueryKeys } from \"./task-query-keys\";\nimport { taskSegments, taskStatusColor, taskStatusIcon } from \"./task-status\";\n\n// TaskManager is the generic clicky-ui task-manager view: it lists every run\n// (TaskRunMeta) from GET {basePath}/tasks with kind/status filters, and expands\n// each row into a live <TaskProgress> for that run id.\n//\n// Requires a react-query QueryClientProvider in the host app (like\n// TimeseriesPanel and CacheBrowser): control actions run through useMutation,\n// and the listing is mirrored into the query cache so sibling views can read it.\n\nexport interface TaskManagerProps {\n /** Base path of the clicky task API, e.g. \"/api/v1\". */\n basePath?: string;\n /** Restrict the listing to a single kind (also hides the kind filter). */\n kind?: string;\n /** Restrict the listing to runs whose labels contain every key/value pair. */\n labels?: Record<string, string>;\n pollMs?: number;\n className?: string;\n /** Keep the run columns usable in a narrow panel. */\n compact?: boolean;\n /**\n * Currently-selected (expanded) run id, e.g. driven from the URL. When set,\n * the matching run renders expanded and selection is controlled — otherwise\n * each row manages its own expand/collapse state.\n */\n selectedId?: string;\n /**\n * Called when a run row is toggled: the new selection id, or null when the\n * open row is collapsed. Pair with selectedId for URL-driven deep links.\n */\n onSelectRun?: (id: string | null) => void;\n /**\n * Contributes host-owned output panes to each task, alongside its own\n * stdout/stderr — an agent's session transcript, say.\n */\n extraTabs?: TaskExtraTabs;\n /**\n * Replaces the content beside a task's name in its header row. Returning null\n * for a task falls back to the default metadata chips.\n */\n headerExtra?: TaskHeaderExtra;\n}\n\ntype RunTableRow = Record<string, unknown> & {\n id: string;\n name: string;\n kind: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n labels?: Record<string, string>;\n run: TaskRunMeta;\n};\n\nconst progressColumn: DataTableColumn<RunTableRow> = {\n key: \"progress\", label: \"Progress\", minWidth: 180,\n accessor: (row) => taskWorkProgress(row.run).done,\n render: (_, row) => {\n const progress = taskWorkProgress(row.run);\n return (\n <div className=\"min-w-36 space-y-1\">\n <span className=\"text-xs tabular-nums text-muted-foreground\">{progress.label}</span>\n {progress.total > 0 && <ProgressBar segments={taskSegments(progress.counts)} total={progress.total} height=\"h-1.5\" />}\n </div>\n );\n },\n};\nconst startedColumn: DataTableColumn<RunTableRow> = { key: \"startedAt\", label: \"Started\", kind: \"timestamp\", sortable: true };\nconst durationColumn: DataTableColumn<RunTableRow> = {\n key: \"duration\", label: \"Duration\", sortable: true,\n accessor: (row) => row.startedAt ? Date.parse(row.finishedAt ?? new Date().toISOString()) - Date.parse(row.startedAt) : null,\n render: (_, row) => <RunDuration startedAt={row.startedAt} finishedAt={row.finishedAt} />,\n};\nconst runColumns: DataTableColumn<RunTableRow>[] = [\n { key: \"name\", label: \"Run\", sortable: true, grow: true, minWidth: 220 },\n {\n key: \"status\", label: \"Status\", sortable: true,\n render: (_, row) => (\n <span className={cn(\"inline-flex items-center gap-1.5 capitalize\", taskStatusColor(row.status))}>\n <Icon icon={taskStatusIcon(row.status)} {...(row.status === \"running\" ? { className: \"animate-spin\" } : {})} />\n {row.status}\n </span>\n ),\n },\n durationColumn,\n progressColumn,\n { key: \"labels\", label: \"Labels\", kind: \"tags\", tags: { maxVisible: 2 }, minWidth: 160 },\n startedColumn,\n { key: \"finishedAt\", label: \"Finished\", kind: \"timestamp\", sortable: true },\n];\nconst compactRunColumns: DataTableColumn<RunTableRow>[] = [\n {\n key: \"name\", label: \"Run\", sortable: true, grow: true, minWidth: 220,\n render: (_, row) => (\n <div className=\"space-y-0.5\">\n <div className=\"font-medium\">{row.name}</div>\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span>{applicationLabel(row.kind)}</span>\n <span className={cn(\"inline-flex items-center gap-1 capitalize\", taskStatusColor(row.status))}>\n <Icon icon={taskStatusIcon(row.status)} />{row.status}\n </span>\n </div>\n </div>\n ),\n },\n progressColumn,\n startedColumn,\n];\n\nexport function TaskManager({\n basePath,\n kind,\n labels,\n pollMs,\n className,\n compact,\n selectedId,\n onSelectRun,\n extraTabs,\n headerExtra,\n}: TaskManagerProps) {\n const [kindFilter, setKindFilter] = useState<string>(kind ?? \"\");\n const [statusFilter, setStatusFilter] = useState<string>(\"\");\n const [labelFilter, setLabelFilter] = useState(\"\");\n const [filterOptions, setFilterOptions] = useState({ kinds: [] as string[], labels: [] as string[] });\n const [localSelectedId, setLocalSelectedId] = useState<string | null>(null);\n const queryClient = useQueryClient();\n const apiBase = basePath ?? \"/api/v1\";\n const activeKind = kind ?? (kindFilter || undefined);\n const activeLabels = useMemo(() => {\n if (!labelFilter) return labels;\n const selected = splitTagToken(labelFilter);\n return { ...labels, [selected.key]: selected.value };\n }, [labels, labelFilter]);\n const { runs, runsKey, status } = useTaskRuns({\n basePath: apiBase,\n kind: activeKind,\n labels: activeLabels,\n status: statusFilter || undefined,\n pollMs,\n });\n const runsQueryKey = useMemo(\n () => taskQueryKeys.runs({\n basePath: apiBase,\n kind: activeKind,\n status: statusFilter || undefined,\n labels: activeLabels,\n }),\n [activeKind, apiBase, activeLabels, statusFilter],\n );\n\n // Changing a filter re-subscribes the transport but leaves the previous\n // listing on screen until the next SSE/poll frame. Publishing that listing\n // under the new key would hand cache consumers rows that do not match their\n // filters, so mirror only once the delivered listing's own key agrees.\n useEffect(() => {\n if (!runsKey || hashKey(runsKey) !== hashKey(runsQueryKey)) return;\n queryClient.setQueryData(runsQueryKey, runs);\n }, [queryClient, runs, runsKey, runsQueryKey]);\n\n useEffect(() => {\n setFilterOptions((previous) => {\n const kinds = [...new Set([...previous.kinds, ...runs.flatMap((run) => run.kind ? [run.kind] : [])])].sort();\n const labels = [...new Set([...previous.labels, ...runs.flatMap((run) => Object.entries(run.labels ?? {}).map(([key, value]) => `${key}=${value}`))])].sort();\n if (kinds.length === previous.kinds.length && labels.length === previous.labels.length) return previous;\n return { kinds, labels };\n });\n }, [runs]);\n const filters: FilterBarFilter[] = [\n ...(!kind ? [{ key: \"kind\", kind: \"enum\" as const, label: \"Kind\", placeholder: \"All kinds\", value: kindFilter, options: filterOptions.kinds.map((value) => ({ value, label: applicationLabel(value) })), onChange: setKindFilter }] : []),\n { key: \"status\", kind: \"enum\", label: \"Status\", placeholder: \"Any status\", value: statusFilter, options: [\n { value: \"running\", label: \"Running\" },\n { value: \"pending\", label: \"Pending\" },\n { value: \"success\", label: \"Success\" },\n { value: \"failed\", label: \"Failed\" },\n { value: \"warning\", label: \"Warning\" },\n { value: \"canceled\", label: \"Canceled\" },\n ], onChange: setStatusFilter },\n { key: \"labels\", kind: \"enum\", label: \"Labels\", placeholder: \"Any label\", value: labelFilter,\n options: filterOptions.labels.filter((token) => !(splitTagToken(token).key in (labels ?? {}))).map((value) => ({ value })),\n onChange: setLabelFilter },\n ];\n const rows = useMemo<RunTableRow[]>(() => runs.map((run) => ({\n id: run.id,\n name: run.name,\n kind: run.kind || \"other\",\n status: run.status,\n ...(run.startedAt ? { startedAt: run.startedAt } : {}),\n ...(run.finishedAt ? { finishedAt: run.finishedAt } : {}),\n ...(run.labels ? { labels: run.labels } : {}),\n run,\n })), [runs]);\n const openId = onSelectRun ? selectedId : localSelectedId;\n\n return (\n <DataTable\n {...(className ? { className } : {})}\n data={rows}\n columns={compact ? compactRunColumns : runColumns}\n emptyMessage=\"No background tasks.\"\n externalFilters={filters}\n filterBarProps={{ trailing: <span className=\"text-xs text-muted-foreground\">{status}</span> }}\n getRowId={(row) => row.id}\n defaultSort={{ key: \"startedAt\", dir: \"desc\" }}\n expandedRowIds={openId ? [openId] : []}\n onExpandedRowIdsChange={(ids) => {\n const next = ids.find((id) => id !== openId) ?? null;\n if (onSelectRun) onSelectRun(next);\n else setLocalSelectedId(next);\n }}\n renderExpandedRow={(row) => (\n <ExpandedRun\n run={row.run}\n basePath={basePath}\n pollMs={pollMs}\n runsQueryKey={runsQueryKey}\n extraTabs={extraTabs}\n headerExtra={headerExtra}\n />\n )}\n />\n );\n}\n\nfunction RunDuration({ startedAt, finishedAt }: { startedAt: string | undefined; finishedAt: string | undefined }) {\n const now = useResourceClock(!!startedAt && !finishedAt);\n if (!startedAt) return <span className=\"text-muted-foreground\">—</span>;\n return <span className=\"tabular-nums\">{formatDuration(Math.max(0, (finishedAt ? Date.parse(finishedAt) : now) - Date.parse(startedAt)))}</span>;\n}\n\nfunction applicationLabel(kind: string): string {\n const names: Record<string, string> = {\n makerprint: \"MakerPrint\",\n openscad: \"OpenSCAD\",\n orca: \"Orca\",\n snapmaker: \"Snapmaker\",\n other: \"Other\",\n };\n return names[kind] ?? kind;\n}\n\nfunction ExpandedRun({\n run,\n basePath,\n pollMs,\n runsQueryKey,\n extraTabs,\n headerExtra,\n}: {\n run: TaskRunMeta;\n basePath: string | undefined;\n pollMs: number | undefined;\n runsQueryKey: QueryKey;\n extraTabs: TaskExtraTabs | undefined;\n headerExtra: TaskHeaderExtra | undefined;\n}) {\n const runId = run.id;\n const { snapshots, status } = useTaskRun({ id: runId, basePath, pollMs });\n const apiBase = basePath ?? \"/api/v1\";\n const queryClient = useQueryClient();\n const pendingControls = useRef(new Map<string, Promise<void>>());\n const cachedSnapshots = useRef<TaskSnapshot[] | undefined>(undefined);\n const runQueryKey = useMemo(\n () => taskQueryKeys.run({ basePath: apiBase, runId }),\n [apiBase, runId],\n );\n const controlMutation = useMutation({\n mutationKey: taskQueryKeys.control({ basePath: apiBase, runId }),\n mutationFn: postTaskControl,\n onSuccess: async () => {\n await Promise.all([\n queryClient.invalidateQueries({ queryKey: runsQueryKey, exact: true, refetchType: \"none\" }),\n queryClient.invalidateQueries({ queryKey: runQueryKey, exact: true, refetchType: \"none\" }),\n ]);\n },\n });\n\n useEffect(() => {\n const previous = cachedSnapshots.current;\n if (previous?.length === snapshots.length && previous.every((snapshot, index) => snapshot === snapshots[index])) {\n return;\n }\n cachedSnapshots.current = snapshots;\n queryClient.setQueryData(runQueryKey, snapshots);\n }, [queryClient, runQueryKey, snapshots]);\n\n const mutateControl = useCallback((request: TaskControlRequest) => {\n const key = `${request.url}:${request.action}`;\n const pending = pendingControls.current.get(key);\n if (pending) return pending;\n controlMutation.reset();\n const mutation = controlMutation.mutateAsync(request).finally(() => {\n if (pendingControls.current.get(key) === mutation) pendingControls.current.delete(key);\n });\n pendingControls.current.set(key, mutation);\n return mutation;\n }, [controlMutation.mutateAsync, controlMutation.reset]);\n const control = (action: TaskControlAction, group: TaskSnapshot) =>\n mutateControl({\n url: `${apiBase}/tasks/${encodeURIComponent(runId)}/control`,\n action,\n target: group,\n });\n const controlTask = (action: TaskControlAction, task: TaskSnapshot) =>\n mutateControl({\n url: `${apiBase}/tasks/${encodeURIComponent(runId)}/tasks/${encodeURIComponent(task.id)}/control`,\n action,\n target: task,\n });\n const progress = taskWorkProgress(run);\n return (\n <div className=\"space-y-3 bg-muted/20 px-4 py-4\">\n <div className=\"flex flex-wrap gap-x-5 gap-y-1 text-xs text-muted-foreground\">\n <span>{progress.completed} completed</span>\n <span>{progress.running} running</span>\n <span>{progress.failed} failed</span>\n <span>{progress.counts.pending + progress.unstarted} waiting</span>\n {Object.entries(run.labels ?? {}).map(([key, value]) => (\n <span key={key} className=\"font-mono\">{key}={value}</span>\n ))}\n </div>\n <TaskProgress\n snapshots={snapshots}\n compact\n connectionStatus={status}\n initialShowAllTasks\n onControl={control}\n onTaskControl={controlTask}\n metricsBaseUrl={`${apiBase}/tasks/metrics/`}\n {...(extraTabs ? { extraTabs } : {})}\n {...(headerExtra ? { headerExtra } : {})}\n />\n </div>\n );\n}\n\ninterface TaskControlRequest {\n url: string;\n action: TaskControlAction;\n target: TaskSnapshot;\n}\n\nasync function postTaskControl({ url, action, target }: TaskControlRequest) {\n const context = `Failed to ${action} ${target.type} \"${target.name}\"`;\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ action }),\n });\n } catch (cause) {\n throw new Error(`${context}: ${cause instanceof Error ? cause.message : \"request failed\"}`, { cause });\n }\n if (!response.ok) {\n const detail = (await response.text()).trim() || `HTTP ${response.status}`;\n throw new Error(`${context}: ${detail}`);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TaskManager.js","names":[],"sources":["../../src/data/TaskManager.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { hashKey, useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport type { QueryKey } from \"@tanstack/react-query\";\nimport { cn } from \"../lib/utils\";\nimport { formatDuration } from \"../lib/format\";\nimport { useResourceClock } from \"../hooks/use-resource-clock\";\nimport { useTaskRun, useTaskRuns } from \"../hooks/use-task-run\";\nimport type { FilterBarFilter } from \"../components/FilterBar\";\nimport { Icon } from \"./Icon\";\nimport { DataTable, type DataTableColumn } from \"./DataTable\";\nimport { splitTagToken } from \"./cells/tag-utils\";\nimport { ProgressBar } from \"./ProgressBar\";\nimport { TaskProgress } from \"./TaskProgress\";\nimport { taskWorkProgress } from \"./task-work-progress\";\nimport type { TaskControlAction, TaskRunMeta, TaskSnapshot } from \"./TaskSnapshot\";\nimport type { TaskHeaderExtra } from \"./TaskHeaderExtras\";\nimport type { TaskExtraTabs } from \"./TaskStreamTabs\";\nimport { taskQueryKeys } from \"./task-query-keys\";\nimport { taskSegments, taskStatusColor, taskStatusIcon } from \"./task-status\";\n\n// TaskManager is the generic clicky-ui task-manager view: it lists every run\n// (TaskRunMeta) from GET {basePath}/tasks with kind/status filters, and expands\n// each row into a live <TaskProgress> for that run id.\n//\n// Requires a react-query QueryClientProvider in the host app (like\n// TimeseriesPanel and CacheBrowser): control actions run through useMutation,\n// and the listing is mirrored into the query cache so sibling views can read it.\n\nexport interface TaskManagerProps {\n /** Base path of the clicky task API, e.g. \"/api/v1\". */\n basePath?: string;\n /** Restrict the listing to a single kind (also hides the kind filter). */\n kind?: string;\n /** Restrict the listing to runs whose labels contain every key/value pair. */\n labels?: Record<string, string>;\n pollMs?: number;\n className?: string;\n /** Keep the run columns usable in a narrow panel. */\n compact?: boolean;\n /**\n * Currently-selected (expanded) run id, e.g. driven from the URL. When set,\n * the matching run renders expanded and selection is controlled — otherwise\n * each row manages its own expand/collapse state.\n */\n selectedId?: string;\n /**\n * Called when a run row is toggled: the new selection id, or null when the\n * open row is collapsed. Pair with selectedId for URL-driven deep links.\n */\n onSelectRun?: (id: string | null) => void;\n /**\n * Contributes host-owned output panes to each task, alongside its own\n * stdout/stderr — an agent's session transcript, say.\n */\n extraTabs?: TaskExtraTabs;\n /**\n * Replaces the content beside a task's name in its header row. Returning null\n * for a task falls back to the default metadata chips.\n */\n headerExtra?: TaskHeaderExtra;\n renderRunDetails?: (run: TaskRunMeta, snapshots: TaskSnapshot[]) => ReactNode;\n controlUrl?: (run: TaskRunMeta, action: TaskControlAction) => string | undefined;\n showTaskControls?: (run: TaskRunMeta) => boolean;\n}\n\ntype RunTableRow = Record<string, unknown> & {\n id: string;\n name: string;\n kind: string;\n status: string;\n startedAt?: string;\n finishedAt?: string;\n labels?: Record<string, string>;\n run: TaskRunMeta;\n};\n\nconst progressColumn: DataTableColumn<RunTableRow> = {\n key: \"progress\", label: \"Progress\", minWidth: 180,\n accessor: (row) => taskWorkProgress(row.run).done,\n render: (_, row) => {\n const progress = taskWorkProgress(row.run);\n return (\n <div className=\"min-w-36 space-y-1\">\n <span className=\"text-xs tabular-nums text-muted-foreground\">{progress.label}</span>\n {progress.total > 0 && <ProgressBar segments={taskSegments(progress.counts)} total={progress.total} height=\"h-1.5\" />}\n </div>\n );\n },\n};\nconst startedColumn: DataTableColumn<RunTableRow> = { key: \"startedAt\", label: \"Started\", kind: \"timestamp\", sortable: true };\nconst durationColumn: DataTableColumn<RunTableRow> = {\n key: \"duration\", label: \"Duration\", sortable: true,\n accessor: (row) => row.startedAt ? Date.parse(row.finishedAt ?? new Date().toISOString()) - Date.parse(row.startedAt) : null,\n render: (_, row) => <RunDuration startedAt={row.startedAt} finishedAt={row.finishedAt} />,\n};\nconst runColumns: DataTableColumn<RunTableRow>[] = [\n { key: \"name\", label: \"Run\", sortable: true, grow: true, minWidth: 220 },\n {\n key: \"status\", label: \"Status\", sortable: true,\n render: (_, row) => (\n <span className={cn(\"inline-flex items-center gap-1.5 capitalize\", taskStatusColor(row.status))}>\n <Icon icon={taskStatusIcon(row.status)} {...(row.status === \"running\" ? { className: \"animate-spin\" } : {})} />\n {row.status}\n </span>\n ),\n },\n durationColumn,\n progressColumn,\n { key: \"labels\", label: \"Labels\", kind: \"tags\", tags: { maxVisible: 2 }, minWidth: 160 },\n startedColumn,\n { key: \"finishedAt\", label: \"Finished\", kind: \"timestamp\", sortable: true },\n];\nconst compactRunColumns: DataTableColumn<RunTableRow>[] = [\n {\n key: \"name\", label: \"Run\", sortable: true, grow: true, minWidth: 220,\n render: (_, row) => (\n <div className=\"space-y-0.5\">\n <div className=\"font-medium\">{row.name}</div>\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span>{applicationLabel(row.kind)}</span>\n <span className={cn(\"inline-flex items-center gap-1 capitalize\", taskStatusColor(row.status))}>\n <Icon icon={taskStatusIcon(row.status)} />{row.status}\n </span>\n </div>\n </div>\n ),\n },\n progressColumn,\n startedColumn,\n];\n\nexport function TaskManager({\n basePath,\n kind,\n labels,\n pollMs,\n className,\n compact,\n selectedId,\n onSelectRun,\n extraTabs,\n headerExtra,\n renderRunDetails,\n controlUrl,\n showTaskControls,\n}: TaskManagerProps) {\n const [kindFilter, setKindFilter] = useState<string>(kind ?? \"\");\n const [statusFilter, setStatusFilter] = useState<string>(\"\");\n const [labelFilter, setLabelFilter] = useState(\"\");\n const [filterOptions, setFilterOptions] = useState({ kinds: [] as string[], labels: [] as string[] });\n const [localSelectedId, setLocalSelectedId] = useState<string | null>(null);\n const queryClient = useQueryClient();\n const apiBase = basePath ?? \"/api/v1\";\n const activeKind = kind ?? (kindFilter || undefined);\n const activeLabels = useMemo(() => {\n if (!labelFilter) return labels;\n const selected = splitTagToken(labelFilter);\n return { ...labels, [selected.key]: selected.value };\n }, [labels, labelFilter]);\n const { runs, runsKey, status } = useTaskRuns({\n basePath: apiBase,\n kind: activeKind,\n labels: activeLabels,\n status: statusFilter || undefined,\n pollMs,\n });\n const runsQueryKey = useMemo(\n () => taskQueryKeys.runs({\n basePath: apiBase,\n kind: activeKind,\n status: statusFilter || undefined,\n labels: activeLabels,\n }),\n [activeKind, apiBase, activeLabels, statusFilter],\n );\n\n // Changing a filter re-subscribes the transport but leaves the previous\n // listing on screen until the next SSE/poll frame. Publishing that listing\n // under the new key would hand cache consumers rows that do not match their\n // filters, so mirror only once the delivered listing's own key agrees.\n useEffect(() => {\n if (!runsKey || hashKey(runsKey) !== hashKey(runsQueryKey)) return;\n queryClient.setQueryData(runsQueryKey, runs);\n }, [queryClient, runs, runsKey, runsQueryKey]);\n\n useEffect(() => {\n setFilterOptions((previous) => {\n const kinds = [...new Set([...previous.kinds, ...runs.flatMap((run) => run.kind ? [run.kind] : [])])].sort();\n const labels = [...new Set([...previous.labels, ...runs.flatMap((run) => Object.entries(run.labels ?? {}).map(([key, value]) => `${key}=${value}`))])].sort();\n if (kinds.length === previous.kinds.length && labels.length === previous.labels.length) return previous;\n return { kinds, labels };\n });\n }, [runs]);\n const filters: FilterBarFilter[] = [\n ...(!kind ? [{ key: \"kind\", kind: \"enum\" as const, label: \"Kind\", placeholder: \"All kinds\", value: kindFilter, options: filterOptions.kinds.map((value) => ({ value, label: applicationLabel(value) })), onChange: setKindFilter }] : []),\n { key: \"status\", kind: \"enum\", label: \"Status\", placeholder: \"Any status\", value: statusFilter, options: [\n { value: \"running\", label: \"Running\" },\n { value: \"pending\", label: \"Pending\" },\n { value: \"success\", label: \"Success\" },\n { value: \"failed\", label: \"Failed\" },\n { value: \"warning\", label: \"Warning\" },\n { value: \"canceled\", label: \"Canceled\" },\n ], onChange: setStatusFilter },\n { key: \"labels\", kind: \"enum\", label: \"Labels\", placeholder: \"Any label\", value: labelFilter,\n options: filterOptions.labels.filter((token) => !(splitTagToken(token).key in (labels ?? {}))).map((value) => ({ value })),\n onChange: setLabelFilter },\n ];\n const rows = useMemo<RunTableRow[]>(() => runs.map((run) => ({\n id: run.id,\n name: run.name,\n kind: run.kind || \"other\",\n status: run.status,\n ...(run.startedAt ? { startedAt: run.startedAt } : {}),\n ...(run.finishedAt ? { finishedAt: run.finishedAt } : {}),\n ...(run.labels ? { labels: run.labels } : {}),\n run,\n })), [runs]);\n const openId = onSelectRun ? selectedId : localSelectedId;\n\n return (\n <DataTable\n {...(className ? { className } : {})}\n data={rows}\n columns={compact ? compactRunColumns : runColumns}\n emptyMessage=\"No background tasks.\"\n externalFilters={filters}\n filterBarProps={{ trailing: <span className=\"text-xs text-muted-foreground\">{status}</span> }}\n getRowId={(row) => row.id}\n defaultSort={{ key: \"startedAt\", dir: \"desc\" }}\n expandedRowIds={openId ? [openId] : []}\n onExpandedRowIdsChange={(ids) => {\n const next = ids.find((id) => id !== openId) ?? null;\n if (onSelectRun) onSelectRun(next);\n else setLocalSelectedId(next);\n }}\n renderExpandedRow={(row) => (\n <ExpandedRun\n run={row.run}\n basePath={basePath}\n pollMs={pollMs}\n runsQueryKey={runsQueryKey}\n extraTabs={extraTabs}\n headerExtra={headerExtra}\n renderRunDetails={renderRunDetails}\n controlUrl={controlUrl}\n showTaskControls={showTaskControls}\n />\n )}\n />\n );\n}\n\nfunction RunDuration({ startedAt, finishedAt }: { startedAt: string | undefined; finishedAt: string | undefined }) {\n const now = useResourceClock(!!startedAt && !finishedAt);\n if (!startedAt) return <span className=\"text-muted-foreground\">—</span>;\n return <span className=\"tabular-nums\">{formatDuration(Math.max(0, (finishedAt ? Date.parse(finishedAt) : now) - Date.parse(startedAt)))}</span>;\n}\n\nfunction applicationLabel(kind: string): string {\n const names: Record<string, string> = {\n makerprint: \"MakerPrint\",\n openscad: \"OpenSCAD\",\n orca: \"Orca\",\n snapmaker: \"Snapmaker\",\n other: \"Other\",\n };\n return names[kind] ?? kind;\n}\n\nfunction ExpandedRun({\n run,\n basePath,\n pollMs,\n runsQueryKey,\n extraTabs,\n headerExtra,\n renderRunDetails,\n controlUrl,\n showTaskControls,\n}: {\n run: TaskRunMeta;\n basePath: string | undefined;\n pollMs: number | undefined;\n runsQueryKey: QueryKey;\n extraTabs: TaskExtraTabs | undefined;\n headerExtra: TaskHeaderExtra | undefined;\n renderRunDetails: TaskManagerProps[\"renderRunDetails\"];\n controlUrl: TaskManagerProps[\"controlUrl\"];\n showTaskControls: TaskManagerProps[\"showTaskControls\"];\n}) {\n const runId = run.id;\n const { snapshots, status } = useTaskRun({ id: runId, basePath, pollMs });\n const apiBase = basePath ?? \"/api/v1\";\n const queryClient = useQueryClient();\n const pendingControls = useRef(new Map<string, Promise<void>>());\n const cachedSnapshots = useRef<TaskSnapshot[] | undefined>(undefined);\n const runQueryKey = useMemo(\n () => taskQueryKeys.run({ basePath: apiBase, runId }),\n [apiBase, runId],\n );\n const controlMutation = useMutation({\n mutationKey: taskQueryKeys.control({ basePath: apiBase, runId }),\n mutationFn: postTaskControl,\n onSuccess: async () => {\n await Promise.all([\n queryClient.invalidateQueries({ queryKey: runsQueryKey, exact: true, refetchType: \"none\" }),\n queryClient.invalidateQueries({ queryKey: runQueryKey, exact: true, refetchType: \"none\" }),\n ]);\n },\n });\n\n useEffect(() => {\n const previous = cachedSnapshots.current;\n if (previous?.length === snapshots.length && previous.every((snapshot, index) => snapshot === snapshots[index])) {\n return;\n }\n cachedSnapshots.current = snapshots;\n queryClient.setQueryData(runQueryKey, snapshots);\n }, [queryClient, runQueryKey, snapshots]);\n\n const mutateControl = useCallback((request: TaskControlRequest) => {\n const key = `${request.url}:${request.action}`;\n const pending = pendingControls.current.get(key);\n if (pending) return pending;\n controlMutation.reset();\n const mutation = controlMutation.mutateAsync(request).finally(() => {\n if (pendingControls.current.get(key) === mutation) pendingControls.current.delete(key);\n });\n pendingControls.current.set(key, mutation);\n return mutation;\n }, [controlMutation.mutateAsync, controlMutation.reset]);\n const control = (action: TaskControlAction, group: TaskSnapshot) =>\n mutateControl({\n url: controlUrl?.(run, action) ?? `${apiBase}/tasks/${encodeURIComponent(runId)}/control`,\n action,\n target: group,\n });\n const controlTask = (action: TaskControlAction, task: TaskSnapshot) =>\n mutateControl({\n url: controlUrl?.(run, action) ?? `${apiBase}/tasks/${encodeURIComponent(runId)}/tasks/${encodeURIComponent(task.id)}/control`,\n action,\n target: task,\n });\n const progress = taskWorkProgress(run);\n return (\n <div className=\"space-y-3 bg-muted/20 px-4 py-4\">\n <div className=\"flex flex-wrap gap-x-5 gap-y-1 text-xs text-muted-foreground\">\n <span>{progress.completed} completed</span>\n <span>{progress.running} running</span>\n <span>{progress.failed} failed</span>\n <span>{progress.counts.pending + progress.unstarted} waiting</span>\n {Object.entries(run.labels ?? {}).map(([key, value]) => (\n <span key={key} className=\"font-mono\">{key}={value}</span>\n ))}\n </div>\n {renderRunDetails?.(run, snapshots)}\n <TaskProgress\n snapshots={snapshots}\n compact\n connectionStatus={status}\n initialShowAllTasks\n onControl={control}\n {...(showTaskControls?.(run) !== false ? { onTaskControl: controlTask } : {})}\n metricsBaseUrl={`${apiBase}/tasks/metrics/`}\n {...(extraTabs ? { extraTabs } : {})}\n {...(headerExtra ? { headerExtra } : {})}\n />\n </div>\n );\n}\n\ninterface TaskControlRequest {\n url: string;\n action: TaskControlAction;\n target: TaskSnapshot;\n}\n\nasync function postTaskControl({ url, action, target }: TaskControlRequest) {\n const context = `Failed to ${action} ${target.type} \"${target.name}\"`;\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ action }),\n });\n } catch (cause) {\n throw new Error(`${context}: ${cause instanceof Error ? cause.message : \"request failed\"}`, { cause });\n }\n if (!response.ok) {\n const detail = (await response.text()).trim() || `HTTP ${response.status}`;\n throw new Error(`${context}: ${detail}`);\n }\n}\n"],"mappings":"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"}
|
|
@@ -12,7 +12,7 @@ let react_jsx_runtime = require("react/jsx-runtime");
|
|
|
12
12
|
* every range — which pushed everything else in that menu out of reach and
|
|
13
13
|
* still truncated the one thing worth reading: what each range costs.
|
|
14
14
|
*/
|
|
15
|
-
function ClickyExportDialog({ open, onClose, title = "Export", formats, scopes, onDownload }) {
|
|
15
|
+
function ClickyExportDialog({ open, onClose, title = "Export", formats, scopes, onDownload, pendingContent, allowCloseWhilePending = false }) {
|
|
16
16
|
const [format, setFormat] = (0, react.useState)(formats[0]?.format);
|
|
17
17
|
const [scope, setScope] = (0, react.useState)(scopes[0]?.scope);
|
|
18
18
|
const [pending, setPending] = (0, react.useState)(false);
|
|
@@ -48,8 +48,8 @@ function ClickyExportDialog({ open, onClose, title = "Export", formats, scopes,
|
|
|
48
48
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_button.Button, {
|
|
49
49
|
variant: "outline",
|
|
50
50
|
onClick: onClose,
|
|
51
|
-
disabled: pending,
|
|
52
|
-
children: "Cancel"
|
|
51
|
+
disabled: pending && !allowCloseWhilePending,
|
|
52
|
+
children: pending && allowCloseWhilePending ? "Continue in background" : "Cancel"
|
|
53
53
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_button.Button, {
|
|
54
54
|
onClick: () => void download(),
|
|
55
55
|
loading: pending,
|
|
@@ -60,6 +60,7 @@ function ClickyExportDialog({ open, onClose, title = "Export", formats, scopes,
|
|
|
60
60
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
61
61
|
className: "space-y-4",
|
|
62
62
|
children: [
|
|
63
|
+
pending && pendingContent,
|
|
63
64
|
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
64
65
|
role: "alert",
|
|
65
66
|
className: "rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClickyExportDialog.cjs","names":["useState","Modal","Button","cn","Icon"],"sources":["../../../src/data/clicky-export/ClickyExportDialog.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { Button } from \"../../components/button\";\nimport { Modal } from \"../../overlay/Modal\";\nimport { Icon, type StaticIconComponent } from \"../Icon\";\nimport { cn } from \"../../lib/utils\";\n\n/** One format the endpoint advertises, already resolved for display. */\nexport type ClickyExportFormatOption = {\n /** Value handed back to `onDownload`. */\n format: string;\n label: string;\n /** What the format is for. */\n description?: string | undefined;\n icon?: StaticIconComponent | undefined;\n iconClassName?: string | undefined;\n};\n\n/** One result range the endpoint advertises. */\nexport type ClickyExportScopeOption = {\n /** Value handed back to `onDownload`; undefined for a legacy endpoint. */\n scope: string | undefined;\n label: string;\n /**\n * What this range costs or caps — \"Limited to 1,000 rows\", \"Streams rows as\n * they are read\". Per format, because a PDF's ceiling is not a CSV's.\n */\n note?: ((format: string) => string | undefined) | undefined;\n};\n\nexport type ClickyExportDialogProps = {\n open: boolean;\n onClose: () => void;\n title?: string;\n formats: ClickyExportFormatOption[];\n scopes: ClickyExportScopeOption[];\n /** Runs the export. Rejecting shows the reason without closing the dialog. */\n onDownload: (request: {\n format: string;\n scope: string | undefined;\n }) => Promise<void> | void;\n};\n\n/**\n * ClickyExportDialog picks a format and a range, then downloads.\n *\n * It exists because the same choice as a menu was 16 rows — every format times\n * every range — which pushed everything else in that menu out of reach and\n * still truncated the one thing worth reading: what each range costs.\n */\nexport function ClickyExportDialog({\n open,\n onClose,\n title = \"Export\",\n formats,\n scopes,\n onDownload,\n}: ClickyExportDialogProps) {\n const [format, setFormat] = useState(formats[0]?.format);\n const [scope, setScope] = useState(scopes[0]?.scope);\n const [pending, setPending] = useState(false);\n const [error, setError] = useState(\"\");\n\n // Reopening starts from a clean slate: the error belongs to the attempt that\n // produced it, not to the next one.\n useEffect(() => {\n if (open) setError(\"\");\n }, [open]);\n\n const activeScope = scopes.find((option) => option.scope === scope);\n\n const download = async () => {\n if (!format) return;\n setPending(true);\n setError(\"\");\n try {\n await onDownload({ format, scope });\n onClose();\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : \"The export failed\");\n } finally {\n setPending(false);\n }\n };\n\n return (\n <Modal\n open={open}\n onClose={onClose}\n title={title}\n size=\"lg\"\n closeOnEsc\n footer={\n <div className=\"flex items-center justify-end gap-2\">\n <Button variant=\"outline\" onClick={onClose} disabled={pending}>\n Cancel\n </Button>\n <Button\n onClick={() => void download()}\n loading={pending}\n disabled={!format}\n >\n Download\n </Button>\n </div>\n }\n >\n <div className=\"space-y-4\">\n {error && (\n <div\n role=\"alert\"\n className=\"rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive\"\n >\n {error}\n </div>\n )}\n\n {scopes.length > 1 ? (\n <fieldset>\n <legend className=\"mb-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground\">\n Rows\n </legend>\n <div className=\"flex flex-wrap gap-2\">\n {scopes.map((option) => (\n <label\n key={option.scope ?? \"default\"}\n className={cn(\n \"cursor-pointer rounded-md border px-3 py-1.5 text-sm transition-colors\",\n option.scope === scope\n ? \"border-foreground/20 bg-accent text-accent-foreground\"\n : \"border-input hover:bg-accent hover:text-accent-foreground\",\n )}\n >\n <input\n type=\"radio\"\n name=\"clicky-export-scope\"\n // Named explicitly: the input is visually hidden behind its\n // own styled label, so nothing else gives it a name.\n aria-label={option.label}\n className=\"sr-only\"\n checked={option.scope === scope}\n onChange={() => setScope(option.scope)}\n />\n {option.label}\n </label>\n ))}\n </div>\n </fieldset>\n ) : (\n scopes[0]?.label && (\n <p className=\"text-sm text-muted-foreground\">{scopes[0].label}</p>\n )\n )}\n\n <fieldset>\n <legend className=\"mb-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground\">\n Format\n </legend>\n <div className=\"grid gap-1 sm:grid-cols-2\">\n {formats.map((option) => {\n const note = activeScope?.note?.(option.format);\n return (\n <label\n key={option.format}\n className={cn(\n \"flex cursor-pointer items-start gap-2 rounded-md border px-3 py-2 text-left transition-colors\",\n option.format === format\n ? \"border-foreground/20 bg-accent text-accent-foreground\"\n : \"border-transparent hover:bg-accent hover:text-accent-foreground\",\n )}\n >\n <input\n type=\"radio\"\n name=\"clicky-export-format\"\n aria-label={option.label}\n className=\"sr-only\"\n checked={option.format === format}\n onChange={() => setFormat(option.format)}\n />\n {option.icon && (\n <Icon\n icon={option.icon}\n className={cn(\n \"mt-0.5 shrink-0 text-sm\",\n option.iconClassName ?? \"text-muted-foreground\",\n )}\n />\n )}\n <span className=\"min-w-0\">\n <span className=\"block text-sm font-medium\">\n {option.label}\n </span>\n {option.description && (\n <span className=\"mt-0.5 block text-xs text-muted-foreground\">\n {option.description}\n </span>\n )}\n {note && (\n <span className=\"mt-0.5 block text-xs text-amber-700 [[data-theme=dark]_&]:text-amber-400\">\n {note}\n </span>\n )}\n </span>\n </label>\n );\n })}\n </div>\n </fieldset>\n\n </div>\n </Modal>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ClickyExportDialog.cjs","names":["useState","Modal","Button","cn","Icon"],"sources":["../../../src/data/clicky-export/ClickyExportDialog.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Button } from \"../../components/button\";\nimport { Modal } from \"../../overlay/Modal\";\nimport { Icon, type StaticIconComponent } from \"../Icon\";\nimport { cn } from \"../../lib/utils\";\n\n/** One format the endpoint advertises, already resolved for display. */\nexport type ClickyExportFormatOption = {\n /** Value handed back to `onDownload`. */\n format: string;\n label: string;\n /** What the format is for. */\n description?: string | undefined;\n icon?: StaticIconComponent | undefined;\n iconClassName?: string | undefined;\n};\n\n/** One result range the endpoint advertises. */\nexport type ClickyExportScopeOption = {\n /** Value handed back to `onDownload`; undefined for a legacy endpoint. */\n scope: string | undefined;\n label: string;\n /**\n * What this range costs or caps — \"Limited to 1,000 rows\", \"Streams rows as\n * they are read\". Per format, because a PDF's ceiling is not a CSV's.\n */\n note?: ((format: string) => string | undefined) | undefined;\n};\n\nexport type ClickyExportDialogProps = {\n open: boolean;\n onClose: () => void;\n title?: string;\n formats: ClickyExportFormatOption[];\n scopes: ClickyExportScopeOption[];\n pendingContent?: ReactNode;\n allowCloseWhilePending?: boolean;\n /** Runs the export. Rejecting shows the reason without closing the dialog. */\n onDownload: (request: {\n format: string;\n scope: string | undefined;\n }) => Promise<void> | void;\n};\n\n/**\n * ClickyExportDialog picks a format and a range, then downloads.\n *\n * It exists because the same choice as a menu was 16 rows — every format times\n * every range — which pushed everything else in that menu out of reach and\n * still truncated the one thing worth reading: what each range costs.\n */\nexport function ClickyExportDialog({\n open,\n onClose,\n title = \"Export\",\n formats,\n scopes,\n onDownload,\n pendingContent,\n allowCloseWhilePending = false,\n}: ClickyExportDialogProps) {\n const [format, setFormat] = useState(formats[0]?.format);\n const [scope, setScope] = useState(scopes[0]?.scope);\n const [pending, setPending] = useState(false);\n const [error, setError] = useState(\"\");\n\n // Reopening starts from a clean slate: the error belongs to the attempt that\n // produced it, not to the next one.\n useEffect(() => {\n if (open) setError(\"\");\n }, [open]);\n\n const activeScope = scopes.find((option) => option.scope === scope);\n\n const download = async () => {\n if (!format) return;\n setPending(true);\n setError(\"\");\n try {\n await onDownload({ format, scope });\n onClose();\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : \"The export failed\");\n } finally {\n setPending(false);\n }\n };\n\n return (\n <Modal\n open={open}\n onClose={onClose}\n title={title}\n size=\"lg\"\n closeOnEsc\n footer={\n <div className=\"flex items-center justify-end gap-2\">\n <Button variant=\"outline\" onClick={onClose} disabled={pending && !allowCloseWhilePending}>\n {pending && allowCloseWhilePending ? \"Continue in background\" : \"Cancel\"}\n </Button>\n <Button\n onClick={() => void download()}\n loading={pending}\n disabled={!format}\n >\n Download\n </Button>\n </div>\n }\n >\n <div className=\"space-y-4\">\n {pending && pendingContent}\n {error && (\n <div\n role=\"alert\"\n className=\"rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive\"\n >\n {error}\n </div>\n )}\n\n {scopes.length > 1 ? (\n <fieldset>\n <legend className=\"mb-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground\">\n Rows\n </legend>\n <div className=\"flex flex-wrap gap-2\">\n {scopes.map((option) => (\n <label\n key={option.scope ?? \"default\"}\n className={cn(\n \"cursor-pointer rounded-md border px-3 py-1.5 text-sm transition-colors\",\n option.scope === scope\n ? \"border-foreground/20 bg-accent text-accent-foreground\"\n : \"border-input hover:bg-accent hover:text-accent-foreground\",\n )}\n >\n <input\n type=\"radio\"\n name=\"clicky-export-scope\"\n // Named explicitly: the input is visually hidden behind its\n // own styled label, so nothing else gives it a name.\n aria-label={option.label}\n className=\"sr-only\"\n checked={option.scope === scope}\n onChange={() => setScope(option.scope)}\n />\n {option.label}\n </label>\n ))}\n </div>\n </fieldset>\n ) : (\n scopes[0]?.label && (\n <p className=\"text-sm text-muted-foreground\">{scopes[0].label}</p>\n )\n )}\n\n <fieldset>\n <legend className=\"mb-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground\">\n Format\n </legend>\n <div className=\"grid gap-1 sm:grid-cols-2\">\n {formats.map((option) => {\n const note = activeScope?.note?.(option.format);\n return (\n <label\n key={option.format}\n className={cn(\n \"flex cursor-pointer items-start gap-2 rounded-md border px-3 py-2 text-left transition-colors\",\n option.format === format\n ? \"border-foreground/20 bg-accent text-accent-foreground\"\n : \"border-transparent hover:bg-accent hover:text-accent-foreground\",\n )}\n >\n <input\n type=\"radio\"\n name=\"clicky-export-format\"\n aria-label={option.label}\n className=\"sr-only\"\n checked={option.format === format}\n onChange={() => setFormat(option.format)}\n />\n {option.icon && (\n <Icon\n icon={option.icon}\n className={cn(\n \"mt-0.5 shrink-0 text-sm\",\n option.iconClassName ?? \"text-muted-foreground\",\n )}\n />\n )}\n <span className=\"min-w-0\">\n <span className=\"block text-sm font-medium\">\n {option.label}\n </span>\n {option.description && (\n <span className=\"mt-0.5 block text-xs text-muted-foreground\">\n {option.description}\n </span>\n )}\n {note && (\n <span className=\"mt-0.5 block text-xs text-amber-700 [[data-theme=dark]_&]:text-amber-400\">\n {note}\n </span>\n )}\n </span>\n </label>\n );\n })}\n </div>\n </fieldset>\n\n </div>\n </Modal>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
1
2
|
import { StaticIconComponent } from '../Icon.js';
|
|
2
3
|
/** One format the endpoint advertises, already resolved for display. */
|
|
3
4
|
export type ClickyExportFormatOption = {
|
|
@@ -26,6 +27,8 @@ export type ClickyExportDialogProps = {
|
|
|
26
27
|
title?: string;
|
|
27
28
|
formats: ClickyExportFormatOption[];
|
|
28
29
|
scopes: ClickyExportScopeOption[];
|
|
30
|
+
pendingContent?: ReactNode;
|
|
31
|
+
allowCloseWhilePending?: boolean;
|
|
29
32
|
/** Runs the export. Rejecting shows the reason without closing the dialog. */
|
|
30
33
|
onDownload: (request: {
|
|
31
34
|
format: string;
|
|
@@ -39,5 +42,5 @@ export type ClickyExportDialogProps = {
|
|
|
39
42
|
* every range — which pushed everything else in that menu out of reach and
|
|
40
43
|
* still truncated the one thing worth reading: what each range costs.
|
|
41
44
|
*/
|
|
42
|
-
export declare function ClickyExportDialog({ open, onClose, title, formats, scopes, onDownload, }: ClickyExportDialogProps): import("react").JSX.Element;
|
|
45
|
+
export declare function ClickyExportDialog({ open, onClose, title, formats, scopes, onDownload, pendingContent, allowCloseWhilePending, }: ClickyExportDialogProps): import("react").JSX.Element;
|
|
43
46
|
//# sourceMappingURL=ClickyExportDialog.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClickyExportDialog.d.ts","sourceRoot":"","sources":["../../../src/data/clicky-export/ClickyExportDialog.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ClickyExportDialog.d.ts","sourceRoot":"","sources":["../../../src/data/clicky-export/ClickyExportDialog.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,OAAO,EAAQ,KAAK,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAGzD,wEAAwE;AACxE,MAAM,MAAM,wBAAwB,GAAG;IACrC,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,IAAI,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;IACvC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC,CAAC;AAEF,gDAAgD;AAChD,MAAM,MAAM,uBAAuB,GAAG;IACpC,0EAA0E;IAC1E,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;CAC7D,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,wBAAwB,EAAE,CAAC;IACpC,MAAM,EAAE,uBAAuB,EAAE,CAAC;IAClC,cAAc,CAAC,EAAE,SAAS,CAAC;IAC3B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,8EAA8E;IAC9E,UAAU,EAAE,CAAC,OAAO,EAAE;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;KAC3B,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,OAAO,EACP,KAAgB,EAChB,OAAO,EACP,MAAM,EACN,UAAU,EACV,cAAc,EACd,sBAA8B,GAC/B,EAAE,uBAAuB,+BA4JzB"}
|
|
@@ -12,7 +12,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* every range — which pushed everything else in that menu out of reach and
|
|
13
13
|
* still truncated the one thing worth reading: what each range costs.
|
|
14
14
|
*/
|
|
15
|
-
function ClickyExportDialog({ open, onClose, title = "Export", formats, scopes, onDownload }) {
|
|
15
|
+
function ClickyExportDialog({ open, onClose, title = "Export", formats, scopes, onDownload, pendingContent, allowCloseWhilePending = false }) {
|
|
16
16
|
const [format, setFormat] = useState(formats[0]?.format);
|
|
17
17
|
const [scope, setScope] = useState(scopes[0]?.scope);
|
|
18
18
|
const [pending, setPending] = useState(false);
|
|
@@ -48,8 +48,8 @@ function ClickyExportDialog({ open, onClose, title = "Export", formats, scopes,
|
|
|
48
48
|
children: [/* @__PURE__ */ jsx(Button, {
|
|
49
49
|
variant: "outline",
|
|
50
50
|
onClick: onClose,
|
|
51
|
-
disabled: pending,
|
|
52
|
-
children: "Cancel"
|
|
51
|
+
disabled: pending && !allowCloseWhilePending,
|
|
52
|
+
children: pending && allowCloseWhilePending ? "Continue in background" : "Cancel"
|
|
53
53
|
}), /* @__PURE__ */ jsx(Button, {
|
|
54
54
|
onClick: () => void download(),
|
|
55
55
|
loading: pending,
|
|
@@ -60,6 +60,7 @@ function ClickyExportDialog({ open, onClose, title = "Export", formats, scopes,
|
|
|
60
60
|
children: /* @__PURE__ */ jsxs("div", {
|
|
61
61
|
className: "space-y-4",
|
|
62
62
|
children: [
|
|
63
|
+
pending && pendingContent,
|
|
63
64
|
error && /* @__PURE__ */ jsx("div", {
|
|
64
65
|
role: "alert",
|
|
65
66
|
className: "rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClickyExportDialog.js","names":[],"sources":["../../../src/data/clicky-export/ClickyExportDialog.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { Button } from \"../../components/button\";\nimport { Modal } from \"../../overlay/Modal\";\nimport { Icon, type StaticIconComponent } from \"../Icon\";\nimport { cn } from \"../../lib/utils\";\n\n/** One format the endpoint advertises, already resolved for display. */\nexport type ClickyExportFormatOption = {\n /** Value handed back to `onDownload`. */\n format: string;\n label: string;\n /** What the format is for. */\n description?: string | undefined;\n icon?: StaticIconComponent | undefined;\n iconClassName?: string | undefined;\n};\n\n/** One result range the endpoint advertises. */\nexport type ClickyExportScopeOption = {\n /** Value handed back to `onDownload`; undefined for a legacy endpoint. */\n scope: string | undefined;\n label: string;\n /**\n * What this range costs or caps — \"Limited to 1,000 rows\", \"Streams rows as\n * they are read\". Per format, because a PDF's ceiling is not a CSV's.\n */\n note?: ((format: string) => string | undefined) | undefined;\n};\n\nexport type ClickyExportDialogProps = {\n open: boolean;\n onClose: () => void;\n title?: string;\n formats: ClickyExportFormatOption[];\n scopes: ClickyExportScopeOption[];\n /** Runs the export. Rejecting shows the reason without closing the dialog. */\n onDownload: (request: {\n format: string;\n scope: string | undefined;\n }) => Promise<void> | void;\n};\n\n/**\n * ClickyExportDialog picks a format and a range, then downloads.\n *\n * It exists because the same choice as a menu was 16 rows — every format times\n * every range — which pushed everything else in that menu out of reach and\n * still truncated the one thing worth reading: what each range costs.\n */\nexport function ClickyExportDialog({\n open,\n onClose,\n title = \"Export\",\n formats,\n scopes,\n onDownload,\n}: ClickyExportDialogProps) {\n const [format, setFormat] = useState(formats[0]?.format);\n const [scope, setScope] = useState(scopes[0]?.scope);\n const [pending, setPending] = useState(false);\n const [error, setError] = useState(\"\");\n\n // Reopening starts from a clean slate: the error belongs to the attempt that\n // produced it, not to the next one.\n useEffect(() => {\n if (open) setError(\"\");\n }, [open]);\n\n const activeScope = scopes.find((option) => option.scope === scope);\n\n const download = async () => {\n if (!format) return;\n setPending(true);\n setError(\"\");\n try {\n await onDownload({ format, scope });\n onClose();\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : \"The export failed\");\n } finally {\n setPending(false);\n }\n };\n\n return (\n <Modal\n open={open}\n onClose={onClose}\n title={title}\n size=\"lg\"\n closeOnEsc\n footer={\n <div className=\"flex items-center justify-end gap-2\">\n <Button variant=\"outline\" onClick={onClose} disabled={pending}>\n Cancel\n </Button>\n <Button\n onClick={() => void download()}\n loading={pending}\n disabled={!format}\n >\n Download\n </Button>\n </div>\n }\n >\n <div className=\"space-y-4\">\n {error && (\n <div\n role=\"alert\"\n className=\"rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive\"\n >\n {error}\n </div>\n )}\n\n {scopes.length > 1 ? (\n <fieldset>\n <legend className=\"mb-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground\">\n Rows\n </legend>\n <div className=\"flex flex-wrap gap-2\">\n {scopes.map((option) => (\n <label\n key={option.scope ?? \"default\"}\n className={cn(\n \"cursor-pointer rounded-md border px-3 py-1.5 text-sm transition-colors\",\n option.scope === scope\n ? \"border-foreground/20 bg-accent text-accent-foreground\"\n : \"border-input hover:bg-accent hover:text-accent-foreground\",\n )}\n >\n <input\n type=\"radio\"\n name=\"clicky-export-scope\"\n // Named explicitly: the input is visually hidden behind its\n // own styled label, so nothing else gives it a name.\n aria-label={option.label}\n className=\"sr-only\"\n checked={option.scope === scope}\n onChange={() => setScope(option.scope)}\n />\n {option.label}\n </label>\n ))}\n </div>\n </fieldset>\n ) : (\n scopes[0]?.label && (\n <p className=\"text-sm text-muted-foreground\">{scopes[0].label}</p>\n )\n )}\n\n <fieldset>\n <legend className=\"mb-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground\">\n Format\n </legend>\n <div className=\"grid gap-1 sm:grid-cols-2\">\n {formats.map((option) => {\n const note = activeScope?.note?.(option.format);\n return (\n <label\n key={option.format}\n className={cn(\n \"flex cursor-pointer items-start gap-2 rounded-md border px-3 py-2 text-left transition-colors\",\n option.format === format\n ? \"border-foreground/20 bg-accent text-accent-foreground\"\n : \"border-transparent hover:bg-accent hover:text-accent-foreground\",\n )}\n >\n <input\n type=\"radio\"\n name=\"clicky-export-format\"\n aria-label={option.label}\n className=\"sr-only\"\n checked={option.format === format}\n onChange={() => setFormat(option.format)}\n />\n {option.icon && (\n <Icon\n icon={option.icon}\n className={cn(\n \"mt-0.5 shrink-0 text-sm\",\n option.iconClassName ?? \"text-muted-foreground\",\n )}\n />\n )}\n <span className=\"min-w-0\">\n <span className=\"block text-sm font-medium\">\n {option.label}\n </span>\n {option.description && (\n <span className=\"mt-0.5 block text-xs text-muted-foreground\">\n {option.description}\n </span>\n )}\n {note && (\n <span className=\"mt-0.5 block text-xs text-amber-700 [[data-theme=dark]_&]:text-amber-400\">\n {note}\n </span>\n )}\n </span>\n </label>\n );\n })}\n </div>\n </fieldset>\n\n </div>\n </Modal>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ClickyExportDialog.js","names":[],"sources":["../../../src/data/clicky-export/ClickyExportDialog.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Button } from \"../../components/button\";\nimport { Modal } from \"../../overlay/Modal\";\nimport { Icon, type StaticIconComponent } from \"../Icon\";\nimport { cn } from \"../../lib/utils\";\n\n/** One format the endpoint advertises, already resolved for display. */\nexport type ClickyExportFormatOption = {\n /** Value handed back to `onDownload`. */\n format: string;\n label: string;\n /** What the format is for. */\n description?: string | undefined;\n icon?: StaticIconComponent | undefined;\n iconClassName?: string | undefined;\n};\n\n/** One result range the endpoint advertises. */\nexport type ClickyExportScopeOption = {\n /** Value handed back to `onDownload`; undefined for a legacy endpoint. */\n scope: string | undefined;\n label: string;\n /**\n * What this range costs or caps — \"Limited to 1,000 rows\", \"Streams rows as\n * they are read\". Per format, because a PDF's ceiling is not a CSV's.\n */\n note?: ((format: string) => string | undefined) | undefined;\n};\n\nexport type ClickyExportDialogProps = {\n open: boolean;\n onClose: () => void;\n title?: string;\n formats: ClickyExportFormatOption[];\n scopes: ClickyExportScopeOption[];\n pendingContent?: ReactNode;\n allowCloseWhilePending?: boolean;\n /** Runs the export. Rejecting shows the reason without closing the dialog. */\n onDownload: (request: {\n format: string;\n scope: string | undefined;\n }) => Promise<void> | void;\n};\n\n/**\n * ClickyExportDialog picks a format and a range, then downloads.\n *\n * It exists because the same choice as a menu was 16 rows — every format times\n * every range — which pushed everything else in that menu out of reach and\n * still truncated the one thing worth reading: what each range costs.\n */\nexport function ClickyExportDialog({\n open,\n onClose,\n title = \"Export\",\n formats,\n scopes,\n onDownload,\n pendingContent,\n allowCloseWhilePending = false,\n}: ClickyExportDialogProps) {\n const [format, setFormat] = useState(formats[0]?.format);\n const [scope, setScope] = useState(scopes[0]?.scope);\n const [pending, setPending] = useState(false);\n const [error, setError] = useState(\"\");\n\n // Reopening starts from a clean slate: the error belongs to the attempt that\n // produced it, not to the next one.\n useEffect(() => {\n if (open) setError(\"\");\n }, [open]);\n\n const activeScope = scopes.find((option) => option.scope === scope);\n\n const download = async () => {\n if (!format) return;\n setPending(true);\n setError(\"\");\n try {\n await onDownload({ format, scope });\n onClose();\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : \"The export failed\");\n } finally {\n setPending(false);\n }\n };\n\n return (\n <Modal\n open={open}\n onClose={onClose}\n title={title}\n size=\"lg\"\n closeOnEsc\n footer={\n <div className=\"flex items-center justify-end gap-2\">\n <Button variant=\"outline\" onClick={onClose} disabled={pending && !allowCloseWhilePending}>\n {pending && allowCloseWhilePending ? \"Continue in background\" : \"Cancel\"}\n </Button>\n <Button\n onClick={() => void download()}\n loading={pending}\n disabled={!format}\n >\n Download\n </Button>\n </div>\n }\n >\n <div className=\"space-y-4\">\n {pending && pendingContent}\n {error && (\n <div\n role=\"alert\"\n className=\"rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive\"\n >\n {error}\n </div>\n )}\n\n {scopes.length > 1 ? (\n <fieldset>\n <legend className=\"mb-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground\">\n Rows\n </legend>\n <div className=\"flex flex-wrap gap-2\">\n {scopes.map((option) => (\n <label\n key={option.scope ?? \"default\"}\n className={cn(\n \"cursor-pointer rounded-md border px-3 py-1.5 text-sm transition-colors\",\n option.scope === scope\n ? \"border-foreground/20 bg-accent text-accent-foreground\"\n : \"border-input hover:bg-accent hover:text-accent-foreground\",\n )}\n >\n <input\n type=\"radio\"\n name=\"clicky-export-scope\"\n // Named explicitly: the input is visually hidden behind its\n // own styled label, so nothing else gives it a name.\n aria-label={option.label}\n className=\"sr-only\"\n checked={option.scope === scope}\n onChange={() => setScope(option.scope)}\n />\n {option.label}\n </label>\n ))}\n </div>\n </fieldset>\n ) : (\n scopes[0]?.label && (\n <p className=\"text-sm text-muted-foreground\">{scopes[0].label}</p>\n )\n )}\n\n <fieldset>\n <legend className=\"mb-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground\">\n Format\n </legend>\n <div className=\"grid gap-1 sm:grid-cols-2\">\n {formats.map((option) => {\n const note = activeScope?.note?.(option.format);\n return (\n <label\n key={option.format}\n className={cn(\n \"flex cursor-pointer items-start gap-2 rounded-md border px-3 py-2 text-left transition-colors\",\n option.format === format\n ? \"border-foreground/20 bg-accent text-accent-foreground\"\n : \"border-transparent hover:bg-accent hover:text-accent-foreground\",\n )}\n >\n <input\n type=\"radio\"\n name=\"clicky-export-format\"\n aria-label={option.label}\n className=\"sr-only\"\n checked={option.format === format}\n onChange={() => setFormat(option.format)}\n />\n {option.icon && (\n <Icon\n icon={option.icon}\n className={cn(\n \"mt-0.5 shrink-0 text-sm\",\n option.iconClassName ?? \"text-muted-foreground\",\n )}\n />\n )}\n <span className=\"min-w-0\">\n <span className=\"block text-sm font-medium\">\n {option.label}\n </span>\n {option.description && (\n <span className=\"mt-0.5 block text-xs text-muted-foreground\">\n {option.description}\n </span>\n )}\n {note && (\n <span className=\"mt-0.5 block text-xs text-amber-700 [[data-theme=dark]_&]:text-amber-400\">\n {note}\n </span>\n )}\n </span>\n </label>\n );\n })}\n </div>\n </fieldset>\n\n </div>\n </Modal>\n );\n}\n"],"mappings":"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"}
|
|
@@ -14,9 +14,9 @@ function detectMode() {
|
|
|
14
14
|
/** Resolve build + runtime version metadata. */
|
|
15
15
|
function getVersionInfo() {
|
|
16
16
|
return {
|
|
17
|
-
commit: read(() => "
|
|
18
|
-
tag: read(() => "clicky-ui@0.3.
|
|
19
|
-
date: read(() => "2026-10-
|
|
17
|
+
commit: read(() => "84bbbe9a3", ""),
|
|
18
|
+
tag: read(() => "clicky-ui@0.3.53", ""),
|
|
19
|
+
date: read(() => "2026-10-27T17:38:02.772Z", ""),
|
|
20
20
|
dirty: read(() => true, false),
|
|
21
21
|
mode: detectMode()
|
|
22
22
|
};
|
|
@@ -14,9 +14,9 @@ function detectMode() {
|
|
|
14
14
|
/** Resolve build + runtime version metadata. */
|
|
15
15
|
function getVersionInfo() {
|
|
16
16
|
return {
|
|
17
|
-
commit: read(() => "
|
|
18
|
-
tag: read(() => "clicky-ui@0.3.
|
|
19
|
-
date: read(() => "2026-10-
|
|
17
|
+
commit: read(() => "84bbbe9a3", ""),
|
|
18
|
+
tag: read(() => "clicky-ui@0.3.53", ""),
|
|
19
|
+
date: read(() => "2026-10-27T17:37:40.605Z", ""),
|
|
20
20
|
dirty: read(() => true, false),
|
|
21
21
|
mode: detectMode()
|
|
22
22
|
};
|
|
@@ -13,13 +13,15 @@ function hasEventSource() {
|
|
|
13
13
|
return typeof globalThis !== "undefined" && typeof globalThis.EventSource !== "undefined";
|
|
14
14
|
}
|
|
15
15
|
function useTaskRun(options = {}) {
|
|
16
|
-
const { id, ids, kind, basePath = DEFAULT_BASE, enabled = true, pollMs = DEFAULT_POLL_MS, forcePoll = false } = options;
|
|
16
|
+
const { id, ids, kind, basePath = DEFAULT_BASE, enabled = true, pollMs = DEFAULT_POLL_MS, forcePoll = false, headers } = options;
|
|
17
|
+
const headersKey = JSON.stringify(headers ?? {});
|
|
17
18
|
const runIdsKey = (ids?.filter(Boolean) ?? (id ? [id] : [])).join(",");
|
|
18
19
|
const subscriptionKey = JSON.stringify([
|
|
19
20
|
runIdsKey,
|
|
20
21
|
kind ?? "",
|
|
21
22
|
basePath,
|
|
22
|
-
enabled
|
|
23
|
+
enabled,
|
|
24
|
+
headersKey
|
|
23
25
|
]);
|
|
24
26
|
const [stateKey, setStateKey] = (0, react.useState)(subscriptionKey);
|
|
25
27
|
const [byId, setById] = (0, react.useState)({});
|
|
@@ -30,6 +32,7 @@ function useTaskRun(options = {}) {
|
|
|
30
32
|
const eventSourceFactoryRef = (0, react.useRef)(eventSourceFactory);
|
|
31
33
|
eventSourceFactoryRef.current = eventSourceFactory;
|
|
32
34
|
(0, react.useEffect)(() => {
|
|
35
|
+
const requestHeaders = JSON.parse(headersKey);
|
|
33
36
|
const runIds = runIdsKey ? runIdsKey.split(",") : [];
|
|
34
37
|
setStateKey(subscriptionKey);
|
|
35
38
|
if (!enabled || runIds.length === 0 && !kind) {
|
|
@@ -59,12 +62,15 @@ function useTaskRun(options = {}) {
|
|
|
59
62
|
[delta.id]: require_task_streams.applyTaskOutputDelta(prev[delta.id] ?? require_task_streams.emptyTaskStreams, delta)
|
|
60
63
|
}));
|
|
61
64
|
};
|
|
62
|
-
if (forcePoll || !hasEventSource()) {
|
|
65
|
+
if (forcePoll || headersKey !== "{}" || !hasEventSource()) {
|
|
63
66
|
let stopped = false;
|
|
64
67
|
let timer;
|
|
65
68
|
const tick = async () => {
|
|
66
69
|
try {
|
|
67
|
-
const responses = await Promise.all(runIds.map((runId) => fetch(`${basePath}/tasks/${encodeURIComponent(runId)}`, { headers: {
|
|
70
|
+
const responses = await Promise.all(runIds.map((runId) => fetch(`${basePath}/tasks/${encodeURIComponent(runId)}`, { headers: {
|
|
71
|
+
Accept: "application/json",
|
|
72
|
+
...requestHeaders
|
|
73
|
+
} })));
|
|
68
74
|
if (responses.some((response) => !response.ok)) throw new Error("Task request failed");
|
|
69
75
|
if (responses.every((response) => response.ok)) {
|
|
70
76
|
const snaps = (await Promise.all(responses.map(async (response) => await response.json()))).flat();
|
|
@@ -119,7 +125,8 @@ function useTaskRun(options = {}) {
|
|
|
119
125
|
basePath,
|
|
120
126
|
enabled,
|
|
121
127
|
pollMs,
|
|
122
|
-
forcePoll
|
|
128
|
+
forcePoll,
|
|
129
|
+
headersKey
|
|
123
130
|
]);
|
|
124
131
|
const current = stateKey === subscriptionKey;
|
|
125
132
|
return {
|
|
@@ -133,7 +140,8 @@ function useTaskRun(options = {}) {
|
|
|
133
140
|
};
|
|
134
141
|
}
|
|
135
142
|
function useTaskRuns(options = {}) {
|
|
136
|
-
const { kind, status: statusFilter, labels, basePath = DEFAULT_BASE, enabled = true, pollMs = DEFAULT_POLL_MS, forcePoll = false } = options;
|
|
143
|
+
const { kind, status: statusFilter, labels, basePath = DEFAULT_BASE, enabled = true, pollMs = DEFAULT_POLL_MS, forcePoll = false, headers } = options;
|
|
144
|
+
const headersKey = JSON.stringify(headers ?? {});
|
|
137
145
|
const [received, setReceived] = (0, react.useState)({
|
|
138
146
|
key: null,
|
|
139
147
|
runs: []
|
|
@@ -144,6 +152,7 @@ function useTaskRuns(options = {}) {
|
|
|
144
152
|
eventSourceFactoryRef.current = eventSourceFactory;
|
|
145
153
|
(0, react.useEffect)(() => {
|
|
146
154
|
if (!enabled) return;
|
|
155
|
+
const requestHeaders = JSON.parse(headersKey);
|
|
147
156
|
const params = new URLSearchParams();
|
|
148
157
|
if (kind) params.set("kind", kind);
|
|
149
158
|
if (statusFilter) params.set("status", statusFilter);
|
|
@@ -159,12 +168,15 @@ function useTaskRuns(options = {}) {
|
|
|
159
168
|
key: requestKey,
|
|
160
169
|
runs: next
|
|
161
170
|
});
|
|
162
|
-
if (forcePoll || !hasEventSource()) {
|
|
171
|
+
if (forcePoll || headersKey !== "{}" || !hasEventSource()) {
|
|
163
172
|
let stopped = false;
|
|
164
173
|
let timer;
|
|
165
174
|
const tick = async () => {
|
|
166
175
|
try {
|
|
167
|
-
const res = await fetch(`${basePath}/tasks${query ? `?${query}` : ""}`, { headers: {
|
|
176
|
+
const res = await fetch(`${basePath}/tasks${query ? `?${query}` : ""}`, { headers: {
|
|
177
|
+
Accept: "application/json",
|
|
178
|
+
...requestHeaders
|
|
179
|
+
} });
|
|
168
180
|
if (!res.ok) throw new Error(`Task listing failed with ${res.status}`);
|
|
169
181
|
if (res.ok) {
|
|
170
182
|
const next = await res.json();
|
|
@@ -203,7 +215,8 @@ function useTaskRuns(options = {}) {
|
|
|
203
215
|
basePath,
|
|
204
216
|
enabled,
|
|
205
217
|
pollMs,
|
|
206
|
-
forcePoll
|
|
218
|
+
forcePoll,
|
|
219
|
+
headersKey
|
|
207
220
|
]);
|
|
208
221
|
return {
|
|
209
222
|
runs: received.runs,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-task-run.cjs","names":["useState","useEventSourceFactory","useRef","applyTaskOutputDelta","emptyTaskStreams","allGroupsTerminal","useMemo","withTaskStreams","taskQueryKeys"],"sources":["../../src/hooks/use-task-run.ts"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n allGroupsTerminal,\n type TaskRunMeta,\n type TaskSnapshot,\n} from \"../data/TaskSnapshot\";\nimport {\n applyTaskOutputDelta,\n emptyTaskStreams,\n type TaskOutputDelta,\n type TaskStreams,\n withTaskStreams,\n} from \"../data/task-streams\";\nimport { taskQueryKeys } from \"../data/task-query-keys\";\nimport { useEventSourceFactory } from \"./event-source\";\n\n// use-task-run / use-task-runs are the generic clicky-ui task clients. They are\n// SSE-first: they subscribe to clicky's SSEHandler (event: task / event: done)\n// and fall back to polling the JSON endpoints only when EventSource is\n// unavailable. They speak only TaskSnapshot / TaskRunMeta — no app concepts.\n\nexport interface TaskTransportOptions {\n /** Base path of the clicky task API, e.g. \"/api/v1\". */\n basePath?: string | undefined;\n /** Disable the subscription entirely (e.g. before an id is known). */\n enabled?: boolean | undefined;\n /** Poll interval (ms) for the fallback / runs listing. */\n pollMs?: number | undefined;\n /** Force the polling transport even when EventSource exists (mainly tests). */\n forcePoll?: boolean | undefined;\n}\n\nconst DEFAULT_BASE = \"/api/v1\";\nconst DEFAULT_POLL_MS = 2_000;\n\nfunction streamUrl(base: string, query: string): string {\n return `${base}/tasks/stream${query ? `?${query}` : \"\"}`;\n}\n\nfunction hasEventSource(): boolean {\n return typeof globalThis !== \"undefined\" && typeof globalThis.EventSource !== \"undefined\";\n}\n\nexport interface UseTaskRunResult {\n snapshots: TaskSnapshot[];\n status: string;\n isComplete: boolean;\n}\n\nexport interface UseTaskRunOptions extends TaskTransportOptions {\n /** Stable run id to follow (drill-down). Mutually exclusive with ids/kind. */\n id?: string | undefined;\n /** Stable run ids to follow together. Mutually exclusive with id/kind. */\n ids?: string[] | undefined;\n /** Follow every run of a kind. */\n kind?: string | undefined;\n}\n\n// useTaskRun follows one or more runs (by id) or a kind, SSE-first. It accumulates the\n// latest snapshot per task/group id and reports completion once every group is\n// terminal.\nexport function useTaskRun(options: UseTaskRunOptions = {}): UseTaskRunResult {\n const {\n id,\n ids,\n kind,\n basePath = DEFAULT_BASE,\n enabled = true,\n pollMs = DEFAULT_POLL_MS,\n forcePoll = false,\n } = options;\n const runIdsKey = (ids?.filter(Boolean) ?? (id ? [id] : [])).join(\",\");\n // State is reset in an effect, so the first render after the subscription\n // changes still holds the previous run's snapshots and completion; stateKey\n // hides them until the reset lands.\n const subscriptionKey = JSON.stringify([runIdsKey, kind ?? \"\", basePath, enabled]);\n const [stateKey, setStateKey] = useState(subscriptionKey);\n\n const [byId, setById] = useState<Record<string, TaskSnapshot>>({});\n // Streams are held beside the snapshots, never inside them: a task frame\n // replaces its snapshot wholesale and would take the accumulated output with\n // it. See ../data/task-streams.\n const [streamsById, setStreamsById] = useState<Record<string, TaskStreams>>({});\n const [status, setStatus] = useState(\"idle\");\n const [isComplete, setIsComplete] = useState(false);\n\n // Read through a ref so a host's inline `value={(url) => ...}` factory prop\n // does not restart the subscription every render — only the effect's own\n // deps below do that.\n const eventSourceFactory = useEventSourceFactory();\n const eventSourceFactoryRef = useRef(eventSourceFactory);\n eventSourceFactoryRef.current = eventSourceFactory;\n\n useEffect(() => {\n const runIds = runIdsKey ? runIdsKey.split(\",\") : [];\n setStateKey(subscriptionKey);\n if (!enabled || (runIds.length === 0 && !kind)) {\n setById({});\n setStreamsById({});\n setStatus(\"idle\");\n setIsComplete(false);\n return;\n }\n setById({});\n setStreamsById({});\n setIsComplete(false);\n\n const params = new URLSearchParams();\n if (runIds.length > 0) params.set(\"tasks\", runIdsKey);\n if (kind) params.set(\"kind\", kind);\n const query = params.toString();\n\n const merge = (incoming: TaskSnapshot[]) => {\n setById((prev) => {\n const next = { ...prev };\n for (const snap of incoming) next[snap.type === \"group\" ? `group:${snap.groupId ?? snap.id}` : snap.id] = snap;\n return next;\n });\n };\n\n // Deltas are applied whether or not the task frame has landed yet: the\n // stream is keyed by task id on its own, so output that arrives first is\n // held rather than dropped.\n const mergeOutput = (delta: TaskOutputDelta) => {\n setStreamsById((prev) => ({\n ...prev,\n [delta.id]: applyTaskOutputDelta(prev[delta.id] ?? emptyTaskStreams, delta),\n }));\n };\n\n // Polling fallback transport.\n if (forcePoll || !hasEventSource()) {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const tick = async () => {\n try {\n const responses = await Promise.all(runIds.map((runId) => fetch(\n `${basePath}/tasks/${encodeURIComponent(runId)}`,\n { headers: { Accept: \"application/json\" } },\n )));\n if (responses.some((response) => !response.ok)) throw new Error(\"Task request failed\");\n if (responses.every((response) => response.ok)) {\n const snaps = (await Promise.all(responses.map(\n async (response) => (await response.json()) as TaskSnapshot[],\n ))).flat();\n if (stopped) return;\n merge(snaps);\n setStatus(\"polling\");\n if (allGroupsTerminal(snaps)) {\n setIsComplete(true);\n return; // stop polling\n }\n }\n } catch {\n if (!stopped) setStatus(\"connection lost — retrying\");\n }\n if (!stopped) timer = setTimeout(tick, pollMs);\n };\n void tick();\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n };\n }\n\n // SSE transport (default).\n const es = eventSourceFactoryRef.current(streamUrl(basePath, query));\n setStatus(\"connecting\");\n es.onopen = () => setStatus(\"connected\");\n es.addEventListener(\"task\", (e) => {\n try {\n merge([JSON.parse((e as MessageEvent).data) as TaskSnapshot]);\n setStatus(\"connected\");\n } catch {\n setStatus(\"invalid task stream data\");\n }\n });\n es.addEventListener(\"output\", (e) => {\n try {\n mergeOutput(JSON.parse((e as MessageEvent).data) as TaskOutputDelta);\n } catch {\n setStatus(\"invalid task output data\");\n }\n });\n es.addEventListener(\"done\", () => {\n setStatus(\"complete\");\n setIsComplete(true);\n es.close();\n });\n es.onerror = () => setStatus(\"connection lost — retrying\");\n return () => es.close();\n }, [subscriptionKey, runIdsKey, kind, basePath, enabled, pollMs, forcePoll]);\n\n const current = stateKey === subscriptionKey;\n // The streams are merged back in only here, at the boundary. The polling\n // fallback never populates them — its JSON snapshots already carry the full\n // output — so this is a no-op on that transport.\n const snapshots = useMemo(\n () => current\n ? Object.values(byId).map((snapshot) => withTaskStreams(snapshot, streamsById[snapshot.id]))\n : [],\n [current, byId, streamsById],\n );\n\n return { snapshots, status: current ? status : \"idle\", isComplete: current && isComplete };\n}\n\nexport interface UseTaskRunsOptions extends TaskTransportOptions {\n kind?: string | undefined;\n status?: string | undefined;\n /** Label equality filters (k=v). */\n labels?: Record<string, string> | undefined;\n}\n\n/** The identity of the listing request that produced a given `runs` array. */\nexport type TaskRunsKey = ReturnType<typeof taskQueryKeys.runs>;\n\nexport interface UseTaskRunsResult {\n runs: TaskRunMeta[];\n status: string;\n /**\n * Query key of the request that actually produced `runs`, or null before the\n * first frame arrives. Changing `kind`/`status`/`labels` re-subscribes but\n * keeps the previous listing on screen until the new transport delivers, so\n * this key lags `taskQueryKeys.runs(options)` during that window. A consumer\n * mirroring `runs` into a cache must compare the two and skip the write while\n * they differ, or it publishes rows that do not match the requested filters.\n */\n runsKey: TaskRunsKey | null;\n}\n\n// useTaskRuns lists all runs (TaskRunMeta) for the manager view. It is SSE-first:\n// it subscribes to clicky's runs stream (event: runs carrying the full listing)\n// and falls back to polling the JSON runs endpoint only when EventSource is\n// unavailable or forced off. The listing has no completion terminal of its own —\n// a manager view stays subscribed to observe new and changing runs.\nexport function useTaskRuns(options: UseTaskRunsOptions = {}): UseTaskRunsResult {\n const {\n kind,\n status: statusFilter,\n labels,\n basePath = DEFAULT_BASE,\n enabled = true,\n pollMs = DEFAULT_POLL_MS,\n forcePoll = false,\n } = options;\n\n // Runs are stored together with the key of the request that delivered them, so\n // a stale listing can never be mistaken for the current filters' result.\n const [received, setReceived] = useState<{\n key: TaskRunsKey | null;\n runs: TaskRunMeta[];\n }>({ key: null, runs: [] });\n const [status, setStatus] = useState(\"idle\");\n\n const eventSourceFactory = useEventSourceFactory();\n const eventSourceFactoryRef = useRef(eventSourceFactory);\n eventSourceFactoryRef.current = eventSourceFactory;\n\n useEffect(() => {\n if (!enabled) return;\n\n const params = new URLSearchParams();\n if (kind) params.set(\"kind\", kind);\n if (statusFilter) params.set(\"status\", statusFilter);\n for (const [k, v] of Object.entries(labels ?? {})) params.append(\"label\", `${k}=${v}`);\n const query = params.toString();\n const requestKey = taskQueryKeys.runs({ basePath, kind, status: statusFilter, labels });\n const receive = (next: TaskRunMeta[]) => setReceived({ key: requestKey, runs: next });\n\n // Polling fallback transport.\n if (forcePoll || !hasEventSource()) {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const tick = async () => {\n try {\n const res = await fetch(`${basePath}/tasks${query ? `?${query}` : \"\"}`, {\n headers: { Accept: \"application/json\" },\n });\n if (!res.ok) throw new Error(`Task listing failed with ${res.status}`);\n if (res.ok) {\n const next = (await res.json()) as TaskRunMeta[];\n if (stopped) return;\n receive(next);\n setStatus(\"polling\");\n }\n } catch {\n if (!stopped) setStatus(\"connection lost — retrying\");\n }\n if (!stopped) timer = setTimeout(tick, pollMs);\n };\n void tick();\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n };\n }\n\n // SSE transport (default).\n const es = eventSourceFactoryRef.current(`${basePath}/tasks/runs/stream${query ? `?${query}` : \"\"}`);\n setStatus(\"connecting\");\n es.onopen = () => setStatus(\"connected\");\n es.addEventListener(\"runs\", (e) => {\n try {\n receive(JSON.parse((e as MessageEvent).data) as TaskRunMeta[]);\n setStatus(\"connected\");\n } catch {\n setStatus(\"invalid task listing data\");\n }\n });\n es.onerror = () => setStatus(\"connection lost — retrying\");\n return () => es.close();\n }, [kind, statusFilter, JSON.stringify(labels ?? {}), basePath, enabled, pollMs, forcePoll]);\n\n return { runs: received.runs, status, runsKey: received.key };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-task-run.cjs","names":["useState","useEventSourceFactory","useRef","applyTaskOutputDelta","emptyTaskStreams","allGroupsTerminal","useMemo","withTaskStreams","taskQueryKeys"],"sources":["../../src/hooks/use-task-run.ts"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n allGroupsTerminal,\n type TaskRunMeta,\n type TaskSnapshot,\n} from \"../data/TaskSnapshot\";\nimport {\n applyTaskOutputDelta,\n emptyTaskStreams,\n type TaskOutputDelta,\n type TaskStreams,\n withTaskStreams,\n} from \"../data/task-streams\";\nimport { taskQueryKeys } from \"../data/task-query-keys\";\nimport { useEventSourceFactory } from \"./event-source\";\n\n// use-task-run / use-task-runs are the generic clicky-ui task clients. They are\n// SSE-first: they subscribe to clicky's SSEHandler (event: task / event: done)\n// and fall back to polling the JSON endpoints only when EventSource is\n// unavailable. They speak only TaskSnapshot / TaskRunMeta — no app concepts.\n\nexport interface TaskTransportOptions {\n /** Base path of the clicky task API, e.g. \"/api/v1\". */\n basePath?: string | undefined;\n /** Disable the subscription entirely (e.g. before an id is known). */\n enabled?: boolean | undefined;\n /** Poll interval (ms) for the fallback / runs listing. */\n pollMs?: number | undefined;\n /** Force the polling transport even when EventSource exists (mainly tests). */\n forcePoll?: boolean | undefined;\n /** Headers for context-scoped task endpoints. Supplying headers uses polling. */\n headers?: Record<string, string> | undefined;\n}\n\nconst DEFAULT_BASE = \"/api/v1\";\nconst DEFAULT_POLL_MS = 2_000;\n\nfunction streamUrl(base: string, query: string): string {\n return `${base}/tasks/stream${query ? `?${query}` : \"\"}`;\n}\n\nfunction hasEventSource(): boolean {\n return typeof globalThis !== \"undefined\" && typeof globalThis.EventSource !== \"undefined\";\n}\n\nexport interface UseTaskRunResult {\n snapshots: TaskSnapshot[];\n status: string;\n isComplete: boolean;\n}\n\nexport interface UseTaskRunOptions extends TaskTransportOptions {\n /** Stable run id to follow (drill-down). Mutually exclusive with ids/kind. */\n id?: string | undefined;\n /** Stable run ids to follow together. Mutually exclusive with id/kind. */\n ids?: string[] | undefined;\n /** Follow every run of a kind. */\n kind?: string | undefined;\n}\n\n// useTaskRun follows one or more runs (by id) or a kind, SSE-first. It accumulates the\n// latest snapshot per task/group id and reports completion once every group is\n// terminal.\nexport function useTaskRun(options: UseTaskRunOptions = {}): UseTaskRunResult {\n const {\n id,\n ids,\n kind,\n basePath = DEFAULT_BASE,\n enabled = true,\n pollMs = DEFAULT_POLL_MS,\n forcePoll = false,\n headers,\n } = options;\n const headersKey = JSON.stringify(headers ?? {});\n const runIdsKey = (ids?.filter(Boolean) ?? (id ? [id] : [])).join(\",\");\n // State is reset in an effect, so the first render after the subscription\n // changes still holds the previous run's snapshots and completion; stateKey\n // hides them until the reset lands.\n const subscriptionKey = JSON.stringify([runIdsKey, kind ?? \"\", basePath, enabled, headersKey]);\n const [stateKey, setStateKey] = useState(subscriptionKey);\n\n const [byId, setById] = useState<Record<string, TaskSnapshot>>({});\n // Streams are held beside the snapshots, never inside them: a task frame\n // replaces its snapshot wholesale and would take the accumulated output with\n // it. See ../data/task-streams.\n const [streamsById, setStreamsById] = useState<Record<string, TaskStreams>>({});\n const [status, setStatus] = useState(\"idle\");\n const [isComplete, setIsComplete] = useState(false);\n\n // Read through a ref so a host's inline `value={(url) => ...}` factory prop\n // does not restart the subscription every render — only the effect's own\n // deps below do that.\n const eventSourceFactory = useEventSourceFactory();\n const eventSourceFactoryRef = useRef(eventSourceFactory);\n eventSourceFactoryRef.current = eventSourceFactory;\n\n useEffect(() => {\n const requestHeaders = JSON.parse(headersKey) as Record<string, string>;\n const runIds = runIdsKey ? runIdsKey.split(\",\") : [];\n setStateKey(subscriptionKey);\n if (!enabled || (runIds.length === 0 && !kind)) {\n setById({});\n setStreamsById({});\n setStatus(\"idle\");\n setIsComplete(false);\n return;\n }\n setById({});\n setStreamsById({});\n setIsComplete(false);\n\n const params = new URLSearchParams();\n if (runIds.length > 0) params.set(\"tasks\", runIdsKey);\n if (kind) params.set(\"kind\", kind);\n const query = params.toString();\n\n const merge = (incoming: TaskSnapshot[]) => {\n setById((prev) => {\n const next = { ...prev };\n for (const snap of incoming) next[snap.type === \"group\" ? `group:${snap.groupId ?? snap.id}` : snap.id] = snap;\n return next;\n });\n };\n\n // Deltas are applied whether or not the task frame has landed yet: the\n // stream is keyed by task id on its own, so output that arrives first is\n // held rather than dropped.\n const mergeOutput = (delta: TaskOutputDelta) => {\n setStreamsById((prev) => ({\n ...prev,\n [delta.id]: applyTaskOutputDelta(prev[delta.id] ?? emptyTaskStreams, delta),\n }));\n };\n\n // Polling fallback transport.\n if (forcePoll || headersKey !== \"{}\" || !hasEventSource()) {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const tick = async () => {\n try {\n const responses = await Promise.all(runIds.map((runId) => fetch(\n `${basePath}/tasks/${encodeURIComponent(runId)}`,\n { headers: { Accept: \"application/json\", ...requestHeaders } },\n )));\n if (responses.some((response) => !response.ok)) throw new Error(\"Task request failed\");\n if (responses.every((response) => response.ok)) {\n const snaps = (await Promise.all(responses.map(\n async (response) => (await response.json()) as TaskSnapshot[],\n ))).flat();\n if (stopped) return;\n merge(snaps);\n setStatus(\"polling\");\n if (allGroupsTerminal(snaps)) {\n setIsComplete(true);\n return; // stop polling\n }\n }\n } catch {\n if (!stopped) setStatus(\"connection lost — retrying\");\n }\n if (!stopped) timer = setTimeout(tick, pollMs);\n };\n void tick();\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n };\n }\n\n // SSE transport (default).\n const es = eventSourceFactoryRef.current(streamUrl(basePath, query));\n setStatus(\"connecting\");\n es.onopen = () => setStatus(\"connected\");\n es.addEventListener(\"task\", (e) => {\n try {\n merge([JSON.parse((e as MessageEvent).data) as TaskSnapshot]);\n setStatus(\"connected\");\n } catch {\n setStatus(\"invalid task stream data\");\n }\n });\n es.addEventListener(\"output\", (e) => {\n try {\n mergeOutput(JSON.parse((e as MessageEvent).data) as TaskOutputDelta);\n } catch {\n setStatus(\"invalid task output data\");\n }\n });\n es.addEventListener(\"done\", () => {\n setStatus(\"complete\");\n setIsComplete(true);\n es.close();\n });\n es.onerror = () => setStatus(\"connection lost — retrying\");\n return () => es.close();\n }, [subscriptionKey, runIdsKey, kind, basePath, enabled, pollMs, forcePoll, headersKey]);\n\n const current = stateKey === subscriptionKey;\n // The streams are merged back in only here, at the boundary. The polling\n // fallback never populates them — its JSON snapshots already carry the full\n // output — so this is a no-op on that transport.\n const snapshots = useMemo(\n () => current\n ? Object.values(byId).map((snapshot) => withTaskStreams(snapshot, streamsById[snapshot.id]))\n : [],\n [current, byId, streamsById],\n );\n\n return { snapshots, status: current ? status : \"idle\", isComplete: current && isComplete };\n}\n\nexport interface UseTaskRunsOptions extends TaskTransportOptions {\n kind?: string | undefined;\n status?: string | undefined;\n /** Label equality filters (k=v). */\n labels?: Record<string, string> | undefined;\n}\n\n/** The identity of the listing request that produced a given `runs` array. */\nexport type TaskRunsKey = ReturnType<typeof taskQueryKeys.runs>;\n\nexport interface UseTaskRunsResult {\n runs: TaskRunMeta[];\n status: string;\n /**\n * Query key of the request that actually produced `runs`, or null before the\n * first frame arrives. Changing `kind`/`status`/`labels` re-subscribes but\n * keeps the previous listing on screen until the new transport delivers, so\n * this key lags `taskQueryKeys.runs(options)` during that window. A consumer\n * mirroring `runs` into a cache must compare the two and skip the write while\n * they differ, or it publishes rows that do not match the requested filters.\n */\n runsKey: TaskRunsKey | null;\n}\n\n// useTaskRuns lists all runs (TaskRunMeta) for the manager view. It is SSE-first:\n// it subscribes to clicky's runs stream (event: runs carrying the full listing)\n// and falls back to polling the JSON runs endpoint only when EventSource is\n// unavailable or forced off. The listing has no completion terminal of its own —\n// a manager view stays subscribed to observe new and changing runs.\nexport function useTaskRuns(options: UseTaskRunsOptions = {}): UseTaskRunsResult {\n const {\n kind,\n status: statusFilter,\n labels,\n basePath = DEFAULT_BASE,\n enabled = true,\n pollMs = DEFAULT_POLL_MS,\n forcePoll = false,\n headers,\n } = options;\n const headersKey = JSON.stringify(headers ?? {});\n\n // Runs are stored together with the key of the request that delivered them, so\n // a stale listing can never be mistaken for the current filters' result.\n const [received, setReceived] = useState<{\n key: TaskRunsKey | null;\n runs: TaskRunMeta[];\n }>({ key: null, runs: [] });\n const [status, setStatus] = useState(\"idle\");\n\n const eventSourceFactory = useEventSourceFactory();\n const eventSourceFactoryRef = useRef(eventSourceFactory);\n eventSourceFactoryRef.current = eventSourceFactory;\n\n useEffect(() => {\n if (!enabled) return;\n const requestHeaders = JSON.parse(headersKey) as Record<string, string>;\n\n const params = new URLSearchParams();\n if (kind) params.set(\"kind\", kind);\n if (statusFilter) params.set(\"status\", statusFilter);\n for (const [k, v] of Object.entries(labels ?? {})) params.append(\"label\", `${k}=${v}`);\n const query = params.toString();\n const requestKey = taskQueryKeys.runs({ basePath, kind, status: statusFilter, labels });\n const receive = (next: TaskRunMeta[]) => setReceived({ key: requestKey, runs: next });\n\n // Polling fallback transport.\n if (forcePoll || headersKey !== \"{}\" || !hasEventSource()) {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const tick = async () => {\n try {\n const res = await fetch(`${basePath}/tasks${query ? `?${query}` : \"\"}`, {\n headers: { Accept: \"application/json\", ...requestHeaders },\n });\n if (!res.ok) throw new Error(`Task listing failed with ${res.status}`);\n if (res.ok) {\n const next = (await res.json()) as TaskRunMeta[];\n if (stopped) return;\n receive(next);\n setStatus(\"polling\");\n }\n } catch {\n if (!stopped) setStatus(\"connection lost — retrying\");\n }\n if (!stopped) timer = setTimeout(tick, pollMs);\n };\n void tick();\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n };\n }\n\n // SSE transport (default).\n const es = eventSourceFactoryRef.current(`${basePath}/tasks/runs/stream${query ? `?${query}` : \"\"}`);\n setStatus(\"connecting\");\n es.onopen = () => setStatus(\"connected\");\n es.addEventListener(\"runs\", (e) => {\n try {\n receive(JSON.parse((e as MessageEvent).data) as TaskRunMeta[]);\n setStatus(\"connected\");\n } catch {\n setStatus(\"invalid task listing data\");\n }\n });\n es.onerror = () => setStatus(\"connection lost — retrying\");\n return () => es.close();\n }, [kind, statusFilter, JSON.stringify(labels ?? {}), basePath, enabled, pollMs, forcePoll, headersKey]);\n\n return { runs: received.runs, status, runsKey: received.key };\n}\n"],"mappings":"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"}
|
|
@@ -9,6 +9,8 @@ export interface TaskTransportOptions {
|
|
|
9
9
|
pollMs?: number | undefined;
|
|
10
10
|
/** Force the polling transport even when EventSource exists (mainly tests). */
|
|
11
11
|
forcePoll?: boolean | undefined;
|
|
12
|
+
/** Headers for context-scoped task endpoints. Supplying headers uses polling. */
|
|
13
|
+
headers?: Record<string, string> | undefined;
|
|
12
14
|
}
|
|
13
15
|
export interface UseTaskRunResult {
|
|
14
16
|
snapshots: TaskSnapshot[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-task-run.d.ts","sourceRoot":"","sources":["../../src/hooks/use-task-run.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,YAAY,EAClB,MAAM,sBAAsB,CAAC;AAQ9B,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAQxD,MAAM,WAAW,oBAAoB;IACnC,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,+EAA+E;IAC/E,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"use-task-run.d.ts","sourceRoot":"","sources":["../../src/hooks/use-task-run.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,YAAY,EAClB,MAAM,sBAAsB,CAAC;AAQ9B,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAQxD,MAAM,WAAW,oBAAoB;IACnC,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,+EAA+E;IAC/E,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAChC,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;CAC9C;AAaD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,iBAAkB,SAAQ,oBAAoB;IAC7D,8EAA8E;IAC9E,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC3B,kCAAkC;IAClC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAKD,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAmJ5E;AAED,MAAM,WAAW,kBAAmB,SAAQ,oBAAoB;IAC9D,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;CAC7C;AAED,8EAA8E;AAC9E,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC;AAEhE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,WAAW,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;CAC7B;AAOD,wBAAgB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,iBAAiB,CAkF/E"}
|
|
@@ -13,13 +13,15 @@ function hasEventSource() {
|
|
|
13
13
|
return typeof globalThis !== "undefined" && typeof globalThis.EventSource !== "undefined";
|
|
14
14
|
}
|
|
15
15
|
function useTaskRun(options = {}) {
|
|
16
|
-
const { id, ids, kind, basePath = DEFAULT_BASE, enabled = true, pollMs = DEFAULT_POLL_MS, forcePoll = false } = options;
|
|
16
|
+
const { id, ids, kind, basePath = DEFAULT_BASE, enabled = true, pollMs = DEFAULT_POLL_MS, forcePoll = false, headers } = options;
|
|
17
|
+
const headersKey = JSON.stringify(headers ?? {});
|
|
17
18
|
const runIdsKey = (ids?.filter(Boolean) ?? (id ? [id] : [])).join(",");
|
|
18
19
|
const subscriptionKey = JSON.stringify([
|
|
19
20
|
runIdsKey,
|
|
20
21
|
kind ?? "",
|
|
21
22
|
basePath,
|
|
22
|
-
enabled
|
|
23
|
+
enabled,
|
|
24
|
+
headersKey
|
|
23
25
|
]);
|
|
24
26
|
const [stateKey, setStateKey] = useState(subscriptionKey);
|
|
25
27
|
const [byId, setById] = useState({});
|
|
@@ -30,6 +32,7 @@ function useTaskRun(options = {}) {
|
|
|
30
32
|
const eventSourceFactoryRef = useRef(eventSourceFactory);
|
|
31
33
|
eventSourceFactoryRef.current = eventSourceFactory;
|
|
32
34
|
useEffect(() => {
|
|
35
|
+
const requestHeaders = JSON.parse(headersKey);
|
|
33
36
|
const runIds = runIdsKey ? runIdsKey.split(",") : [];
|
|
34
37
|
setStateKey(subscriptionKey);
|
|
35
38
|
if (!enabled || runIds.length === 0 && !kind) {
|
|
@@ -59,12 +62,15 @@ function useTaskRun(options = {}) {
|
|
|
59
62
|
[delta.id]: applyTaskOutputDelta(prev[delta.id] ?? emptyTaskStreams, delta)
|
|
60
63
|
}));
|
|
61
64
|
};
|
|
62
|
-
if (forcePoll || !hasEventSource()) {
|
|
65
|
+
if (forcePoll || headersKey !== "{}" || !hasEventSource()) {
|
|
63
66
|
let stopped = false;
|
|
64
67
|
let timer;
|
|
65
68
|
const tick = async () => {
|
|
66
69
|
try {
|
|
67
|
-
const responses = await Promise.all(runIds.map((runId) => fetch(`${basePath}/tasks/${encodeURIComponent(runId)}`, { headers: {
|
|
70
|
+
const responses = await Promise.all(runIds.map((runId) => fetch(`${basePath}/tasks/${encodeURIComponent(runId)}`, { headers: {
|
|
71
|
+
Accept: "application/json",
|
|
72
|
+
...requestHeaders
|
|
73
|
+
} })));
|
|
68
74
|
if (responses.some((response) => !response.ok)) throw new Error("Task request failed");
|
|
69
75
|
if (responses.every((response) => response.ok)) {
|
|
70
76
|
const snaps = (await Promise.all(responses.map(async (response) => await response.json()))).flat();
|
|
@@ -119,7 +125,8 @@ function useTaskRun(options = {}) {
|
|
|
119
125
|
basePath,
|
|
120
126
|
enabled,
|
|
121
127
|
pollMs,
|
|
122
|
-
forcePoll
|
|
128
|
+
forcePoll,
|
|
129
|
+
headersKey
|
|
123
130
|
]);
|
|
124
131
|
const current = stateKey === subscriptionKey;
|
|
125
132
|
return {
|
|
@@ -133,7 +140,8 @@ function useTaskRun(options = {}) {
|
|
|
133
140
|
};
|
|
134
141
|
}
|
|
135
142
|
function useTaskRuns(options = {}) {
|
|
136
|
-
const { kind, status: statusFilter, labels, basePath = DEFAULT_BASE, enabled = true, pollMs = DEFAULT_POLL_MS, forcePoll = false } = options;
|
|
143
|
+
const { kind, status: statusFilter, labels, basePath = DEFAULT_BASE, enabled = true, pollMs = DEFAULT_POLL_MS, forcePoll = false, headers } = options;
|
|
144
|
+
const headersKey = JSON.stringify(headers ?? {});
|
|
137
145
|
const [received, setReceived] = useState({
|
|
138
146
|
key: null,
|
|
139
147
|
runs: []
|
|
@@ -144,6 +152,7 @@ function useTaskRuns(options = {}) {
|
|
|
144
152
|
eventSourceFactoryRef.current = eventSourceFactory;
|
|
145
153
|
useEffect(() => {
|
|
146
154
|
if (!enabled) return;
|
|
155
|
+
const requestHeaders = JSON.parse(headersKey);
|
|
147
156
|
const params = new URLSearchParams();
|
|
148
157
|
if (kind) params.set("kind", kind);
|
|
149
158
|
if (statusFilter) params.set("status", statusFilter);
|
|
@@ -159,12 +168,15 @@ function useTaskRuns(options = {}) {
|
|
|
159
168
|
key: requestKey,
|
|
160
169
|
runs: next
|
|
161
170
|
});
|
|
162
|
-
if (forcePoll || !hasEventSource()) {
|
|
171
|
+
if (forcePoll || headersKey !== "{}" || !hasEventSource()) {
|
|
163
172
|
let stopped = false;
|
|
164
173
|
let timer;
|
|
165
174
|
const tick = async () => {
|
|
166
175
|
try {
|
|
167
|
-
const res = await fetch(`${basePath}/tasks${query ? `?${query}` : ""}`, { headers: {
|
|
176
|
+
const res = await fetch(`${basePath}/tasks${query ? `?${query}` : ""}`, { headers: {
|
|
177
|
+
Accept: "application/json",
|
|
178
|
+
...requestHeaders
|
|
179
|
+
} });
|
|
168
180
|
if (!res.ok) throw new Error(`Task listing failed with ${res.status}`);
|
|
169
181
|
if (res.ok) {
|
|
170
182
|
const next = await res.json();
|
|
@@ -203,7 +215,8 @@ function useTaskRuns(options = {}) {
|
|
|
203
215
|
basePath,
|
|
204
216
|
enabled,
|
|
205
217
|
pollMs,
|
|
206
|
-
forcePoll
|
|
218
|
+
forcePoll,
|
|
219
|
+
headersKey
|
|
207
220
|
]);
|
|
208
221
|
return {
|
|
209
222
|
runs: received.runs,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-task-run.js","names":[],"sources":["../../src/hooks/use-task-run.ts"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n allGroupsTerminal,\n type TaskRunMeta,\n type TaskSnapshot,\n} from \"../data/TaskSnapshot\";\nimport {\n applyTaskOutputDelta,\n emptyTaskStreams,\n type TaskOutputDelta,\n type TaskStreams,\n withTaskStreams,\n} from \"../data/task-streams\";\nimport { taskQueryKeys } from \"../data/task-query-keys\";\nimport { useEventSourceFactory } from \"./event-source\";\n\n// use-task-run / use-task-runs are the generic clicky-ui task clients. They are\n// SSE-first: they subscribe to clicky's SSEHandler (event: task / event: done)\n// and fall back to polling the JSON endpoints only when EventSource is\n// unavailable. They speak only TaskSnapshot / TaskRunMeta — no app concepts.\n\nexport interface TaskTransportOptions {\n /** Base path of the clicky task API, e.g. \"/api/v1\". */\n basePath?: string | undefined;\n /** Disable the subscription entirely (e.g. before an id is known). */\n enabled?: boolean | undefined;\n /** Poll interval (ms) for the fallback / runs listing. */\n pollMs?: number | undefined;\n /** Force the polling transport even when EventSource exists (mainly tests). */\n forcePoll?: boolean | undefined;\n}\n\nconst DEFAULT_BASE = \"/api/v1\";\nconst DEFAULT_POLL_MS = 2_000;\n\nfunction streamUrl(base: string, query: string): string {\n return `${base}/tasks/stream${query ? `?${query}` : \"\"}`;\n}\n\nfunction hasEventSource(): boolean {\n return typeof globalThis !== \"undefined\" && typeof globalThis.EventSource !== \"undefined\";\n}\n\nexport interface UseTaskRunResult {\n snapshots: TaskSnapshot[];\n status: string;\n isComplete: boolean;\n}\n\nexport interface UseTaskRunOptions extends TaskTransportOptions {\n /** Stable run id to follow (drill-down). Mutually exclusive with ids/kind. */\n id?: string | undefined;\n /** Stable run ids to follow together. Mutually exclusive with id/kind. */\n ids?: string[] | undefined;\n /** Follow every run of a kind. */\n kind?: string | undefined;\n}\n\n// useTaskRun follows one or more runs (by id) or a kind, SSE-first. It accumulates the\n// latest snapshot per task/group id and reports completion once every group is\n// terminal.\nexport function useTaskRun(options: UseTaskRunOptions = {}): UseTaskRunResult {\n const {\n id,\n ids,\n kind,\n basePath = DEFAULT_BASE,\n enabled = true,\n pollMs = DEFAULT_POLL_MS,\n forcePoll = false,\n } = options;\n const runIdsKey = (ids?.filter(Boolean) ?? (id ? [id] : [])).join(\",\");\n // State is reset in an effect, so the first render after the subscription\n // changes still holds the previous run's snapshots and completion; stateKey\n // hides them until the reset lands.\n const subscriptionKey = JSON.stringify([runIdsKey, kind ?? \"\", basePath, enabled]);\n const [stateKey, setStateKey] = useState(subscriptionKey);\n\n const [byId, setById] = useState<Record<string, TaskSnapshot>>({});\n // Streams are held beside the snapshots, never inside them: a task frame\n // replaces its snapshot wholesale and would take the accumulated output with\n // it. See ../data/task-streams.\n const [streamsById, setStreamsById] = useState<Record<string, TaskStreams>>({});\n const [status, setStatus] = useState(\"idle\");\n const [isComplete, setIsComplete] = useState(false);\n\n // Read through a ref so a host's inline `value={(url) => ...}` factory prop\n // does not restart the subscription every render — only the effect's own\n // deps below do that.\n const eventSourceFactory = useEventSourceFactory();\n const eventSourceFactoryRef = useRef(eventSourceFactory);\n eventSourceFactoryRef.current = eventSourceFactory;\n\n useEffect(() => {\n const runIds = runIdsKey ? runIdsKey.split(\",\") : [];\n setStateKey(subscriptionKey);\n if (!enabled || (runIds.length === 0 && !kind)) {\n setById({});\n setStreamsById({});\n setStatus(\"idle\");\n setIsComplete(false);\n return;\n }\n setById({});\n setStreamsById({});\n setIsComplete(false);\n\n const params = new URLSearchParams();\n if (runIds.length > 0) params.set(\"tasks\", runIdsKey);\n if (kind) params.set(\"kind\", kind);\n const query = params.toString();\n\n const merge = (incoming: TaskSnapshot[]) => {\n setById((prev) => {\n const next = { ...prev };\n for (const snap of incoming) next[snap.type === \"group\" ? `group:${snap.groupId ?? snap.id}` : snap.id] = snap;\n return next;\n });\n };\n\n // Deltas are applied whether or not the task frame has landed yet: the\n // stream is keyed by task id on its own, so output that arrives first is\n // held rather than dropped.\n const mergeOutput = (delta: TaskOutputDelta) => {\n setStreamsById((prev) => ({\n ...prev,\n [delta.id]: applyTaskOutputDelta(prev[delta.id] ?? emptyTaskStreams, delta),\n }));\n };\n\n // Polling fallback transport.\n if (forcePoll || !hasEventSource()) {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const tick = async () => {\n try {\n const responses = await Promise.all(runIds.map((runId) => fetch(\n `${basePath}/tasks/${encodeURIComponent(runId)}`,\n { headers: { Accept: \"application/json\" } },\n )));\n if (responses.some((response) => !response.ok)) throw new Error(\"Task request failed\");\n if (responses.every((response) => response.ok)) {\n const snaps = (await Promise.all(responses.map(\n async (response) => (await response.json()) as TaskSnapshot[],\n ))).flat();\n if (stopped) return;\n merge(snaps);\n setStatus(\"polling\");\n if (allGroupsTerminal(snaps)) {\n setIsComplete(true);\n return; // stop polling\n }\n }\n } catch {\n if (!stopped) setStatus(\"connection lost — retrying\");\n }\n if (!stopped) timer = setTimeout(tick, pollMs);\n };\n void tick();\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n };\n }\n\n // SSE transport (default).\n const es = eventSourceFactoryRef.current(streamUrl(basePath, query));\n setStatus(\"connecting\");\n es.onopen = () => setStatus(\"connected\");\n es.addEventListener(\"task\", (e) => {\n try {\n merge([JSON.parse((e as MessageEvent).data) as TaskSnapshot]);\n setStatus(\"connected\");\n } catch {\n setStatus(\"invalid task stream data\");\n }\n });\n es.addEventListener(\"output\", (e) => {\n try {\n mergeOutput(JSON.parse((e as MessageEvent).data) as TaskOutputDelta);\n } catch {\n setStatus(\"invalid task output data\");\n }\n });\n es.addEventListener(\"done\", () => {\n setStatus(\"complete\");\n setIsComplete(true);\n es.close();\n });\n es.onerror = () => setStatus(\"connection lost — retrying\");\n return () => es.close();\n }, [subscriptionKey, runIdsKey, kind, basePath, enabled, pollMs, forcePoll]);\n\n const current = stateKey === subscriptionKey;\n // The streams are merged back in only here, at the boundary. The polling\n // fallback never populates them — its JSON snapshots already carry the full\n // output — so this is a no-op on that transport.\n const snapshots = useMemo(\n () => current\n ? Object.values(byId).map((snapshot) => withTaskStreams(snapshot, streamsById[snapshot.id]))\n : [],\n [current, byId, streamsById],\n );\n\n return { snapshots, status: current ? status : \"idle\", isComplete: current && isComplete };\n}\n\nexport interface UseTaskRunsOptions extends TaskTransportOptions {\n kind?: string | undefined;\n status?: string | undefined;\n /** Label equality filters (k=v). */\n labels?: Record<string, string> | undefined;\n}\n\n/** The identity of the listing request that produced a given `runs` array. */\nexport type TaskRunsKey = ReturnType<typeof taskQueryKeys.runs>;\n\nexport interface UseTaskRunsResult {\n runs: TaskRunMeta[];\n status: string;\n /**\n * Query key of the request that actually produced `runs`, or null before the\n * first frame arrives. Changing `kind`/`status`/`labels` re-subscribes but\n * keeps the previous listing on screen until the new transport delivers, so\n * this key lags `taskQueryKeys.runs(options)` during that window. A consumer\n * mirroring `runs` into a cache must compare the two and skip the write while\n * they differ, or it publishes rows that do not match the requested filters.\n */\n runsKey: TaskRunsKey | null;\n}\n\n// useTaskRuns lists all runs (TaskRunMeta) for the manager view. It is SSE-first:\n// it subscribes to clicky's runs stream (event: runs carrying the full listing)\n// and falls back to polling the JSON runs endpoint only when EventSource is\n// unavailable or forced off. The listing has no completion terminal of its own —\n// a manager view stays subscribed to observe new and changing runs.\nexport function useTaskRuns(options: UseTaskRunsOptions = {}): UseTaskRunsResult {\n const {\n kind,\n status: statusFilter,\n labels,\n basePath = DEFAULT_BASE,\n enabled = true,\n pollMs = DEFAULT_POLL_MS,\n forcePoll = false,\n } = options;\n\n // Runs are stored together with the key of the request that delivered them, so\n // a stale listing can never be mistaken for the current filters' result.\n const [received, setReceived] = useState<{\n key: TaskRunsKey | null;\n runs: TaskRunMeta[];\n }>({ key: null, runs: [] });\n const [status, setStatus] = useState(\"idle\");\n\n const eventSourceFactory = useEventSourceFactory();\n const eventSourceFactoryRef = useRef(eventSourceFactory);\n eventSourceFactoryRef.current = eventSourceFactory;\n\n useEffect(() => {\n if (!enabled) return;\n\n const params = new URLSearchParams();\n if (kind) params.set(\"kind\", kind);\n if (statusFilter) params.set(\"status\", statusFilter);\n for (const [k, v] of Object.entries(labels ?? {})) params.append(\"label\", `${k}=${v}`);\n const query = params.toString();\n const requestKey = taskQueryKeys.runs({ basePath, kind, status: statusFilter, labels });\n const receive = (next: TaskRunMeta[]) => setReceived({ key: requestKey, runs: next });\n\n // Polling fallback transport.\n if (forcePoll || !hasEventSource()) {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const tick = async () => {\n try {\n const res = await fetch(`${basePath}/tasks${query ? `?${query}` : \"\"}`, {\n headers: { Accept: \"application/json\" },\n });\n if (!res.ok) throw new Error(`Task listing failed with ${res.status}`);\n if (res.ok) {\n const next = (await res.json()) as TaskRunMeta[];\n if (stopped) return;\n receive(next);\n setStatus(\"polling\");\n }\n } catch {\n if (!stopped) setStatus(\"connection lost — retrying\");\n }\n if (!stopped) timer = setTimeout(tick, pollMs);\n };\n void tick();\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n };\n }\n\n // SSE transport (default).\n const es = eventSourceFactoryRef.current(`${basePath}/tasks/runs/stream${query ? `?${query}` : \"\"}`);\n setStatus(\"connecting\");\n es.onopen = () => setStatus(\"connected\");\n es.addEventListener(\"runs\", (e) => {\n try {\n receive(JSON.parse((e as MessageEvent).data) as TaskRunMeta[]);\n setStatus(\"connected\");\n } catch {\n setStatus(\"invalid task listing data\");\n }\n });\n es.onerror = () => setStatus(\"connection lost — retrying\");\n return () => es.close();\n }, [kind, statusFilter, JSON.stringify(labels ?? {}), basePath, enabled, pollMs, forcePoll]);\n\n return { runs: received.runs, status, runsKey: received.key };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-task-run.js","names":[],"sources":["../../src/hooks/use-task-run.ts"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n allGroupsTerminal,\n type TaskRunMeta,\n type TaskSnapshot,\n} from \"../data/TaskSnapshot\";\nimport {\n applyTaskOutputDelta,\n emptyTaskStreams,\n type TaskOutputDelta,\n type TaskStreams,\n withTaskStreams,\n} from \"../data/task-streams\";\nimport { taskQueryKeys } from \"../data/task-query-keys\";\nimport { useEventSourceFactory } from \"./event-source\";\n\n// use-task-run / use-task-runs are the generic clicky-ui task clients. They are\n// SSE-first: they subscribe to clicky's SSEHandler (event: task / event: done)\n// and fall back to polling the JSON endpoints only when EventSource is\n// unavailable. They speak only TaskSnapshot / TaskRunMeta — no app concepts.\n\nexport interface TaskTransportOptions {\n /** Base path of the clicky task API, e.g. \"/api/v1\". */\n basePath?: string | undefined;\n /** Disable the subscription entirely (e.g. before an id is known). */\n enabled?: boolean | undefined;\n /** Poll interval (ms) for the fallback / runs listing. */\n pollMs?: number | undefined;\n /** Force the polling transport even when EventSource exists (mainly tests). */\n forcePoll?: boolean | undefined;\n /** Headers for context-scoped task endpoints. Supplying headers uses polling. */\n headers?: Record<string, string> | undefined;\n}\n\nconst DEFAULT_BASE = \"/api/v1\";\nconst DEFAULT_POLL_MS = 2_000;\n\nfunction streamUrl(base: string, query: string): string {\n return `${base}/tasks/stream${query ? `?${query}` : \"\"}`;\n}\n\nfunction hasEventSource(): boolean {\n return typeof globalThis !== \"undefined\" && typeof globalThis.EventSource !== \"undefined\";\n}\n\nexport interface UseTaskRunResult {\n snapshots: TaskSnapshot[];\n status: string;\n isComplete: boolean;\n}\n\nexport interface UseTaskRunOptions extends TaskTransportOptions {\n /** Stable run id to follow (drill-down). Mutually exclusive with ids/kind. */\n id?: string | undefined;\n /** Stable run ids to follow together. Mutually exclusive with id/kind. */\n ids?: string[] | undefined;\n /** Follow every run of a kind. */\n kind?: string | undefined;\n}\n\n// useTaskRun follows one or more runs (by id) or a kind, SSE-first. It accumulates the\n// latest snapshot per task/group id and reports completion once every group is\n// terminal.\nexport function useTaskRun(options: UseTaskRunOptions = {}): UseTaskRunResult {\n const {\n id,\n ids,\n kind,\n basePath = DEFAULT_BASE,\n enabled = true,\n pollMs = DEFAULT_POLL_MS,\n forcePoll = false,\n headers,\n } = options;\n const headersKey = JSON.stringify(headers ?? {});\n const runIdsKey = (ids?.filter(Boolean) ?? (id ? [id] : [])).join(\",\");\n // State is reset in an effect, so the first render after the subscription\n // changes still holds the previous run's snapshots and completion; stateKey\n // hides them until the reset lands.\n const subscriptionKey = JSON.stringify([runIdsKey, kind ?? \"\", basePath, enabled, headersKey]);\n const [stateKey, setStateKey] = useState(subscriptionKey);\n\n const [byId, setById] = useState<Record<string, TaskSnapshot>>({});\n // Streams are held beside the snapshots, never inside them: a task frame\n // replaces its snapshot wholesale and would take the accumulated output with\n // it. See ../data/task-streams.\n const [streamsById, setStreamsById] = useState<Record<string, TaskStreams>>({});\n const [status, setStatus] = useState(\"idle\");\n const [isComplete, setIsComplete] = useState(false);\n\n // Read through a ref so a host's inline `value={(url) => ...}` factory prop\n // does not restart the subscription every render — only the effect's own\n // deps below do that.\n const eventSourceFactory = useEventSourceFactory();\n const eventSourceFactoryRef = useRef(eventSourceFactory);\n eventSourceFactoryRef.current = eventSourceFactory;\n\n useEffect(() => {\n const requestHeaders = JSON.parse(headersKey) as Record<string, string>;\n const runIds = runIdsKey ? runIdsKey.split(\",\") : [];\n setStateKey(subscriptionKey);\n if (!enabled || (runIds.length === 0 && !kind)) {\n setById({});\n setStreamsById({});\n setStatus(\"idle\");\n setIsComplete(false);\n return;\n }\n setById({});\n setStreamsById({});\n setIsComplete(false);\n\n const params = new URLSearchParams();\n if (runIds.length > 0) params.set(\"tasks\", runIdsKey);\n if (kind) params.set(\"kind\", kind);\n const query = params.toString();\n\n const merge = (incoming: TaskSnapshot[]) => {\n setById((prev) => {\n const next = { ...prev };\n for (const snap of incoming) next[snap.type === \"group\" ? `group:${snap.groupId ?? snap.id}` : snap.id] = snap;\n return next;\n });\n };\n\n // Deltas are applied whether or not the task frame has landed yet: the\n // stream is keyed by task id on its own, so output that arrives first is\n // held rather than dropped.\n const mergeOutput = (delta: TaskOutputDelta) => {\n setStreamsById((prev) => ({\n ...prev,\n [delta.id]: applyTaskOutputDelta(prev[delta.id] ?? emptyTaskStreams, delta),\n }));\n };\n\n // Polling fallback transport.\n if (forcePoll || headersKey !== \"{}\" || !hasEventSource()) {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const tick = async () => {\n try {\n const responses = await Promise.all(runIds.map((runId) => fetch(\n `${basePath}/tasks/${encodeURIComponent(runId)}`,\n { headers: { Accept: \"application/json\", ...requestHeaders } },\n )));\n if (responses.some((response) => !response.ok)) throw new Error(\"Task request failed\");\n if (responses.every((response) => response.ok)) {\n const snaps = (await Promise.all(responses.map(\n async (response) => (await response.json()) as TaskSnapshot[],\n ))).flat();\n if (stopped) return;\n merge(snaps);\n setStatus(\"polling\");\n if (allGroupsTerminal(snaps)) {\n setIsComplete(true);\n return; // stop polling\n }\n }\n } catch {\n if (!stopped) setStatus(\"connection lost — retrying\");\n }\n if (!stopped) timer = setTimeout(tick, pollMs);\n };\n void tick();\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n };\n }\n\n // SSE transport (default).\n const es = eventSourceFactoryRef.current(streamUrl(basePath, query));\n setStatus(\"connecting\");\n es.onopen = () => setStatus(\"connected\");\n es.addEventListener(\"task\", (e) => {\n try {\n merge([JSON.parse((e as MessageEvent).data) as TaskSnapshot]);\n setStatus(\"connected\");\n } catch {\n setStatus(\"invalid task stream data\");\n }\n });\n es.addEventListener(\"output\", (e) => {\n try {\n mergeOutput(JSON.parse((e as MessageEvent).data) as TaskOutputDelta);\n } catch {\n setStatus(\"invalid task output data\");\n }\n });\n es.addEventListener(\"done\", () => {\n setStatus(\"complete\");\n setIsComplete(true);\n es.close();\n });\n es.onerror = () => setStatus(\"connection lost — retrying\");\n return () => es.close();\n }, [subscriptionKey, runIdsKey, kind, basePath, enabled, pollMs, forcePoll, headersKey]);\n\n const current = stateKey === subscriptionKey;\n // The streams are merged back in only here, at the boundary. The polling\n // fallback never populates them — its JSON snapshots already carry the full\n // output — so this is a no-op on that transport.\n const snapshots = useMemo(\n () => current\n ? Object.values(byId).map((snapshot) => withTaskStreams(snapshot, streamsById[snapshot.id]))\n : [],\n [current, byId, streamsById],\n );\n\n return { snapshots, status: current ? status : \"idle\", isComplete: current && isComplete };\n}\n\nexport interface UseTaskRunsOptions extends TaskTransportOptions {\n kind?: string | undefined;\n status?: string | undefined;\n /** Label equality filters (k=v). */\n labels?: Record<string, string> | undefined;\n}\n\n/** The identity of the listing request that produced a given `runs` array. */\nexport type TaskRunsKey = ReturnType<typeof taskQueryKeys.runs>;\n\nexport interface UseTaskRunsResult {\n runs: TaskRunMeta[];\n status: string;\n /**\n * Query key of the request that actually produced `runs`, or null before the\n * first frame arrives. Changing `kind`/`status`/`labels` re-subscribes but\n * keeps the previous listing on screen until the new transport delivers, so\n * this key lags `taskQueryKeys.runs(options)` during that window. A consumer\n * mirroring `runs` into a cache must compare the two and skip the write while\n * they differ, or it publishes rows that do not match the requested filters.\n */\n runsKey: TaskRunsKey | null;\n}\n\n// useTaskRuns lists all runs (TaskRunMeta) for the manager view. It is SSE-first:\n// it subscribes to clicky's runs stream (event: runs carrying the full listing)\n// and falls back to polling the JSON runs endpoint only when EventSource is\n// unavailable or forced off. The listing has no completion terminal of its own —\n// a manager view stays subscribed to observe new and changing runs.\nexport function useTaskRuns(options: UseTaskRunsOptions = {}): UseTaskRunsResult {\n const {\n kind,\n status: statusFilter,\n labels,\n basePath = DEFAULT_BASE,\n enabled = true,\n pollMs = DEFAULT_POLL_MS,\n forcePoll = false,\n headers,\n } = options;\n const headersKey = JSON.stringify(headers ?? {});\n\n // Runs are stored together with the key of the request that delivered them, so\n // a stale listing can never be mistaken for the current filters' result.\n const [received, setReceived] = useState<{\n key: TaskRunsKey | null;\n runs: TaskRunMeta[];\n }>({ key: null, runs: [] });\n const [status, setStatus] = useState(\"idle\");\n\n const eventSourceFactory = useEventSourceFactory();\n const eventSourceFactoryRef = useRef(eventSourceFactory);\n eventSourceFactoryRef.current = eventSourceFactory;\n\n useEffect(() => {\n if (!enabled) return;\n const requestHeaders = JSON.parse(headersKey) as Record<string, string>;\n\n const params = new URLSearchParams();\n if (kind) params.set(\"kind\", kind);\n if (statusFilter) params.set(\"status\", statusFilter);\n for (const [k, v] of Object.entries(labels ?? {})) params.append(\"label\", `${k}=${v}`);\n const query = params.toString();\n const requestKey = taskQueryKeys.runs({ basePath, kind, status: statusFilter, labels });\n const receive = (next: TaskRunMeta[]) => setReceived({ key: requestKey, runs: next });\n\n // Polling fallback transport.\n if (forcePoll || headersKey !== \"{}\" || !hasEventSource()) {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const tick = async () => {\n try {\n const res = await fetch(`${basePath}/tasks${query ? `?${query}` : \"\"}`, {\n headers: { Accept: \"application/json\", ...requestHeaders },\n });\n if (!res.ok) throw new Error(`Task listing failed with ${res.status}`);\n if (res.ok) {\n const next = (await res.json()) as TaskRunMeta[];\n if (stopped) return;\n receive(next);\n setStatus(\"polling\");\n }\n } catch {\n if (!stopped) setStatus(\"connection lost — retrying\");\n }\n if (!stopped) timer = setTimeout(tick, pollMs);\n };\n void tick();\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n };\n }\n\n // SSE transport (default).\n const es = eventSourceFactoryRef.current(`${basePath}/tasks/runs/stream${query ? `?${query}` : \"\"}`);\n setStatus(\"connecting\");\n es.onopen = () => setStatus(\"connected\");\n es.addEventListener(\"runs\", (e) => {\n try {\n receive(JSON.parse((e as MessageEvent).data) as TaskRunMeta[]);\n setStatus(\"connected\");\n } catch {\n setStatus(\"invalid task listing data\");\n }\n });\n es.onerror = () => setStatus(\"connection lost — retrying\");\n return () => es.close();\n }, [kind, statusFilter, JSON.stringify(labels ?? {}), basePath, enabled, pollMs, forcePoll, headersKey]);\n\n return { runs: received.runs, status, runsKey: received.key };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flanksource/clicky-ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.54",
|
|
4
4
|
"description": "Flanksource Clicky UI — React component library built on shadcn/ui with light/dark and density theming.",
|
|
5
5
|
"homepage": "https://github.com/flanksource/clicky-ui#readme",
|
|
6
6
|
"bugs": "https://github.com/flanksource/clicky-ui/issues",
|