Files
windmill/frontend/src/lib/components/EditorBar.svelte
T
Ruben FiszelandClaude Opus 5 a9112b72a5 fix: make workspace preprocessor scripts selectable in flow preprocessor steps (#10786)
* fix: pick workspace preprocessor scripts in the flow preprocessor step

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqKXVsBXynMFtMZ26uvLw7

* fix: explain the empty preprocessor list and keep the editor bar hub populated

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqKXVsBXynMFtMZ26uvLw7

* fix: derive the editor bar's script kind from the preprocessor slot

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqKXVsBXynMFtMZ26uvLw7

* fix: keep the preprocessor entrypoint when resetting a step's content

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqKXVsBXynMFtMZ26uvLw7

* docs: state the preprocessor reset invariant instead of the old control flow

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqKXVsBXynMFtMZ26uvLw7

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 01:05:26 +02:00

1378 lines
43 KiB
Svelte

<script module lang="ts">
export const EDITOR_BAR_WIDTH_THRESHOLD = 1420
// Below this width even the icon-only helpers cluster is too crowded —
// collapse them into a single "Helpers" dropdown menu.
export const EDITOR_BAR_HELPERS_COMPACT_THRESHOLD = 800
// Tighter threshold for editors embedded inline in narrow panes
// (flow-step editor, raw-app inline script editor).
export const EDITOR_BAR_HELPERS_INLINE_THRESHOLD = 600
function getImportWmillTsStatement(lang: string | undefined) {
if (lang === 'deno') {
return `import * as wmill from "npm:windmill-client@1"\n`
} else if (lang === 'bun' || lang === 'bunnative') {
return `import * as wmill from "windmill-client"\n`
} else if (lang === 'nativets') {
return `import * as wmill from "./windmill.ts"\n`
}
return ''
}
</script>
<script lang="ts">
import { ResourceService, VariableService, WorkspaceService, type Script } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { base } from '$lib/base'
import type Editor from './Editor.svelte'
import ItemPicker from './ItemPicker.svelte'
import VariableEditor from './VariableEditor.svelte'
import Button from './common/button/Button.svelte'
import HighlightCode from './HighlightCode.svelte'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import { Drawer } from './common'
import WorkspaceScriptPicker from './flows/pickers/WorkspaceScriptPicker.svelte'
import PickHubScript from './flows/pickers/PickHubScript.svelte'
import ToggleHubWorkspace from './ToggleHubWorkspace.svelte'
import Skeleton from './common/skeleton/Skeleton.svelte'
import { SCRIPT_EDITOR_SHOW_EXPLORE_OTHER_SCRIPTS } from '$lib/consts'
import { createEventDispatcher, untrack } from 'svelte'
import { sendUserToast } from '$lib/toast'
import { getScriptByPath, scriptLangToEditorLang } from '$lib/scripts'
import { bashRunsInCustomImage } from '$lib/script_helpers'
import Toggle from './Toggle.svelte'
import {
DatabaseIcon,
DiffIcon,
DollarSign,
File,
GitBranch,
History,
Library,
Link,
Package,
Plus,
RotateCw,
Sigma,
Save,
Settings,
Users
} from 'lucide-svelte'
import { capitalize, formatS3Object, isObject, toCamel, type Item } from '$lib/utils'
import DropdownV2 from './DropdownV2.svelte'
import type { Schema, SchemaProperty, SupportedLanguage } from '$lib/common'
import ScriptVersionHistory from './ScriptVersionHistory.svelte'
import { getResetCode } from '$lib/script_helpers'
import Popover from './Popover.svelte'
import ResourceEditorDrawer from './ResourceEditorDrawer.svelte'
import type { EditorBarUi } from './custom_ui'
import EditorSettings from './EditorSettings.svelte'
import { quicktype, InputData, JSONSchemaInput, FetchingJSONSchemaStore } from 'quicktype-core'
import S3FilePicker from './S3FilePicker.svelte'
import DucklakeIcon from './icons/DucklakeIcon.svelte'
import MetricsDrawer from './metrics/MetricsDrawer.svelte'
import { endsWithUnterminatedStatement } from './sqlDdl'
import FlowInlineScriptAiButton from './copilot/FlowInlineScriptAIButton.svelte'
import GitRepoPopoverPicker from './GitRepoPopoverPicker.svelte'
import { insertDelegateToGitRepoInCode } from '$lib/ansibleUtils'
interface Props {
lang: SupportedLanguage | 'bunnative' | undefined
editor: Editor | undefined
websocketAlive: {
pyright: boolean
ruff: boolean
deno: boolean
go: boolean
shellcheck: boolean
}
iconOnly?: boolean
compactHelpers?: boolean
validCode?: boolean
// Asset-parse validity, when the context cares about it (pipeline
// editor). `undefined` = not applicable; `false` makes the badge red
// even if the main function parses.
validAssets?: boolean | undefined
kind?: 'script' | 'trigger' | 'approval' | 'preprocessor'
template?:
| 'pgsql'
| 'mysql'
| 'script'
| 'docker'
| 'powershell'
| 'bunnative'
| 'claudesandbox'
| 'wac_python'
| 'wac_typescript'
| 'ci_test_bun'
| 'ci_test_python'
collabMode?: boolean
collabLive?: boolean
collabUsers?: { name: string }[]
scriptPath?: string | undefined
args?: Record<string, any>
noHistory?: boolean
saveToWorkspace?: boolean
customUi?: EditorBarUi
lastDeployedCode?: string | undefined
diffMode?: boolean
showHistoryDrawer?: boolean
right?: import('svelte').Snippet
openAiChat?: boolean
moduleId?: string
// Workspace to scope variable/resource/data-table lookups to. Defaults to
// the nav `$workspaceStore`; an AI-session live editor passes the session's
// acting workspace (a fork) so the helper pickers hit the right workspace.
workspace?: string
}
let {
lang,
editor,
websocketAlive,
iconOnly = false,
compactHelpers = false,
validCode = true,
validAssets = undefined,
kind = 'script',
template = 'script',
collabMode = false,
collabLive = false,
collabUsers = [],
scriptPath = undefined,
noHistory = false,
saveToWorkspace = false,
customUi = {},
lastDeployedCode = undefined,
diffMode = false,
showHistoryDrawer = $bindable(false),
right,
openAiChat = false,
moduleId = undefined,
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let contextualVariablePicker: ItemPicker | undefined = $state()
let variablePicker: ItemPicker | undefined = $state()
let resourcePicker: ItemPicker | undefined = $state()
let resourceTypePicker: ItemPicker | undefined = $state()
let variableEditor: VariableEditor | undefined = $state()
let resourceEditor: ResourceEditorDrawer | undefined = $state()
let s3FilePicker: S3FilePicker | undefined = $state()
let ducklakePicker: ItemPicker | undefined = $state()
let dataTablePicker: ItemPicker | undefined = $state()
let databasePicker: ItemPicker | undefined = $state()
let metricsDrawer: MetricsDrawer | undefined = $state()
let gitRepoPickerOpen = $state(false)
let showContextVarPicker = $derived(
[
'python3',
'bash',
'powershell',
'go',
'deno',
'bun',
'bunnative',
'nativets',
'php',
'rust',
'csharp',
'nu',
'java',
'ruby',
'rlang',
'ansible',
'postgresql',
'mysql',
'bigquery',
'mssql',
'oracledb',
'snowflake',
'duckdb'
// for related places search: ADD_NEW_LANG
].includes(lang ?? '')
)
let showVarPicker = $derived(
[
'python3',
'bash',
'powershell',
'go',
'deno',
'bun',
'bunnative',
'nativets',
'php',
'rust',
'csharp',
'nu',
'java',
'ruby',
'rlang'
// for related places search: ADD_NEW_LANG
].includes(lang ?? '')
)
let showResourcePicker = $derived(
[
'python3',
'bash',
'powershell',
'go',
'deno',
'bun',
'bunnative',
'nativets',
'php',
'rust',
'csharp',
'nu',
'java',
'ruby',
'rlang'
// for related places search: ADD_NEW_LANG
].includes(lang ?? '')
)
let showS3Picker = $derived(
['duckdb', 'python3'].includes(lang ?? '') ||
['typescript', 'javascript'].includes(scriptLangToEditorLang(lang))
)
// Declared metrics compile to a SELECT, so only a DuckDB script can take the
// insertion; the other DuckLake-capable languages call it through the SDK.
let showMetricsDrawer = $derived(lang === 'duckdb')
let showDucklakePicker = $derived(
['duckdb', 'python3'].includes(lang ?? '') ||
['typescript', 'javascript'].includes(scriptLangToEditorLang(lang))
)
let showDataTablePicker = $derived(
['duckdb', 'python3'].includes(lang ?? '') ||
['typescript', 'javascript'].includes(scriptLangToEditorLang(lang))
)
let showDatabasePicker = $derived(['duckdb'].includes(lang ?? ''))
let showGitRepoPicker = $derived(lang === 'ansible')
let showResourceTypePicker = $derived(
['typescript', 'javascript'].includes(scriptLangToEditorLang(lang)) ||
lang === 'python3' ||
lang === 'php' ||
lang === 'rust'
)
let codeViewer: Drawer | undefined = $state()
let codeObj: { language: SupportedLanguage; content: string } | undefined = $state(undefined)
function getHelperItems(): Item[] {
const items: Item[] = []
if (showContextVarPicker && customUi?.contextVar != false) {
items.push({
displayName: 'Context variable',
icon: DollarSign,
action: () => contextualVariablePicker?.openDrawer()
})
}
if (showVarPicker && customUi?.variable != false) {
items.push({
displayName: 'Variable',
icon: DollarSign,
action: () => variablePicker?.openDrawer()
})
}
if (showS3Picker && customUi?.s3object != false) {
items.push({
displayName: 'S3 object',
icon: File,
action: () => s3FilePicker?.open()
})
}
if (showResourcePicker && customUi?.resource != false) {
items.push({
displayName: 'Resource',
icon: Package,
action: () => resourcePicker?.openDrawer()
})
}
if (showGitRepoPicker && customUi?.resource != false) {
items.push({
displayName: 'Git repository',
icon: GitBranch,
action: () => (gitRepoPickerOpen = true)
})
}
if (showResourceTypePicker && customUi?.type != false) {
items.push({
displayName: 'Resource type',
icon: Package,
action: () => resourceTypePicker?.openDrawer()
})
}
if (showDatabasePicker && customUi?.database != false) {
items.push({
displayName: 'Database',
icon: DatabaseIcon,
action: () => databasePicker?.openDrawer()
})
}
if (showDucklakePicker && customUi?.ducklake != false) {
items.push({
displayName: 'Ducklake',
icon: DucklakeIcon,
action: () => ducklakePicker?.openDrawer()
})
}
if (showMetricsDrawer && customUi?.metrics != false) {
items.push({
displayName: 'Metrics',
icon: Sigma,
action: () => metricsDrawer?.open()
})
}
if (showDataTablePicker && customUi?.dataTable != false) {
items.push({
displayName: 'Data table',
icon: DatabaseIcon,
action: () => dataTablePicker?.openDrawer()
})
}
if (customUi?.reset != false) {
items.push({
displayName: 'Reset content',
icon: RotateCw,
action: () => clearContent(),
separatorTop: items.length > 0
})
}
return items
}
function insertDelegateToGitRepo(resourcePath: string) {
if (!editor) return
const currentCode = editor.getCode()
const newCode = insertDelegateToGitRepoInCode(currentCode, resourcePath)
editor.setCode(newCode)
}
function addEditorActions() {
editor?.addAction('insert-variable', 'Windmill: Insert variable', () => {
variablePicker?.openDrawer()
})
editor?.addAction('insert-resource', 'Windmill: Insert resource', () => {
resourcePicker?.openDrawer()
})
}
$effect(() => {
editor && untrack(() => addEditorActions())
})
async function loadVariables() {
return await VariableService.listVariable({ workspace: ws ?? '' })
}
async function loadContextualVariables() {
return await VariableService.listContextualVariables({
workspace: ws ?? 'NO_W'
})
}
let scriptPicker: Drawer | undefined = $state()
let pick_existing: 'hub' | 'workspace' = $state('hub')
let filter = $state('')
async function onScriptPick(e: { detail: { path: string } }) {
codeObj = undefined
codeViewer?.openDrawer?.()
codeObj = await getScriptByPath(e.detail.path ?? '', ws)
}
const dispatch = createEventDispatcher()
function compile(schema: Schema) {
function rec(x: { [name: string]: SchemaProperty } | undefined, root = false) {
let res = '{\n'
const entries = Object.entries(isObject(x) ? x : {})
if (entries.length == 0) {
return 'any'
}
let i = 0
for (let [name, prop] of entries) {
if (prop?.type == 'object') {
res += `${name}: ${rec(prop.properties)}`
} else if (prop?.type == 'array') {
res += `${name}: ${prop?.items?.type ?? 'any'}[]`
} else {
let typ = prop?.type ?? 'any'
if (typ == 'integer') {
typ = 'number'
}
res += `${name}: ${typ}`
}
i++
if (i < entries.length) {
res += ',\n'
}
}
if (!root) {
res += '\n}'
}
return res
}
return rec(schema?.properties, true)
}
async function quicktypeJSONSchema(targetLanguage, typeName, jsonSchemaString, rendererOptions) {
const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore())
// We could add multiple schemas for multiple types,
// but here we're just making one type from JSON schema.
await schemaInput.addSource({ name: typeName, schema: jsonSchemaString })
const inputData = new InputData()
inputData.addInput(schemaInput)
return await quicktype({
inputData,
lang: targetLanguage,
rendererOptions
})
}
async function resourceTypePickCallback(name: string) {
if (!editor) return
const resourceType = await ResourceService.getResourceType({
workspace: ws ?? 'NO_W',
path: name
})
if (lang == 'python3') {
const pySchema = pythonCompile(resourceType.schema as any)
editor.insertAtCursor(`class ${name}(TypedDict):\n${pySchema}\n`)
const code = editor.getCode()
if (!code.includes('from typing import TypedDict')) {
editor.insertAtBeginning('from typing import TypedDict\n')
}
} else if (lang === 'php') {
const phpSchema = phpCompile(resourceType.schema as any)
const rtName = toCamel(capitalize(name))
editor.insertAtCursor(`if (!class_exists('${rtName}')) {\nclass ${rtName} {\n${phpSchema}\n`)
editor.backspace()
editor.insertAtCursor('}')
} else if (lang === 'rust') {
const { lines: lines } = await quicktypeJSONSchema(
'rust',
name,
JSON.stringify(resourceType.schema),
{
'leading-comments': false,
density: 'dense',
'derive-debug': true
}
)
editor.insertAtCurrentLine(lines.join('\n'))
} else {
const tsSchema = compile(resourceType.schema as any)
editor.insertAtCursor(`type ${toCamel(capitalize(name))} = ${tsSchema}`)
}
sendUserToast(`${name} inserted at cursor`)
}
function phpCompile(schema: Schema) {
let res = ' '
const properties = schema?.properties
const entries = Object.entries(isObject(properties) ? properties : {})
if (entries.length === 0) {
return ''
}
let i = 0
for (let [name, prop] of entries) {
let typ = 'array'
if (prop?.type === 'array') {
typ = 'array'
} else if (prop?.type === 'string') {
typ = 'string'
} else if (prop?.type === 'number') {
typ = 'float'
} else if (prop?.type === 'integer') {
typ = 'int'
} else if (prop?.type === 'boolean') {
typ = 'bool'
}
res += `public ${typ} $${name};`
i++
if (i < entries.length) {
res += '\n'
}
}
return res
}
function pythonCompile(schema: Schema) {
let res = ''
const properties = schema?.properties
const entries = Object.entries(isObject(properties) ? properties : {})
if (entries.length === 0) {
// the result is inserted as a `class X(TypedDict):` body
return 'pass'
}
let i = 0
for (let [name, prop] of entries) {
let typ = 'dict'
if (prop?.type === 'array') {
typ = 'list'
} else if (prop?.type === 'string') {
typ = 'str'
} else if (prop?.type === 'number') {
typ = 'float'
} else if (prop?.type === 'integer') {
typ = 'int'
} else if (prop?.type === 'boolean') {
typ = 'bool'
}
res += `${name}: ${typ}`
i++
if (i < entries.length) {
res += '\n'
}
}
return res
}
function clearContent() {
if (editor) {
const resetCode = getResetCode(lang, kind as Script['kind'], template)
editor.setCode(resetCode)
}
}
function windmillPathToCamelCaseName(path: string): string {
const parts = path.split('/')
const lastPart = parts[parts.length - 1]
const words = lastPart.split('_')
return words
.map((word, index) => {
if (index === 0) {
// Lowercase the first word
return word.toLowerCase()
} else {
// Capitalize the first letter of subsequent words
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
}
})
.join('')
}
</script>
{#if scriptPath}
<Drawer bind:open={showHistoryDrawer} size="1200px">
<DrawerContent title="Versions History" on:close={() => (showHistoryDrawer = false)}>
<ScriptVersionHistory {scriptPath} />
</DrawerContent>
</Drawer>
{/if}
<Drawer bind:this={scriptPicker} size="900px">
<DrawerContent title="Code" on:close={scriptPicker.closeDrawer}>
{#if pick_existing == 'hub'}
<!-- The hub publishes no preprocessor script, so that kind falls back to the action list
rather than showing an empty hub. -->
<PickHubScript
bind:filter
kind={kind == 'preprocessor' ? 'script' : kind}
on:pick={onScriptPick}
>
<ToggleHubWorkspace bind:selected={pick_existing} />
</PickHubScript>
{:else}
<WorkspaceScriptPicker bind:filter {kind} on:pick={onScriptPick}>
<ToggleHubWorkspace bind:selected={pick_existing} />
</WorkspaceScriptPicker>
{/if}
</DrawerContent>
</Drawer>
<Drawer bind:this={codeViewer} size="600px">
<DrawerContent title="Code" on:close={codeViewer.closeDrawer}>
{#if codeObj}
<HighlightCode language={codeObj?.language} code={codeObj?.content} />
{:else}
<Skeleton layout={[[40]]} />
{/if}
</DrawerContent>
</Drawer>
<ItemPicker
bind:this={contextualVariablePicker}
pickCallback={(path, name) => {
if (!editor) return
if (lang == 'deno') {
editor.insertAtCursor(`Deno.env.get('${name}')`)
} else if (lang === 'bun' || lang === 'bunnative' || lang == 'nativets') {
editor.insertAtCursor(`process.env["${name}"]`)
} else if (lang == 'python3') {
if (!editor.getCode().includes('import os')) {
editor.insertAtBeginning('import os\n')
}
editor.insertAtCursor(`os.environ.get("${name}")`)
} else if (lang == 'go') {
if (!editor.getCode().includes('"os"')) {
editor.insertAtLine('import "os"\n', 2)
}
editor.insertAtCursor(`os.Getenv("${name}")`)
} else if (lang == 'bash') {
editor.insertAtCursor(`$${name}`)
} else if (lang == 'powershell') {
editor.insertAtCursor(`$Env:${name}`)
} else if (lang == 'php') {
editor.insertAtCursor(`getenv('${name}');`)
} else if (lang == 'rust') {
editor.insertAtCursor(`std::env::var("${name}").unwrap();`)
} else if (lang == 'csharp') {
editor.insertAtCursor(`Environment.GetEnvironmentVariable("${name}");`)
} else if (lang == 'nu') {
editor.insertAtCursor(`$env.${name}`)
} else if (lang == 'java') {
editor.insertAtCursor(`System.getenv("${name}");`)
// for related places search: ADD_NEW_LANG
} else if (lang == 'ruby') {
editor.insertAtCursor(`ENV['${name}']`)
} else if (lang == 'rlang') {
editor.insertAtCursor(`Sys.getenv("${name}")`)
} else if (lang == 'ansible') {
editor.insertAtCursor(`{{ lookup('env', '${name}') }}`)
} else if (
['postgresql', 'mysql', 'bigquery', 'mssql', 'oracledb', 'snowflake', 'duckdb'].includes(
lang ?? ''
)
) {
editor.insertAtCursor(`%%${name}%%`)
}
sendUserToast(`${name} inserted at cursor`)
}}
tooltip="Contextual Variables are variables whose values are contextual to the Script
execution. They are are automatically set by Windmill."
documentationLink="https://www.windmill.dev/docs/core_concepts/variables_and_secrets#contextual-variables"
itemName="Contextual Variable"
extraField="name"
loadItems={loadContextualVariables}
/>
<ItemPicker
bind:this={variablePicker}
pickCallback={(path, name) => {
if (!editor) return
if (['javascript', 'typescript'].includes(scriptLangToEditorLang(lang))) {
if (!editor.getCode().includes('import * as wmill from')) {
editor.insertAtBeginning(getImportWmillTsStatement(lang))
}
editor.insertAtCursor(`(await wmill.getVariable('${path}'))`)
} else if (lang == 'python3') {
if (!editor.getCode().includes('import wmill')) {
editor.insertAtBeginning('import wmill\n')
}
editor.insertAtCursor(`wmill.get_variable("${path}")`)
} else if (lang == 'go') {
if (!editor.getCode().includes('wmill "github.com/windmill-labs/windmill-go-client"')) {
editor.insertAtLine('import wmill "github.com/windmill-labs/windmill-go-client"\n\n', 3)
}
editor.insertAtCursor(`v, _ := wmill.GetVariable("${path}")`)
} else if (lang == 'bash') {
if (bashRunsInCustomImage(editor.getCode())) {
// Custom image: no wmill CLI. Fall back to curl, then busybox wget
// (the default `# sandbox alpine:latest` image ships wget, not curl).
// get_value returns a JSON-quoted string, so strip the outer quotes
// to match the `jq -r .value` output of the non-sandbox branch.
editor.insertAtCursor(
`{ curl -sf -H "Authorization: Bearer $WM_TOKEN" "$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/variables/get_value/${path}" 2>/dev/null || wget -qO- --header="Authorization: Bearer $WM_TOKEN" "$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/variables/get_value/${path}"; } | sed 's/^"//;s/"$//'`
)
} else {
editor.insertAtCursor(`wmill variable get ${path} --json | jq -r .value`)
}
} else if (lang == 'powershell') {
editor.insertAtCursor(`$Headers = @{\n"Authorization" = "Bearer $Env:WM_TOKEN"`)
editor.arrowDown()
editor.insertAtCursor(
`\nInvoke-RestMethod -Headers $Headers -Uri "$Env:BASE_INTERNAL_URL/api/w/$Env:WM_WORKSPACE/variables/get_value/${path}"`
)
} else if (lang == 'php') {
editor.insertAtCursor(`$ch = curl_init(getenv('BASE_INTERNAL_URL') . '/api/w/' . getenv('WM_WORKSPACE') . '/variables/get_value/${path}');
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . getenv('WM_TOKEN')));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$var = json_decode(curl_exec($ch));`)
} else if (lang == 'csharp') {
editor.insertAtCursor(`var baseUrl = Environment.GetEnvironmentVariable("BASE_INTERNAL_URL");
var workspace = Environment.GetEnvironmentVariable("WM_WORKSPACE");
var uri = $"{baseUrl}/api/w/{workspace}/variables/get_value/${path}";
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable("WM_TOKEN")}");
string ${windmillPathToCamelCaseName(path)} = await client.GetStringAsync(uri);
`)
} else if (lang == 'nu') {
editor.insertAtCursor(`get_variable ${path}`)
} else if (lang == 'java') {
editor.insertAtCursor(`(Wmill.getVariable("${path}"))`)
// for related places search: ADD_NEW_LANG
} else if (lang == 'ruby') {
if (!editor.getCode().includes("require 'windmill/mini'")) {
editor.insertAtBeginning("require 'windmill/mini'\n")
}
editor.insertAtCursor(`get_variable("${path}")`)
} else if (lang == 'rlang') {
editor.insertAtCursor(`get_variable("${path}")`)
}
sendUserToast(`${name} inserted at cursor`)
}}
tooltip="Variables are dynamic values that have a key associated to them and can be retrieved during the execution of a Script or Flow."
documentationLink="https://www.windmill.dev/docs/core_concepts/variables_and_secrets"
itemName="Variable"
extraField="path"
loadItems={loadVariables}
buttons={{ 'Edit/View': (x) => variableEditor?.editVariable(x) }}
>
{#snippet submission()}
<div class="flex flex-row">
<Button
variant="accent"
startIcon={{ icon: Plus }}
on:click={() => {
variableEditor?.initNew()
}}
>
New variable
</Button>
</div>
{/snippet}
</ItemPicker>
<ItemPicker
bind:this={resourcePicker}
pickCallback={(path, _, resType) => {
if (!editor) return
if (['javascript', 'typescript'].includes(scriptLangToEditorLang(lang))) {
if (!editor.getCode().includes('import * as wmill from')) {
editor.insertAtBeginning(getImportWmillTsStatement(lang))
}
editor.insertAtCursor(`(await wmill.getResource('${path}'))`)
} else if (lang == 'python3') {
if (!editor.getCode().includes('import wmill')) {
editor.insertAtBeginning('import wmill\n')
}
editor.insertAtCursor(`wmill.get_resource("${path}")`)
} else if (lang == 'go') {
if (!editor.getCode().includes('wmill "github.com/windmill-labs/windmill-go-client"')) {
editor.insertAtLine('import wmill "github.com/windmill-labs/windmill-go-client"\n\n', 3)
}
editor.insertAtCursor(`r, _ := wmill.GetResource("${path}")`)
} else if (lang == 'bash') {
if (bashRunsInCustomImage(editor.getCode())) {
// Custom image: no wmill CLI. Fall back to curl, then busybox wget
// (the default `# sandbox alpine:latest` image ships wget, not curl).
// get_value_interpolated returns JSON, matching the `jq .value` branch.
editor.insertAtCursor(
`curl -sf -H "Authorization: Bearer $WM_TOKEN" "$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/resources/get_value_interpolated/${path}" 2>/dev/null || wget -qO- --header="Authorization: Bearer $WM_TOKEN" "$BASE_INTERNAL_URL/api/w/$WM_WORKSPACE/resources/get_value_interpolated/${path}"`
)
} else {
editor.insertAtCursor(`wmill resource get ${path} --json | jq .value`)
}
} else if (lang == 'powershell') {
editor.insertAtCursor(`$Headers = @{\n"Authorization" = "Bearer $Env:WM_TOKEN"`)
editor.arrowDown()
editor.insertAtCursor(
`\nInvoke-RestMethod -Headers $Headers -Uri "$Env:BASE_INTERNAL_URL/api/w/$Env:WM_WORKSPACE/resources/get_value_interpolated/${path}"`
)
} else if (lang == 'php') {
editor.insertAtCursor(`$ch = curl_init(getenv('BASE_INTERNAL_URL') . '/api/w/' . getenv('WM_WORKSPACE') . '/resources/get_value_interpolated/${path}');
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . getenv('WM_TOKEN')));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$res = json_decode(curl_exec($ch));`)
} else if (lang == 'csharp') {
if (!editor.getCode().includes(`using System.Text.Json.Nodes;`)) {
editor.insertAtBeginning(`using System.Text.Json.Nodes;\n`)
}
editor.insertAtCursor(`var baseUrl = Environment.GetEnvironmentVariable("BASE_INTERNAL_URL");
var workspace = Environment.GetEnvironmentVariable("WM_WORKSPACE");
var uri = $"{baseUrl}/api/w/{workspace}/resources/get_value_interpolated/${path}";
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {Environment.GetEnvironmentVariable("WM_TOKEN")}");
JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetStringAsync(uri));
`)
} else if (lang == 'nu') {
editor.insertAtCursor(`get_resource ${path}`)
} else if (lang == 'java') {
editor.insertAtCursor(`(Wmill.getResource("${path}"))`)
// for related places search: ADD_NEW_LANG
} else if (lang == 'ruby') {
if (!editor.getCode().includes("require 'windmill/mini'")) {
editor.insertAtBeginning("require 'windmill/mini'\n")
}
editor.insertAtCursor(`get_resource("${path}")`)
} else if (lang == 'rlang') {
editor.insertAtCursor(`get_resource("${path}")`)
} else if (lang == 'duckdb') {
let t = { postgresql: 'postgres', mysql: 'mysql', bigquery: 'bigquery' }[resType]
if (!t) {
sendUserToast(`Resource type ${resType} is not supported in DuckDB`, true)
editor.insertAtCursor(`'res://${path}'`)
return
} else {
editor.insertAtCursor(`ATTACH 'res://${path}' AS db (TYPE ${t});`)
}
}
sendUserToast(`${path} inserted at cursor`)
}}
tooltip="Resources represent connections to third party systems. Resources are a good way to define a connection to a frequently used third party system such as a database."
documentationLink="https://www.windmill.dev/docs/core_concepts/resources_and_types"
itemName="Resource"
buttons={{ 'Edit/View': (x) => resourceEditor?.initEdit(x) }}
extraField="description"
extraField2="resource_type"
loadItems={async () => await ResourceService.listResource({ workspace: ws ?? 'NO_W' })}
>
{#snippet submission()}
<div class="flex flex-row gap-x-1 mr-2">
<Button
startIcon={{ icon: Plus }}
target="_blank"
variant="accent"
href="{base}/resources?connect_app=undefined"
>
Add resource
</Button>
</div>
{/snippet}
</ItemPicker>
{#if showResourceTypePicker}
<ItemPicker
bind:this={resourceTypePicker}
pickCallback={async (_, name) => {
resourceTypePickCallback(name)
}}
tooltip="Resources Types are the schemas associated with a Resource. They define the structure of the data that is returned from a Resource."
documentationLink="https://www.windmill.dev/docs/core_concepts/resources_and_types"
itemName="Resource Type"
extraField="name"
loadItems={async () => await ResourceService.listResourceType({ workspace: ws ?? 'NO_W' })}
/>
{/if}
<ResourceEditorDrawer
bind:this={resourceEditor}
workspace={ws}
on:refresh={resourcePicker.openDrawer}
/>
<VariableEditor bind:this={variableEditor} workspace={ws} on:create={variablePicker.openDrawer} />
{#if showDucklakePicker}
<ItemPicker
bind:this={ducklakePicker}
pickCallback={async (_, name) => {
if (lang === 'duckdb') {
const connStr = name == 'main' ? 'ducklake' : `ducklake://${name}`
editor?.insertAtCursor(`ATTACH '${connStr}' AS dl; USE dl;\n`)
} else if (lang === 'python3') {
if (!editor?.getCode().includes('import wmill')) {
editor?.insertAtBeginning('import wmill\n')
}
editor?.insertAtCursor(`dl = wmill.ducklake(${name == 'main' ? '' : `'${name}'`})\n`)
} else if (['javascript', 'typescript'].includes(scriptLangToEditorLang(lang))) {
if (!editor?.getCode().includes('import * as wmill from')) {
editor?.insertAtBeginning(getImportWmillTsStatement(lang))
}
editor?.insertAtCursor(`let sql = wmill.ducklake(${name == 'main' ? '' : `'${name}'`})\n`)
}
}}
tooltip="Attach a Ducklake to your scripts. Ducklake allows you to manipulate large data on S3 blob files through a traditional SQL interface."
documentationLink="https://www.windmill.dev/docs/core_concepts/persistent_storage/ducklake"
itemName="ducklake"
loadItems={async () =>
(await WorkspaceService.listDucklakes({ workspace: ws ?? 'NO_W' })).map((path) => ({ path }))}
>
{#snippet submission()}
<div class="flex flex-row gap-x-1 mr-2">
<Button
startIcon={{ icon: Settings }}
target="_blank"
variant="accent"
href="{base}/workspace_settings?tab=ducklake"
>
Go to settings
</Button>
</div>
{/snippet}
</ItemPicker>
{/if}
{#if showDataTablePicker}
<ItemPicker
bind:this={dataTablePicker}
pickCallback={async (_, name) => {
if (lang === 'duckdb') {
const connStr = name == 'main' ? 'datatable' : `datatable://${name}`
editor?.insertAtCursor(`ATTACH '${connStr}' AS dt; USE dt;\n`)
} else if (lang === 'python3') {
if (!editor?.getCode().includes('import wmill')) {
editor?.insertAtBeginning('import wmill\n')
}
editor?.insertAtCursor(`db = wmill.datatable(${name == 'main' ? '' : `'${name}'`})\n`)
} else if (['javascript', 'typescript'].includes(scriptLangToEditorLang(lang))) {
if (!editor?.getCode().includes('import * as wmill from')) {
editor?.insertAtBeginning(getImportWmillTsStatement(lang))
}
editor?.insertAtCursor(`let sql = wmill.datatable(${name == 'main' ? '' : `'${name}'`})\n`)
editor?.insertAtCursor(`let query_result = await sql\`SELECT * FROM _\`.fetchOne()\n`)
}
}}
tooltip="Attach a datatable to your script."
documentationLink="https://www.windmill.dev/docs/core_concepts/persistent_storage/data_tables"
itemName="data table"
loadItems={async () =>
(await WorkspaceService.listDataTables({ workspace: ws ?? 'NO_W' })).map((d) => ({
path: d.name
}))}
>
{#snippet submission()}
<div class="flex flex-row gap-x-1 mr-2">
<Button
startIcon={{ icon: Settings }}
target="_blank"
variant="accent"
href="{base}/workspace_settings?tab=windmill_data_tables"
>
Go to settings
</Button>
</div>
{/snippet}
</ItemPicker>
{/if}
{#if showDatabasePicker}
<ItemPicker
bind:this={databasePicker}
pickCallback={(path, _, resType) => {
if (!editor) return
if (lang == 'duckdb') {
let t = { postgresql: 'postgres', mysql: 'mysql', bigquery: 'bigquery' }[resType]
editor.insertAtCursor(`ATTACH 'res://${path}' AS db (TYPE ${t});`)
}
sendUserToast(`${path} inserted at cursor`)
}}
tooltip="Attach a database resource in your script. This allows you to query data from the database using SQL."
documentationLink="https://www.windmill.dev/docs/core_concepts/resources_and_types"
itemName="Database"
buttons={{ 'Edit/View': (x) => resourceEditor?.initEdit(x) }}
extraField="description"
extraField2="resource_type"
loadItems={async () =>
await ResourceService.listResource({
workspace: ws ?? 'NO_W',
resourceType: 'postgresql,mysql,bigquery'
})}
></ItemPicker>
{/if}
{#if showMetricsDrawer && customUi?.metrics != false}
<!-- Appended rather than inserted at the cursor: the snippet is a whole statement
block, and when it relies on an ATTACH already in the script it must follow it. -->
<MetricsDrawer
bind:this={metricsDrawer}
workspace={ws}
getCode={() => editor?.getCode() ?? ''}
onInsert={(sql) => {
// Terminate whatever the script ends with: appending a fresh statement
// after an unterminated one produces invalid SQL. The separator starts on
// its own line so the `;` cannot land inside a trailing line comment.
const existing = editor?.getCode() ?? ''
const sep =
existing.trim() === '' ? '' : endsWithUnterminatedStatement(existing) ? '\n;\n\n' : '\n\n'
editor?.append(sep + sql + '\n')
}}
/>
{/if}
<S3FilePicker
bind:this={s3FilePicker}
readOnlyMode={false}
onSelectAndClose={(s3obj) => {
let s = `'${formatS3Object(s3obj)}'`
if (lang === 'duckdb') {
editor?.insertAtCursor(`SELECT * FROM ${s};`)
} else if (lang === 'python3') {
if (!editor?.getCode().includes('import wmill')) {
editor?.insertAtBeginning('import wmill\n')
}
editor?.insertAtCursor(`wmill.load_s3_file(${s})`)
} else if (['javascript', 'typescript'].includes(scriptLangToEditorLang(lang))) {
if (!editor?.getCode().includes('import * as wmill from')) {
editor?.insertAtBeginning(getImportWmillTsStatement(lang))
}
editor?.insertAtCursor(`wmill.loadS3File(${s})`)
}
}}
/>
<div class="flex justify-between items-center overflow-y-auto w-full p-0.5">
<div class="flex gap-3 items-center">
<div
title={!validCode
? 'Main function not parsable'
: validAssets === false
? 'Assets not parsable'
: 'Parsable'}
class="rounded-full w-2 h-2 mx-2 {validCode && validAssets !== false
? 'bg-green-300'
: 'bg-red-300'}"
></div>
<div class="flex items-center gap-2">
{#if compactHelpers}
{#snippet helpersDropdown()}
<DropdownV2 items={getHelperItems} placement="bottom-start">
{#snippet buttonReplacement()}
<Button
nonCaptureEvent
variant="subtle"
unifiedSize="sm"
startIcon={{ icon: Plus }}
title="Helpers"
>
Helpers
</Button>
{/snippet}
</DropdownV2>
{/snippet}
{#if showGitRepoPicker && customUi?.resource != false}
<!-- Wrap the Helpers dropdown so the Git-repo popover anchors to
the visible Helpers button rather than an sr-only placeholder. -->
<GitRepoPopoverPicker
bind:isOpen={gitRepoPickerOpen}
on:selected={(e) => insertDelegateToGitRepo(e.detail.resourcePath)}
>
{@render helpersDropdown()}
</GitRepoPopoverPicker>
{:else}
{@render helpersDropdown()}
{/if}
{:else}
{#if showContextVarPicker && customUi?.contextVar != false}
<Button
aiId="editor-bar-add-context-variable"
aiDescription="Add context variable"
title="Add context variable"
variant="subtle"
on:click={contextualVariablePicker.openDrawer}
unifiedSize="sm"
startIcon={{ icon: DollarSign }}
{iconOnly}
>+Context var
</Button>
{/if}
{#if showVarPicker && customUi?.variable != false}
<Button
aiId="editor-bar-add-variable"
aiDescription="Add variable"
title="Add variable"
variant="subtle"
on:click={variablePicker.openDrawer}
unifiedSize="sm"
startIcon={{ icon: DollarSign }}
{iconOnly}
>
+Variable
</Button>
{/if}
{#if showS3Picker && customUi?.s3object != false}
<Button
aiId="editor-bar-add-s3-object"
aiDescription="Add S3 Object"
title="Add S3 object"
variant="subtle"
on:click={() => s3FilePicker?.open()}
unifiedSize="sm"
startIcon={{ icon: File }}
{iconOnly}
>+S3 Object
</Button>
{/if}
{#if showResourcePicker && customUi?.resource != false}
<Button
aiId="editor-bar-add-resource"
aiDescription="Add resource"
title="Add resource"
unifiedSize="sm"
variant="subtle"
on:click={resourcePicker.openDrawer}
{iconOnly}
startIcon={{ icon: Package }}
>
+Resource
</Button>
{/if}
{#if showGitRepoPicker && customUi?.resource != false}
<GitRepoPopoverPicker
bind:isOpen={gitRepoPickerOpen}
on:selected={(e) => insertDelegateToGitRepo(e.detail.resourcePath)}
>
<Button
aiId="editor-bar-add-git-repo"
aiDescription="Delegate to Git repository"
title="Delegate to Git repository"
unifiedSize="sm"
variant="subtle"
on:click={() => (gitRepoPickerOpen = true)}
{iconOnly}
startIcon={{ icon: GitBranch }}
>
+Git Repo
</Button>
</GitRepoPopoverPicker>
{/if}
{#if showResourceTypePicker && customUi?.type != false}
<Button
aiId="editor-bar-add-resource-type"
aiDescription="Add resource type"
title="Add resource type"
variant="subtle"
unifiedSize="sm"
on:click={() => resourceTypePicker?.openDrawer()}
{iconOnly}
startIcon={{ icon: Package }}
>
+Type
</Button>
{/if}
{#if showDatabasePicker && customUi?.database != false}
<Button
aiId="editor-bar-add-database"
aiDescription="Add database"
title="Add database"
variant="subtle"
on:click={() => databasePicker?.openDrawer()}
unifiedSize="sm"
startIcon={{ icon: DatabaseIcon }}
{iconOnly}
>+Database
</Button>
{/if}
{#if showDucklakePicker && customUi?.ducklake != false}
<Button
aiId="editor-bar-use-ducklake"
aiDescription="Use Ducklake"
title="Use Ducklake"
variant="subtle"
on:click={() => ducklakePicker?.openDrawer()}
unifiedSize="sm"
startIcon={{ icon: DucklakeIcon }}
{iconOnly}
>+Ducklake
</Button>
{/if}
{#if showMetricsDrawer && customUi?.metrics != false}
<Button
aiId="editor-bar-metrics"
aiDescription="Open the measures and dimensions declared on DuckLake tables"
title="Metrics"
variant="subtle"
on:click={() => metricsDrawer?.open()}
unifiedSize="sm"
startIcon={{ icon: Sigma }}
{iconOnly}
>Metrics
</Button>
{/if}
{#if showDataTablePicker && customUi?.dataTable != false}
<Button
aiId="editor-bar-use-datatable"
aiDescription="Use DataTable"
title="Use DataTable"
variant="subtle"
on:click={() => dataTablePicker?.openDrawer()}
unifiedSize="sm"
startIcon={{ icon: DatabaseIcon }}
{iconOnly}
>+Data table
</Button>
{/if}
{#if customUi?.reset != false}
<Button
aiId="editor-bar-reset-content"
aiDescription="Reset content"
title="Reset Content"
unifiedSize="sm"
variant="subtle"
on:click={clearContent}
{iconOnly}
startIcon={{ icon: RotateCw }}
>
Reset
</Button>
{/if}
{/if}
{#if customUi?.assistants != false}
{#if lang == 'deno' || lang == 'python3' || lang == 'go' || lang == 'bash'}
<Button
aiId="editor-bar-reload-assistants"
aiDescription="Reload assistants"
unifiedSize="sm"
variant="subtle"
on:click={() => editor?.reloadWebsocket()}
startIcon={{
icon: RotateCw,
classes: websocketAlive[lang] == false ? 'animate-spin' : ''
}}
title="Reload assistants"
>
{#if !iconOnly}
Assistants
{/if}
<span class="-my-1">
{#if lang == 'deno'}
(<span class={websocketAlive.deno ? 'green' : 'text-red-700'}>Deno</span>)
{:else if lang == 'go'}
(<span class={websocketAlive.go ? 'green' : 'text-red-700'}>Go</span>)
{:else if lang == 'python3'}
(<span class={websocketAlive.pyright ? 'green' : 'text-red-700'}>Pyright</span>
<span class={websocketAlive.ruff ? 'green' : 'text-red-700'}>Ruff</span>)
{:else if lang == 'bash'}
(<span class={websocketAlive.shellcheck ? 'green' : 'text-red-700'}>Shellcheck</span
>)
{/if}
</span>
</Button>
{/if}
{/if}
{#if customUi?.diffMode != false}
<div class="flex items-center px-3">
<Toggle
options={{ right: 'Diff' }}
size="sm"
checked={diffMode}
disabled={!lastDeployedCode}
on:change={(e) => {
const turnOn = e.detail
dispatch(turnOn ? 'showDiffMode' : 'hideDiffMode')
}}
/>
<Popover>
{#snippet text()}
Toggle diff mode
{/snippet}
<DiffIcon class="ml-1 text-primary" size={14} />
</Popover>
</div>
{/if}
{#if collabMode && customUi?.multiplayer != false}
<div class="flex items-center px-3">
<Toggle
options={{ right: '' }}
size="xs"
checked={collabLive}
on:change={() => dispatch('toggleCollabMode')}
/>
<Popover>
{#snippet text()}
Multiplayer
{/snippet}
<Users class="ml-1 text-primary" size={14} />
</Popover>
{#if collabLive}
<button
title="Show invite link"
class="p-1 rounded hover:bg-gray-400 mx-1 border"
onclick={() => dispatch('collabPopup')}><Link size={14} /></button
>
<div class="isolate flex -space-x-2 pl-2">
{#each collabUsers as user}
<span
class="inline-flex h-6 w-6 items-center justify-center rounded-full ring-2 ring-white bg-gray-600"
title={user.name}
>
<span class="text-sm font-semibold leading-none text-white">
{user.name.substring(0, 2).toLocaleUpperCase()}
</span>
</span>
{/each}
</div>
{/if}
</div>
{/if}
{#if customUi?.aiGen != false}
{#if openAiChat}
<FlowInlineScriptAiButton
{moduleId}
flushEditor={() => editor?.flushPendingChanges()}
btnProps={{ variant: 'subtle' }}
/>
{/if}
{/if}
<EditorSettings {customUi} btnProps={{ variant: 'subtle', unifiedSize: 'md' }} />
</div>
</div>
<div class="flex flex-row items-center gap-2 whitespace-nowrap">
{@render right?.()}
{#if scriptPath && !noHistory && customUi?.history != false}
<Button
unifiedSize="sm"
variant="subtle"
on:click={() => (showHistoryDrawer = true)}
{iconOnly}
startIcon={{ icon: History }}
title="See history"
>
History
</Button>
{/if}
{#if SCRIPT_EDITOR_SHOW_EXPLORE_OTHER_SCRIPTS && customUi?.library != false}
<Button
unifiedSize="sm"
variant="subtle"
on:click={scriptPicker.openDrawer}
{iconOnly}
startIcon={{ icon: Library }}
title="Explore other scripts"
>
Library
</Button>
{/if}
{#if saveToWorkspace && customUi?.saveToWorkspace != false}
<Button
unifiedSize="sm"
variant="subtle"
startIcon={{ icon: Save }}
on:click={() => dispatch('createScriptFromInlineScript')}
iconOnly={false}
>
Save to workspace
</Button>
{/if}
</div>
</div>
<style lang="postcss">
span.green {
@apply text-green-600 animate-[pulse_5s_ease-in-out_infinite];
}
</style>