diff --git a/frontend/src/lib/components/ScriptEditor.svelte b/frontend/src/lib/components/ScriptEditor.svelte
index 88436327d4..c1cb4c7255 100644
--- a/frontend/src/lib/components/ScriptEditor.svelte
+++ b/frontend/src/lib/components/ScriptEditor.svelte
@@ -83,6 +83,7 @@
import { setContext } from 'svelte'
import HideButton from './apps/editor/settingsPanel/HideButton.svelte'
import { base } from '$lib/base'
+ import DispatchEventsButton from '$lib/components/runs/DispatchEventsButton.svelte'
import { SUPPORTED_CHAT_SCRIPT_LANGUAGES } from './copilot/chat/script/core'
import { getStringError } from './copilot/chat/utils'
import type { ScriptOptions } from './copilot/chat/ContextManager.svelte'
@@ -1810,6 +1811,30 @@
it visually pinned to the top edge without
relying on cross-browser overflow behaviour. -->
+ {#if testJob?.id && testJob.type === 'CompletedJob' && $workspaceStore}
+
+
+ {/if}
{#if testIsLoading}
diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetRunsPanel.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetRunsPanel.svelte
index a1a2f0d52d..0d0908cffd 100644
--- a/frontend/src/lib/components/assets/AssetGraph/AssetRunsPanel.svelte
+++ b/frontend/src/lib/components/assets/AssetGraph/AssetRunsPanel.svelte
@@ -20,6 +20,7 @@
import { Badge } from '$lib/components/common'
import { Popover } from '$lib/components/meltComponents'
import { twMerge } from 'tailwind-merge'
+ import DispatchEventsButton from '$lib/components/runs/DispatchEventsButton.svelte'
interface Props {
// Producers of this asset. Each contributes its own listExtendedJobs
@@ -288,6 +289,12 @@
href={`${base}/run/${selectedJob.id}?workspace=${$workspaceStore}`}
target="_blank">Open ↗
+ {#if $workspaceStore}
+
+ {/if}
{:else if runnableProducers.length === 0}
No producer for this asset
{:else if loading && jobs.length === 0}
diff --git a/frontend/src/lib/components/runs/DispatchEventsButton.svelte b/frontend/src/lib/components/runs/DispatchEventsButton.svelte
new file mode 100644
index 0000000000..19455e18de
--- /dev/null
+++ b/frontend/src/lib/components/runs/DispatchEventsButton.svelte
@@ -0,0 +1,48 @@
+
+
+{#if list.length > 0}
+
+ {#snippet trigger()}
+
+
+ {list.length} dispatched
+
+ {/snippet}
+ {#snippet content()}
+
+
+
+ {/snippet}
+
+{/if}
diff --git a/frontend/src/lib/components/runs/DispatchEventsPanel.svelte b/frontend/src/lib/components/runs/DispatchEventsPanel.svelte
index e24ee15579..545ff35488 100644
--- a/frontend/src/lib/components/runs/DispatchEventsPanel.svelte
+++ b/frontend/src/lib/components/runs/DispatchEventsPanel.svelte
@@ -1,8 +1,7 @@
{#if list.length > 0}
Dispatch
-
-
-
- Outcome
- Subscriber
- Asset
- Detail
-
-
-
- {#each list as ev (ev.created_at + ev.subscriber_path + ev.asset_path)}
-
-
- {#if ev.outcome === 'dispatched'}
-
- dispatched
-
- {:else if ev.outcome === 'join_pending'}
-
- join pending
-
- {:else}
-
- skipped
-
- {/if}
-
-
- {ev.subscriber_path}
-
-
- {ev.asset_kind}://{ev.asset_path}
-
-
- {#if ev.outcome === 'dispatched' && ev.child_job_id}
-
- {ev.child_job_id.slice(0, 8)}…
-
-
- {#if ev.partition}
-
- · partition {ev.partition}
- {/if}
- {#if ev.debounce_s && ev.debounce_s > 0}
- · debounced {ev.debounce_s}s
- {/if}
- {:else if ev.outcome === 'join_pending'}
- {ev.received_inputs ?? 0}/{ev.required_inputs ?? 0} inputs received
- {#if ev.partition}
-
- for partition {ev.partition}
- {/if}
- {:else}
- {reasonLabel(ev.reason)}
- {/if}
-
-
- {/each}
-
-
+
{/if}
diff --git a/frontend/src/lib/components/runs/DispatchEventsTable.svelte b/frontend/src/lib/components/runs/DispatchEventsTable.svelte
new file mode 100644
index 0000000000..6ed288a6b8
--- /dev/null
+++ b/frontend/src/lib/components/runs/DispatchEventsTable.svelte
@@ -0,0 +1,90 @@
+
+
+
+
+
+ Outcome
+ Subscriber
+ Asset
+ Detail
+
+
+
+ {#each events as ev (ev.created_at + ev.subscriber_path + ev.asset_path)}
+
+
+ {#if ev.outcome === 'dispatched'}
+
+ dispatched
+
+ {:else if ev.outcome === 'join_pending'}
+
+ join pending
+
+ {:else}
+
+ skipped
+
+ {/if}
+
+
+ {ev.subscriber_path}
+
+
+ {ev.asset_kind}://{ev.asset_path}
+
+
+ {#if ev.outcome === 'dispatched' && ev.child_job_id}
+
+ {ev.child_job_id.slice(0, 8)}…
+
+
+ {#if ev.partition}
+
+ · partition {ev.partition}
+ {/if}
+ {#if ev.debounce_s && ev.debounce_s > 0}
+ · debounced {ev.debounce_s}s
+ {/if}
+ {:else if ev.outcome === 'join_pending'}
+ {ev.received_inputs ?? 0}/{ev.required_inputs ?? 0} inputs received
+ {#if ev.partition}
+
+ for partition {ev.partition}
+ {/if}
+ {:else}
+ {reasonLabel(ev.reason)}
+ {/if}
+
+
+ {/each}
+
+