mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-18 16:03:24 +00:00
* feat(sessions): show an item's deployed page in the preview panel The preview panel showed an item's editor and nothing else. It now shows the deployed page too, as a tab of its own. **Two sides, two tabs.** The editor and the deployed page are different things to look at, so `isItemTabFor` keys on the side rather than the item: asking for a side already open focuses it, asking for the other opens it alongside. Two editors for one item still cannot coexist — they would race its single (kind, path) cell — but an editor beside a viewer is safe, since the viewer reads the deployed version over the API and holds no cell. The tab breadcrumb keeps re-pointing in place; `Exit & see details` and the detail page's `Edit` open the other side instead of consuming the one you are on. A tab's label carries `(edit)` when it is the editor side. **The detail pages moved out of their routes.** `/scripts/get` and `/flows/get` are now thin wrappers over `ScriptDetail` and `FlowDetail`, which the panel renders too. Everything they reach — drawers, triggers, saved inputs — is scoped to the viewer's workspace through `setOperatingWorkspace`, and every navigation they attempt is caught by `interceptNav` and turned into a move inside the panel, so nothing takes the browser out of the session. A plain click is intercepted; ⌘-click and middle-click still open a real tab, which is why the pages keep their `href`s. **The picker opens what exists.** A row opens the deployed page, or the editor when nothing is deployed there, and carries a Draft / Draft only badge in the review dock's words. `WorkspaceItem` gained `draftOnly` and `hasDraft` from the listers, which already returned both; deploys now invalidate the picker cache through `itemDeployed`, so a just-deployed item stops reading as a draft. Rows also carry a hover Edit action, reachable from the keyboard with the pick modifier. `open_preview` gained a `mode`, narrowed out of the advertised schema for a session that cannot write drafts and re-checked per path in the handler, where a refusal reports "couldn't check" apart from "denied". The mode is resolved before those checks, so a call that omits it is gated as the editor it will become. Alongside, the workspace a detail page acts on is now the one it is showing rather than the one the browser is navigated to: `MoveDrawer`, `toggleWorkspaceErrorHandler` and the pages' own permission gates read the operating workspace and its acting user, and `RunForm` mints a password argument's ephemeral variable there too — in a fork session all of these previously answered for the parent. `InWorkspaceAppViewer` hands the app's `ctx` user down as a prop instead of writing the global `userStore`, which from a session tab was re-pointing every permission check on the page. Fixes found on the way: `DetailPageLayout` claimed `h-screen` inside a panel that is not the viewport; Edit on a historical script version dropped the version from the intercepted click; the Run button stayed spinning after a run that left the page mounted; and the window-level run shortcut fired from a collapsed panel and from keys another handler had already claimed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(sessions): historical edits, stale not-found, shared edit rights, tab labels Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): keep deployed-page links and workspace reads in the session Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): scope detail hrefs to the session, re-point the viewer's own tab Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): keep a previewed raw app's route out of the session URL Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): resolve edit-in-fork against the session workspace Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): read advanced run tags from the session workspace Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): fork from the session workspace, star only navigation items Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): mount only the side a preview tab shows Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): address CI review round 1 findings Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): route links in a deployed page's drawers through the panel Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): tell the chat which deployed page the panel shows Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): only a 404 reads as an undeployed raw app Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): scope the unparseable-JSON run gate to the form Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): a flow deleted from the panel stays in its tab Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): the picker's edit shortcut works on the current row Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): load-error toasts say what failed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(sessions): keep an oversized invalid JSON editor in its form's run gate Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * revert: keep main's draft_only description in openapi.yaml Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
245 lines
7.1 KiB
Svelte
245 lines
7.1 KiB
Svelte
<script lang="ts">
|
|
import { stopPropagation, createBubbler } from 'svelte/legacy'
|
|
|
|
const bubble = createBubbler()
|
|
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
|
import { classNames, displayDate, emptyString, sendUserToast } from '$lib/utils'
|
|
import { type Flow, FlowService, type FlowVersion } from '$lib/gen'
|
|
import { Skeleton } from '$lib/components/common'
|
|
import Button from '../common/button/Button.svelte'
|
|
import { ArrowRight, Loader2, Pencil, X } from 'lucide-svelte'
|
|
import { getContext } from 'svelte'
|
|
import type { FlowEditorContext } from './types'
|
|
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
|
|
|
const operatingWorkspace = useOperatingWorkspace()
|
|
|
|
interface Props {
|
|
path: string
|
|
allowFork?: boolean
|
|
onHistoryRestore?: () => void
|
|
}
|
|
|
|
let { path, allowFork = false, onHistoryRestore }: Props = $props()
|
|
|
|
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
|
|
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $operatingWorkspace)
|
|
|
|
let loading: boolean = $state(false)
|
|
|
|
let versions: FlowVersion[] = $state([])
|
|
|
|
let selectedVersion: FlowVersion | undefined = $state(undefined)
|
|
let selected: Flow | undefined = $state(undefined)
|
|
let deploymentMsgUpdateMode = $state(false)
|
|
let deploymentMsgUpdate: string | undefined = $state(undefined)
|
|
let selectedVersionIndex: number | undefined = $state(undefined)
|
|
|
|
async function loadFlow(version: number) {
|
|
selected = await FlowService.getFlowVersion({
|
|
workspace: opWs!,
|
|
version
|
|
})
|
|
}
|
|
|
|
async function loadVersions() {
|
|
loading = true
|
|
versions = await FlowService.getFlowHistory({
|
|
workspace: opWs!,
|
|
path: path
|
|
})
|
|
loading = false
|
|
}
|
|
|
|
async function updateDeploymentMsg(version: number | undefined) {
|
|
if (
|
|
selectedVersion === undefined ||
|
|
version === undefined ||
|
|
emptyString(deploymentMsgUpdate)
|
|
) {
|
|
return
|
|
}
|
|
await FlowService.updateFlowHistory({
|
|
workspace: opWs!,
|
|
version,
|
|
requestBody: {
|
|
deployment_msg: deploymentMsgUpdate!
|
|
}
|
|
})
|
|
selectedVersion.deployment_msg = deploymentMsgUpdate
|
|
deploymentMsgUpdateMode = false
|
|
loadVersions()
|
|
}
|
|
|
|
async function restoreVersion(flow: Flow | undefined) {
|
|
if (!flow) return
|
|
await FlowService.updateFlow({
|
|
workspace: opWs!,
|
|
requestBody: {
|
|
...flow,
|
|
path
|
|
},
|
|
path
|
|
})
|
|
onHistoryRestore?.()
|
|
sendUserToast('Flow restored from previous deployment')
|
|
}
|
|
|
|
loadVersions()
|
|
|
|
$effect.pre(() => {
|
|
selectedVersion !== undefined && loadFlow(selectedVersion.id)
|
|
})
|
|
|
|
// Get available versions for comparison (versions after selected one)
|
|
let availableVersions = $derived(
|
|
selectedVersionIndex !== undefined ? versions.slice(selectedVersionIndex + 1) : []
|
|
)
|
|
</script>
|
|
|
|
<Splitpanes class="!overflow-visible">
|
|
<Pane size={20}>
|
|
<div class="flex flex-col gap-2 w-full px-2 py-2">
|
|
{#if !loading}
|
|
{#if versions.length > 0}
|
|
<div class="flex gap-2 flex-col">
|
|
{#each versions ?? [] as version (version.id)}
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<div
|
|
class={classNames(
|
|
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-surface-hover hover:text-primary',
|
|
selectedVersion?.id == version.id ? 'bg-surface-selected text-primary' : ''
|
|
)}
|
|
role="button"
|
|
tabindex="0"
|
|
onclick={() => {
|
|
const versionIndex = versions.findIndex((v) => v.id === version.id)
|
|
selectedVersion = version
|
|
selectedVersionIndex = versionIndex
|
|
deploymentMsgUpdate = undefined
|
|
deploymentMsgUpdateMode = false
|
|
}}
|
|
>
|
|
<span class="text-xs truncate">
|
|
{#if emptyString(version.deployment_msg)}Version {version.id}{:else}{version.deployment_msg}{/if}
|
|
</span>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{:else}
|
|
<div class="text-sm text-primary">No items</div>
|
|
{/if}
|
|
{:else}
|
|
<Skeleton layout={[[40], [40], [40], [40], [40]]} />
|
|
{/if}
|
|
</div>
|
|
</Pane>
|
|
<Pane size={80}>
|
|
<div class="h-full w-full overflow-auto pt-2">
|
|
{#if selectedVersion}
|
|
{#if selected}
|
|
<div class="px-2 flex flex-col gap-2">
|
|
<div class="flex justify-between">
|
|
<span class="flex flex-row text-sm p-1 text-primary">
|
|
{#if deploymentMsgUpdateMode}
|
|
<div class="flex w-full">
|
|
<input
|
|
type="text"
|
|
bind:value={deploymentMsgUpdate}
|
|
class="!w-auto grow"
|
|
onclick={stopPropagation(() => {})}
|
|
onkeydown={stopPropagation(bubble('keydown'))}
|
|
onkeypress={(e) => {
|
|
e.stopPropagation()
|
|
if (e.key === 'Enter') updateDeploymentMsg(selectedVersion?.id)
|
|
}}
|
|
/>
|
|
<Button
|
|
size="xs"
|
|
color="blue"
|
|
buttonType="button"
|
|
btnClasses="!p-1 !w-[34px] !ml-1"
|
|
aria-label="Save deployment message"
|
|
on:click={() => {
|
|
updateDeploymentMsg(selectedVersion?.id)
|
|
}}
|
|
>
|
|
<ArrowRight size={14} />
|
|
</Button>
|
|
<Button
|
|
size="xs"
|
|
color="light"
|
|
buttonType="button"
|
|
btnClasses="!p-1 !w-[34px] !ml-1"
|
|
aria-label="Abort"
|
|
on:click={() => {
|
|
deploymentMsgUpdateMode = false
|
|
deploymentMsgUpdate = undefined
|
|
}}
|
|
>
|
|
<X size={14} />
|
|
</Button>
|
|
</div>
|
|
{:else}
|
|
{#if selectedVersion.deployment_msg}
|
|
{selectedVersion.deployment_msg}
|
|
{:else}
|
|
Deployed {displayDate(selected.edited_at)} by {selected.edited_by}
|
|
{/if}
|
|
<button
|
|
onclick={() => {
|
|
deploymentMsgUpdate = selectedVersion?.deployment_msg
|
|
deploymentMsgUpdateMode = true
|
|
}}
|
|
title="Update commit message"
|
|
class="flex items-center px-1 rounded-sm hover:text-primary text-secondary h-5"
|
|
aria-label="Update commit message"
|
|
>
|
|
<Pencil size={14} />
|
|
</button>
|
|
{/if}
|
|
</span>
|
|
<div class="flex p-1 gap-2">
|
|
{#if allowFork}
|
|
<div class="flex">
|
|
<Button
|
|
size="sm"
|
|
variant="accent"
|
|
on:click={() =>
|
|
window.open(
|
|
`/flows/add?template_id=${selectedVersion?.id}&template=${path}&workspace=${$operatingWorkspace}`,
|
|
'_blank'
|
|
)}
|
|
>
|
|
Restore as fork
|
|
</Button>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="flex">
|
|
<Button size="sm" variant="accent" on:click={() => restoreVersion(selected)}
|
|
>Redeploy with that version
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{#await import('$lib/components/FlowViewer.svelte')}
|
|
<Loader2 class="animate-spin" />
|
|
{:then Module}
|
|
<Module.default
|
|
flow={selected}
|
|
{availableVersions}
|
|
selectedVersionId={selectedVersion?.id}
|
|
/>
|
|
{/await}
|
|
</div>
|
|
{:else}
|
|
<Skeleton layout={[[40]]} />
|
|
{/if}
|
|
{:else}
|
|
<div class="text-sm p-2 text-primary">Select a deployment version to see its details</div>
|
|
{/if}
|
|
</div>
|
|
</Pane>
|
|
</Splitpanes>
|