diff --git a/frontend/src/lib/components/MemoryFootprintViewer.svelte b/frontend/src/lib/components/MemoryFootprintViewer.svelte index 9547861bc3..14ad1a2b06 100644 --- a/frontend/src/lib/components/MemoryFootprintViewer.svelte +++ b/frontend/src/lib/components/MemoryFootprintViewer.svelte @@ -3,7 +3,7 @@ import { type MetricDataPoint, MetricsService } from '$lib/gen' import { displayTime } from '$lib/utils' - import { workspaceStore } from '$lib/stores' + import { useOperatingWorkspace } from './operatingWorkspace.svelte' import { CategoryScale, Chart as ChartJS, @@ -26,6 +26,8 @@ let { jobId, jobUpdateLastFetch }: Props = $props(); + const operatingWorkspace = useOperatingWorkspace() + let jobMetricsLastFetch: Date | undefined = undefined let jobMemoryStats: MetricDataPoint[] | undefined = $state(undefined) @@ -38,7 +40,7 @@ async function loadMetricsData() { try { let jobStatsPromise = MetricsService.getJobMetrics({ - workspace: $workspaceStore!, + workspace: $operatingWorkspace!, id: jobId, requestBody: { from_timestamp: jobMetricsLastFetch?.toISOString(), diff --git a/frontend/src/lib/components/runs/RunDetail.svelte b/frontend/src/lib/components/runs/RunDetail.svelte new file mode 100644 index 0000000000..1241bc9c60 --- /dev/null +++ b/frontend/src/lib/components/runs/RunDetail.svelte @@ -0,0 +1,1450 @@ + + + + + + +{#if (job?.job_kind == 'flow' || isFlowPreview(job?.job_kind)) && job?.['running'] && job?.parent_job == undefined} + + + {#snippet actions()} +
+
+ +
+ +
+ {/snippet} +

+					
+			
+
+
+{/if} +{#if !job || (job?.job_kind != 'flow' && job?.job_kind != 'flownode' && job?.job_kind != 'flowpreview')} + +{/if} + +{#snippet flowRestartButton( + state: ReturnType, + step: string, + triggerStyle: 'button' | 'link' +)} + {#if job} + { + onNavigate('/run/' + newJobId + '?workspace=' + workspaceId) + }} + flowPath={job.script_path} + flowVersionId={job.script_hash ? parseInt(job.script_hash, 16) : undefined} + disabled={!$enterpriseLicense} + enterpriseOnly={!$enterpriseLicense} + {triggerStyle} + unifiedSize="sm" + /> + {/if} +{/snippet} + + + + + +{#if loadError?.status === 403} +
+
+ +
+

+ This run exists in {workspaceId}, but you don't have + permission to view it. +

+

+ Ask a colleague who can see it to open the run and use the + Share button to send you a read-only link. Opening that + link will grant you access to this run (and its steps). +

+
+
+
+ +
+
+
+{:else if notfound || (job?.workspace_id != undefined && workspaceId != undefined && job?.workspace_id != workspaceId)} + {@const currentWorkspace = $userWorkspaces.find((w) => w.id === workspaceId)} + {@const otherWorkspaces = $userWorkspaces.filter((w) => w.id !== workspaceId)} +
+
+
+ +
+

Run not found

+

+ No run with ID + {runId} + exists in + {currentWorkspace?.name ?? workspaceId}. It + may belong to another workspace, or it may have been deleted. +

+
+ + {#if otherWorkspaces.length > 0} +
+

Look in another workspace

+
+ {#each otherWorkspaces as workspace (workspace.id)} + + {/each} +
+
+ {/if} + +
+ +
+
+{:else} + + + {#if embedded} +
+ {@render runActions()} +
+ {:else} + + {/if} + + {#snippet runActions()} + {@const isScript = job?.job_kind === 'script'} + {@const isHubFlowPreview = isFlowPreview(job?.job_kind) && isHubFlowPath(job?.script_path)} + {@const runsHref = `/runs/${job?.script_path}${!isScript ? '?jobKind=flow' : ''}`} + {#if job && 'deleted' in job && !job?.deleted && ($superadmin || (actingUser?.is_admin ?? false))} + { + job?.id && deleteCompletedJob(job.id) + }, + type: 'delete' + } + ]} + > + {#snippet buttonReplacement()} + + {/if} + {/if} + {#if job} + job && shareReadLink(job.id) + }, + { + displayName: 'Copy public link', + icon: Globe, + disabled: !canSharePublicly, + tooltip: canSharePublicly + ? "Read-only link that anyone on the internet can open, without logging in. It shows a minimal version of this page: this run's inputs, result and logs, and for a flow its graph plus every step's inputs, result, logs and code. The link cannot be revoked." + : 'Sharing a run publicly is restricted in this workspace. Ask an admin to share it, or to grant you a bypass on the ruleset.', + action: () => job && sharePublicLink(job.id) + } + ]} + > + {#snippet buttonReplacement()} + + {/snippet} + + {/if} + {@const stem = job?.job_kind === 'script_hub' ? '/scripts' : `/${job?.job_kind}s`} + + {@const viewHref = + isScript && embedded + ? `/scripts/get/${job?.script_path}?version=${job?.script_hash}` + : `${stem}/get/${isScript ? job?.script_hash : job?.script_path}`} + {#if (job?.job_kind == 'flow' || isFlowPreview(job?.job_kind)) && job?.['running'] && job?.parent_job == undefined} +
+ { + debugInfo() + } + } + ]} + class="h-auto" + > + {#snippet buttonReplacement()} + + {/snippet} + +
+ {/if} + {#if isFlowPreview(job?.job_kind) || isScriptPreview(job?.job_kind)} + + {/if} + {#if persistentScriptDefinition !== undefined} + + {/if} + {#if canRunNow} + + {/if} + {#if job && job?.type != 'CompletedJob' && (!job?.schedule_path || job?.['running'] == true)} + {#if !forceCancel} + + {:else} + + {/if} + {/if} + {#if job?.schedule_path} + + {/if} + {#if restartStep !== undefined && canRestart(restart)} + {@render flowRestartButton(restart, restartStep, 'button')} + {/if} + {#if job?.job_kind === 'script' || job?.job_kind === 'script_hub' || job?.job_kind === 'flow'} + + {/if} + {#if job?.job_kind === 'script' || job?.job_kind === 'flow'} + {#if !actingUser?.operator} + {#if editRights.canEditItem(isScript ? 'script' : 'flow', job?.script_path ?? '', {})} + + {#if showEditButton} + + + {/if} + {/if} + {#if !showEditButton && !isCloudHosted() && editInForkAllowed(workspaceId, $userWorkspaces)} + + {/if} + {/if} + {/if} + {#if job?.job_kind === 'script' || job?.job_kind === 'script_hub' || job?.job_kind === 'flow'} + + {/if} + {/snippet} +
+ +
+ + {#if job} + { + persistentScriptDrawer?.open?.(persistentScriptDefinition) + }} + {concurrencyKey} + /> + {/if} +
+ {#if job?.['deleted']} +
+ + The content of this run was deleted (by an admin, no less) + +
+
+ {/if} + + + {#if job?.job_kind === 'flow' || job?.job_kind === 'flowpreview'} +
+ + {#snippet errorAction()} + + {#if failedTopLevelStep && failedTopLevelStep !== AGENT_STEP_ID} + at step {failedTopLevelStep} + {#if $enterpriseLicense && canRestart(failedRestart)} + · + {@render flowRestartButton(failedRestart, failedTopLevelStep, 'link')} + on this flow version, or on a new one after you fix it + {/if} + {/if} + {/snippet} + + {#if suspendStatus} + + {/if} +
+ {/if} + + +
+
Inputs
+
+ + {#if job && currentJobIsLongRunning && showExplicitProgressTip && !scriptProgress && 'running' in job} + + + + {/if} +
+
+ + {#if job} + + {/if} + + {#if isNotFlow(job?.job_kind)} + {#if ['python3', 'bun', 'deno'].includes(job?.language ?? '') && (job?.job_kind == 'script' || isScriptPreview(job?.job_kind))} + + {/if} +
+ {#if job?.workflow_as_code_status && job.job_kind !== 'aiagent'} +
+

Workflow Timeline

+
+ +
+
+ {/if} + {#if scriptProgress} + + {/if} + + + {#if job?.language === 'dbt' && job?.script_path} +
+

Models

+ +
+ {/if} + + + {#if job} +
+

Result

+
+ {#if job.result_stream || (job.type == 'CompletedJob' && job.result !== undefined)} + + {:else} +
No output is available yet
+ {/if} +
+
+ {#if job.id && job.workspace_id} + + + {/if} + {/if} + + +
+ + + + + + {#if isScriptPreview(job?.job_kind)} + + {/if} + + + + {#if job} +
+ {#if viewTab == 'logs'} +
+ +
+ {:else if viewTab == 'assets'} +
+ +
+ {:else if viewTab == 'traces'} +
+ +
+ {:else if viewTab == 'code'} + {#if job && 'raw_code' in job && job.raw_code} +
+ +
+ {:else if job} + No code available + {:else} + + {/if} + {:else if viewTab == 'stats'} +
+ +
+ {:else} +
Select a tab to view content
+ {/if} +
+ {/if} +
+
+ {:else if !job?.['deleted']} +
+ +
+ {#if job?.id} + { + job = newJob + }} + onDone={({ job: newJob }) => { + job = newJob + }} + initialJob={job} + {workspaceId} + bind:selectedJobStep + bind:suspendStatus + bind:isOwner + bind:localModuleStates={graphModuleStates} + bind:expandedSubflows + /> + {:else} + + {/if} +
+ {/if} +
+{/if} + + diff --git a/frontend/src/lib/components/runs/ScriptRetryChain.svelte b/frontend/src/lib/components/runs/ScriptRetryChain.svelte index 2b2fc2449b..f7334467da 100644 --- a/frontend/src/lib/components/runs/ScriptRetryChain.svelte +++ b/frontend/src/lib/components/runs/ScriptRetryChain.svelte @@ -6,7 +6,10 @@ import { resource } from 'runed' import { canSkipRetryChainQuery } from './scriptRetryChain' - let { job }: { job: Job } = $props() + let { + job, + onNavigate = goto + }: { job: Job; onNavigate?: (url: string) => void | Promise } = $props() type AttemptColor = 'green' | 'red' | 'blue' | 'gray' type Attempt = { id: string; color: AttemptColor; current: boolean } @@ -141,7 +144,7 @@ size="xs" color="gray" variant="border" - onclick={() => goto(`/run/${self?.handledJob}?workspace=${ws}`)} + onclick={() => onNavigate(`/run/${self?.handledJob}?workspace=${ws}`)} > Handled run @@ -163,7 +166,7 @@ size="xs" color={attempt.color} variant={attempt.current ? 'contained' : 'border'} - onclick={() => goto(`/run/${attempt.id}?workspace=${ws}`)} + onclick={() => onNavigate(`/run/${attempt.id}?workspace=${ws}`)} > {i === 0 ? 'Original' : `Attempt ${i}`} @@ -180,7 +183,7 @@ size="xs" color={handler.color} variant="border" - onclick={() => goto(`/run/${handler.id}?workspace=${ws}`)} + onclick={() => onNavigate(`/run/${handler.id}?workspace=${ws}`)} > {HANDLER_LABEL[handler.kind]} diff --git a/frontend/src/lib/components/sessions/ItemViewerView.svelte b/frontend/src/lib/components/sessions/ItemViewerView.svelte index ca5fc2a0e5..5c8299de15 100644 --- a/frontend/src/lib/components/sessions/ItemViewerView.svelte +++ b/frontend/src/lib/components/sessions/ItemViewerView.svelte @@ -28,6 +28,8 @@ kind, path, version, + locationHash = '', + dbtRetryFrom, workspaceId, tabId, container, @@ -42,6 +44,10 @@ path: string /** Deployed version this tab is pinned to, from its URL's `?version=`. */ version?: string + /** `#`-fragment of the tab's URL: the args a run's `Run again` prefills the form with. */ + locationHash?: string + /** The failed dbt run that `Run again` offers to resume, from `?dbt_retry_from=`. */ + dbtRetryFrom?: string workspaceId: string /** Whether this side is the one on screen. Hidden sides stay mounted, so this gates * the window-level keyboard handler — several instances listen on the window. */ @@ -83,10 +89,20 @@ reloadKey++ } - const searchParams = $derived(version ? new URLSearchParams({ version }) : undefined) + const searchParams = $derived( + version || dbtRetryFrom + ? new URLSearchParams({ + ...(version ? { version } : {}), + ...(dbtRetryFrom ? { dbt_retry_from: dbtRetryFrom } : {}) + }) + : undefined + ) // Remounts the detail page below, which loads on mount. - const loadKey = $derived(`${workspaceId}/${kind}/${path}/${version ?? ''}/${reloadKey}`) + // The fragment is part of it: the detail pages seed their form from it once, on mount. + const loadKey = $derived( + `${workspaceId}/${kind}/${path}/${version ?? ''}/${locationHash}/${reloadKey}` + ) // Nothing deployed at this path. Stamped with the load it answers, so a refetch or a // re-pointed tab is never answered from an earlier load's verdict. let notFoundFor: string | undefined = $state(undefined) @@ -214,6 +230,7 @@ hash={path} workspace={workspaceId} {searchParams} + {locationHash} {onNavigate} {active} embedded @@ -224,6 +241,7 @@ {path} workspace={workspaceId} {searchParams} + {locationHash} {onNavigate} {active} embedded diff --git a/frontend/src/lib/components/sessions/PreviewTabHost.svelte b/frontend/src/lib/components/sessions/PreviewTabHost.svelte index 3b8e722441..1ecb921c98 100644 --- a/frontend/src/lib/components/sessions/PreviewTabHost.svelte +++ b/frontend/src/lib/components/sessions/PreviewTabHost.svelte @@ -97,6 +97,9 @@ ? (new URL(tab.url, 'http://x').searchParams.get('version') ?? undefined) : undefined ) + // What a run's `Run again` hands the deployed page it opens: the run's args in the + // fragment, and for a dbt run the failure it could resume instead. + const viewerUrl = $derived(slot.kind === 'viewer' ? new URL(tab.url, 'http://x') : undefined) // The path an editor reports is its deployed one, which a staged rename can make // differ from this tab's — so the view opens on what actually exists. @@ -147,6 +150,7 @@ // A page item's editor reads its draft only when it loads, so a reload remounts it. let pageItemReloadNonce = $state(0) let pageListReloadNonce = $state(0) + let runReloadNonce = $state(0) export function reload() { // A live editor shares the runtime store the chat mutates, so generic chat @@ -168,6 +172,10 @@ pageListReloadNonce++ return } + if (slot.kind === 'run') { + runReloadNonce++ + return + } try { const win = frame?.contentWindow if (!win) return @@ -393,6 +401,8 @@ kind={itemKind} path={itemPath} version={pinnedVersion} + locationHash={viewerUrl?.hash} + dbtRetryFrom={viewerUrl?.searchParams.get('dbt_retry_from') ?? undefined} {workspaceId} tabId={tab.id} container={overlayHostEl} @@ -444,6 +454,29 @@ {/await} {/if} +{:else if slot.kind === 'run' && mounted && runtime} +
+ {#if overlayHostEl} + {#await import('./RunViewerView.svelte')} + {@render editorLoading()} + {:then Module} + + + {/await} + {/if} +
{:else if slot.kind === 'artifact' && mounted}
+ /* + * A run in a session preview tab, rendered by the component the workspace `/run/` page + * renders. Everything it reaches is scoped to the run's workspace through `workspace`, and + * every navigation it attempts is caught here and turned into a move inside the preview + * panel, so nothing takes the browser out of the session. + */ + import { setContext, untrack } from 'svelte' + import RunDetail from '$lib/components/runs/RunDetail.svelte' + import type { SessionRuntime } from './sessionRuntime.svelte' + import { + parseHistoricalScriptEdit, + parsePreviewItemRoute, + parseRunRoute, + previewLocationLabel, + barePath + } from './previewRouter' + + let { + runtime, + jobId, + workspaceId, + tabId, + container, + reloadNonce = 0 + }: { + runtime: SessionRuntime + jobId: string + workspaceId: string + /** The preview tab hosting this run, which a move to another run re-points. */ + tabId: string + /** The tab's host element, which also holds the overlays the page portals out. */ + container: HTMLElement | undefined + /** Bumped to load the run again from the server. */ + reloadNonce?: number + } = $props() + + // Mounted in the preview panel, outside the chat's own subtree, as ItemViewerView is: the + // run page's AI entry points would otherwise offer a session from inside one. + setContext( + 'aiChatManager', + untrack(() => runtime.manager) + ) + + // Everything the run page navigates to, resolved against the preview panel. Another run — + // a re-run, a retry, the parent job — is still this tab's subject, so it re-points in + // place; an item or any other page opens beside it. Nothing reaches the top-level router. + function onNavigate(url: string): void { + const parsed = new URL(url, window.location.origin) + const href = `${parsed.pathname}${parsed.search}${parsed.hash}` + if (parseRunRoute(href)) { + runtime.previewTabs.navigate({ type: 'page', href, label: previewLocationLabel(href) }, tabId) + return + } + const route = parsePreviewItemRoute(href) + // Legacy drag-and-drop apps and historical script edits have no in-panel host, so they + // fall through to a page tab. So does `Run again`, whose args ride in the fragment, and + // a link to one version: opening an item focuses a tab already on it as it stands, + // where a page tab's URL is written whole, pin and fragment included. + if ( + route && + (route.kind !== 'app' || route.raw_app) && + !parseHistoricalScriptEdit(href) && + !parsed.hash && + !parsed.searchParams.has('version') + ) { + runtime.previewTabs.open({ + type: 'item', + item: { path: route.itemPath, summary: '', kind: route.kind, raw_app: route.raw_app }, + mode: route.mode + }) + return + } + runtime.previewTabs.open({ type: 'page', href, label: previewLocationLabel(barePath(href)) }) + } + + // Every in-app link under the tab, not only the ones that call `onNavigate`: a plain anchor + // followed by the browser would take it out of the session. Listened for on `body`, as + // ItemViewerView does and for the same reason. + $effect(() => { + const el = container + if (!el) return + function onClick(e: MouseEvent) { + if (e.defaultPrevented || e.button !== 0) return + if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return + const target = e.target as Element | null + if (!target || !el!.contains(target)) return + const a = target.closest?.('a[href]') as HTMLAnchorElement | null + if (!a || (a.target && a.target !== '_self') || a.hasAttribute('download')) return + const url = new URL(a.href, window.location.href) + if (url.origin !== window.location.origin) return + e.preventDefault() + if (a.getAttribute('href')?.startsWith('#')) return + onNavigate(`${url.pathname}${url.search}${url.hash}`) + } + document.body.addEventListener('click', onClick) + return () => document.body.removeEventListener('click', onClick) + }) + + +
+ + {#key `${workspaceId}/${jobId}/${reloadNonce}`} + + {/key} +
diff --git a/frontend/src/lib/components/sessions/previewRouter.test.ts b/frontend/src/lib/components/sessions/previewRouter.test.ts index 038691da55..8c9da253bb 100644 --- a/frontend/src/lib/components/sessions/previewRouter.test.ts +++ b/frontend/src/lib/components/sessions/previewRouter.test.ts @@ -316,6 +316,18 @@ describe('resolvePreviewTab', () => { expect(resolvePreviewTab('/runs')).toEqual({ kind: 'iframe' }) }) + it('mounts a run in process, in the workspace its URL names', () => { + expect(resolvePreviewTab('/run/01a0?workspace=fork')).toEqual({ + kind: 'run', + jobId: '01a0', + workspace: 'fork' + }) + expect(resolvePreviewTab('/run/01a0')).toEqual({ kind: 'run', jobId: '01a0' }) + expect(resolvePreviewTab('/run/01a0?workspace=fork&view_token=t')).toEqual({ kind: 'iframe' }) + // The runs of one item are a list page, not a run. + expect(resolvePreviewTab('/runs/f/a/b')).toEqual({ kind: 'iframe' }) + }) + it('mounts the list page of a page item kind in process, whatever its filters', () => { expect(resolvePreviewTab('/variables?path_start=f%2Fa%2F')).toEqual({ kind: 'pagelist', diff --git a/frontend/src/lib/components/sessions/previewRouter.ts b/frontend/src/lib/components/sessions/previewRouter.ts index 2de5354c4d..e9d8c432fe 100644 --- a/frontend/src/lib/components/sessions/previewRouter.ts +++ b/frontend/src/lib/components/sessions/previewRouter.ts @@ -464,9 +464,9 @@ export function previewLocationLabel(url: string): string { if (page) return page.label const trigger = triggerLabelForPath(url) if (trigger) return trigger - const run = barePath(url).match(/^\/run\/([^/?#]+)/) + const run = parseRunRoute(url) // Job ids are time-ordered, so runs started moments apart share their head. - if (run) return `Run ${decodeURIComponent(run[1]).slice(-8)}` + if (run) return `Run ${run.jobId.slice(-8)}` // The runs of one item, as a detail page's Runs button links them. const itemRuns = barePath(url).match(/^\/runs\/(.+)$/) if (itemRuns) return `Runs · ${decodeURIComponent(itemRuns[1]).split('/').pop()}` @@ -550,6 +550,25 @@ export function parseRunFormRoute(url: string): { toolCallId: string; label: str } } +/** The run a `/run/` location shows, in the workspace its query names (the opener's, + * when it names none). */ +export function parseRunRoute( + url: string +): { jobId: string; workspace?: string; viewToken: boolean } | null { + const m = barePath(url).match(/^\/run\/([^/?#]+)$/) + if (!m) return null + try { + const query = new URL(url, 'http://_').searchParams + return { + jobId: decodeURIComponent(m[1]), + workspace: query.get('workspace') || undefined, + viewToken: query.has('view_token') + } + } catch { + return null + } +} + export function runFormUrl(toolCallId: string, label: string): string { return `runform:${encodeURIComponent(toolCallId)}#${encodeURIComponent(label)}` } @@ -575,7 +594,7 @@ export const isArtifactKey = (key: string) => key.startsWith('artifact:') // flow, raw app) mounts its per-(kind,path) cell editor on `/edit/` and its viewer // on `/get/`; a `/pipeline/` route mounts the data-pipeline graph editor of // that folder (`path` is the folder); the list page of a page item kind mounts that -// list; everything +// list; a `/run/` route without a share token mounts that run's page; everything // else (static pages, regular drag-and-drop apps, any other route) stays an iframe. export type PreviewSlot = | { kind: 'editor'; editorKind: SessionTargetKind | 'pipeline'; path: string } @@ -584,6 +603,7 @@ export type PreviewSlot = | { kind: 'runform'; toolCallId: string } | { kind: 'pageitem'; ref: PageItemRef } | { kind: 'pagelist'; path: string } + | { kind: 'run'; jobId: string; workspace?: string } | { kind: 'iframe' } export function resolvePreviewTab(url: string): PreviewSlot { @@ -595,6 +615,12 @@ export function resolvePreviewTab(url: string): PreviewSlot { if (pageItem) return { kind: 'pageitem', ref: pageItem } const route = barePath(url) if (isPageItemListPath(route)) return { kind: 'pagelist', path: route } + const run = parseRunRoute(url) + // A share link's token authorizes every request of the window that installs it, so a run + // opened through one keeps a frame of its own rather than lending the token to the session. + if (run && !run.viewToken) { + return { kind: 'run', jobId: run.jobId, ...(run.workspace && { workspace: run.workspace }) } + } const pipelineFolder = parsePipelineRoute(url) if (pipelineFolder) { return { kind: 'editor', editorKind: 'pipeline', path: pipelineFolder } @@ -628,6 +654,7 @@ export function workspacePageHref(location: string): string | undefined { case 'editor': case 'viewer': case 'pagelist': + case 'run': case 'iframe': return location default: { diff --git a/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts b/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts index 2995366ae0..8d242912dc 100644 --- a/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts +++ b/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts @@ -471,6 +471,7 @@ function createRuntime(session: Session): SessionRuntime { slotKind !== 'iframe' && slotKind !== 'pageitem' && slotKind !== 'pagelist' && + slotKind !== 'run' && slotKind !== 'viewer' ) return undefined @@ -593,8 +594,8 @@ function createRuntime(session: Session): SessionRuntime { onTabOpened: logTabUsage }) - // Let the jobs tray open a run in this session's preview panel (as an iframe - // tab over the run page). The global side-panel chat leaves this unset and + // Let the jobs tray open a run in this session's preview panel (as a tab on + // the run's page). The global side-panel chat leaves this unset and // falls back to a new browser tab. manager.openRunInPreview = ({ jobId, workspace, label }) => { previewTabs.open({ diff --git a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte index 48931c7871..ca5d8c83ad 100644 --- a/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/run/[...run]/+page.svelte @@ -1,1373 +1,13 @@ - - - - -{#if (job?.job_kind == 'flow' || isFlowPreview(job?.job_kind)) && job?.['running'] && job?.parent_job == undefined} - - - {#snippet actions()} -
-
- -
- -
- {/snippet} -

-					
-			
-
-
-{/if} -{#if !job || (job?.job_kind != 'flow' && job?.job_kind != 'flownode' && job?.job_kind != 'flowpreview')} - -{/if} - -{#snippet flowRestartButton( - state: ReturnType, - step: string, - triggerStyle: 'button' | 'link' -)} - {#if job} - { - goto('/run/' + newJobId + '?workspace=' + $workspaceStore) - }} - flowPath={job.script_path} - flowVersionId={job.script_hash ? parseInt(job.script_hash, 16) : undefined} - disabled={!$enterpriseLicense} - enterpriseOnly={!$enterpriseLicense} - {triggerStyle} - unifiedSize="sm" - /> - {/if} -{/snippet} - - - - - -{#if loadError?.status === 403} -
-
- -
-

- This run exists in {$workspaceStore}, but you don't - have permission to view it. -

-

- Ask a colleague who can see it to open the run and use the - Share button to send you a read-only link. Opening that - link will grant you access to this run (and its steps). -

-
-
-
- -
-
-
-{:else if notfound || (job?.workspace_id != undefined && $workspaceStore != undefined && job?.workspace_id != $workspaceStore)} - {@const currentWorkspace = $userWorkspaces.find((w) => w.id === $workspaceStore)} - {@const otherWorkspaces = $userWorkspaces.filter((w) => w.id !== $workspaceStore)} -
-
-
- -
-

Run not found

-

- No run with ID - {page.params.run} - exists in - {currentWorkspace?.name ?? $workspaceStore}. It may belong to another workspace, or it may have been deleted. -

-
- - {#if otherWorkspaces.length > 0} -
-

Look in another workspace

-
- {#each otherWorkspaces as workspace (workspace.id)} - - {/each} -
-
- {/if} - -
- -
-
-{:else} - - - - - {#snippet runActions()} - {@const isScript = job?.job_kind === 'script'} - {@const isHubFlowPreview = isFlowPreview(job?.job_kind) && isHubFlowPath(job?.script_path)} - {@const runsHref = `/runs/${job?.script_path}${!isScript ? '?jobKind=flow' : ''}`} - {#if job && 'deleted' in job && !job?.deleted && ($superadmin || ($userStore?.is_admin ?? false))} - { - job?.id && deleteCompletedJob(job.id) - }, - type: 'delete' - } - ]} - > - {#snippet buttonReplacement()} - - {/if} - {/if} - {#if job} - job && shareReadLink(job.id) - }, - { - displayName: 'Copy public link', - icon: Globe, - disabled: !canSharePublicly, - tooltip: canSharePublicly - ? "Read-only link that anyone on the internet can open, without logging in. It shows a minimal version of this page: this run's inputs, result and logs, and for a flow its graph plus every step's inputs, result, logs and code. The link cannot be revoked." - : 'Sharing a run publicly is restricted in this workspace. Ask an admin to share it, or to grant you a bypass on the ruleset.', - action: () => job && sharePublicLink(job.id) - } - ]} - > - {#snippet buttonReplacement()} - - {/snippet} - - {/if} - {@const stem = job?.job_kind === 'script_hub' ? '/scripts' : `/${job?.job_kind}s`} - {@const viewHref = `${stem}/get/${isScript ? job?.script_hash : job?.script_path}`} - {#if (job?.job_kind == 'flow' || isFlowPreview(job?.job_kind)) && job?.['running'] && job?.parent_job == undefined} -
- { - debugInfo() - } - } - ]} - class="h-auto" - > - {#snippet buttonReplacement()} - - {/snippet} - -
- {/if} - {#if isFlowPreview(job?.job_kind) || isScriptPreview(job?.job_kind)} - - {/if} - {#if persistentScriptDefinition !== undefined} - - {/if} - {#if canRunNow} - - {/if} - {#if job && job?.type != 'CompletedJob' && (!job?.schedule_path || job?.['running'] == true)} - {#if !forceCancel} - - {:else} - - {/if} - {/if} - {#if job?.schedule_path} - - {/if} - {#if restartStep !== undefined && canRestart(restart)} - {@render flowRestartButton(restart, restartStep, 'button')} - {/if} - {#if job?.job_kind === 'script' || job?.job_kind === 'script_hub' || job?.job_kind === 'flow'} - - {/if} - {#if job?.job_kind === 'script' || job?.job_kind === 'flow'} - {#if !$userStore?.operator} - {#if editRights.canEditItem(isScript ? 'script' : 'flow', job?.script_path ?? '', {})} - - {#if showEditButton} - - - {/if} - {/if} - {#if !showEditButton && !isCloudHosted() && editInForkAllowed($workspaceStore, $userWorkspaces)} - - {/if} - {/if} - {/if} - {#if job?.job_kind === 'script' || job?.job_kind === 'script_hub' || job?.job_kind === 'flow'} - - {/if} - {/snippet} -
- -
- - {#if job} - { - persistentScriptDrawer?.open?.(persistentScriptDefinition) - }} - {concurrencyKey} - /> - {/if} -
- {#if job?.['deleted']} -
- - The content of this run was deleted (by an admin, no less) - -
-
- {/if} - - - {#if job?.job_kind === 'flow' || job?.job_kind === 'flowpreview'} -
- - {#snippet errorAction()} - - {#if failedTopLevelStep && failedTopLevelStep !== AGENT_STEP_ID} - at step {failedTopLevelStep} - {#if $enterpriseLicense && canRestart(failedRestart)} - · - {@render flowRestartButton(failedRestart, failedTopLevelStep, 'link')} - on this flow version, or on a new one after you fix it - {/if} - {/if} - {/snippet} - - {#if suspendStatus} - - {/if} -
- {/if} - - -
-
Inputs
-
- - {#if job && currentJobIsLongRunning && showExplicitProgressTip && !scriptProgress && 'running' in job} - - - - {/if} -
-
- - {#if job} - - {/if} - - {#if isNotFlow(job?.job_kind)} - {#if ['python3', 'bun', 'deno'].includes(job?.language ?? '') && (job?.job_kind == 'script' || isScriptPreview(job?.job_kind))} - - {/if} -
- {#if job?.workflow_as_code_status && job.job_kind !== 'aiagent'} -
-

Workflow Timeline

-
- -
-
- {/if} - {#if scriptProgress} - - {/if} - - - {#if job?.language === 'dbt' && job?.script_path} -
-

Models

- -
- {/if} - - - {#if job} -
-

Result

-
- {#if job.result_stream || (job.type == 'CompletedJob' && job.result !== undefined)} - - {:else} -
No output is available yet
- {/if} -
-
- {#if job.id && job.workspace_id} - - - {/if} - {/if} - - -
- - - - - - {#if isScriptPreview(job?.job_kind)} - - {/if} - - - - {#if job} -
- {#if viewTab == 'logs'} -
- -
- {:else if viewTab == 'assets'} -
- -
- {:else if viewTab == 'traces'} -
- -
- {:else if viewTab == 'code'} - {#if job && 'raw_code' in job && job.raw_code} -
- -
- {:else if job} - No code available - {:else} - - {/if} - {:else if viewTab == 'stats'} -
- -
- {:else} -
Select a tab to view content
- {/if} -
- {/if} -
-
- {:else if !job?.['deleted']} -
- -
- {#if job?.id} - { - job = newJob - }} - onDone={({ job: newJob }) => { - job = newJob - }} - initialJob={job} - workspaceId={$workspaceStore} - bind:selectedJobStep - bind:suspendStatus - bind:isOwner - bind:localModuleStates={graphModuleStates} - bind:expandedSubflows - /> - {:else} - - {/if} -
- {/if} -
-{/if} - -