Files
windmill/frontend/src/lib/components/Dev.svelte
T
Guilhem 70b7c8c998 refactor(frontend): capture refactor UI (#5591)
* migrate postgresEditorInner to svelte 5 syntaxe

* Add postgress

* add postgres capture section

* migrate kafka trigger config to svelte 5

* use snippets in kafka config

* feat: Update KafkaTriggerEditorInner with editMode, hideTarget and other props from WebsocketTriggerEditorInner

* feat: Create KafkaTriggerPanelV2 following WebsocketTriggersPanelV2 model

* feat: Integrate KafkaTriggerPanelV2 into TriggersEditorV2

* feat: add kafka capture panel

* use svelte 5 syntaxe for nats config

* use snippet in nats config

* feat: add editMode for nats trigger config

* fix import

* create NatsTriggerPanelV2.svelte for NATS trigger

* integrate NatsTriggerPanelV2 into TriggersEditorV2.svelte

* Create NatsCapture.svelte and update CaptureWrapperV2.svelte to use it

* migrate MqttTriggerEditorInner to svelte 5

* add mqtt trigger to new trigger panel

* migrate SqsTriggerEditorInner to svelte 5 syntaxe

* feat: add sqs triggers panel

* Add badge to MQTT config section

* remove unnecessary binding

* add gcp to the trigger list

* add prop size to GCP icon

* add gcp trigger utils

* migrate GcpTriggerEditorInner to svelte5 syntaxe

* add gcp trigger panel

* Revert "add gcp trigger utils"

This reverts commit 58f0df9985.

* fix type error

* add gcp to trigger bar

* add capture gcp panel

* add schedule poll panel

* feat: add trigger table horizontal bar

* add http trigger label

* make triggerbar vertical

* nit

* add trigger delete button for drafts

* enable edit from table for all triggers

* fix automatic edit mode for draft trigger

* fix proxy dispatching issue

* fix primary star positionning

* fix apply args to preprocessor

* automatically load bigpayloads

* clean capture wrapper code

* nit

* fix capture config not passed

* use Url component

* remove unused prop captureTable

* use new schedule panel for scheduled poll

* update triggersWrapperV2

* improve webhook capture panel

* remove debug log

* save draft with flow draft

* add path in draft label when available

* remove flowcard title

* use trigger label for trigger badge dropdown

* break from table to sidebar at 1000px

* remove triggers badge allways show sidebar behavior

* fix multiple primary addition

* improve primary schedule deploy logic

* remove debug logs

* gather imports

* add path for draft primary schedule

* handle redirection

* Add confirmation modal to deploy triggers

* remove debug log

* Save draft route trigger on deploy

* Allow draft for deployed triggers

* add component trigger editor toolbar

* improve update trigger utility function

* Add reset button for deployed draft triggers

* allow draft trigger for kafka triggers

* allow draft trigger for websocket triggers

* allow draft trigger for postgres triggers

* Add toggle to trigger toolbar

* allow draft for nats triggers

* allow draft for mqtt triggers

* allow draft sqs triggers

* allow draft for gcp triggers

* revert changes on the schedule editor

* update all triggers to use the same toolbar

* remove debug logs

* remove PrimarySchedule Panel and merge primary schedule logic with all triggers

* load schema from draft when editing a draft schedule

* prevent overflow in trigger table

* fix path saving for draft schedule

* fix trigger badge dropdown display

* get draft status from trigger

* remove unused event

* nit

* Add deploy badge to trigger

* replace checkbox with toggle for the modal

* fix selection of triggers with draft

* fix nit

* fix nit

* add missing config for loading schedule

* save schedule from config

* save http route from cfg

* save websocket trigger from cfg

* save postgres trigger from cfg

* save kafka trigger from cfg

* save sqs trigger from cfg

* save nats trigger from cfg

* save mqtt triggers from cfg

* save gcp trigger from config

* nit

* fix path update when editing a trigger

* remove unused saveCb

* fix trigger node navigation

* remove debug log

* add min size for trigger editor capture panel

* Integrate new triggers Editor to script builder

* remove previous triggersEdior page

* Remove former triggers Editor

* Migrate detail page to new triggers editor UI

* Remove unused dev TriggersBadge

* migrate triggers to V2

* Migrate capture components to V2

* improve triggers badge UI

* fix trigger panel with scheduled poll

* fix auto select all draft on deploy

* simplify permission check

* Add custom label to every trigger editor

* fix draft not deleting when saving triggers and add option to deploy now

* remove unused utility function

* fix triggers loading from draft

* make advanced route section collapsible

* handle subflow triggers display

* center triggers badge

* fix panel redirection when adding a new triggers from node

* Remove unnecessary alerts

* Fix proxy issue in schedule editor inner

* Remove unused schedule pages

* nit

* nit cli panel

* Migrate selectedTrigger store to V2

* handle extra header button for trigger editor panel

* prevent reload on toggle schedule enable

* remove unused utility function

* remove primaryScheduleStore

* handle triggerCount with drafts

* fix editing in Schedule Poll Mode

* Remove triggers default value context store

* fix triggers label max width

* Remove dbg logs

* improve no trigger selected message

* nit

* Do not show enable toggle for http triggers

* Adapt ui to dark mode

* nit

* remove captureOn context value

* improve dark mode

* fix nit

* fix merge issue

* nit

* nit

* fix schedule permission initialisation

* nit

* Disable deploy now if needed

* Improve utilitu function for handling with triggers

* improve trigger badge look

* fix kafka

* nit

* fix schedule display

* fix toggling edit mode

* fix edit/cancel edit

* nit

* prevent capture config update in editor

* migrate webhook panel to svelte 5

* fix postgres trigger creation

* fix bind issue in nats triggers

* fix modal triggers message flow/script

* fix apply args for script

* fix gcp triggers update when creating

* fix gcp capture

* nits

* add loading state for deploying all triggers

* remove default mqtt

* update base_endpoint for gcp

* nit

* fix

* fix

* remove base endpoint

* add catch in case of error while creating a trigger

* Add animation is capture panels

* show preprocessor extra when there is a preprocessor

* Add loading state for captures

* add missing fields for capture

* migrate URL to svelte 5

* show icon by default in confirmation modal

* do not show draft triggers in detail badge list

* migrate to new captures

* revert unwanted change

* migrate to preprocesor v2

* nit

* fix email capture

* nits and fix

* nits

* update repo ref

* Feat: auto save draft instad of save button

* use triggerIndex as selection

* Add modified tag to triggers

* Update trigger modal

* fix trigger node select

* fix schedule page

* migrate TriggersEditor to svelte 5

* remove unused props

* fix schedule editor init

* fix store reactivity issue

* use class instead of store for triggers context

* nit

* nit

* fix schedule init

* Add postgres error message

* nit

* fix deploy new script

* load schema in shedule editor

* improve transitions

* Improve capture indication

* revert unwanted changes

* improve update

* nit

* migrate captures section to svelte 5

* only show trash on hover triggers row

* Add token count

* improve trigger display

* Add relevent naming for triggers

* Use wrapper div insted of width binding in flow card

* add trigger sorting

* nit

* handle reload from URL for draft triggers

* dirty script and flow draft with draft triggers

* handle cloud hosted triggers

* nit

* nit

* fix nats reactivity

* remove debug log

* handle deployed draft triggers in the flow editor

* Add warning for event stream capture

---------

Co-authored-by: dieriba <dieriba.pro@gmail.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-05-21 23:45:19 +02:00

757 lines
21 KiB
Svelte

<script lang="ts">
import SchemaForm from '$lib/components/SchemaForm.svelte'
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
import { Button, Drawer } from '$lib/components/common'
import { WindmillIcon } from '$lib/components/icons'
import LogPanel from '$lib/components/scriptEditor/LogPanel.svelte'
import {
type CompletedJob,
type Job,
JobService,
OpenAPI,
type Preview,
type OpenFlow,
type FlowModule,
WorkspaceService,
type InputTransform,
type RawScript,
type PathScript,
type TriggersCount
} from '$lib/gen'
import { inferArgs } from '$lib/infer'
import { setCopilotInfo, userStore, workspaceStore } from '$lib/stores'
import { emptySchema, sendUserToast } from '$lib/utils'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { onDestroy, onMount, setContext } from 'svelte'
import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte'
import { page } from '$app/stores'
import { getUserExt } from '$lib/user'
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
import FlowModuleSchemaMap from './flows/map/FlowModuleSchemaMap.svelte'
import FlowEditorPanel from './flows/content/FlowEditorPanel.svelte'
import { deepEqual } from 'fast-equals'
import { writable } from 'svelte/store'
import type { FlowState } from './flows/flowState'
import { initHistory } from '$lib/history'
import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types'
import { dfs } from './flows/dfs'
import { loadSchemaFromModule } from './flows/flowInfers'
import { CornerDownLeft, Play } from 'lucide-svelte'
import Toggle from './Toggle.svelte'
import { setLicense } from '$lib/enterpriseUtils'
import type { FlowCopilotContext, FlowCopilotModule } from './copilot/flow'
import { pickScript } from './flows/flowStateUtils'
import {
approximateFindPythonRelativePath,
isTypescriptRelativePath,
parseTypescriptDeps
} from '$lib/relative_imports'
import Tooltip from './Tooltip.svelte'
import type { TriggerContext } from './triggers'
import { workspaceAIClients } from './copilot/lib'
import type { FlowPropPickerConfig, PropPickerContext } from './prop_picker'
import type { PickableProperties } from './flows/previousResults'
import { Triggers } from './triggers/triggers.svelte'
$: token = $page.url.searchParams.get('wm_token') ?? undefined
$: workspace = $page.url.searchParams.get('workspace') ?? undefined
$: themeDarkRaw = $page.url.searchParams.get('activeColorTheme')
$: themeDark = themeDarkRaw == '2' || themeDarkRaw == '4'
$: if (token) {
OpenAPI.WITH_CREDENTIALS = true
OpenAPI.TOKEN = token
loadUser()
}
let flowCopilotContext: FlowCopilotContext = {
drawerStore: writable<Drawer | undefined>(undefined),
modulesStore: writable<FlowCopilotModule[]>([]),
currentStepStore: writable<string | undefined>(undefined),
genFlow,
shouldUpdatePropertyType: writable<{
[key: string]: 'static' | 'javascript' | undefined
}>({}),
exprsToSet: writable<{
[key: string]: InputTransform | any | undefined
}>({}),
generatedExprs: writable<{
[key: string]: string | undefined
}>({}),
stepInputsLoading: writable<boolean>(false)
}
const { modulesStore } = flowCopilotContext
async function genFlow(idx: number, flowModules: FlowModule[], stepOnly = false) {
let module = stepOnly ? $modulesStore[0] : $modulesStore[idx]
if (module && module.selectedCompletion) {
const [hubScriptModule, hubScriptState] = await pickScript(
module.selectedCompletion.path,
`${module.selectedCompletion.summary} (${module.selectedCompletion.app})`,
module.id,
undefined
)
const flowModule: FlowModule & {
value: RawScript | PathScript
} = {
id: module.id,
value: hubScriptModule.value,
summary: hubScriptModule.summary
}
$flowStateStore[module.id] = hubScriptState
flowModules.splice(idx, 0, flowModule)
$flowStore = $flowStore
sendUserToast('Added module', false)
}
}
setContext('FlowCopilotContext', flowCopilotContext)
async function setupCopilotInfo() {
if (workspace) {
workspaceAIClients.init(workspace)
try {
const info = await WorkspaceService.getCopilotInfo({ workspace })
setCopilotInfo(info)
} catch (err) {
console.error('Could not get copilot info', err)
setCopilotInfo({})
}
}
}
$: if (workspace) {
$workspaceStore = workspace
setupCopilotInfo()
}
$: if (workspace && token) {
loadUser()
}
async function loadUser() {
try {
const user = await getUserExt(workspace!)
$userStore = user
} catch (e) {
sendUserToast(`Failed to load user ${e}`, true)
}
}
let darkModeToggle: DarkModeToggle
let darkMode: boolean | undefined = undefined
let modeInitialized = false
function initializeMode() {
modeInitialized = true
darkModeToggle.toggle()
}
$: darkModeToggle &&
themeDark != darkMode &&
darkMode != undefined &&
!modeInitialized &&
initializeMode()
let testJobLoader: TestJobLoader
let socket: WebSocket | undefined = undefined
// Test args input
let args: Record<string, any> = {}
let isValid: boolean = true
// Test
let testIsLoading = false
let testJob: Job | undefined
let pastPreviews: CompletedJob[] = []
let validCode = true
type LastEditScript = {
content: string
path: string
language: Preview['language']
lock?: string
isCodebase?: boolean
tag?: string
}
let currentScript: LastEditScript | undefined = undefined
let schema = emptySchema()
const href = window.location.href
const indexQ = href.indexOf('?')
const searchParams = indexQ > -1 ? new URLSearchParams(href.substring(indexQ)) : undefined
if (searchParams?.has('local')) {
connectWs()
}
let useLock = false
let lockChanges = false
let timeout: NodeJS.Timeout | undefined = undefined
let loadingCodebaseButton = false
let lastCommandId = ''
const el = (event) => {
// sendUserToast(`Received message from parent ${event.data.type}`, true)
if (event.data.type == 'runTest') {
runTest()
event.preventDefault()
} else if (event.data.type == 'replaceScript') {
replaceScript(event.data)
} else if (event.data.type == 'testBundle') {
if (event.data.id == lastCommandId) {
testBundle(event.data.file, event.data.isTar)
} else {
sendUserToast(`Bundle received ${lastCommandId} was obsolete, ignoring`, true)
}
} else if (event.data.type == 'testPreviewBundle') {
if (event.data.id == lastCommandId && currentScript) {
testJobLoader.runPreview(
currentScript.path,
event.data.file,
currentScript.language,
args,
currentScript.tag,
useLock ? currentScript.lock : undefined
)
} else {
sendUserToast(`Bundle received ${lastCommandId} was obsolete, ignoring`, true)
}
} else if (event.data.type == 'testBundleError') {
loadingCodebaseButton = false
sendUserToast(
'Error bundling script:' +
event.data.errorMessage +
' ' +
(typeof event.data.error == 'object'
? JSON.stringify(event.data.error)
: event.data.error),
true
)
} else if (event.data.type == 'replaceFlow') {
lockChanges = true
replaceFlow(event.data)
timeout && clearTimeout(timeout)
timeout = setTimeout(() => {
lockChanges = false
}, 500)
} else if (event.data.type == 'error') {
sendUserToast(event.data.error.message, true)
}
}
setLicense()
onMount(() => {
window.addEventListener('message', el, false)
document.addEventListener('keydown', (e) => {
const obj = {
altKey: e.altKey,
code: e.code,
ctrlKey: e.ctrlKey,
isComposing: e.isComposing,
key: e.key,
location: e.location,
metaKey: e.metaKey,
repeat: e.repeat,
shiftKey: e.shiftKey
}
if ((obj.ctrlKey || obj.metaKey) && obj.key == 'a') {
e.stopPropagation()
return
}
window.parent?.postMessage({ type: 'keydown', key: JSON.stringify(obj) }, '*')
})
window.parent?.postMessage({ type: 'refresh' }, '*')
})
async function testBundle(file: string, isTar: boolean) {
testJobLoader?.abstractRun(async () => {
try {
const form = new FormData()
form.append(
'preview',
JSON.stringify({
content: currentScript?.content,
kind: isTar ? 'tarbundle' : 'bundle',
path: currentScript?.path,
args,
language: currentScript?.language,
tag: currentScript?.tag
})
)
// sendUserToast(JSON.stringify(file))
if (isTar) {
var array: number[] = []
file = atob(file)
for (var i = 0; i < file.length; i++) {
array.push(file.charCodeAt(i))
}
let blob = new Blob([new Uint8Array(array)], { type: 'application/octet-stream' })
form.append('file', blob)
} else {
form.append('file', file)
}
const url = '/api/w/' + workspace + '/jobs/run/preview_bundle'
const req = await fetch(url, {
method: 'POST',
body: form,
headers: {
Authorization: 'Bearer ' + token
}
})
if (req.status != 201) {
throw Error(
`Script snapshot creation was not successful: ${req.status} - ${
req.statusText
} - ${await req.text()}`
)
}
return await req.text()
} catch (e) {
sendUserToast(`Failed to send bundle ${e}`, true)
throw Error(e)
}
})
loadingCodebaseButton = false
}
onDestroy(() => {
window.removeEventListener('message', el)
if (socket && socket.readyState === WebSocket.OPEN) {
socket?.close()
}
})
function connectWs() {
try {
if (socket) {
socket.close()
}
} catch (e) {
console.error('Failed to close websocket', e)
}
const port = searchParams?.get('port') || '3001'
try {
socket = new WebSocket(`ws://localhost:${port}/ws`)
// Listen for messages
socket.addEventListener('message', (event) => {
replaceData(event.data)
})
function replaceData(msg: string) {
let data: any | undefined = undefined
try {
data = JSON.parse(msg)
} catch {
console.log('Received invalid JSON: ' + msg)
return
}
if (data.type == 'script') {
replaceScript(data)
} else if (data.type == 'flow') {
replaceFlow(data)
} else {
sendUserToast(`Received invalid message type ${data.type}`, true)
}
}
} catch (e) {
sendUserToast('Failed to connect to local server', true)
console.error(e)
}
}
let typescriptBundlePreviewMode = false
function runTest() {
if (mode == 'script') {
if (!currentScript) {
return
}
if (currentScript.isCodebase) {
loadingCodebaseButton = true
lastCommandId = Math.random().toString(36).substring(7)
window.parent?.postMessage({ type: 'testBundle', id: lastCommandId }, '*')
} else {
if (relativePaths.length > 0 && typescriptBundlePreviewMode) {
lastCommandId = Math.random().toString(36).substring(7)
window.parent?.postMessage(
{ type: 'testPreviewBundle', external: ['!/*'], id: lastCommandId },
'*'
)
} else {
//@ts-ignore
testJobLoader.runPreview(
currentScript.path,
currentScript.content,
currentScript.language,
args,
currentScript.tag,
useLock ? currentScript.lock : undefined
)
}
}
} else {
flowPreviewButtons?.openPreview()
}
}
async function loadPastTests(): Promise<void> {
if (!currentScript) {
return
}
console.log('Loading past tests')
pastPreviews = await JobService.listCompletedJobs({
workspace: $workspaceStore!,
jobKinds: 'preview',
createdBy: $userStore?.username,
scriptPathExact: currentScript.path
})
}
async function onKeyDown(event: KeyboardEvent) {
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyC') {
document.execCommand('copy')
} else if ((event.ctrlKey || event.metaKey) && event.code === 'KeyX') {
document.execCommand('cut')
} else if ((event.ctrlKey || event.metaKey) && event.code === 'KeyV') {
event.preventDefault()
document.execCommand('paste')
}
}
let relativePaths: any[] = []
let lastPath: string | undefined = undefined
async function replaceScript(lastEdit: LastEditScript) {
mode = 'script'
currentScript = lastEdit
if (lastPath !== lastEdit.path) {
schema = emptySchema()
relativePaths = []
}
try {
await inferArgs(lastEdit.language, lastEdit.content, schema)
if (lastEdit?.language == 'bun') {
relativePaths = await parseTypescriptDeps(lastEdit.content).filter(isTypescriptRelativePath)
} else if (lastEdit?.language == 'python3') {
relativePaths = approximateFindPythonRelativePath(lastEdit.content)
}
schema = schema
lastPath = lastEdit.path
validCode = true
} catch (e) {
console.error(e)
validCode = false
}
}
let mode: 'script' | 'flow' = 'script'
const flowStore = writable({
summary: '',
value: { modules: [] },
extra_perms: {},
schema: emptySchema()
} as OpenFlow)
type LastEditFlow = {
flow: OpenFlow
uriPath: string
}
let lastUriPath: string | undefined = undefined
async function replaceFlow(lastEdit: LastEditFlow) {
mode = 'flow'
lastUriPath = lastEdit.uriPath
// sendUserToast(JSON.stringify(lastEdit.flow), true)
// return
try {
if (!deepEqual(lastEdit.flow, $flowStore)) {
if (!lastEdit.flow.summary) {
lastEdit.flow.summary = 'New flow'
}
if (!lastEdit.flow.value?.modules) {
lastEdit.flow.value = { modules: [] }
}
$flowStore = lastEdit.flow
inferModuleArgs($selectedIdStore)
}
} catch (e) {
console.error('issue setting new flowstore', e)
}
}
const flowStateStore = writable({} as FlowState)
const previewArgsStore = writable<Record<string, any>>({})
const scriptEditorDrawer = writable(undefined)
const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined)
const history = initHistory($flowStore)
const testStepStore = writable<Record<string, any>>({})
const selectedIdStore = writable('settings-metadata')
const triggersCount = writable<TriggersCount | undefined>(undefined)
setContext<TriggerContext>('TriggerContext', {
triggersCount: triggersCount,
simplifiedPoll: writable(false),
showCaptureHint: writable(undefined),
triggersState: new Triggers()
})
setContext<FlowEditorContext>('FlowEditorContext', {
selectedId: selectedIdStore,
previewArgs: previewArgsStore,
scriptEditorDrawer,
moving,
history,
pathStore: writable(''),
flowStateStore,
flowStore,
testStepStore,
saveDraft: () => {},
initialPathStore: writable(''),
fakeInitialPath: '',
flowInputsStore: writable<FlowInput>({}),
customUi: {},
insertButtonOpen: writable(false),
executionCount: writable(0),
flowInputEditorState: writable<FlowInputEditorState>({
selectedTab: undefined,
editPanelSize: undefined,
payloadData: undefined
})
})
setContext<PropPickerContext>('PropPickerContext', {
flowPropPickerConfig: writable<FlowPropPickerConfig | undefined>(undefined),
pickablePropertiesFiltered: writable<PickableProperties | undefined>(undefined)
})
$: updateFlow($flowStore)
let lastSent: OpenFlow | undefined = undefined
function updateFlow(flow: OpenFlow) {
if (lockChanges) {
return
}
if (!deepEqual(flow, lastSent)) {
lastSent = JSON.parse(JSON.stringify(flow))
window?.parent.postMessage({ type: 'flow', flow, uriPath: lastUriPath }, '*')
}
}
$: $selectedIdStore && inferModuleArgs($selectedIdStore)
let flowPreviewButtons: FlowPreviewButtons
let reload = 0
async function inferModuleArgs(selectedIdStore: string) {
if (selectedIdStore == '') {
return
}
//@ts-ignore
dfs($flowStore.value.modules, async (mod) => {
if (mod.id == selectedIdStore) {
if (
mod.value.type == 'rawscript' ||
mod.value.type === 'script' ||
mod.value.type === 'flow'
) {
const { input_transforms, schema } = await loadSchemaFromModule(mod)
if (mod.value.type == 'rawscript' && mod.value.lock != undefined) {
mod.value.lock = undefined
}
mod.value.input_transforms = input_transforms
if (!deepEqual(schema, $flowStateStore[mod.id]?.schema)) {
if (!$flowStateStore[mod.id]) {
$flowStateStore[mod.id] = { schema }
} else {
$flowStateStore[mod.id].schema = schema
}
reload++
}
}
}
})
}
</script>
<svelte:window on:keydown={onKeyDown} />
<TestJobLoader
on:done={loadPastTests}
bind:this={testJobLoader}
bind:isLoading={testIsLoading}
bind:job={testJob}
/>
<main class="h-screen w-full">
{#if mode == 'script'}
<div class="flex flex-col min-h-full overflow-auto">
<div class="absolute top-0 left-2">
<DarkModeToggle bind:darkMode bind:this={darkModeToggle} forcedDarkMode={false} />
</div>
<div class="text-center w-full text-lg truncate py-1 text-primary">
{currentScript?.path ?? 'Not editing a script'}
<span class="text-2xs text-secondary">{currentScript?.language ?? ''}</span><span
class="text-2xs text-secondary">{currentScript?.isCodebase ? ' (codebase)' : ''}</span
>
</div>
<div class="absolute top-2 right-2 !text-tertiary text-xs">
{#if $userStore != undefined}
As {$userStore?.username} in {$workspaceStore}
{:else}
<span class="text-red-600">Unable to login</span>
{/if}
</div>
{#if !validCode}
<div class="text-center w-full text-lg truncate py-1 text-red-500">Invalid code</div>
{/if}
<div class="flex flex-row-reverse py-1">
<Toggle
size="xs"
bind:checked={useLock}
options={{ left: 'Infer lockfile', right: 'Use current lockfile' }}
/>
</div>
{#if (currentScript?.language == 'bun' || currentScript?.language == 'python3') && currentScript?.content != undefined}
{#if relativePaths.length > 0}
<div class="flex flex-row-reverse py-1">
{#if currentScript?.language == 'bun' && !currentScript?.isCodebase}
<Toggle
size="xs"
bind:checked={typescriptBundlePreviewMode}
options={{
left: '',
right: 'bundle relative paths for preview',
rightTooltip:
'(Beta) Instead of only sending the current file for preview and rely on already deployed code for the common logic, bundle all code that is imported in relative paths'
}}
/>
{:else if currentScript?.language == 'python3'}
<div class="text-xs text-yellow-500"
>relative imports detected<Tooltip
>Beware that when using relative imports, the code used in preview for those is
the one that is already deployed. If you make update to the common logic, you will
need to `wmill sync push` to see it reflected in the preview runs.</Tooltip
></div
>
{/if}
</div>
{/if}
{/if}
<div class="flex justify-center pt-1">
{#if testIsLoading}
<Button on:click={testJobLoader?.cancelJob} btnClasses="w-full" color="red" size="xs">
<WindmillIcon
white={true}
class="mr-2 text-white"
height="20px"
width="20px"
spin="fast"
/>
Cancel
</Button>
{:else}
<Button
disabled={currentScript === undefined}
color="dark"
on:click={(e) => {
runTest()
}}
btnClasses="w-full"
size="xs"
startIcon={{
icon: Play,
classes: 'animate-none'
}}
shortCut={{ Icon: CornerDownLeft, hide: testIsLoading }}
>
{#if loadingCodebaseButton}
Bundle is building...
{:else if testIsLoading}
Running
{:else}
Test
{/if}
</Button>
{/if}
</div>
<Splitpanes horizontal class="h-full">
<Pane size={33}>
<div class="px-2">
<div class="break-words relative font-sans">
<SchemaForm compact {schema} bind:args bind:isValid />
</div>
</div>
</Pane>
<Pane size={67}>
<LogPanel
{workspace}
lang={currentScript?.language}
previewJob={testJob}
{pastPreviews}
previewIsLoading={testIsLoading}
/>
</Pane>
</Splitpanes>
</div>
{:else}
<!-- <div class="h-full w-full grid grid-cols-2"> -->
<div class="h-full w-full">
<div class="flex flex-col max-h-screen h-full relative">
<div class="absolute top-0 left-2">
<DarkModeToggle bind:darkMode bind:this={darkModeToggle} forcedDarkMode={false} />
{#if $userStore}
As {$userStore?.username} in {$workspaceStore}
{:else}
<span class="text-red-600">Unable to login on {$workspaceStore}</span>
{/if}
</div>
<div class="flex justify-center pt-1 z-50 absolute right-2 top-2 gap-2">
<FlowPreviewButtons bind:this={flowPreviewButtons} />
</div>
<Splitpanes horizontal class="h-full max-h-screen grow">
<Pane size={67}>
{#if $flowStore?.value?.modules}
<div id="flow-editor"></div>
<FlowModuleSchemaMap
bind:modules={$flowStore.value.modules}
disableAi
disableTutorials
smallErrorHandler={true}
disableStaticInputs
/>
{:else}
<div class="text-red-400 mt-20">Missing flow modules</div>
{/if}
</Pane>
<Pane size={33}>
{#key reload}
<FlowEditorPanel
enableAi
noEditor
on:applyArgs={(ev) => {
if (ev.detail.kind === 'preprocessor') {
$testStepStore['preprocessor'] = ev.detail.args ?? {}
$selectedIdStore = 'preprocessor'
} else {
$previewArgsStore = ev.detail.args ?? {}
flowPreviewButtons?.openPreview()
}
}}
/>
{/key}
</Pane>
</Splitpanes>
</div>
</div>
{/if}
</main>