mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
updates
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { JobService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { forLater, getDbClockNow } from '$lib/forLater'
|
||||
import { displayDate } from '$lib/utils'
|
||||
|
||||
let {
|
||||
jobId,
|
||||
@@ -17,6 +19,16 @@
|
||||
|
||||
let scheduledFor = $state(undefined) as undefined | number
|
||||
|
||||
// Bumped when a scheduled-for-later deadline passes so `isScheduledForLater`
|
||||
// re-evaluates — `forLater` reads the clock but isn't otherwise reactive, so
|
||||
// a job parked in the future would never flip to "queued" without this tick.
|
||||
let clockTick = $state(0)
|
||||
let deadlineTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
let isScheduledForLater = $derived.by(() => {
|
||||
clockTick
|
||||
return scheduledFor != undefined && forLater(scheduledFor)
|
||||
})
|
||||
|
||||
let scheduledForTimeout: number | undefined
|
||||
$effect(() => {
|
||||
if (jobId && workspace) {
|
||||
@@ -41,9 +53,25 @@
|
||||
}
|
||||
})
|
||||
|
||||
// Flip out of the "scheduled for later" state exactly when the deadline
|
||||
// passes, so a job whose scheduled time arrives with no free worker then
|
||||
// correctly surfaces its queue position.
|
||||
$effect(() => {
|
||||
// Fetch queue position when loading and we have jobId
|
||||
if (scheduledFor) {
|
||||
clearTimeout(deadlineTimeout)
|
||||
if (scheduledFor != undefined) {
|
||||
const ms = new Date(scheduledFor).getTime() - 5000 - getDbClockNow().getTime()
|
||||
if (ms > 0) {
|
||||
deadlineTimeout = setTimeout(() => clockTick++, ms)
|
||||
}
|
||||
}
|
||||
return () => clearTimeout(deadlineTimeout)
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
// Only poll the queue position for jobs actually waiting for a worker —
|
||||
// a job scheduled for the future isn't "in line", so its position is
|
||||
// meaningless (and the query counts every job scheduled before it).
|
||||
if (scheduledFor && !isScheduledForLater) {
|
||||
// Initial fetch
|
||||
fetchQueuePosition()
|
||||
|
||||
@@ -86,7 +114,11 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if queueState}
|
||||
{#if isScheduledForLater && scheduledFor != undefined}
|
||||
<div class="text-xs ml-4">
|
||||
<span class="text-blue-600">Scheduled for <b>{displayDate(new Date(scheduledFor))}</b></span>
|
||||
</div>
|
||||
{:else if queueState}
|
||||
<div class="text-xs ml-4">
|
||||
<span class="text-orange-600">Queue position: <b>{queueState.position}</b></span>
|
||||
{#if !minimal}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import TriggerNode, { type TriggerNodeKind } from './TriggerNode.svelte'
|
||||
import AddNode from './AddNode.svelte'
|
||||
import AssetGraphEdge from './AssetGraphEdge.svelte'
|
||||
import PanToNode from './PanToNode.svelte'
|
||||
import { layoutAssetGraph } from './assetGraphLayout'
|
||||
import { buildDownstreamMap } from './graphTraversal'
|
||||
import type { AssetGraphResponse, AssetGraphSelection, NativeTriggerKind } from './types'
|
||||
@@ -132,6 +133,12 @@
|
||||
// Script paths of the expanded (pinned) run(s) — a soft blue ring that
|
||||
// persists until collapsed.
|
||||
selectedRunPaths?: string[]
|
||||
// Graph-id (`${kind}:${path}`) of a node to smoothly pan into the
|
||||
// center of the viewport — set by the page when a new draft node is
|
||||
// created so the user's eye follows it. Opt-in: only the pipeline
|
||||
// editor passes it, so the asset-graph page is unaffected. The page
|
||||
// clears it once the pan has had time to settle.
|
||||
panToNodeId?: string | undefined
|
||||
}
|
||||
let {
|
||||
graph,
|
||||
@@ -152,7 +159,8 @@
|
||||
onOpenWebhook,
|
||||
onOpenDataUpload,
|
||||
hoveredPaths,
|
||||
selectedRunPaths
|
||||
selectedRunPaths,
|
||||
panToNodeId
|
||||
}: Props = $props()
|
||||
|
||||
// `${kind}:${path}` ids for the hovered / pinned runs (both script and flow
|
||||
@@ -560,7 +568,11 @@
|
||||
// class applies — a node is either a runnable or an asset).
|
||||
const assetEmph = assetEmphasis.get(n.id)
|
||||
const assetClass =
|
||||
assetEmph === 'output' ? 'wm-asset-output' : assetEmph === 'input' ? 'wm-asset-input' : undefined
|
||||
assetEmph === 'output'
|
||||
? 'wm-asset-output'
|
||||
: assetEmph === 'input'
|
||||
? 'wm-asset-input'
|
||||
: undefined
|
||||
return {
|
||||
id: n.id,
|
||||
type: n.type,
|
||||
@@ -810,6 +822,7 @@
|
||||
--background-color={false}
|
||||
>
|
||||
<div class="absolute inset-0 !bg-surface-secondary h-full"></div>
|
||||
<PanToNode targetId={panToNodeId} {nodes} />
|
||||
<Controls position="top-right" orientation="horizontal" showLock={false} class="!mr-10" />
|
||||
<MiniMap
|
||||
pannable
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { useSvelteFlow, type Node } from '@xyflow/svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import { NODE } from '$lib/components/graph/util'
|
||||
|
||||
// Smoothly pans the viewport so a freshly-created node lands in the middle,
|
||||
// keeping the current zoom. Lives inside <SvelteFlow> so useSvelteFlow() has
|
||||
// the flow context (same pattern as ViewportResizer). The parent owns the
|
||||
// session lifetime — it clears `targetId` once the layout has settled.
|
||||
let { targetId, nodes }: { targetId: string | undefined; nodes: Node[] } = $props()
|
||||
|
||||
const { setCenter, getViewport } = useSvelteFlow()
|
||||
|
||||
// Re-centering across reactive settles is intentional: opening the details
|
||||
// pane resizes the canvas, which shifts every node's x a tick later. The
|
||||
// `lastKey` guard re-fires setCenter only when the target's own center
|
||||
// actually moves, so unrelated node updates (activity polls) don't restart
|
||||
// the animation.
|
||||
let lastKey = ''
|
||||
$effect(() => {
|
||||
const id = targetId
|
||||
const ns = nodes
|
||||
if (!id) {
|
||||
lastKey = ''
|
||||
return
|
||||
}
|
||||
untrack(() => {
|
||||
const node = ns.find((n) => n.id === id)
|
||||
if (!node) return // not laid out yet — re-runs when `nodes` updates
|
||||
const cx = node.position.x + NODE.width / 2
|
||||
const cy = node.position.y + NODE.height / 2
|
||||
const key = `${id}:${cx}:${cy}`
|
||||
if (key === lastKey) return
|
||||
lastKey = key
|
||||
const { zoom } = getViewport()
|
||||
void setCenter(cx, cy, { zoom, duration: 400 })
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -21,8 +21,8 @@ export async function computeDrift() {
|
||||
}
|
||||
}
|
||||
|
||||
export function forLater(scheduledString: string): boolean {
|
||||
return getDbClockNow() < subtractSeconds(new Date(scheduledString), 5)
|
||||
export function forLater(scheduled: string | number | Date): boolean {
|
||||
return getDbClockNow() < subtractSeconds(new Date(scheduled), 5)
|
||||
}
|
||||
const limit = pLimit(1)
|
||||
|
||||
|
||||
@@ -324,7 +324,8 @@
|
||||
if (state && (Array.isArray(state.drafts) || typeof state.activeDraftPath === 'string')) {
|
||||
return {
|
||||
drafts: Array.isArray(state.drafts) ? state.drafts : [],
|
||||
activeDraftPath: typeof state.activeDraftPath === 'string' ? state.activeDraftPath : undefined
|
||||
activeDraftPath:
|
||||
typeof state.activeDraftPath === 'string' ? state.activeDraftPath : undefined
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -441,7 +442,11 @@
|
||||
try {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
if (isEmpty) localStorage.removeItem(key)
|
||||
else localStorage.setItem(key, encodeState({ drafts: serialized, activeDraftPath: activePath }))
|
||||
else
|
||||
localStorage.setItem(
|
||||
key,
|
||||
encodeState({ drafts: serialized, activeDraftPath: activePath })
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('failed to mirror pipeline state', e)
|
||||
@@ -612,6 +617,21 @@
|
||||
} as unknown as Script
|
||||
}
|
||||
|
||||
// Graph-id of the node the canvas should smoothly pan to. Set when a new
|
||||
// draft node is created; cleared on a timer once the relayout (the details
|
||||
// pane opening resizes the canvas, shifting every node) has settled so the
|
||||
// pan lands on the final position rather than a pre-resize one.
|
||||
let panToNodeId = $state<string | undefined>(undefined)
|
||||
let panToNodeTimer: ReturnType<typeof setTimeout> | undefined = undefined
|
||||
function focusPipelineNode(id: string) {
|
||||
panToNodeId = id
|
||||
if (panToNodeTimer) clearTimeout(panToNodeTimer)
|
||||
panToNodeTimer = setTimeout(() => {
|
||||
if (panToNodeId === id) panToNodeId = undefined
|
||||
panToNodeTimer = undefined
|
||||
}, 600)
|
||||
}
|
||||
|
||||
function openMaterializerDraft(
|
||||
language: ScriptLang,
|
||||
scriptPath: string,
|
||||
@@ -632,6 +652,10 @@
|
||||
activeDraftPath = scriptPath
|
||||
selection = undefined
|
||||
|
||||
// Follow the new node with a smooth pan. The id matches the runnable
|
||||
// node the canvas builds for a draft script (`script:<path>`).
|
||||
focusPipelineNode(`script:${scriptPath}`)
|
||||
|
||||
// User filled the optional prompt on the path stage — fire off a
|
||||
// chat request so the AI bootstraps the body. The seeded template
|
||||
// (already in `script.content`) acts as scaffolding the AI
|
||||
@@ -797,7 +821,8 @@
|
||||
let assets: AssetWithAltAccessType[] = []
|
||||
try {
|
||||
const inferred = await inferAssets(script.language, script.content)
|
||||
if (inferred?.status !== 'error') assets = (inferred?.assets ?? []) as AssetWithAltAccessType[]
|
||||
if (inferred?.status !== 'error')
|
||||
assets = (inferred?.assets ?? []) as AssetWithAltAccessType[]
|
||||
} catch {
|
||||
// Same fallback as above — an unparsable body deploys with no
|
||||
// lineage rather than the stale snapshot.
|
||||
@@ -2354,6 +2379,7 @@
|
||||
onAddPipelineScript={mode === 'edit' ? handleAddPipelineScript : undefined}
|
||||
onRunnableMenuRemove={mode === 'edit' ? handleRunnableMenuRemove : undefined}
|
||||
onRunProducer={mode === 'edit' ? handleRunProducer : undefined}
|
||||
{panToNodeId}
|
||||
/>
|
||||
{#if mode === 'edit'}
|
||||
<!-- View mode surfaces activity as a full right pane
|
||||
@@ -2437,6 +2463,13 @@
|
||||
: undefined)
|
||||
if (running && openPath) {
|
||||
activeRunnable = { kind: 'script', path: openPath }
|
||||
// Mark the tested runnable as launched-from-here so the
|
||||
// folder poll's catch-up pulse won't re-flash its edge a
|
||||
// poll-interval after a fast job already finished (the
|
||||
// edge is animated zero-latency by `activeRunnable`, and
|
||||
// the test loader clears that the instant it completes).
|
||||
// Also upgrades to the fast poll so the badge lands sooner.
|
||||
activeRunnables.arm(`script:${openPath}`)
|
||||
// Editor Test path clears via its own callbacks, not
|
||||
// the job-id effect — drop any stale tracked id so a
|
||||
// prior canvas run's completion can't clear this hint.
|
||||
|
||||
Reference in New Issue
Block a user