mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-17 08:03:06 +00:00
fix(sessions): keep "View runs" inside the session, and record released cells only where they are read
The schedule editor's "View runs" anchor navigated the top-level document, which from a session tab means leaving the session — and onto the navigation workspace, not the acting one. A host that is a page of its own now takes the destination instead and re-points its tab at the Runs page, like the way back to the list. The released-cell record is scoped to the kinds whose settle reads it back: elsewhere it retained a whole flow or app draft, or a variable's decrypted secret that the release is otherwise the end of. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcyBRxE8j3ujCYkum9KrC4
This commit is contained in:
co-authored by
Claude Opus 5
parent
3581a0e380
commit
d6388514ca
@@ -22,6 +22,7 @@
|
||||
import type { EntityEditorKind } from './previewRouter'
|
||||
import type { EntityToolEffect } from './previewReload'
|
||||
import { withMenuHidden } from './sessionMode.svelte'
|
||||
import { pageHref, RUNS_PATH } from './previewPaths'
|
||||
import ArtifactViewer from '../copilot/chat/artifacts/ArtifactViewer.svelte'
|
||||
import { setOverlayHost } from '../common/overlayHost.svelte'
|
||||
|
||||
@@ -193,6 +194,21 @@
|
||||
: undefined
|
||||
)
|
||||
|
||||
// "View runs" from a hosted editor: the Runs page is a preview page like any
|
||||
// other, so it belongs in the preview rather than in the top-level document the
|
||||
// anchor would navigate — and the frame gives it the acting workspace. Re-points
|
||||
// this tab, like the way back to the list: the runs are where the editor was.
|
||||
const viewRuns = $derived(
|
||||
runtime
|
||||
? (query: string) =>
|
||||
runtime.previewTabs.navigate({
|
||||
type: 'page',
|
||||
href: `${pageHref(RUNS_PATH)}?${query}`,
|
||||
label: 'Runs'
|
||||
})
|
||||
: undefined
|
||||
)
|
||||
|
||||
// What an editor reports is about an item, not about this tab: other warm
|
||||
// sessions can hold the same one, and by the time the write it awaited returns
|
||||
// this tab may hold something else. The page resolves both by matching on the
|
||||
@@ -413,6 +429,7 @@
|
||||
onBack={backToList}
|
||||
onRemoved={(from, ws) => reportRemoved?.('trigger_schedule', from, ws)}
|
||||
onSavedTo={(to, from, ws) => reportSaved?.('trigger_schedule', to, from, ws)}
|
||||
onViewRuns={viewRuns}
|
||||
/>
|
||||
{/await}
|
||||
{:else if slot.entityKind === 'resource'}
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
workspaceId,
|
||||
onBack,
|
||||
onRemoved,
|
||||
onSavedTo
|
||||
onSavedTo,
|
||||
onViewRuns
|
||||
}: {
|
||||
/** The schedule this tab edits (the row its location deep-links). */
|
||||
path: string
|
||||
@@ -29,6 +30,8 @@
|
||||
* ACTIVE PREVIEW and the draft key all do — and a plain save moves none but
|
||||
* leaves the others holding a baseline the deploy has replaced. */
|
||||
onSavedTo?: (newPath: string, fromPath: string, fromWorkspace: string) => void
|
||||
/** Show this schedule's runs, given the Runs page's filter query. */
|
||||
onViewRuns?: (query: string) => void
|
||||
} = $props()
|
||||
|
||||
// Captured at init, so it must read the current prop rather than close over it.
|
||||
@@ -70,6 +73,7 @@
|
||||
useDrawer={false}
|
||||
showDraftBanner
|
||||
{onRemoved}
|
||||
{onViewRuns}
|
||||
onUpdate={(saved: string | undefined, from: string, fromWs: string, keptEdit: boolean) => {
|
||||
if (from === path && !keptEdit) generation++
|
||||
// A draft-only schedule opens with its path editable (saving CREATEs), so
|
||||
|
||||
@@ -76,7 +76,12 @@
|
||||
// The schedule is gone — a draft-only one whose draft the banner discarded —
|
||||
// with the workspace and path it was showing. A host addressing it by those
|
||||
// (a session tab) has to stop showing it.
|
||||
onRemoved = undefined
|
||||
onRemoved = undefined,
|
||||
// Where "View runs" goes, given the filter query for this schedule. A host
|
||||
// mounted inside a page of its own (a session tab) takes it: the plain
|
||||
// anchor navigates the whole app, which from there means leaving the session
|
||||
// — and lands on the navigation workspace rather than the acting one.
|
||||
onViewRuns = undefined
|
||||
} = $props()
|
||||
|
||||
let optionTabSelected:
|
||||
@@ -893,13 +898,15 @@
|
||||
>
|
||||
{#snippet extra()}
|
||||
{#if !drawerLoading && edit}
|
||||
{@const runsQuery = `schedule_path=${path}&job_trigger_kind=schedule&show_future_jobs=true`}
|
||||
<div class="mr-12 flex flex-row gap-3">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
startIcon={{ icon: List }}
|
||||
disabled={!allowSchedule || pathError != '' || emptyString(script_path)}
|
||||
href={`${base}/runs/?schedule_path=${path}&job_trigger_kind=schedule&show_future_jobs=true`}
|
||||
href={onViewRuns ? undefined : `${base}/runs/?${runsQuery}`}
|
||||
on:click={() => onViewRuns?.(runsQuery)}
|
||||
>
|
||||
View runs
|
||||
</Button>
|
||||
|
||||
@@ -143,13 +143,12 @@ function noteMarker(set: Set<string>, key: string): void {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* What a cell held when its last holder let go. An editor stays editable while
|
||||
* its save is in flight, so a tab closed in that window releases a cell that may
|
||||
* carry an edit newer than the write — and once released there is no handle left
|
||||
* to read it from. Same cap and reason as the marker sets; taken by whoever asks
|
||||
* (`settleDraftAfterWrite`), and dropped when the key is acquired again, since a
|
||||
* live entry answers for itself.
|
||||
* What a cell held when its last holder let go: an editor stays editable while its
|
||||
* save is in flight, so a tab left in that window releases a cell carrying an edit
|
||||
* newer than the write. Only for the kinds whose settle reads it back — elsewhere
|
||||
* it would retain a whole flow or app draft, or a variable's decrypted secret.
|
||||
*/
|
||||
const RELEASE_RECORDED_KINDS: readonly UserDraftItemKind[] = ['resource', 'variable']
|
||||
const releasedValues = new Map<string, unknown>()
|
||||
/** Read once: what it describes is settled by the caller that reads it. */
|
||||
function takeReleasedValue<V>(
|
||||
@@ -1060,7 +1059,9 @@ function releaseEntry(mk: string): void {
|
||||
// only here, once, at refcount 0. This is what lets multiple holders (warm
|
||||
// session previews + the nav editor) share the entry and drop in any order.
|
||||
if (entry.count <= 0) {
|
||||
noteReleasedValue(mk, snapshotDraftValue(entry.state.val))
|
||||
if (RELEASE_RECORDED_KINDS.includes(entry.itemKind)) {
|
||||
noteReleasedValue(mk, snapshotDraftValue(entry.state.val))
|
||||
}
|
||||
// The live entry was authoritative while mounted; once gone, drop any
|
||||
// cached write for this key so a later read falls back to the server
|
||||
// rather than a value the editor may have changed in the meantime.
|
||||
|
||||
Reference in New Issue
Block a user