mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 16:02:11 +00:00
70b7c8c998
* 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>
757 lines
21 KiB
Svelte
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>
|