fix(frontend): improve raw app history (#7625)

* fix raw app header overflow

* update ui-builder hash

* Make monaco default size match brand guidelines

* nit

* Move run button to test panel

* wip improve history

* add current checkout point

* fix logic to switch wetween history state

* improve history visualisation

* improve animations

* nit

* remove test page

* fix timing issue when selecty history entries

* update ui_builder hash

* remove dev file

* nit

* revert setting editor font to 13.5 px

* update ui builder

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
Guilhem
2026-01-20 18:30:52 +00:00
committed by GitHub
parent baf060df74
commit 687175c6a8
10 changed files with 487 additions and 174 deletions

View File

@@ -20,7 +20,7 @@ console.log('Running postinstall for root project');
import { x } from 'tar'
const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-c37c2f6.tar.gz'
const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-b0e4676.tar.gz'
const outputTarPath = path.join(process.cwd(), 'ui_builder.tar.gz')
const extractTo = path.join(process.cwd(), 'static/ui_builder/')

View File

@@ -1,32 +1,30 @@
<script lang="ts">
import { Button } from '$lib/components/common'
import { Button, ButtonType } from '$lib/components/common'
import { CornerDownLeft, Loader2 } from 'lucide-svelte'
export let isLoading
export let hideShortcut = false
export let onRun: () => Promise<void>
export let onCancel: () => Promise<void>
interface Props {
isLoading: boolean
hideShortcut?: boolean
onRun: () => Promise<void>
onCancel: () => Promise<void>
size?: ButtonType.UnifiedSize
}
let { isLoading, hideShortcut = false, onRun, onCancel, size = 'sm' }: Props = $props()
</script>
{#if !isLoading}
<Button
loading={isLoading}
size="sm"
unifiedSize={size}
variant="accent"
btnClasses="!px-2 !py-1"
on:click={() => onRun()}
onClick={() => onRun()}
shortCut={{ Icon: CornerDownLeft, hide: hideShortcut }}
>
Run
</Button>
{:else}
<Button
size="sm"
variant="accent"
destructive
btnClasses="!px-2 !py-1"
on:click={() => onCancel()}
>
<Button unifiedSize={size} variant="accent" destructive onClick={() => onCancel()}>
<Loader2 size={14} class="animate-spin mr-2" />
Cancel
</Button>

View File

@@ -297,7 +297,7 @@
}
})
let fontSize = $derived(small ? 12 : 14)
let fontSize = $derived(small ? 12 : 13.5)
async function loadMonaco() {
setMonacoJsonOptions()

View File

@@ -190,6 +190,20 @@
)
}
function setFilesAndSelectInIframe(newFiles: Record<string, string>, pathToSelect: string) {
const files = Object.fromEntries(
Object.entries(newFiles).filter(([path, _]) => !path.endsWith('/'))
)
iframe?.contentWindow?.postMessage(
{
type: 'setFilesAndSelect',
files: files,
pathToSelect: pathToSelect
},
'*'
)
}
function populateRunnables() {
iframe?.contentWindow?.postMessage(
{
@@ -298,9 +312,9 @@
files = {}
}
files[path] = content
setFilesInIframe(files)
selectedDocument = path
handleSelectFile(path)
// Use combined setFilesAndSelect to avoid race condition
setFilesAndSelectInIframe(files, path)
return lint()
},
deleteFrontendFile: (path) => {
@@ -733,9 +747,10 @@
}
function handleHistorySelect(id: number) {
// Create a snapshot if we have pending changes before navigating
if (historyManager.needsSnapshotBeforeNav) {
historyManager.manualSnapshot(files ?? {}, runnables, summary, data)
// Save current state temporarily (not as a snapshot) when navigating to history
// Only if we're currently at the "current" state (not already viewing history)
if (historyManager.selectedEntryId === undefined) {
historyManager.saveTemporaryCurrentState(files ?? {}, runnables, summary, data)
}
const entry = historyManager.selectEntry(id)
@@ -756,19 +771,15 @@
summary = entry.summary
data = structuredClone($state.snapshot(entry.data))
setFilesInIframe(entry.files)
populateRunnables()
// Re-select the current document if it exists in the new files
// If there's a selected document that exists in the new files, use the combined message
if (selectedDocument && entry.files[selectedDocument] !== undefined) {
iframe?.contentWindow?.postMessage(
{
type: 'selectFile',
path: selectedDocument
},
'*'
)
// Use combined setFilesAndSelect message to avoid race condition
setFilesAndSelectInIframe(entry.files, selectedDocument)
} else {
// Otherwise just set files normally
setFilesInIframe(entry.files)
}
populateRunnables()
} catch (error) {
console.error('Failed to apply entry:', error)
sendUserToast('Failed to apply entry: ' + (error as Error).message, true)
@@ -819,8 +830,8 @@
onRedo={handleRedo}
/>
<Splitpanes id="o2" class="grow">
<Pane bind:size={sidebarPanelSize} maxSize={20}>
<Splitpanes id="o2" class="grow min-h-0">
<Pane bind:size={sidebarPanelSize} maxSize={20} class="h-full overflow-y-auto">
<RawAppSidebar
bind:files={
() => files,
@@ -855,6 +866,15 @@
{historyManager}
historySelectedId={historyManager.selectedEntryId}
onHistorySelect={handleHistorySelect}
onHistorySelectCurrent={() => {
// Restore the temporary current state if it exists
const tempState = historyManager.getAndClearTemporaryState()
if (tempState) {
applyEntry(tempState)
}
// Clear selection to indicate we're at current state
historyManager.clearSelection()
}}
onManualSnapshot={() => {
historyManager.manualSnapshot(files ?? {}, runnables, summary, data, true)
}}

View File

@@ -1,16 +1,24 @@
<script lang="ts">
import type { HistoryEntry, HistoryBranch } from './RawAppHistoryManager.svelte'
import { classNames, displayDate } from '$lib/utils'
import { GitBranch } from 'lucide-svelte'
import { displayDate } from '$lib/utils'
import { Circle, CircleDot, CircleDotDashed, CircleDashed } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
interface Props {
entries: HistoryEntry[]
branches: HistoryBranch[]
selectedId: number | undefined
onSelect: (id: number) => void
onSelectCurrent?: () => void
}
let { entries, branches, selectedId, onSelect }: Props = $props()
let { entries, branches, selectedId, onSelect, onSelectCurrent }: Props = $props()
// Constants for layout
const LINE_X = 8 // X position of main timeline line (center of dots)
const BRANCH_OFFSET_X = 45 // How far branches are offset horizontally
const ENTRY_HEIGHT = 28 // Height of each entry
const CURRENT_HEIGHT = 32 // Height of current state button
// Build a map of fork points to their branches for rendering
const branchesByForkPoint = $derived(
@@ -26,98 +34,298 @@
)
)
// Entries in reverse order (newest first)
// Entries in reverse order (newest first) for display
const reversedEntries = $derived(entries.slice().reverse())
// Calculate positions for rendering
const entryPositions = $derived.by(() => {
const positions: Record<number, { x: number; y: number; index: number }> = {}
let currentY = CURRENT_HEIGHT + 12 // Start below current button
reversedEntries.forEach((entry, index) => {
// For each entry, calculate space needed for branches above it
const entryBranches = branchesByForkPoint[entry.id] ?? []
const branchSpaceAbove = entryBranches.reduce((total, branch) => {
return total + branch.entries.length * ENTRY_HEIGHT
}, 0)
// Add space for branches above this entry
currentY += branchSpaceAbove
// Position this entry
positions[entry.id] = { x: LINE_X, y: currentY, index }
// Move to next entry position
currentY += ENTRY_HEIGHT
})
return positions
})
// Calculate total height needed for the container
const totalHeight = $derived.by(() => {
const mainTimelineMaxY = Object.values(entryPositions).reduce(
(max, pos) => Math.max(max, pos.y),
0
)
const branchMaxY = Object.values(branchPositions)
.flatMap((groups) => Object.values(groups).flatMap((nodes) => nodes.map((node) => node.y)))
.reduce((max, y) => Math.max(max, y), 0)
return Math.max(mainTimelineMaxY, branchMaxY, 150) + 20 // Add padding at bottom
})
// Calculate branch positions
const branchPositions = $derived.by(() => {
const positions: Record<
number,
Record<number, Array<{ id: number; x: number; y: number }>>
> = {}
Object.entries(branchesByForkPoint).forEach(([forkPointId, forkBranches]) => {
const forkPoint = entryPositions[Number(forkPointId)]
if (!forkPoint) return
positions[Number(forkPointId)] = {}
forkBranches.forEach((branch) => {
positions[Number(forkPointId)][branch.id] = []
// Branch flows from fork point upward (toward newer/current)
// Start just above the fork point and continue upward
// Oldest branch entry is closest to fork point, newest is furthest
let currentY = forkPoint.y - ENTRY_HEIGHT
branch.entries.forEach((entry) => {
positions[Number(forkPointId)][branch.id].push({
id: entry.id,
x: LINE_X + BRANCH_OFFSET_X,
y: currentY
})
currentY -= ENTRY_HEIGHT // Move upward (decreasing Y)
})
})
})
return positions
})
// Helper to create SVG path for branch connection
function createBranchPath(startX: number, startY: number, endX: number, endY: number): string {
// Control points are vertically offset to create vertical tangents
const controlOffset = Math.abs(startY - endY) * 0.85 // 85% of vertical distance for very intense curve
// Both tangents point upward (toward smaller Y values)
// Start control point: above the start point
// End control point: above the end point (since endY < startY, we add to go further up)
return `M ${startX} ${startY} C ${startX} ${startY - controlOffset}, ${endX} ${endY + controlOffset}, ${endX} ${endY}`
}
</script>
{#snippet timelineButton(
label: string,
timestamp: string | null,
isSelected: boolean,
onClick: () => void,
position: { x: number; y: number },
type: 'current' | 'main' | 'branch' = 'main'
)}
{@const colors = {
current: {
selected: 'text-accent',
default: 'text-primary',
textSelected: 'text-accent',
bgSelected: 'bg-surface-accent-selected'
},
main: {
selected: 'text-accent',
default: 'text-primary',
textSelected: 'text-accent',
bgSelected: 'bg-surface-accent-selected'
},
branch: {
selected: 'text-amber-500 dark:text-amber-400',
default: 'text-secondary',
textSelected: 'text-amber-600 dark:text-amber-400',
bgSelected: 'bg-amber-50 dark:bg-amber-900/20'
}
}}
{@const color = colors[type]}
<button
onclick={onClick}
aria-label={timestamp ? `Snapshot from ${displayDate(timestamp, true, false)}` : label}
aria-current={isSelected ? 'true' : 'false'}
class={'absolute flex items-center gap-1 transition-all duration-200 animate-fadeIn'}
style="left: {position.x - 6}px; top: {position.y - 6}px;"
>
{#if isSelected}
{#if type === 'current'}
<CircleDotDashed size={12} class="bg-surface {color.selected}" />
{:else}
<CircleDot size={12} class="bg-surface {color.selected}" />
{/if}
{:else if type === 'current'}
<CircleDashed size={12} class="bg-surface {color.default}" />
{:else}
<Circle size={12} class="bg-surface {color.default}" />
{/if}
<span
class={twMerge(
'text-2xs truncate px-2 py-1 rounded-md',
type === 'current' ? 'font-semibold text-emphasis' : 'font-normal',
isSelected ? color.textSelected : 'text-tertiary',
isSelected ? color.bgSelected : 'hover:bg-surface-hover'
)}
>
{timestamp ? displayDate(timestamp, true, false) : label}
</span>
</button>
{/snippet}
{#if entries.length === 0}
<div class="text-tertiary py-2 text-center text-2xs">
No snapshots yet. Auto-saved every 5 min.
</div>
{:else}
<div class="relative w-full">
<!-- Timeline line -->
<div class="absolute left-[0.95rem] top-2 bottom-2 w-px bg-gray-200 dark:bg-gray-700"></div>
<div
class="relative w-full"
style="height: {totalHeight}px; transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1);"
role="list"
aria-label="History timeline"
>
<!-- SVG for lines and connections -->
<svg class="absolute inset-0 pointer-events-none" style="width: 100%; height: 100%;">
<!-- Always show the main line if there are entries -->
{#if reversedEntries.length > 0}
{@const mainTimelineMaxY = Math.max(...Object.values(entryPositions).map((pos) => pos.y))}
{@const branchMaxY = Object.values(branchPositions)
.flatMap((groups) =>
Object.values(groups).flatMap((nodes) => nodes.map((node) => node.y))
)
.reduce((max, y) => Math.max(max, y), 0)}
{@const maxY = Math.max(mainTimelineMaxY, branchMaxY)}
<line
x1={LINE_X}
y1={8}
x2={LINE_X}
y2={maxY + 8}
stroke="currentColor"
stroke-width="1"
opacity="1"
class="text-primary"
style="transition: y2 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
/>
{/if}
{#each reversedEntries as entry, i (entry.id)}
{@const isSelected = selectedId === entry.id}
{@const isFirst = i === 0}
{@const entryBranches = branchesByForkPoint[entry.id] ?? []}
<!-- Branch connection lines -->
{#each Object.entries(branchPositions) as [forkPointId, branchGroups] (forkPointId)}
{@const forkPoint = entryPositions[Number(forkPointId)]}
{#if forkPoint}
{#each Object.entries(branchGroups) as [branchId, branchNodes] (branchId)}
{#if branchNodes.length > 0}
<!-- Connection from fork point to first branch entry -->
<path
d={createBranchPath(forkPoint.x, forkPoint.y, branchNodes[0].x, branchNodes[0].y)}
stroke="currentColor"
stroke-width="1.5"
fill="none"
stroke-dasharray="4 2"
opacity="0.6"
class="text-secondary transition-all duration-300"
/>
<!-- Render branches ABOVE their fork point (newest first within branch) -->
{#each entryBranches as branch (branch.id)}
<div
class="ml-4 relative border-l border-dashed border-gray-300 dark:border-gray-600 pl-2 my-1"
>
<div class="absolute left-3 bottom-2 text-tertiary">
<GitBranch size={10} />
</div>
<!-- Branch entries in reverse order (newest first) -->
{#each branch.entries.slice().reverse() as branchEntry (branchEntry.id)}
{@const isBranchSelected = selectedId === branchEntry.id}
<button
onclick={() => onSelect(branchEntry.id)}
class={classNames(
'relative flex items-center gap-2 py-1 pr-1 pl-2 w-full text-left rounded transition-colors',
'hover:bg-surface-hover',
isBranchSelected ? 'bg-amber-50 dark:bg-amber-900/20' : ''
)}
>
<!-- Branch dot -->
<div
class={classNames(
'w-1 h-1 rounded-full',
isBranchSelected
? 'bg-amber-500 dark:bg-amber-400'
: 'bg-gray-300 dark:bg-gray-600'
)}
></div>
<span
class={classNames(
'text-2xs truncate',
isBranchSelected
? 'text-amber-600 dark:text-amber-400 font-medium'
: 'text-tertiary'
)}
>
{displayDate(branchEntry.timestamp.toISOString(), true, false)}
</span>
</button>
<!-- Vertical line connecting branch entries within this branch -->
{#if branchNodes.length > 1}
{@const firstNode = branchNodes[0]}
{@const lastNode = branchNodes[branchNodes.length - 1]}
<line
x1={firstNode.x}
y1={firstNode.y}
x2={lastNode.x}
y2={lastNode.y}
stroke="currentColor"
stroke-width="1.5"
stroke-dasharray="4 2"
opacity="0.6"
class="text-secondary"
style="transition: y1 0.3s cubic-bezier(0.4, 0, 0.2, 1), y2 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
/>
{/if}
{/if}
{/each}
</div>
{/if}
{/each}
</svg>
<!-- Main timeline entry -->
<button
onclick={() => onSelect(entry.id)}
class={classNames(
'relative flex items-center gap-2 py-1 pr-1 pl-3 w-full text-left rounded transition-colors',
'hover:bg-surface-hover',
isSelected ? 'bg-blue-50 dark:bg-blue-900/20' : ''
)}
>
<!-- Timeline dot -->
<div
class={classNames(
'absolute left-0 w-1.5 h-1.5 rounded-full border-[1.5px] bg-surface',
isSelected
? 'border-blue-500 dark:border-blue-400'
: 'border-gray-300 dark:border-gray-600'
)}
></div>
<!-- Interactive elements -->
<div class="relative">
<!-- Current Working State -->
{@render timelineButton(
'Current',
null,
selectedId === undefined,
() => onSelectCurrent?.(),
{ x: LINE_X, y: 6 },
'current'
)}
<span
class={classNames(
'text-2xs truncate',
isSelected ? 'text-blue-600 dark:text-blue-400 font-medium' : 'text-secondary'
<!-- Main timeline entries -->
{#each reversedEntries as entry (entry.id)}
{@const pos = entryPositions[entry.id]}
{@const isSelected = selectedId === entry.id}
{@const entryBranches = branchesByForkPoint[entry.id] ?? []}
{#if pos}
<!-- Main entry button -->
{@render timelineButton(
'',
entry.timestamp.toISOString(),
isSelected,
() => onSelect(entry.id),
pos,
'main'
)}
>
{#if isFirst && !isSelected}
<span class="text-tertiary">Latest · </span>
<!-- Branch entries -->
{#if entryBranches.length > 0}
{#each Object.values(branchPositions[entry.id] ?? {}) as branchNodes}
{#each branchNodes as branchNode (branchNode.id)}
{@const branchEntry = branches
.flatMap((b) => b.entries)
.find((e) => e.id === branchNode.id)}
{@const isBranchSelected = selectedId === branchNode.id}
{#if branchEntry}
{@render timelineButton(
'',
branchEntry.timestamp.toISOString(),
isBranchSelected,
() => onSelect(branchNode.id),
{ x: branchNode.x, y: branchNode.y - 4 },
'branch'
)}
{/if}
{/each}
{/each}
{/if}
{displayDate(entry.timestamp.toISOString(), true, false)}
</span>
</button>
{/each}
{/if}
{/each}
</div>
</div>
{/if}
<style>
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fadeIn {
animation: fadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
</style>

View File

@@ -70,6 +70,8 @@ export class RawAppHistoryManager {
private branchIdCounter = $state(0)
// Track if current state has pending changes
private hasPendingChanges = $state(false)
// Temporary storage for current state when viewing history
private temporaryCurrentState = $state<HistoryEntry | undefined>(undefined)
// Derived state
public readonly hasEntries = $derived(this.entries.length > 0)
@@ -93,6 +95,9 @@ export class RawAppHistoryManager {
this.currentIndex === -1 && this.currentBranchId === undefined && this.hasPendingChanges
)
// Whether we have a temporary current state saved
public readonly hasTemporaryState = $derived(this.temporaryCurrentState !== undefined)
public readonly canUndo = $derived(
this.currentIndex > 0 ||
(this.currentIndex === -1 && this.entries.length > 1) ||
@@ -178,19 +183,25 @@ export class RawAppHistoryManager {
* When making changes from a historical position, create a branch from the "future"
*/
markPendingChanges(): void {
// If we're on a branch and making changes, that branch becomes main
if (this.currentBranchId !== undefined) {
this.promoteBranchToMain()
}
// If we're at a historical position on main timeline
else if (this.currentIndex !== -1 && this.currentIndex < this.entries.length - 1) {
this.createBranchFromFuture()
}
// If we're viewing history (not at current state)
if (this.currentIndex !== -1 || this.currentBranchId !== undefined) {
// Convert temporary current state to a real snapshot first
this.convertTemporaryToSnapshot()
// Clear selection - we're now in a new unsaved state
this.currentIndex = -1
this.currentBranchId = undefined
this.currentBranchEntryIndex = -1
// If we're on a branch and making changes, that branch becomes main
if (this.currentBranchId !== undefined) {
this.promoteBranchToMain()
}
// If we're at a historical position on main timeline
else if (this.currentIndex !== -1 && this.currentIndex < this.entries.length - 1) {
this.createBranchFromFuture()
}
// Clear selection - we're now at current state
this.currentIndex = -1
this.currentBranchId = undefined
this.currentBranchEntryIndex = -1
}
this.hasPendingChanges = true
}
@@ -341,12 +352,44 @@ export class RawAppHistoryManager {
}
/**
* Clear selection (go back to latest state)
* Clear selection (go back to current state)
*/
clearSelection(): void {
this.currentIndex = -1
this.currentBranchId = undefined
this.currentBranchEntryIndex = -1
this.hasPendingChanges = false
}
/**
* Save current state temporarily when viewing history
*/
public saveTemporaryCurrentState(
files: Record<string, string>,
runnables: Record<string, Runnable>,
summary: string,
data: RawAppData
): void {
this.temporaryCurrentState = this.createSnapshot(files, runnables, summary, data)
}
/**
* Get and clear temporary current state
*/
public getAndClearTemporaryState(): HistoryEntry | undefined {
const temp = this.temporaryCurrentState
this.temporaryCurrentState = undefined
return temp
}
/**
* Convert temporary current state to a real snapshot
*/
convertTemporaryToSnapshot(): void {
if (this.temporaryCurrentState) {
this.addSnapshot(this.temporaryCurrentState)
this.temporaryCurrentState = undefined
}
}
/**
@@ -407,6 +450,7 @@ export class RawAppHistoryManager {
this.currentIndex = -1
this.currentBranchId = undefined
this.currentBranchEntryIndex = -1
this.temporaryCurrentState = undefined
}
/**

View File

@@ -16,7 +16,6 @@
import DiffEditor from '$lib/components/DiffEditor.svelte'
import type { InlineScript, StaticAppInput, UserAppInput, CtxAppInput } from '../apps/inputType'
import CacheTtlPopup from '../apps/editor/inlineScriptsPanel/CacheTtlPopup.svelte'
import RunButton from '$lib/components/RunButton.svelte'
import { computeFields } from '../apps/editor/inlineScriptsPanel/utils'
import EditorBar from '../EditorBar.svelte'
import { LanguageIcon } from '../common/languageIcons'
@@ -40,6 +39,7 @@
signDebugRequest,
getDebugErrorMessage
} from '$lib/components/debug'
import TextInput from '../text_input/TextInput.svelte'
interface Props {
inlineScript: (InlineScript & { language: ScriptLang }) | undefined
@@ -47,9 +47,7 @@
id: string
fields?: Record<string, StaticAppInput | UserAppInput | CtxAppInput>
path: string
isLoading?: boolean
onRun: () => Promise<void>
onCancel: () => Promise<void>
editor?: Editor | undefined
lastDeployedCode?: string | undefined
/** Called when code is selected in the editor */
@@ -70,9 +68,7 @@
id,
fields = $bindable(undefined),
path,
isLoading = false,
onRun,
onCancel,
editor = $bindable(undefined),
lastDeployedCode,
onSelectionChange
@@ -170,7 +166,9 @@
const dapServerUrl = $derived(
getDebugServerUrl((inlineScript?.language || 'python3') as DebugLanguage)
)
const debugFilePath = $derived(`/tmp/script${getDebugFileExtension(inlineScript?.language ?? '')}`)
const debugFilePath = $derived(
`/tmp/script${getDebugFileExtension(inlineScript?.language ?? '')}`
)
const isDebuggableScript = $derived(isDebuggable(inlineScript?.language ?? ''))
const showDebugPanel = $derived(
debugMode && $debugState.connected && ($debugState.running || $debugState.stopped)
@@ -306,7 +304,11 @@
let signedPayload
try {
signedPayload = await signDebugRequest($workspaceStore ?? '', code ?? '', inlineScript.language ?? 'python3')
signedPayload = await signDebugRequest(
$workspaceStore ?? '',
code ?? '',
inlineScript.language ?? 'python3'
)
debugSessionJobId = signedPayload.job_id
} catch (signError) {
sendUserToast(getDebugErrorMessage(signError), true)
@@ -314,12 +316,15 @@
}
// Get static args from fields
const args = Object.entries(fields ?? {}).reduce<Record<string, unknown>>((acc, [key, obj]) => {
if (obj.type === 'static') {
acc[key] = obj.value
}
return acc
}, {})
const args = Object.entries(fields ?? {}).reduce<Record<string, unknown>>(
(acc, [key, obj]) => {
if (obj.type === 'static') {
acc[key] = obj.value
}
return acc
},
{}
)
await dapClient.connect()
await dapClient.initialize()
@@ -597,19 +602,21 @@
</div>
{#if name !== undefined}
<div class="flex flex-row gap-2 w-full items-center">
<input
onkeydown={stopPropagation(bubble('keydown'))}
<TextInput
inputProps={{
onkeydown: () => stopPropagation(bubble('keydown')),
placeholder: 'Inline script name'
}}
bind:value={name}
placeholder="Inline script name"
class="!text-xs !rounded-sm !shadow-none"
size="sm"
/>
</div>
<Button
title="Clear script"
size="xs2"
color="light"
variant="contained"
variant="subtle"
aria-label="Clear script"
destructive
unifiedSize="sm"
on:click={() => dispatch('delete')}
endIcon={{ icon: Trash2 }}
iconOnly
@@ -622,14 +629,13 @@
<Button
variant="default"
size="xs2"
unifiedSize="sm"
on:click={async () => {
editor?.format()
}}
>
Format
</Button>
<RunButton {isLoading} {onRun} {onCancel} />
</div>
</div>
@@ -639,7 +645,7 @@
{editor}
lang={inlineScript.language}
{websocketAlive}
iconOnly={width < 950}
iconOnly={width < 1250}
kind={'script'}
template={'script'}
on:showDiffMode={showDiffMode}
@@ -705,7 +711,11 @@
if (inlineScript.schema == undefined) {
inlineScript.schema = emptySchema()
}
await inferInlineScriptSchema(inlineScript?.language, e.detail, inlineScript.schema)
await inferInlineScriptSchema(
inlineScript?.language,
e.detail,
inlineScript.schema
)
if (JSON.stringify(inlineScript.schema) != oldSchema) {
inlineScript = inlineScript
syncFields()
@@ -786,12 +796,17 @@
<Modal title="Debug Feature (Beta)" bind:open={showDebugBetaWarning}>
<div class="flex items-start gap-3">
<div class="flex-shrink-0">
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-yellow-100 dark:bg-yellow-800/50">
<div
class="flex h-10 w-10 items-center justify-center rounded-full bg-yellow-100 dark:bg-yellow-800/50"
>
<AlertTriangle class="h-5 w-5 text-yellow-600 dark:text-yellow-400" />
</div>
</div>
<div class="text-secondary text-sm">
<p>The Debug feature is currently in <strong>beta</strong>. You may encounter unexpected behavior or limitations.</p>
<p
>The Debug feature is currently in <strong>beta</strong>. You may encounter unexpected
behavior or limitations.</p
>
<p class="mt-2">By continuing, you acknowledge that this feature is experimental.</p>
</div>
</div>

View File

@@ -25,6 +25,7 @@
import { DebugToolbar, DebugPanel, debugState } from '$lib/components/debug'
import LogViewer from '$lib/components/LogViewer.svelte'
import DisplayResult from '$lib/components/DisplayResult.svelte'
import RunButton from '$lib/components/RunButton.svelte'
import { userStore, workspaceStore } from '$lib/stores'
type RunnableWithInlineScript = RunnableWithFields & {
@@ -67,7 +68,7 @@
}
}
let selectedTab = $state('inputs')
let selectedTab = $state('test')
let args = $state({})
function getSchema(runnable: RunnableWithFields) {
@@ -88,16 +89,18 @@
let inlineScriptEditor: RawAppInlineScriptEditor | undefined = $state()
// Get debug state from the editor
const editorDebugState = $derived(inlineScriptEditor?.getDebugState?.() ?? {
debugMode: false,
isDebuggableScript: false,
showDebugPanel: false,
hasDebugResult: false,
dapClient: null,
selectedDebugFrameId: null,
debugSessionJobId: null,
debugBreakpoints: new Set()
})
const editorDebugState = $derived(
inlineScriptEditor?.getDebugState?.() ?? {
debugMode: false,
isDebuggableScript: false,
showDebugPanel: false,
hasDebugResult: false,
dapClient: null,
selectedDebugFrameId: null,
debugSessionJobId: null,
debugBreakpoints: new Set()
}
)
// Reactive debug state values
const debugMode = $derived(editorDebugState.debugMode)
@@ -190,13 +193,7 @@
bind:inlineScript={runnable.inlineScript}
bind:name={runnable.name}
bind:fields={runnable.fields}
isLoading={testIsLoading}
onRun={testPreview}
onCancel={async () => {
if (jobLoader) {
await jobLoader.cancelJob()
}
}}
on:delete
path={appPath}
{onSelectionChange}
@@ -221,8 +218,8 @@
</Pane>
<Pane size={45}>
<Tabs bind:selected={selectedTab}>
<Tab value="inputs" label="Inputs" />
<Tab value="test" label="Test" />
<Tab value="inputs" label="Inputs" />
{#snippet content()}
{#if selectedTab == 'inputs'}
{#if runnable?.fields}
@@ -275,6 +272,18 @@
<Splitpanes horizontal class="grow">
<Pane size={50}>
<div class="px-2 py-3 h-full overflow-auto">
<div class="mx-auto w-fit">
<RunButton
isLoading={testIsLoading}
onRun={testPreview}
onCancel={async () => {
if (jobLoader) {
await jobLoader.cancelJob()
}
}}
size="md"
/>
</div>
<SchemaForm
on:keydownCmdEnter={testPreview}
disabledArgs={Object.entries(runnable?.fields ?? {})
@@ -307,7 +316,9 @@
/>
</div>
{:else}
<div class="h-full flex items-center justify-center text-sm text-tertiary">
<div
class="h-full flex items-center justify-center text-sm text-tertiary"
>
{#if $debugState.running && !$debugState.stopped}
Running...
{:else if $debugState.stopped}

View File

@@ -24,6 +24,7 @@
historyManager?: RawAppHistoryManager
historySelectedId?: number | undefined
onHistorySelect?: (id: number) => void
onHistorySelectCurrent?: () => void
onManualSnapshot?: () => void
dataTableRefs?: DataTableRef[]
onDataTableRefsChange?: (refs: DataTableRef[]) => void
@@ -44,6 +45,7 @@
historyManager,
historySelectedId,
onHistorySelect,
onHistorySelectCurrent,
onManualSnapshot,
dataTableRefs = [],
onDataTableRefsChange,
@@ -444,6 +446,7 @@
branches={historyManager.allBranches}
selectedId={historySelectedId}
onSelect={onHistorySelect}
onSelectCurrent={onHistorySelectCurrent}
/>
</PanelSection>
{/if}

View File

@@ -1,6 +1,20 @@
#!/bin/bash
# Auto-detect operating system
if [[ "$OSTYPE" == "darwin"* ]]; then
IS_MAC=true
else
IS_MAC=false
fi
cd ../../windmill-code-ui-builder
HASH=$(git rev-parse --short HEAD)
HASH=${HASH::-1}
echo "Using UI Builder hash: ${HASH}"
sed -i "s/ui_builder-[^.]*\.tar\.gz/ui_builder-${HASH}.tar.gz/" ../windmill/frontend/scripts/untar_ui_builder.js
if [ "$IS_MAC" = true ]; then
sed -i '' "s/ui_builder-[^.]*\.tar\.gz/ui_builder-${HASH}.tar.gz/" ../windmill/frontend/scripts/untar_ui_builder.js
else
sed -i "s/ui_builder-[^.]*\.tar\.gz/ui_builder-${HASH}.tar.gz/" ../windmill/frontend/scripts/untar_ui_builder.js
fi