mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 08:01:35 +00:00
all
This commit is contained in:
@@ -37,6 +37,11 @@
|
||||
// metadata out themselves (the picker already has its own).
|
||||
showMetadata?: boolean
|
||||
class?: string
|
||||
// Bump this to force a re-fetch (metadata + preview). Used by the
|
||||
// asset detail pane after an upstream producer run completes —
|
||||
// without it, the "Asset not yet materialized" empty state stays
|
||||
// pinned until the user re-selects the asset.
|
||||
refreshKey?: any
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -45,7 +50,8 @@
|
||||
loadFilePreviewRequest = HelpersService.loadFilePreview,
|
||||
loadFileMetadataRequest = HelpersService.loadFileMetadata,
|
||||
showMetadata = false,
|
||||
class: className = ''
|
||||
class: className = '',
|
||||
refreshKey
|
||||
}: Props = $props()
|
||||
|
||||
let csvSeparatorChar: string = $state(',')
|
||||
@@ -91,12 +97,15 @@
|
||||
return body.includes('not found') || body.includes('404')
|
||||
}
|
||||
|
||||
// Reload whenever the file key changes. Tracking the workspace too —
|
||||
// the asset graph spans workspaces so a re-mount with the same key but
|
||||
// a different ws should refetch.
|
||||
// Reload whenever the file key, workspace, or external refreshKey
|
||||
// changes. The refreshKey path is what lets the asset pane re-check
|
||||
// existence after an upstream run completes — moving from the
|
||||
// "not yet materialized" empty state to the actual preview without
|
||||
// requiring the user to re-click the asset.
|
||||
$effect(() => {
|
||||
const key = fileKey
|
||||
const ws = $workspaceStore
|
||||
void refreshKey
|
||||
if (!key || !ws) {
|
||||
fileMetadata = undefined
|
||||
filePreview = undefined
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import { parsePipelineAnnotations, type PipelineAnnotations } from './parsePipelineAnnotations'
|
||||
import SummaryPathDisplay from '$lib/components/SummaryPathDisplay.svelte'
|
||||
import S3FilePreview from '$lib/components/S3FilePreview.svelte'
|
||||
import DataTablePreview from './DataTablePreview.svelte'
|
||||
import AssetRunsPanel from './AssetRunsPanel.svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { fade } from 'svelte/transition'
|
||||
@@ -69,6 +70,9 @@
|
||||
// re-fetches the listing immediately (rather than waiting on its
|
||||
// background poll tick).
|
||||
runsRefreshKey?: any
|
||||
// Job id of the most recently dispatched run. Forwarded to the
|
||||
// runs panel so that clicking play auto-selects the new run.
|
||||
runsPendingJobId?: string | undefined
|
||||
}
|
||||
let {
|
||||
selection,
|
||||
@@ -81,9 +85,17 @@
|
||||
onScriptRenamed,
|
||||
onScriptRemoved,
|
||||
selectionProducers = [],
|
||||
runsRefreshKey
|
||||
runsRefreshKey,
|
||||
runsPendingJobId
|
||||
}: Props = $props()
|
||||
|
||||
// Bumped when the runs panel reports a watched job has reached a
|
||||
// terminal state. Drives S3FilePreview's refreshKey so the preview
|
||||
// re-checks existence after a producer run finishes — moves the
|
||||
// "not yet materialized" empty state to the actual preview without
|
||||
// requiring the user to re-click the asset.
|
||||
let previewRefreshKey = $state(0)
|
||||
|
||||
// When `draftScript` is provided we bypass the fetch entirely and edit
|
||||
// it locally; saving calls ScriptService.createScript to deploy it.
|
||||
let scriptRes = resource(
|
||||
@@ -334,25 +346,38 @@
|
||||
|
||||
<div class="flex-1 min-h-0 relative">
|
||||
{#if selection?.kind === 'asset' && !isDraft}
|
||||
{#if selection.asset_kind === 's3object'}
|
||||
<!-- Vertical split: S3 contents on top, runs detail (with
|
||||
popover-driven history) on the bottom. Both visible at
|
||||
once so users can correlate "what's in the file" with
|
||||
"which run produced it" without flipping tabs. The
|
||||
splitter lets users grow whichever pane matters more. -->
|
||||
<Splitpanes horizontal class="!h-full">
|
||||
<Pane size={55} minSize={20}>
|
||||
<S3FilePreview fileKey={selection.path} showMetadata class="h-full" />
|
||||
</Pane>
|
||||
<Pane size={45} minSize={20}>
|
||||
<AssetRunsPanel producers={selectionProducers} refreshKey={runsRefreshKey} />
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
{:else}
|
||||
<div class="p-3 text-xs text-secondary">
|
||||
Asset details. Use the producer/consumer arrows in the graph to navigate.
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Vertical split: top pane is kind-specific (S3 has a content
|
||||
preview; other kinds fall back to a navigational hint until
|
||||
they grow their own previews); bottom pane is the runs panel,
|
||||
which is generic — runs are keyed by producer script path,
|
||||
not by asset kind, so it's useful for every asset. -->
|
||||
<Splitpanes horizontal class="!h-full">
|
||||
<Pane size={55} minSize={20}>
|
||||
{#if selection.asset_kind === 's3object'}
|
||||
<S3FilePreview
|
||||
fileKey={selection.path}
|
||||
showMetadata
|
||||
class="h-full"
|
||||
refreshKey={previewRefreshKey}
|
||||
/>
|
||||
{:else if selection.asset_kind === 'datatable'}
|
||||
<DataTablePreview path={selection.path} class="h-full" refreshKey={previewRefreshKey} />
|
||||
{:else}
|
||||
<div class="p-3 text-xs text-secondary">
|
||||
No inline preview yet for {selection.asset_kind}. Use the producer/consumer arrows in
|
||||
the graph to navigate. Runs of the upstream script are below.
|
||||
</div>
|
||||
{/if}
|
||||
</Pane>
|
||||
<Pane size={45} minSize={20}>
|
||||
<AssetRunsPanel
|
||||
producers={selectionProducers}
|
||||
refreshKey={runsRefreshKey}
|
||||
pendingJobId={runsPendingJobId}
|
||||
onRunCompleted={() => (previewRefreshKey += 1)}
|
||||
/>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
{:else if selection?.kind === 'runnable' && selection.runnable_kind === 'flow' && !isDraft}
|
||||
<div class="p-3 text-xs text-secondary">
|
||||
Flows are not editable inline. Use the open-in-editor button above.
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import type { ScriptLang } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import { base } from '$lib/base'
|
||||
|
||||
// Shape used for both the data prop and the run callback. Drafts carry
|
||||
// `content` / `language` so the page-level run handler can dispatch to
|
||||
@@ -74,22 +73,10 @@
|
||||
// Fire every script producer in parallel — matches what would
|
||||
// happen if every upstream trigger fired together. The handler
|
||||
// internally dispatches to runScriptByPath / runScriptPreview
|
||||
// based on producer.unsaved.
|
||||
const jobs = (await Promise.all(scriptProducers.map((p) => handler(p)))).filter(
|
||||
(j): j is string => !!j
|
||||
)
|
||||
if (jobs.length === 1) {
|
||||
sendUserToast(`Running ${scriptProducers[0].path}`, false, [
|
||||
{
|
||||
label: 'Open run',
|
||||
callback: () => {
|
||||
window.open(`${base}/run/${jobs[0]}?workspace=${$workspaceStore}`, '_blank')
|
||||
}
|
||||
}
|
||||
])
|
||||
} else if (jobs.length > 1) {
|
||||
sendUserToast(`Running ${jobs.length} producers of this asset`)
|
||||
}
|
||||
// based on producer.unsaved. No success toast — the runs panel
|
||||
// auto-selects the new job and shows status/logs/output, so
|
||||
// the toast was redundant.
|
||||
await Promise.all(scriptProducers.map((p) => handler(p)))
|
||||
} catch (err: any) {
|
||||
sendUserToast(`Failed to run: ${err.body ?? err.message}`, true)
|
||||
} finally {
|
||||
|
||||
@@ -33,8 +33,19 @@
|
||||
// uses it after dispatching a run so the new job appears without
|
||||
// waiting for the next poll tick.
|
||||
refreshKey?: any
|
||||
// Most-recently-dispatched job id. When this changes, the panel
|
||||
// switches selection to it so the user lands on their just-started
|
||||
// run without an extra click. Independent of refreshKey so callers
|
||||
// can refresh without forcing a selection change.
|
||||
pendingJobId?: string | undefined
|
||||
// Fires when the watched job transitions to terminal (success or
|
||||
// failure). The asset detail pane uses this to re-check the
|
||||
// asset's preview — a successful run materializes the asset, so
|
||||
// "not yet materialized" can move to the actual preview without
|
||||
// the user re-selecting.
|
||||
onRunCompleted?: () => void
|
||||
}
|
||||
let { producers, refreshKey }: Props = $props()
|
||||
let { producers, refreshKey, pendingJobId, onRunCompleted }: Props = $props()
|
||||
|
||||
let runnableProducers = $derived(producers.filter((p) => p.kind === 'script'))
|
||||
// Stable string key for the producer set. The parent re-derives
|
||||
@@ -98,20 +109,54 @@
|
||||
}
|
||||
})
|
||||
|
||||
// Track the last pendingJobId we honored. Without this guard, manually
|
||||
// selecting a *different* run from the history popover would be undone
|
||||
// by a re-run of this effect (e.g. on parent re-derivation) — every
|
||||
// trip would slam selectedId back to pendingJobId.
|
||||
let appliedPendingJobId = $state<string | undefined>(undefined)
|
||||
$effect(() => {
|
||||
const id = pendingJobId
|
||||
if (id && id !== appliedPendingJobId) {
|
||||
appliedPendingJobId = id
|
||||
selectedId = id
|
||||
}
|
||||
})
|
||||
|
||||
// Track the id we last started watching so we don't restart the
|
||||
// JobLoader stream on every effect re-run. The earlier flood (~4400
|
||||
// `/jobs_u/get/<id>` requests per second) came from JobLoader.watchJob
|
||||
// being called on every effect tick: each call resets internal state
|
||||
// and fires a fresh `getJob`, so a few extra effect runs per second
|
||||
// snowballed into thousands of fetches.
|
||||
let lastWatchedId = $state<string | undefined>(undefined)
|
||||
|
||||
$effect(() => {
|
||||
// Watch the selected job as soon as one is picked.
|
||||
const id = selectedId
|
||||
if (id === lastWatchedId) return
|
||||
lastWatchedId = id
|
||||
if (!id) {
|
||||
selectedJob = undefined
|
||||
void jobLoader?.clearCurrentJob?.()
|
||||
// untrack: clearCurrentJob reads JobLoader internals (tracked
|
||||
// state in another component); without untrack, those reads
|
||||
// become deps of this effect and any change to them
|
||||
// (e.g. JobLoader's own bind:isLoading writes) would re-run
|
||||
// it.
|
||||
untrack(() => {
|
||||
selectedJob = undefined
|
||||
void jobLoader?.clearCurrentJob?.()
|
||||
})
|
||||
return
|
||||
}
|
||||
void jobLoader?.watchJob(id, {
|
||||
done: () => {
|
||||
// When a run we're watching finishes, refresh the listing so
|
||||
// its row shows the terminal status.
|
||||
void refresh()
|
||||
}
|
||||
untrack(() => {
|
||||
void jobLoader?.watchJob(id, {
|
||||
done: () => {
|
||||
// When a run we're watching finishes, refresh the listing
|
||||
// so its row shows the terminal status, and notify the
|
||||
// parent so the asset preview can re-check existence —
|
||||
// a successful run materializes the asset.
|
||||
void refresh()
|
||||
onRunCompleted?.()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
// Inline preview for a datatable asset. Reuses the existing
|
||||
// DBManagerContent component (the same one the global Database Manager
|
||||
// drawer uses) so users get the same rich rows/schema view here that
|
||||
// they'd get clicking into a datatable from the Assets page.
|
||||
//
|
||||
// The asset path comes in as `<datatable>/<table>` (e.g. `main/event_summary`).
|
||||
// We split that into a `datatable://<datatable>` resourcePath + a
|
||||
// specificTable, mirroring what dbManagerDrawerModel.parse does for the
|
||||
// equivalent URL state.
|
||||
import DBManagerContent from '$lib/components/DBManagerContent.svelte'
|
||||
import type { DbInput } from '$lib/components/dbTypes'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
interface Props {
|
||||
// Asset path as parsed from `datatable://<datatable>/<table>` —
|
||||
// already stripped of the prefix. May be `<datatable>` only (no
|
||||
// table) or `<datatable>/<table>`. We surface a small empty-state
|
||||
// hint when the table half is missing.
|
||||
path: string | undefined
|
||||
// Bump this to force a re-mount and re-fetch (parallel to
|
||||
// S3FilePreview's refreshKey). The asset detail pane uses it after
|
||||
// an upstream run completes so the table contents get re-read
|
||||
// without the user re-clicking.
|
||||
refreshKey?: any
|
||||
class?: string
|
||||
}
|
||||
|
||||
let { path, refreshKey, class: className = '' }: Props = $props()
|
||||
|
||||
// Default schema for datatable inputs is `public` — same default the
|
||||
// dbManagerDrawerModel applies when the URL omits it. The DB manager
|
||||
// resolves the workspace-specific Postgres schema at the connection
|
||||
// level, so we don't have to look it up here.
|
||||
const DEFAULT_SCHEMA = 'public'
|
||||
|
||||
let parsed = $derived.by(() => {
|
||||
const p = path ?? ''
|
||||
const slash = p.indexOf('/')
|
||||
if (slash < 0) return { datatable: p, table: undefined as string | undefined }
|
||||
return { datatable: p.slice(0, slash), table: p.slice(slash + 1) }
|
||||
})
|
||||
|
||||
let input = $derived<DbInput | undefined>(
|
||||
parsed.datatable
|
||||
? {
|
||||
type: 'database',
|
||||
resourceType: 'postgresql',
|
||||
resourcePath: `datatable://${parsed.datatable}`,
|
||||
specificSchema: DEFAULT_SCHEMA,
|
||||
specificTable: parsed.table
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
|
||||
// Selected schema/table for the manager. We pin to public + the parsed
|
||||
// table so the manager opens straight to the right rows; users can
|
||||
// still navigate via the sidebar if they want to see siblings.
|
||||
let selectedSchemaKey = $state<string | undefined>(DEFAULT_SCHEMA)
|
||||
let selectedTableKey = $state<string | undefined>(parsed.table)
|
||||
|
||||
$effect(() => {
|
||||
// Re-pin selection when the path changes (e.g. user clicks a different
|
||||
// datatable asset in the graph).
|
||||
selectedSchemaKey = DEFAULT_SCHEMA
|
||||
selectedTableKey = parsed.table
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class={twMerge('flex flex-col h-full min-h-0', className)}>
|
||||
{#if !input}
|
||||
<div class="p-3 text-xs text-tertiary">No datatable selected.</div>
|
||||
{:else if !parsed.table}
|
||||
<div class="p-3 text-xs text-tertiary">
|
||||
Pick a specific table — this asset only references the datatable
|
||||
<span class="font-mono">{parsed.datatable}</span>.
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Re-mount on refreshKey so DBManagerContent's internal resources
|
||||
re-fetch (loadAllTablesMetaData, schemas, etc.) without us
|
||||
having to thread refresh hooks through the manager. The {#key}
|
||||
block is a coarse but effective way to do it. -->
|
||||
{#key refreshKey}
|
||||
<DBManagerContent {input} showRepl={false} bind:selectedSchemaKey bind:selectedTableKey />
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -563,6 +563,9 @@
|
||||
// the listing immediately — the new (preview or script) job appears in
|
||||
// the history popover without waiting on its 3 s poll tick.
|
||||
let runsRefreshKey = $state(0)
|
||||
// The most recently dispatched job id — surfaces to AssetRunsPanel so
|
||||
// the new run auto-selects without an extra click.
|
||||
let runsPendingJobId = $state<string | undefined>(undefined)
|
||||
|
||||
// Producers (write/rw edges) for the currently-selected asset, derived
|
||||
// from `graphWithDraft.edges`. Threaded into the details pane so the
|
||||
@@ -815,7 +818,10 @@
|
||||
requestBody: {}
|
||||
})
|
||||
}
|
||||
if (jobId) runsRefreshKey++
|
||||
if (jobId) {
|
||||
runsPendingJobId = jobId
|
||||
runsRefreshKey++
|
||||
}
|
||||
return jobId
|
||||
}}
|
||||
/>
|
||||
@@ -826,6 +832,7 @@
|
||||
selection={activeDraft ? undefined : selection}
|
||||
selectionProducers={activeDraft ? [] : selectionProducers}
|
||||
{runsRefreshKey}
|
||||
{runsPendingJobId}
|
||||
draftScript={activeDraft?.script}
|
||||
workspace={$workspaceStore}
|
||||
onAnnotationsChange={(scriptPath, annotations) => {
|
||||
|
||||
Reference in New Issue
Block a user