Files
windmill/frontend/src/lib/components/ScriptBuilder.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

1701 lines
54 KiB
Svelte

<script lang="ts">
import {
DraftService,
type NewScript,
ScriptService,
type NewScriptWithDraft,
type Script,
type TriggersCount,
PostgresTriggerService,
CaptureService
} from '$lib/gen'
import { inferArgs } from '$lib/infer'
import { initialCode } from '$lib/script_helpers'
import {
defaultScripts,
enterpriseLicense,
usedTriggerKinds,
userStore,
workspaceStore
} from '$lib/stores'
import {
cleanValueProperties,
emptySchema,
emptyString,
encodeState,
generateRandomString,
orderedJsonStringify,
replaceFalseWithUndefined,
type Value
} from '$lib/utils'
import Path from './Path.svelte'
import ScriptEditor from './ScriptEditor.svelte'
import { Alert, Badge, Button, Drawer, SecondsInput, Tab, TabContent, Tabs } from './common'
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
import type { SupportedLanguage } from '$lib/common'
import Tooltip from './Tooltip.svelte'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import ErrorHandlerToggleButton from '$lib/components/details/ErrorHandlerToggleButton.svelte'
import {
Bug,
Calendar,
CheckCircle,
Code,
Pen,
Plus,
Rocket,
Save,
Settings,
Shuffle,
X
} from 'lucide-svelte'
import { sendUserToast } from '$lib/toast'
import { isCloudHosted } from '$lib/cloud'
import Awareness from './Awareness.svelte'
import { fade } from 'svelte/transition'
import Popover from './Popover.svelte'
import Toggle from './Toggle.svelte'
import ScriptSchema from './ScriptSchema.svelte'
import Section from './Section.svelte'
import Label from './Label.svelte'
import type DiffDrawer from './DiffDrawer.svelte'
import type Editor from './Editor.svelte'
import WorkerTagPicker from './WorkerTagPicker.svelte'
import MetadataGen from './copilot/MetadataGen.svelte'
import { writable } from 'svelte/store'
import { defaultScriptLanguages, processLangs } from '$lib/scripts'
import DefaultScripts from './DefaultScripts.svelte'
import { createEventDispatcher, onMount, setContext } from 'svelte'
import Summary from './Summary.svelte'
import type { ScriptBuilderWhitelabelCustomUi } from './custom_ui'
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
import TriggersEditor from './triggers/TriggersEditor.svelte'
import type { ScheduleTrigger, TriggerContext } from './triggers'
import {
TS_PREPROCESSOR_MODULE_CODE,
TS_PREPROCESSOR_SCRIPT_INTRO,
PYTHON_PREPROCESSOR_MODULE_CODE,
PYTHON_PREPROCESSOR_SCRIPT_INTRO
} from '$lib/script_helpers'
import CaptureTable from './triggers/CaptureTable.svelte'
import type { SavedAndModifiedValue } from './common/confirmationModal/unsavedTypes'
import type { ScriptBuilderFunctionExports } from './scriptBuilder'
import DeployButton from './DeployButton.svelte'
import {
type NewScriptWithDraftAndDraftTriggers,
type Trigger,
deployTriggers,
filterDraftTriggers,
handleSelectTriggerFromKind
} from './triggers/utils'
import DraftTriggersConfirmationModal from './common/confirmationModal/DraftTriggersConfirmationModal.svelte'
import { Triggers } from './triggers/triggers.svelte'
export let script: NewScript & { draft_triggers?: Trigger[] }
export let fullyLoaded: boolean = true
export let initialPath: string = ''
export let template: 'docker' | 'bunnative' | 'script' = 'script'
export let initialArgs: Record<string, any> = {}
export let lockedLanguage = false
export let showMeta: boolean = false
export let neverShowMeta: boolean = false
export let diffDrawer: DiffDrawer | undefined = undefined
export let savedScript: NewScriptWithDraftAndDraftTriggers | undefined = undefined
export let searchParams: URLSearchParams = new URLSearchParams()
export let disableHistoryChange = false
export let replaceStateFn: (url: string) => void = (url) =>
window.history.replaceState(null, '', url)
export let customUi: ScriptBuilderWhitelabelCustomUi = {}
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
export let functionExports: ((exports: ScriptBuilderFunctionExports) => void) | undefined =
undefined
export function getInitialAndModifiedValues(): SavedAndModifiedValue {
return {
savedValue: savedScript,
modifiedValue: {
...script,
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
}
}
}
// used for new scripts for captures
const fakeInitialPath =
'u/' +
($userStore!.username?.includes('@')
? $userStore!.username.split('@')[0].replace(/[^a-zA-Z0-9_]/g, '')
: $userStore!.username!) +
'/' +
generateRandomString(12)
let deployedValue: Value | undefined = undefined // Value to diff against
let deployedBy: string | undefined = undefined // Author
let confirmCallback: () => void = () => {} // What happens when user clicks `override` in warning
let open: boolean = false // Is confirmation modal open
let args: Record<string, any> = initialArgs // Test args input
let selectedInputTab: 'main' | 'preprocessor' = 'main'
let hasPreprocessor = false
let metadataOpen =
!neverShowMeta &&
(showMeta ||
(initialPath == '' &&
searchParams.get('state') == undefined &&
searchParams.get('collab') == undefined))
let editor: Editor | undefined = undefined
let scriptEditor: ScriptEditor | undefined = undefined
let captureTable: CaptureTable | undefined = undefined
// Draft triggers confirmation modal
let draftTriggersModalOpen = false
let confirmDeploymentCallback: (triggersToDeploy: Trigger[]) => void = () => {}
async function handleDraftTriggersConfirmed(event: CustomEvent<{ selectedTriggers: Trigger[] }>) {
const { selectedTriggers } = event.detail
// Continue with saving the flow
draftTriggersModalOpen = false
confirmDeploymentCallback(selectedTriggers)
}
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule) // keep for legacy
const triggersCount = writable<TriggersCount | undefined>(
savedPrimarySchedule
? { schedule_count: 1, primary_schedule: { schedule: savedPrimarySchedule.cron } }
: undefined
)
const simplifiedPoll = writable(false)
export function setPrimarySchedule(schedule: ScheduleTrigger | undefined | false) {
primaryScheduleStore.set(schedule)
loadTriggers()
}
export function setDraftTriggers(triggers: Trigger[] | undefined) {
triggersState.setTriggers([
...(triggers ?? []),
...triggersState.triggers.filter((t) => !t.draftConfig)
])
loadTriggers()
}
const dispatch = createEventDispatcher()
$: initialPath != '' && loadTriggers()
onMount(() => {
if (functionExports) {
console.log('functionExports set')
functionExports({
setPreviewArgs: (args: Record<string, any>) => {
scriptEditor?.setArgs(args)
},
runPreview: async () => await scriptEditor?.runTest(),
setCode: (code: string, language?: Script['language']) => {
if (language) {
script.language = language
}
editor?.setCode(code)
},
getCode: () => editor?.getCode() ?? ''
})
}
})
async function loadTriggers() {
$triggersCount = await ScriptService.getTriggersCountOfScript({
workspace: $workspaceStore!,
path: initialPath
})
await triggersState.fetchTriggers(
triggersCount,
$workspaceStore,
initialPath,
false,
$primaryScheduleStore,
$userStore
)
if (savedScript && savedScript.draft && savedScript.draft.draft_triggers) {
savedScript = filterDraftTriggers(
savedScript,
triggersState
) as NewScriptWithDraftAndDraftTriggers
}
}
// Add triggers context store
const triggersState = new Triggers(
[
{ type: 'webhook', path: '', isDraft: false },
{ type: 'email', path: '', isDraft: false },
...(script.draft_triggers ?? [])
],
undefined,
saveSessionDraft
)
const captureOn = writable<boolean | undefined>(undefined)
const showCaptureHint = writable<boolean | undefined>(undefined)
setContext<TriggerContext>('TriggerContext', {
triggersCount,
simplifiedPoll,
showCaptureHint: showCaptureHint,
triggersState
})
const enterpriseLangs = ['bigquery', 'snowflake', 'mssql', 'oracledb']
export function setCode(code: string): void {
editor?.setCode(code)
}
$: langs = processLangs(
script.language,
$defaultScripts?.order ?? Object.keys(defaultScriptLanguages)
)
.map((l) => [defaultScriptLanguages[l], l])
.filter((x) => $defaultScripts?.hidden == undefined || !$defaultScripts.hidden.includes(x[1]))
.filter((x) => {
if (customUi?.settingsPanel?.metadata?.languages === undefined) {
return true
}
return customUi.settingsPanel.metadata.languages.includes(x[1] as SupportedLanguage)
}) as [string, SupportedLanguage | 'docker' | 'bunnative'][]
const scriptKindOptions: {
value: Script['kind']
title: string
Icon: any
desc?: string
documentationLink?: string
}[] = [
{
value: 'script',
title: 'Action',
Icon: Code
},
{
value: 'trigger',
title: 'Trigger',
desc: 'First module of flows to trigger them based on external changes. These kind of scripts are usually running on a schedule to periodically look for changes.',
documentationLink: 'https://www.windmill.dev/docs/flows/flow_trigger',
Icon: Rocket
},
{
value: 'approval',
title: 'Approval',
desc: 'Send notifications externally to ask for approval to continue a flow.',
documentationLink: 'https://www.windmill.dev/docs/flows/flow_approval',
Icon: CheckCircle
},
{
value: 'failure',
title: 'Error Handler',
desc: 'Handle errors in flows after all retry attempts have been exhausted.',
documentationLink: 'https://www.windmill.dev/docs/flows/flow_error_handler',
Icon: Bug
},
{
value: 'preprocessor',
title: 'Preprocessor',
desc: 'Transform incoming requests before they are passed to the flow.',
documentationLink: 'https://www.windmill.dev/docs/core_concepts/preprocessors',
Icon: Shuffle
}
]
let pathError = ''
let loadingSave = false
let loadingDraft = false
$: {
;['collab', 'path'].forEach((x) => {
if (searchParams.get(x)) {
searchParams.delete(x)
}
})
}
$: !disableHistoryChange && encodeScriptState(script)
function encodeScriptState(script: NewScript) {
replaceStateFn(
'#' +
encodeState({
...script,
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
})
)
}
let timeout: NodeJS.Timeout | undefined = undefined
function saveSessionDraft() {
timeout && clearTimeout(timeout)
timeout = setTimeout(() => {
encodeScriptState(script)
}, 500)
}
if (script.content == '') {
initContent(script.language, script.kind, template)
}
async function isTemplateScript() {
const params = new URLSearchParams(window.location.search)
const templateId = params.get('id')
if (templateId === null) {
return undefined
}
try {
const templateScript = await PostgresTriggerService.getTemplateScript({
workspace: $workspaceStore!,
id: templateId
})
return templateScript
} catch (error) {
sendUserToast(
'An error occured when trying to load your template script, please try again later',
true
)
}
}
async function initContent(
language: SupportedLanguage,
kind: Script['kind'] | undefined,
template: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' | 'bunnative'
) {
scriptEditor?.disableCollaboration()
const templateScript = await isTemplateScript()
script.content = initialCode(language, kind, template, templateScript != undefined)
if (templateScript) {
script.content += '\r\n' + templateScript
}
scriptEditor?.inferSchema(script.content, language, true)
if (script.content != editor?.getCode()) {
setCode(script.content)
}
}
async function handleEditScript(stay: boolean, deployMsg?: string): Promise<void> {
// Fetch latest version and fetch entire script after if needed
let actual_parent_hash: string | undefined = undefined
try {
if (initialPath && initialPath != '') {
actual_parent_hash = (
await ScriptService.getScriptLatestVersion({
workspace: $workspaceStore!,
path: initialPath
})
)?.script_hash
}
} catch (error) {
//
}
// Usually when we create new script, we put current hash as a parent_hash
// But if we specify parent_hash that is already used, than we get error
// In order to fix it we make sure that client's understanding of parent_hash
// is aligns with understanding of backend.
if (actual_parent_hash == undefined || script.parent_hash == actual_parent_hash) {
// Handle directly
await editScript(stay, script.parent_hash!, deployMsg)
} else {
// Fetch entire script, since we need it to show Diff
await syncWithDeployed()
if (
deployedValue &&
script &&
orderedJsonStringify({ ...deployedValue, hash: undefined }) ===
orderedJsonStringify(
replaceFalseWithUndefined({ ...script, hash: undefined, parent_hash: undefined })
)
) {
await editScript(stay, actual_parent_hash, deployMsg)
} else {
// Handle through confirmation modal
confirmCallback = async () => {
open = false
if (actual_parent_hash) {
await editScript(stay, actual_parent_hash, deployMsg)
} else {
sendUserToast('Could not fetch latest version of the script', true)
}
}
// Open confirmation modal
open = true
}
}
}
async function syncWithDeployed() {
const latestScript = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path: initialPath,
withStarredInfo: true
})
deployedValue = replaceFalseWithUndefined({
...latestScript,
workspace_id: undefined,
created_at: undefined,
created_by: undefined,
extra_perms: undefined,
lock: undefined,
lock_error_logs: undefined,
parent_hashes: undefined
})
deployedBy = latestScript.created_by
}
async function editScript(
stay: boolean,
parentHash: string,
deploymentMsg?: string,
triggersToDeploy?: Trigger[]
): Promise<void> {
if (!triggersToDeploy) {
// Check if there are draft triggers that need confirmation
const draftTriggers = triggersState.triggers.filter((trigger) => trigger.draftConfig)
if (draftTriggers.length > 0) {
draftTriggersModalOpen = true
confirmDeploymentCallback = async (triggersToDeploy: Trigger[]) => {
await editScript(stay, parentHash, deploymentMsg, triggersToDeploy)
}
return
}
}
loadingSave = true
try {
try {
localStorage.removeItem(script.path)
} catch (e) {
console.error('error interacting with local storage', e)
}
script.schema = script.schema ?? emptySchema()
try {
const result = await inferArgs(
script.language,
script.content,
script.schema as any,
script.kind === 'preprocessor' ? 'preprocessor' : undefined
)
if (script.kind === 'preprocessor') {
script.no_main_func = undefined
script.has_preprocessor = undefined
} else {
script.no_main_func = result?.no_main_func || undefined
script.has_preprocessor = result?.has_preprocessor || undefined
}
} catch (error) {
sendUserToast(`Could not parse code, are you sure it is valid?`, true)
}
const newHash = await ScriptService.createScript({
workspace: $workspaceStore!,
requestBody: {
path: script.path,
summary: script.summary,
description: script.description ?? '',
content: script.content,
parent_hash: parentHash,
schema: script.schema,
is_template: script.is_template,
language: script.language,
kind: script.kind,
tag: script.tag,
envs: script.envs,
dedicated_worker: script.dedicated_worker,
concurrent_limit: script.concurrent_limit,
concurrency_time_window_s: script.concurrency_time_window_s,
cache_ttl: script.cache_ttl,
ws_error_handler_muted: script.ws_error_handler_muted,
priority: script.priority,
restart_unless_cancelled: script.restart_unless_cancelled,
delete_after_use: script.delete_after_use,
timeout: script.timeout,
concurrency_key: emptyString(script.concurrency_key) ? undefined : script.concurrency_key,
visible_to_runner_only: script.visible_to_runner_only,
no_main_func: script.no_main_func,
has_preprocessor: script.has_preprocessor,
deployment_message: deploymentMsg || undefined,
on_behalf_of_email: script.on_behalf_of_email
}
})
if (!initialPath) {
await CaptureService.moveCapturesAndConfigs({
workspace: $workspaceStore!,
path: fakeInitialPath,
requestBody: {
new_path: script.path
},
runnableKind: 'script'
})
}
if (triggersToDeploy) {
await deployTriggers(
triggersToDeploy,
$workspaceStore,
!!$userStore?.is_admin || !!$userStore?.is_super_admin,
usedTriggerKinds,
script.path,
true
)
}
const { draft_triggers: _, ...newScript } = structuredClone(script)
savedScript = structuredClone(newScript) as NewScriptWithDraft
triggersState.setTriggers([])
if (!disableHistoryChange) {
history.replaceState(history.state, '', `/scripts/edit/${script.path}`)
}
if (stay || script.kind !== 'script' || script.no_main_func) {
script.parent_hash = newHash
sendUserToast('Deployed')
} else {
dispatch('deploy', newHash)
}
} catch (error) {
dispatch('deployError', error)
sendUserToast(`Error while saving the script: ${error.body || error.message}`, true)
}
loadingSave = false
}
async function saveDraft(forceSave = false): Promise<void> {
if (initialPath != '' && !savedScript) {
return
}
if (savedScript) {
const draftOrDeployed = cleanValueProperties(savedScript.draft || savedScript)
const currentTriggers = structuredClone(triggersState.getDraftTriggersSnapshot())
const current = cleanValueProperties({ ...script, draft_triggers: currentTriggers })
if (!forceSave && orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(current)) {
sendUserToast('No changes detected, ignoring', false, [
{
label: 'Save anyway',
callback: () => {
saveDraft(true)
}
}
])
return
}
}
loadingDraft = true
try {
try {
localStorage.removeItem(script.path)
} catch (e) {
console.error('error interacting with local storage', e)
}
script.schema = script.schema ?? emptySchema()
try {
const result = await inferArgs(
script.language,
script.content,
script.schema as any,
script.kind === 'preprocessor' ? 'preprocessor' : undefined
)
if (script.kind === 'preprocessor') {
script.no_main_func = undefined
script.has_preprocessor = undefined
} else {
script.no_main_func = result?.no_main_func || undefined
script.has_preprocessor = result?.has_preprocessor || undefined
}
} catch (error) {
sendUserToast(`Could not parse code, are you sure it is valid?`, true)
}
if (initialPath == '' || savedScript?.draft_only) {
if (savedScript?.draft_only) {
await ScriptService.deleteScriptByPath({
workspace: $workspaceStore!,
path: initialPath,
keepCaptures: true
})
script.parent_hash = undefined
}
if (!initialPath || script.path != initialPath) {
await CaptureService.moveCapturesAndConfigs({
workspace: $workspaceStore!,
path: initialPath || fakeInitialPath,
requestBody: {
new_path: script.path
},
runnableKind: 'script'
})
}
await ScriptService.createScript({
workspace: $workspaceStore!,
requestBody: {
path: script.path,
summary: script.summary,
description: script.description ?? '',
content: script.content,
schema: script.schema,
is_template: script.is_template,
language: script.language,
kind: script.kind,
tag: script.tag,
draft_only: true,
envs: script.envs,
concurrent_limit: script.concurrent_limit,
concurrency_time_window_s: script.concurrency_time_window_s,
cache_ttl: script.cache_ttl,
ws_error_handler_muted: script.ws_error_handler_muted,
priority: script.priority,
restart_unless_cancelled: script.restart_unless_cancelled,
delete_after_use: script.delete_after_use,
timeout: script.timeout,
concurrency_key: emptyString(script.concurrency_key)
? undefined
: script.concurrency_key,
visible_to_runner_only: script.visible_to_runner_only,
no_main_func: script.no_main_func,
has_preprocessor: script.has_preprocessor,
on_behalf_of_email: script.on_behalf_of_email
}
})
}
const draftTriggers = triggersState.getDraftTriggersSnapshot()
await DraftService.createDraft({
workspace: $workspaceStore!,
requestBody: {
path: initialPath == '' || savedScript?.draft_only ? script.path : initialPath,
typ: 'script',
value: {
...script,
draft_triggers: draftTriggers
}
}
})
savedScript = {
...(initialPath == '' || savedScript?.draft_only
? { ...structuredClone(script), draft_only: true }
: savedScript),
draft: {
...structuredClone(script),
draft_triggers: draftTriggers
}
} as NewScriptWithDraftAndDraftTriggers
let savedAtNewPath = false
if (initialPath == '' || (savedScript?.draft_only && script.path !== initialPath)) {
savedAtNewPath = true
initialPath = script.path
dispatch('saveInitial', script.path)
}
dispatch('saveDraft', { path: script.path, savedAtNewPath, script })
sendUserToast('Saved as draft')
} catch (error) {
sendUserToast(
`Error while saving the script as a draft: ${error.body || error.message}`,
true
)
dispatch('saveDraftError', error)
}
loadingDraft = false
}
function computeDropdownItems(
initialPath: string,
savedScript: NewScriptWithDraftAndDraftTriggers | undefined,
diffDrawer: DiffDrawer | undefined
) {
let dropdownItems: { label: string; onClick: () => void }[] =
initialPath != '' && customUi?.topBar?.extraDeployOptions != false
? [
{
label: 'Deploy & Stay here',
onClick: () => {
handleEditScript(true)
}
},
{
label: 'Fork',
onClick: () => {
window.open(`/scripts/add?template=${initialPath}`)
}
},
...(customUi?.topBar?.diff !== false && savedScript && diffDrawer
? [
{
label: 'Show diff',
onClick: async () => {
if (!savedScript) {
return
}
await syncWithDeployed()
const currentDraftTriggers = structuredClone(
triggersState.getDraftTriggersSnapshot()
)
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: deployedValue ?? savedScript,
draft: savedScript['draft'],
current: {
...script,
draft_triggers: currentDraftTriggers
}
})
}
}
]
: []),
...(!script.draft_only && script.kind === 'script' && !script.no_main_func
? [
{
label: 'Exit & See details',
onClick: () => {
dispatch('seeDetails', initialPath)
}
}
]
: [])
]
: []
return dropdownItems.length > 0 ? dropdownItems : undefined
}
function onKeyDown(event: KeyboardEvent) {
switch (event.key) {
case 's':
if (event.ctrlKey || event.metaKey) {
saveDraft()
event.preventDefault()
}
break
}
}
let path: Path | undefined = undefined
let dirtyPath = false
let selectedTab: 'metadata' | 'runtime' | 'ui' | 'triggers' = (() => {
if (customUi?.settingsPanel?.disableMetadata !== true) {
// first option: either no custom UI or metadata is enabled
return 'metadata'
}
if (customUi?.settingsPanel?.disableRuntime !== true) {
return 'runtime'
}
if (customUi?.settingsPanel?.disableGeneratedUi !== true) {
return 'ui'
}
if (customUi?.settingsPanel?.disableTriggers !== true) {
return 'triggers'
}
return 'metadata'
})()
setContext('disableTooltips', customUi?.disableTooltips === true)
function langToLanguage(lang: SupportedLanguage | 'docker' | 'bunnative'): SupportedLanguage {
if (lang == 'docker') {
return 'bash'
}
if (lang == 'bunnative') {
return 'bun'
}
return lang
}
async function applyArgs(e) {
selectedInputTab = e.detail.kind
metadataOpen = false
if (scriptEditor) {
scriptEditor.updateArgs(e.detail.args ?? {})
}
}
function openTriggers(ev) {
metadataOpen = true
selectedTab = 'triggers'
handleSelectTriggerFromKind(triggersState, triggersCount, initialPath, ev.detail.kind)
captureOn.set(true)
}
function addPreprocessor() {
const code = editor?.getCode()
if (code) {
const preprocessorCode =
script.language === 'python3'
? PYTHON_PREPROCESSOR_SCRIPT_INTRO + PYTHON_PREPROCESSOR_MODULE_CODE
: TS_PREPROCESSOR_SCRIPT_INTRO + TS_PREPROCESSOR_MODULE_CODE
const mainIndex = code.indexOf(
script.language === 'python3' ? 'def main' : 'export async function main'
)
if (mainIndex === -1) {
editor?.setCode(code + preprocessorCode)
} else {
editor?.setCode(
code.slice(0, mainIndex) + preprocessorCode + '\n' + code.slice(mainIndex) + '\n\n'
)
}
}
selectedInputTab = 'preprocessor'
}
function handleDeployTrigger(trigger: Trigger) {
const { id, path, type } = trigger
//Update the saved script to remove the draft trigger that is deployed
if (savedScript && savedScript.draft && savedScript.draft.draft_triggers) {
const newSavedDraftTrigers = savedScript.draft.draft_triggers.filter(
(t) => t.id !== id || t.path !== path || t.type !== type
)
savedScript.draft.draft_triggers =
newSavedDraftTrigers.length > 0 ? newSavedDraftTrigers : undefined
}
}
</script>
<svelte:window on:keydown={onKeyDown} />
<slot />
<DeployOverrideConfirmationModal
bind:deployedBy
bind:confirmCallback
bind:open
{diffDrawer}
bind:deployedValue
currentValue={script}
/>
<DraftTriggersConfirmationModal
bind:open={draftTriggersModalOpen}
draftTriggers={triggersState.triggers.filter((t) => t.draftConfig)}
on:canceled={() => {
draftTriggersModalOpen = false
}}
on:confirmed={handleDraftTriggersConfirmed}
/>
{#if !$userStore?.operator}
<Drawer
placement="right"
bind:open={metadataOpen}
size={selectedTab === 'ui' || selectedTab === 'triggers' ? '1200px' : '800px'}
>
<DrawerContent noPadding title="Settings" on:close={() => (metadataOpen = false)}>
<!-- svelte-ignore a11y-autofocus -->
<div class="flex flex-col h-full">
<Tabs bind:selected={selectedTab} wrapperClass="flex-none w-full">
{#if customUi?.settingsPanel?.disableMetadata !== true}
<Tab value="metadata">Metadata</Tab>
{/if}
{#if customUi?.settingsPanel?.disableRuntime !== true}
<Tab value="runtime">Runtime</Tab>
{/if}
{#if customUi?.settingsPanel?.disableGeneratedUi !== true}
<Tab value="ui">
Generated UI
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/json_schema_and_parsing"
>
The arguments are synced with the main signature but you may refine the parts that
cannot be inferred from the type directly.
</Tooltip>
</Tab>
{/if}
{#if customUi?.settingsPanel?.disableTriggers !== true}
<Tab value="triggers">
Triggers
<Tooltip documentationLink="https://www.windmill.dev/docs/getting_started/triggers">
Configure how this script will be triggered.
</Tooltip>
</Tab>
{/if}
<svelte:fragment slot="content">
<div class="min-h-0 grow overflow-y-auto">
<TabContent value="metadata">
<div class="flex flex-col gap-8 px-4 py-2">
<Section label="Metadata">
<svelte:fragment slot="action">
{#if customUi?.settingsPanel?.metadata?.disableMute !== true}
<div class="flex flex-row items-center gap-2">
<ErrorHandlerToggleButton
kind="script"
scriptOrFlowPath={script.path}
bind:errorHandlerMuted={script.ws_error_handler_muted}
iconOnly={false}
/>
</div>
{/if}
</svelte:fragment>
<div class="flex flex-col gap-4">
<Label label="Summary">
<MetadataGen
label="Summary"
bind:content={script.summary}
lang={script.language}
code={script.content}
promptConfigName="summary"
generateOnAppear
on:change={() => {
if (initialPath == '' && script.summary?.length > 0 && !dirtyPath) {
path?.setName(
script.summary
.toLowerCase()
.replace(/[^a-z0-9_]/g, '_')
.replace(/-+/g, '_')
.replace(/^-|-$/g, '')
)
}
}}
elementProps={{
type: 'text',
placeholder: 'Short summary to be displayed when listed'
}}
/>
</Label>
<Label label="Path">
<svelte:fragment slot="header">
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/roles_and_permissions#path"
>
The unique identifier of the script in the workspace that defines
permissions
</Tooltip>
</svelte:fragment>
<Path
bind:this={path}
bind:error={pathError}
bind:path={script.path}
bind:dirty={dirtyPath}
{initialPath}
autofocus={false}
namePlaceholder="script"
kind="script"
/>
</Label>
<Label label="Description">
<MetadataGen
bind:content={script.description}
lang={script.language}
code={script.content}
promptConfigName="description"
elementType="textarea"
elementProps={{
placeholder: 'What this script does and how to use it'
}}
/>
</Label>
</div>
</Section>
<Section label="Language">
<svelte:fragment slot="action"><DefaultScripts /></svelte:fragment>
{#if lockedLanguage}
<div class="text-sm text-tertiary italic mb-2">
As a forked script, the language '{script.language}' cannot be modified.
</div>
{/if}
<div class=" grid grid-cols-3 gap-2">
{#each langs as [label, lang] (lang)}
{@const isPicked =
(lang == script.language && template == 'script') ||
(template == 'bunnative' && lang == 'bunnative') ||
(template == 'docker' && lang == 'docker')}
<Popover
disablePopup={!enterpriseLangs.includes(lang) || !!$enterpriseLicense}
>
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'light'}
btnClasses={isPicked
? '!border-2 !bg-blue-50/75 dark:!bg-frost-900/75'
: 'm-[1px]'}
on:click={() => {
if (lang == 'docker') {
if (isCloudHosted()) {
sendUserToast(
'You cannot use Docker scripts on the multi-tenant platform. Use a dedicated instance or self-host windmill instead.',
true,
[
{
label: 'Learn more',
callback: () => {
window.open(
'https://www.windmill.dev/docs/advanced/docker',
'_blank'
)
}
}
]
)
return
}
template = 'docker'
} else if (lang == 'bunnative') {
template = 'bunnative'
} else {
template = 'script'
}
let language = langToLanguage(lang)
//
initContent(language, script.kind, template)
script.language = language
}}
disabled={lockedLanguage ||
(enterpriseLangs.includes(lang) && !$enterpriseLicense)}
>
<LanguageIcon {lang} />
<span class="ml-2 py-2 truncate">{label}</span>
{#if lang === 'ansible' || lang === 'nu'}
<span class="text-tertiary !text-xs"> BETA </span>
{/if}
</Button>
<svelte:fragment slot="text"
>{label} is only available with an enterprise license</svelte:fragment
>
</Popover>
{/each}
</div>
</Section>
{#if customUi?.settingsPanel?.metadata?.disableScriptKind !== true}
<Section label="Script kind">
<svelte:fragment slot="header">
<Tooltip
documentationLink="https://www.windmill.dev/docs/script_editor/script_kinds"
>
Tag this script's purpose within flows such that it is available as the
corresponding action.
</Tooltip>
</svelte:fragment>
<ToggleButtonGroup
class="h-10"
selected={script.kind}
on:selected={({ detail }) => {
template = 'script'
script.kind = detail
initContent(script.language, detail, template)
}}
let:item
>
{#each scriptKindOptions as { value, title, desc, documentationLink, Icon }}
<ToggleButton
label={title}
{value}
tooltip={desc}
{documentationLink}
icon={Icon}
showTooltipIcon={Boolean(desc)}
{item}
/>
{/each}
</ToggleButtonGroup>
</Section>
{/if}
</div>
</TabContent>
<TabContent value="runtime">
<div class="flex flex-col gap-8 px-4 py-2">
<Section label="Concurrency limits" eeOnly>
<svelte:fragment slot="header">
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/concurrency_limits"
>
Allowed concurrency within a given timeframe
</Tooltip>
</svelte:fragment>
<div class="flex flex-col gap-4">
<Label label="Max number of executions within the time window">
<div class="flex flex-row gap-2 max-w-sm">
<input
disabled={!$enterpriseLicense}
bind:value={script.concurrent_limit}
type="number"
/>
<Button
size="sm"
color="light"
on:click={() => {
script.concurrent_limit = undefined
script.concurrency_time_window_s = undefined
script.concurrency_key = undefined
}}
variant="border">Remove Limits</Button
>
</div>
</Label>
<Label label="Time window in seconds">
<SecondsInput
disabled={!$enterpriseLicense}
bind:seconds={script.concurrency_time_window_s}
/>
</Label>
<Label label="Custom concurrency key (optional)">
<svelte:fragment slot="header">
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/concurrency_limits#custom-concurrency-key"
>
Concurrency keys are global, you can have them be workspace specific
using the variable `$workspace`. You can also use an argument's value
using `$args[name_of_arg]`</Tooltip
>
</svelte:fragment>
<input
disabled={!$enterpriseLicense}
type="text"
autofocus
bind:value={script.concurrency_key}
placeholder={`$workspace/script/${script.path}-$args[foo]`}
/>
</Label>
</div>
</Section>
<Section label="Worker group tag (queue)">
<svelte:fragment slot="header">
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups"
>
The script will be executed on a worker configured to listen to this worker
group tag (queue). For instance, you could setup an "highmem", or "gpu" tag.
</Tooltip>
</svelte:fragment>
<WorkerTagPicker bind:tag={script.tag} />
</Section>
<Section label="Cache">
<svelte:fragment slot="header">
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/caching"
>
Cache the results for each possible inputs
</Tooltip>
</svelte:fragment>
<div class="flex gap-2 shrink flex-col">
<Toggle
size="sm"
checked={Boolean(script.cache_ttl)}
on:change={() => {
if (script.cache_ttl && script.cache_ttl != undefined) {
script.cache_ttl = undefined
} else {
script.cache_ttl = 300
}
}}
options={{
right: 'Cache the results for each possible inputs'
}}
/>
<span class="text-secondary text-sm leading-none">
How long to the keep cache valid
</span>
{#if script.cache_ttl}
<SecondsInput bind:seconds={script.cache_ttl} />
{:else}
<SecondsInput disabled />
{/if}
</div>
</Section>
<Section label="Timeout">
<svelte:fragment slot="header">
<Tooltip
documentationLink="https://www.windmill.dev/docs/script_editor/settings#timeout"
>
Add a custom timeout for this script
</Tooltip>
</svelte:fragment>
<div class="flex gap-2 shrink flex-col">
<Toggle
size="sm"
checked={Boolean(script.timeout)}
on:change={() => {
if (script.timeout && script.timeout != undefined) {
script.timeout = undefined
} else {
script.timeout = 300
}
}}
options={{
right: 'Add a custom timeout for this script'
}}
/>
<span class="text-secondary text-sm leading-none"> Timeout duration </span>
{#if script.timeout}
<SecondsInput bind:seconds={script.timeout} />
{:else}
<SecondsInput disabled />
{/if}
</div>
</Section>
<Section label="Perpetual script">
<svelte:fragment slot="header">
<Tooltip
documentationLink="https://www.windmill.dev/docs/script_editor/perpetual_scripts"
>
Restart the script upon ending unless cancelled
</Tooltip>
</svelte:fragment>
<div class="flex gap-2 shrink flex-col">
<Toggle
size="sm"
checked={Boolean(script.restart_unless_cancelled)}
on:change={() => {
if (script.restart_unless_cancelled) {
script.restart_unless_cancelled = undefined
} else {
script.restart_unless_cancelled = true
}
}}
options={{
right: 'Restart upon ending unless cancelled'
}}
/>
</div>
</Section>
<Section label="Dedicated workers" eeOnly>
<Toggle
disabled={!$enterpriseLicense ||
isCloudHosted() ||
(script.language != 'bun' &&
script.language != 'python3' &&
script.language != 'deno')}
size="sm"
checked={Boolean(script.dedicated_worker)}
on:change={() => {
if (script.dedicated_worker) {
script.dedicated_worker = undefined
} else {
script.dedicated_worker = true
}
}}
options={{
right: 'Script is run on dedicated workers'
}}
/>
{#if script.dedicated_worker}
<div class="py-2">
<Alert type="info" title="Require dedicated workers">
One worker in a worker group needs to be configured with dedicated worker
set to: <pre>{$workspaceStore}:{script.path}</pre>
</Alert>
</div>
{/if}
<svelte:fragment slot="header">
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/dedicated_workers"
>
In this mode, the script is meant to be run on dedicated workers that run
the script at native speed. Can reach >1500rps per dedicated worker. Only
available on enterprise edition and for Python3, Deno and Bun. For other
languages, the efficiency is already on par with deidcated workers since
they do not spawn a full runtime</Tooltip
>
</svelte:fragment>
</Section>
<Section label="Delete after use">
<svelte:fragment slot="header">
<Tooltip
documentationLink="https://www.windmill.dev/docs/script_editor/settings#delete-after-use"
>
WARNING: This settings ONLY applies to synchronous webhooks or when the
script is used within a flow. If used individually, this script must be
triggered using a synchronous endpoint to have the desired effect.
<br />
<br />
The logs, arguments and results of the job will be completely deleted from Windmill
once it is complete and the result has been returned.
<br />
<br />
The deletion is irreversible.
{#if !$enterpriseLicense}
<br />
<br />
This option is only available on Windmill Enterprise Edition.
{/if}
</Tooltip>
</svelte:fragment>
<div class="flex gap-2 shrink flex-col">
<Toggle
disabled={!$enterpriseLicense}
size="sm"
checked={Boolean(script.delete_after_use)}
on:change={() => {
if (script.delete_after_use) {
script.delete_after_use = undefined
} else {
script.delete_after_use = true
}
}}
options={{
right: 'Delete logs, arguments and results after use'
}}
/>
</div>
</Section>
{#if !isCloudHosted()}
<Section label="High priority script" eeOnly>
<Toggle
disabled={!$enterpriseLicense || isCloudHosted()}
size="sm"
checked={script.priority !== undefined && script.priority > 0}
on:change={() => {
if (script.priority) {
script.priority = undefined
} else {
script.priority = 100
}
}}
options={{
right: 'Label as high priority'
}}
>
<svelte:fragment slot="right">
<input
type="number"
class="!w-16 ml-4"
disabled={script.priority === undefined}
bind:value={script.priority}
on:focus
on:change={() => {
if (script.priority && script.priority > 100) {
script.priority = 100
} else if (script.priority && script.priority < 0) {
script.priority = 0
}
}}
/>
</svelte:fragment>
</Toggle>
<svelte:fragment slot="header">
<!-- TODO: Add EE-only badge when we have it -->
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/jobs#high-priority-jobs"
>
Jobs from script labeled as high priority take precedence over the other
jobs when in the jobs queue.
{#if !$enterpriseLicense}This is a feature only available on enterprise
edition.{/if}
</Tooltip>
</svelte:fragment>
</Section>
{/if}
<Section label="Runs visibility">
<svelte:fragment slot="header">
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/monitor_past_and_future_runs#invisible-runs"
>
When this option is enabled, manual executions of this script are invisible
to users other than the user running it, including the owner(s). This
setting can be overridden when this script is run manually from the advanced
menu.
</Tooltip>
</svelte:fragment>
<div class="flex gap-2 shrink flex-col">
<Toggle
size="sm"
checked={Boolean(script.visible_to_runner_only)}
on:change={() => {
if (script.visible_to_runner_only) {
script.visible_to_runner_only = undefined
} else {
script.visible_to_runner_only = true
}
}}
options={{
right: 'Make runs invisible to others'
}}
/>
</div>
</Section>
<Section label="On behalf of last editor">
<svelte:fragment slot="header">
<Tooltip>
When this option is enabled, the script will be run with the permissions of
the last editor.
</Tooltip>
</svelte:fragment>
<div class="flex gap-2 shrink flex-col">
<Toggle
size="sm"
checked={Boolean(script.on_behalf_of_email)}
on:change={() => {
if (script.on_behalf_of_email) {
script.on_behalf_of_email = undefined
} else {
script.on_behalf_of_email = $userStore?.email
}
}}
options={{
right: 'Run on behalf of last editor'
}}
/>
</div>
</Section>
{#if !isCloudHosted()}
<Section label="Custom env variables">
<svelte:fragment slot="header">
<Tooltip
documentationLink="https://www.windmill.dev/docs/script_editor/custom_environment_variables"
>
Additional static custom env variables to pass to the script.
</Tooltip>
</svelte:fragment>
{#if script.envs && script.envs.length > 0}
<Alert type="warning" title="Not passed in previews" size="xs">
Static envs variables are not passed in preview but solely on deployed
scripts.
</Alert>
{/if}
<div class="w-full mt-2">
<span class="text-tertiary text-xs pb-2"
>Format is: `{'<KEY>=<VALUE>'}`</span
>
{#if Array.isArray(script.envs ?? [])}
{#each script.envs ?? [] as v, i}
<div class="flex max-w-md mt-1 w-full items-center relative">
<input type="text" bind:value={v} placeholder="<KEY>=<VALUE>" />
<button
transition:fade|local={{ duration: 50 }}
class="rounded-full p-1 bg-surface/60 duration-200 hover:bg-gray-200 absolute right-2"
aria-label="Clear"
on:click={() => {
script.envs && script.envs.splice(i, 1)
script.envs = script.envs
}}
>
<X size={14} />
</button>
</div>
{/each}
{/if}
</div>
<div class="flex mt-2">
<Button
variant="border"
color="light"
size="xs"
on:click={() => {
if (script.envs == undefined || !Array.isArray(script.envs)) {
script.envs = []
}
script.envs = script.envs.concat('')
}}
>
<div class="flex flex-row gap-1">
<Plus size="16" />
Add item
</div>
</Button>
</div>
</Section>
{/if}
</div>
</TabContent>
<TabContent value="ui" class="h-full p-4">
<ScriptSchema
bind:schema={script.schema}
customUi={customUi?.settingsPanel?.metadata?.editableSchemaForm}
/>
</TabContent>
<TabContent value="triggers" class="h-full">
<TriggersEditor
on:applyArgs={applyArgs}
on:addPreprocessor={addPreprocessor}
on:exitTriggers={() => {
captureTable?.loadCaptures(true)
}}
currentPath={script.path}
{initialPath}
{fakeInitialPath}
noEditor={true}
newItem={initialPath == ''}
isFlow={false}
{hasPreprocessor}
canHavePreprocessor={script.language === 'bun' ||
script.language === 'deno' ||
script.language === 'python3'}
args={hasPreprocessor && selectedInputTab !== 'preprocessor' ? {} : args}
isDeployed={savedScript && !savedScript?.draft_only}
schema={script.schema}
hash={script.parent_hash}
onDeployTrigger={handleDeployTrigger}
/>
<!-- <ScriptSchedules {initialPath} schema={script.schema} schedule={scheduleStore} /> -->
</TabContent>
</div>
</svelte:fragment>
</Tabs>
</div>
</DrawerContent>
</Drawer>
<div class="flex flex-col h-screen">
<div class="flex h-12 items-center px-4">
<div class="justify-between flex gap-2 lg:gap-8 w-full items-center">
<div class="flex flex-row gap-2 grow max-w-md">
<div class="center-center">
<button
disabled={customUi?.topBar?.settings == false}
on:click={async () => {
metadataOpen = true
}}
>
<LanguageIcon lang={script.language} height={20} />
</button>
</div>
<Summary
disabled={customUi?.topBar?.editableSummary == false}
bind:value={script.summary}
/>
</div>
<div class="gap-4 flex">
{#if triggersState.triggers?.some((t) => t.type === 'schedule')}
{@const primarySchedule = triggersState.triggers.findIndex((t) => t.isPrimary)}
{@const schedule = triggersState.triggers.findIndex((t) => t.type === 'schedule')}
<Button
btnClasses="hidden lg:inline-flex"
startIcon={{ icon: Calendar }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
metadataOpen = true
selectedTab = 'triggers'
triggersState.selectedTriggerIndex = primarySchedule ?? schedule
}}
>
{triggersState.triggers[primarySchedule]?.draftConfig?.schedule ??
triggersState.triggers[primarySchedule]?.lightConfig?.schedule ??
''}
</Button>
{/if}
{#if customUi?.topBar?.path != false}
<div class="flex justify-start w-full border rounded-md overflow-hidden">
<div>
{#if customUi?.topBar?.editablePath != false}
<button
on:click={async () => {
metadataOpen = true
}}
>
<Badge
color="gray"
class="center-center !bg-surface-secondary !text-tertiary !h-[28px] !w-[70px] rounded-none hover:!bg-surface-hover transition-all"
>
<Pen size={12} class="mr-2" /> Path
</Badge>
</button>
{/if}
</div>
<input
type="text"
readonly
value={script.path}
size={script.path?.length || 50}
class="font-mono !text-xs !min-w-[96px] !max-w-[300px] !w-full !h-[28px] !my-0 !py-0 !border-l-0 !rounded-l-none !border-0 !shadow-none"
on:focus={({ currentTarget }) => {
currentTarget.select()
}}
/>
</div>
{/if}
</div>
{#if $enterpriseLicense && initialPath != ''}
<Awareness />
{/if}
<div class="flex flex-row gap-x-1 lg:gap-x-2">
{#if customUi?.topBar?.settings != false}
<Button
color="light"
variant="border"
size="xs"
on:click={() => {
metadataOpen = true
}}
startIcon={{ icon: Settings }}
>
<span class="hidden lg:flex"> Settings </span>
</Button>
{/if}
<Button
loading={loadingDraft}
size="xs"
startIcon={{ icon: Save }}
on:click={() => saveDraft()}
disabled={initialPath != '' && !savedScript}
shortCut={{
key: 'S'
}}
>
<span class="hidden lg:flex"> Draft </span>
</Button>
<DeployButton
loading={!fullyLoaded}
{loadingSave}
newFlow={false}
dropdownItems={computeDropdownItems(initialPath, savedScript, diffDrawer)}
on:save={({ detail }) => handleEditScript(false, detail)}
/>
</div>
</div>
</div>
<ScriptEditor
bind:selectedTab={selectedInputTab}
{customUi}
collabMode
edit={initialPath != ''}
on:format={() => {
saveDraft()
}}
on:saveDraft={() => {
saveDraft()
}}
on:openTriggers={openTriggers}
on:applyArgs={applyArgs}
on:addPreprocessor={addPreprocessor}
bind:editor
bind:this={scriptEditor}
bind:schema={script.schema}
path={script.path}
stablePathForCaptures={initialPath || fakeInitialPath}
bind:code={script.content}
lang={script.language}
{initialArgs}
kind={script.kind}
{template}
tag={script.tag}
lastSavedCode={savedScript?.draft?.content}
lastDeployedCode={savedScript?.draft_only ? undefined : savedScript?.content}
bind:args
bind:hasPreprocessor
bind:captureTable
/>
</div>
{:else}
Script Builder not available to operators
{/if}