mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 16:02:11 +00:00
fix: improve websockets handling for flow editor
This commit is contained in:
@@ -80,19 +80,27 @@
|
||||
}
|
||||
export let shouldBindKey: boolean = true
|
||||
export let fixedOverflowWidgets = true
|
||||
export let path: string = randomHash()
|
||||
export let path: string | undefined = undefined
|
||||
export let yContent: Text | undefined = undefined
|
||||
export let awareness: any | undefined = undefined
|
||||
|
||||
if (path == '' || path == undefined || path.startsWith('/')) {
|
||||
path = randomHash()
|
||||
const rHash = randomHash()
|
||||
$: filePath = computePath(path)
|
||||
|
||||
function computePath(path: string | undefined): string {
|
||||
if (path == '' || path == undefined || path.startsWith('/')) {
|
||||
return rHash
|
||||
} else {
|
||||
return path as string
|
||||
}
|
||||
}
|
||||
|
||||
let initialPath: string = path
|
||||
let initialPath: string | undefined = path
|
||||
|
||||
$: path != initialPath && lang == 'typescript' && handlePathChange()
|
||||
|
||||
let websockets: [MonacoLanguageClient, WebSocket][] = []
|
||||
let websockets: WebSocket[] = []
|
||||
let languageClients: MonacoLanguageClient[] = []
|
||||
let websocketInterval: NodeJS.Timer | undefined
|
||||
let lastWsAttempt: Date = new Date()
|
||||
let nbWsAttempt = 0
|
||||
@@ -101,7 +109,7 @@
|
||||
|
||||
const uri =
|
||||
lang == 'typescript'
|
||||
? `file:///${path}.${langToExt(lang)}`
|
||||
? `file:///${filePath}.${langToExt(lang)}`
|
||||
: `file:///tmp/monaco/${randomHash()}.${langToExt(lang)}`
|
||||
|
||||
buildWorkerDefinition('../../../workers', import.meta.url, false)
|
||||
@@ -271,7 +279,7 @@
|
||||
) {
|
||||
try {
|
||||
const webSocket = new WebSocket(url)
|
||||
|
||||
websockets.push(webSocket)
|
||||
webSocket.onopen = async () => {
|
||||
const socket = toSocket(webSocket)
|
||||
const reader = new WebSocketMessageReader(socket)
|
||||
@@ -285,7 +293,7 @@
|
||||
// if (middlewareOptions != undefined) {
|
||||
// languageClient.registerNotUsedFeatures()
|
||||
// }
|
||||
websockets.push([languageClient, webSocket])
|
||||
languageClients.push(languageClient)
|
||||
|
||||
// HACK ALERT: for some reasons, the client need to be restarted to take into account the 'go get <dep>' command
|
||||
// the only way I could figure out to listen for this event is this. I'm sure there is a better way to do this
|
||||
@@ -355,7 +363,7 @@
|
||||
|
||||
let encodedImportMap = ''
|
||||
if (lang == 'typescript') {
|
||||
if (path && path.split('/').length > 2) {
|
||||
if (filePath && filePath.split('/').length > 2) {
|
||||
let expiration = new Date()
|
||||
expiration.setHours(expiration.getHours() + 2)
|
||||
const token = await UserService.createToken({
|
||||
@@ -367,7 +375,7 @@
|
||||
'file:///': root + '/'
|
||||
}
|
||||
}
|
||||
let path_splitted = path.split('/')
|
||||
let path_splitted = filePath.split('/')
|
||||
for (let c = 0; c < path_splitted.length; c++) {
|
||||
let key = 'file://./'
|
||||
for (let i = 0; i < c; i++) {
|
||||
@@ -551,28 +559,35 @@
|
||||
|
||||
let pathTimeout: NodeJS.Timeout | undefined = undefined
|
||||
function handlePathChange() {
|
||||
console.log('path changed, reloading language server', initialPath, path)
|
||||
initialPath = path
|
||||
pathTimeout && clearTimeout(pathTimeout)
|
||||
pathTimeout = setTimeout(reloadWebsocket, 3000)
|
||||
pathTimeout = setTimeout(reloadWebsocket, 1000)
|
||||
}
|
||||
|
||||
async function closeWebsockets() {
|
||||
command && command.dispose()
|
||||
command = undefined
|
||||
for (const x of websockets) {
|
||||
|
||||
console.debug(`disposing ${websockets.length} language clients and closing websockets`)
|
||||
for (const x of languageClients) {
|
||||
try {
|
||||
await x[0].dispose()
|
||||
x[1].close()
|
||||
await x.dispose()
|
||||
} catch (err) {
|
||||
console.log('error disposing language client, closing websocket', err)
|
||||
try {
|
||||
x[1].close()
|
||||
} catch (err) {
|
||||
console.log('error disposing websocket, closin', err)
|
||||
}
|
||||
console.debug('error disposing language client', err)
|
||||
}
|
||||
}
|
||||
console.log('disposed language client and closed websocket')
|
||||
languageClients = []
|
||||
|
||||
for (const x of websockets) {
|
||||
try {
|
||||
await x.close()
|
||||
} catch (err) {
|
||||
console.debug('error closing websocket', err)
|
||||
}
|
||||
}
|
||||
|
||||
console.debug('done closing websockets')
|
||||
websockets = []
|
||||
websocketInterval && clearInterval(websocketInterval)
|
||||
}
|
||||
@@ -642,6 +657,7 @@
|
||||
!websocketAlive.go &&
|
||||
!websocketInterval
|
||||
) {
|
||||
console.log('reconnecting to language servers on focus')
|
||||
reloadWebsocket()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -250,7 +250,7 @@
|
||||
</span>
|
||||
{/if}
|
||||
{#if isStaticTemplate(inputCat) && propertyType == 'static' && !noDynamicToggle}
|
||||
<div class="mt-2 min-h-[28px] rounded border border-1 border-gray-500">
|
||||
<div class="mt-2 min-h-[28px] rounded border border-1 border-gray-400">
|
||||
{#if arg}
|
||||
<TemplateEditor
|
||||
bind:this={monacoTemplate}
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
args = {}
|
||||
}
|
||||
|
||||
export function setArgs(nargs: Record<string, InputTransform | any>) {
|
||||
args = nargs
|
||||
}
|
||||
|
||||
function removeExtraKey() {
|
||||
const nargs = {}
|
||||
Object.keys(args ?? {}).forEach((key) => {
|
||||
@@ -57,7 +61,7 @@
|
||||
{#if keys.length > 0}
|
||||
{#each keys as argName (argName)}
|
||||
{#if (!filter || filter.includes(argName)) && Object.keys(schema.properties ?? {}).includes(argName)}
|
||||
<div class="z-10">
|
||||
<div class="z-10 pt-1.5">
|
||||
<InputTransformForm
|
||||
{previousModuleId}
|
||||
bind:arg={args[argName]}
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
import ModulePreview from '$lib/components/ModulePreview.svelte'
|
||||
import { createScriptFromInlineScript, fork } from '$lib/components/flows/flowStateUtils'
|
||||
|
||||
import { RawScript, type FlowModule, type PathFlow, type PathScript } from '$lib/gen'
|
||||
import { RawScript, type FlowModule } from '$lib/gen'
|
||||
import FlowCard from '../common/FlowCard.svelte'
|
||||
import FlowModuleHeader from './FlowModuleHeader.svelte'
|
||||
import { getLatestHashForScript, scriptLangToEditorLang } from '$lib/scripts'
|
||||
import PropPickerWrapper from '../propPicker/PropPickerWrapper.svelte'
|
||||
import { afterUpdate, getContext } from 'svelte'
|
||||
import { afterUpdate, getContext, tick } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import { loadSchemaFromModule } from '../utils'
|
||||
import FlowModuleScript from './FlowModuleScript.svelte'
|
||||
@@ -38,9 +38,6 @@
|
||||
export let parentModule: FlowModule | undefined = undefined
|
||||
export let previousModule: FlowModule | undefined
|
||||
|
||||
let value = flowModule.value as PathFlow | RawScript | PathScript
|
||||
$: value = flowModule.value as PathFlow | RawScript | PathScript
|
||||
|
||||
let editor: Editor
|
||||
let modulePreview: ModulePreview
|
||||
let websocketAlive = {
|
||||
@@ -59,10 +56,6 @@
|
||||
let validCode = true
|
||||
let width = 1200
|
||||
|
||||
let inputTransforms: Record<string, any> = value.input_transforms
|
||||
|
||||
$: value.input_transforms = inputTransforms
|
||||
|
||||
$: stepPropPicker = failureModule
|
||||
? {
|
||||
pickableProperties: {
|
||||
@@ -91,16 +84,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
let inputTransformSchemaForm: InputTransformSchemaForm | undefined = undefined
|
||||
async function reload(flowModule: FlowModule) {
|
||||
try {
|
||||
const { input_transforms, schema } = await loadSchemaFromModule(flowModule)
|
||||
validCode = true
|
||||
setTimeout(() => {
|
||||
if (!deepEqual(value.input_transforms, input_transforms)) {
|
||||
inputTransforms = input_transforms
|
||||
}
|
||||
})
|
||||
|
||||
inputTransformSchemaForm?.setArgs(input_transforms)
|
||||
|
||||
await tick()
|
||||
if (!deepEqual(schema, $flowStateStore[flowModule.id]?.schema)) {
|
||||
if (!$flowStateStore[flowModule.id]) {
|
||||
$flowStateStore[flowModule.id] = { schema }
|
||||
@@ -127,15 +119,12 @@
|
||||
totalTopGap = panesTop - wrapperTop
|
||||
})
|
||||
|
||||
let isScript = true
|
||||
$: isScript != (value.type === 'script') && (isScript = value.type === 'script')
|
||||
|
||||
let forceReload = 0
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
{#if value}
|
||||
{#if flowModule.value}
|
||||
<div class="h-full" bind:this={wrapper} bind:clientWidth={width}>
|
||||
<FlowCard bind:flowModule>
|
||||
<svelte:fragment slot="header">
|
||||
@@ -173,12 +162,12 @@
|
||||
/>
|
||||
</svelte:fragment>
|
||||
|
||||
{#if value.type === 'rawscript'}
|
||||
{#if flowModule.value.type === 'rawscript'}
|
||||
<div class="border-b-2 shadow-sm px-1">
|
||||
<EditorBar
|
||||
{validCode}
|
||||
{editor}
|
||||
lang={value['language'] ?? 'deno'}
|
||||
lang={flowModule.value['language'] ?? 'deno'}
|
||||
{websocketAlive}
|
||||
iconOnly={width < 850}
|
||||
/>
|
||||
@@ -191,65 +180,68 @@
|
||||
style="max-height: calc(100% - {totalTopGap}px) !important;"
|
||||
>
|
||||
<Splitpanes horizontal>
|
||||
<Pane size={isScript ? 30 : 50} minSize={20}>
|
||||
{#if value.type === 'rawscript'}
|
||||
<Editor
|
||||
path={value['path']}
|
||||
bind:websocketAlive
|
||||
bind:this={editor}
|
||||
class="h-full relative"
|
||||
bind:code={value.content}
|
||||
deno={value.language === RawScript.language.DENO}
|
||||
lang={scriptLangToEditorLang(value.language)}
|
||||
automaticLayout={true}
|
||||
cmdEnterAction={async () => {
|
||||
selected = 'test'
|
||||
if ($selectedId == flowModule.id) {
|
||||
if (value.type === 'rawscript') {
|
||||
value.content = editor.getCode()
|
||||
<Pane size={flowModule.value.type == 'script' ? 30 : 50} minSize={20}>
|
||||
{#if flowModule.value.type === 'rawscript'}
|
||||
{#key flowModule.id}
|
||||
<Editor
|
||||
path={flowModule.value.path}
|
||||
bind:websocketAlive
|
||||
bind:this={editor}
|
||||
class="h-full relative"
|
||||
bind:code={flowModule.value.content}
|
||||
deno={flowModule.value.language === RawScript.language.DENO}
|
||||
lang={scriptLangToEditorLang(flowModule.value.language)}
|
||||
automaticLayout={true}
|
||||
cmdEnterAction={async () => {
|
||||
selected = 'test'
|
||||
if ($selectedId == flowModule.id) {
|
||||
if (flowModule.value.type === 'rawscript') {
|
||||
flowModule.value.content = editor.getCode()
|
||||
}
|
||||
await reload(flowModule)
|
||||
modulePreview?.runTestWithStepArgs()
|
||||
}
|
||||
}}
|
||||
on:change={async (event) => {
|
||||
if (flowModule.value.type === 'rawscript') {
|
||||
flowModule.value.content = event.detail
|
||||
}
|
||||
await reload(flowModule)
|
||||
modulePreview?.runTestWithStepArgs()
|
||||
}
|
||||
}}
|
||||
on:change={async (event) => {
|
||||
if (flowModule.value.type === 'rawscript') {
|
||||
flowModule.value.content = event.detail
|
||||
}
|
||||
await reload(flowModule)
|
||||
}}
|
||||
formatAction={() => {
|
||||
reload(flowModule)
|
||||
saveDraft()
|
||||
}}
|
||||
fixedOverflowWidgets={true}
|
||||
/>
|
||||
{:else if value.type === 'script'}
|
||||
{#key forceReload}
|
||||
<FlowModuleScript path={value.path} hash={value.hash} />
|
||||
}}
|
||||
formatAction={() => {
|
||||
reload(flowModule)
|
||||
saveDraft()
|
||||
}}
|
||||
fixedOverflowWidgets={true}
|
||||
/>
|
||||
{/key}
|
||||
{:else if value.type === 'flow'}
|
||||
<FlowPathViewer path={value.path} />
|
||||
{:else if flowModule.value.type === 'script'}
|
||||
{#key forceReload}
|
||||
<FlowModuleScript path={flowModule.value.path} hash={flowModule.value.hash} />
|
||||
{/key}
|
||||
{:else if flowModule.value.type === 'flow'}
|
||||
<FlowPathViewer path={flowModule.value.path} />
|
||||
{/if}
|
||||
</Pane>
|
||||
<Pane size={isScript ? 70 : 50} minSize={20}>
|
||||
<Pane size={flowModule.value.type == 'script' ? 70 : 50} minSize={20}>
|
||||
<Tabs bind:selected>
|
||||
<Tab value="inputs"><span class="font-semibold">Step Input</span></Tab>
|
||||
<Tab value="test"><span class="font-semibold text-md">Test this step</span></Tab>
|
||||
<Tab value="advanced">Advanced</Tab>
|
||||
</Tabs>
|
||||
<div class="h-[calc(100%-32px)]">
|
||||
{#if selected === 'inputs'}
|
||||
{#if selected === 'inputs' && (flowModule.value.type == 'rawscript' || flowModule.value.type == 'script' || flowModule.value.type == 'flow')}
|
||||
<div class="h-full overflow-auto">
|
||||
<PropPickerWrapper
|
||||
pickableProperties={stepPropPicker.pickableProperties}
|
||||
error={failureModule}
|
||||
>
|
||||
<InputTransformSchemaForm
|
||||
bind:this={inputTransformSchemaForm}
|
||||
schema={$flowStateStore[$selectedId]?.schema ?? {}}
|
||||
previousModuleId={previousModule?.id}
|
||||
bind:args={value.input_transforms}
|
||||
bind:extraLib={stepPropPicker.extraLib}
|
||||
bind:args={flowModule.value.input_transforms}
|
||||
extraLib={stepPropPicker.extraLib}
|
||||
/>
|
||||
</PropPickerWrapper>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<title>Windmill SDK example</title>
|
||||
</head>
|
||||
<body>
|
||||
<img src="/assets/windmill.svg" alt="Windmill" class="w-32 h-32 mx-auto" />
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user