mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 08:01:35 +00:00
feat: in-flow editor mvp
This commit is contained in:
@@ -3701,14 +3701,21 @@ components:
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
language:
|
||||
type: string
|
||||
enum:
|
||||
- deno
|
||||
- python3
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- script
|
||||
- flow
|
||||
- rawscript
|
||||
required:
|
||||
- type
|
||||
- path
|
||||
|
||||
FlowPreview:
|
||||
type: object
|
||||
|
||||
+17
-5
@@ -22,6 +22,7 @@ use crate::{
|
||||
audit::{audit_log, ActionKind},
|
||||
db::UserDB,
|
||||
error::{Error, JsonResult, Result},
|
||||
jobs::RawCode,
|
||||
scripts::Schema,
|
||||
users::Authed,
|
||||
utils::{Pagination, StripPath},
|
||||
@@ -90,6 +91,7 @@ pub enum InputTransform {
|
||||
pub enum FlowModuleValue {
|
||||
Script { path: String },
|
||||
Flow { path: String },
|
||||
RawScript(RawCode),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -307,12 +309,22 @@ mod tests {
|
||||
},
|
||||
);
|
||||
let fv = FlowValue {
|
||||
modules: vec![FlowModule {
|
||||
input_transform: hm,
|
||||
value: FlowModuleValue::Script {
|
||||
path: "test".to_string(),
|
||||
modules: vec![
|
||||
FlowModule {
|
||||
input_transform: hm,
|
||||
value: FlowModuleValue::Script {
|
||||
path: "test".to_string(),
|
||||
},
|
||||
},
|
||||
}],
|
||||
FlowModule {
|
||||
input_transform: HashMap::new(),
|
||||
value: FlowModuleValue::RawScript(RawCode {
|
||||
content: "test".to_string(),
|
||||
language: crate::scripts::ScriptLang::Deno,
|
||||
path: None,
|
||||
}),
|
||||
},
|
||||
],
|
||||
failure_module: Some(FlowModule {
|
||||
input_transform: HashMap::new(),
|
||||
value: FlowModuleValue::Flow {
|
||||
|
||||
+5
-3
@@ -950,10 +950,11 @@ struct CancelJob {
|
||||
reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct RawCode {
|
||||
content: String,
|
||||
path: Option<String>,
|
||||
language: ScriptLang,
|
||||
pub content: String,
|
||||
pub path: Option<String>,
|
||||
pub language: ScriptLang,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -1504,6 +1505,7 @@ async fn push_next_flow_job(
|
||||
FlowModuleValue::Script { path: script_path } => {
|
||||
script_path_to_payload(script_path, &mut tx, &job.workspace_id).await?
|
||||
}
|
||||
FlowModuleValue::RawScript(raw_code) => JobPayload::Code(raw_code.clone()),
|
||||
a @ _ => {
|
||||
tracing::info!("Unrecognized module values {:?}", a);
|
||||
Err(Error::BadRequest(format!(
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
let editor: monaco.editor.IStandaloneCodeEditor
|
||||
export let deno = false
|
||||
export let lang = deno ? 'typescript' : 'python'
|
||||
export let code: string
|
||||
export let code: string = ''
|
||||
export let hash: string = (Math.random() + 1).toString(36).substring(2)
|
||||
export let cmdEnterAction: (() => void) | undefined = undefined
|
||||
export let formatAction: (() => void) | undefined = undefined
|
||||
@@ -277,6 +277,9 @@
|
||||
scrollBeyondLastLine: false,
|
||||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
scrollbar: {
|
||||
alwaysConsumeMouseWheel: false
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
import type { Schema } from '$lib/common'
|
||||
import { FlowModuleValue, type Flow } from '$lib/gen'
|
||||
import { loadSchema } from '$lib/scripts'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { emptySchema } from '$lib/utils'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
import FlowPreview from './FlowPreview.svelte'
|
||||
import { loadFlowSchemas } from './flows/loadFlowSchemas'
|
||||
import ModuleStep from './ModuleStep.svelte'
|
||||
import SchemaEditor from './SchemaEditor.svelte'
|
||||
import type SchemaForm from './SchemaForm.svelte'
|
||||
@@ -28,12 +26,6 @@
|
||||
flow.value.modules = flow.value.modules.concat(newModule)
|
||||
schemas.push(emptySchema())
|
||||
}
|
||||
|
||||
async function loadSchemas() {
|
||||
schemas = await loadFlowSchemas(flow, $workspaceStore!)
|
||||
}
|
||||
|
||||
$: $workspaceStore && loadSchemas()
|
||||
</script>
|
||||
|
||||
<!-- <PageHeader title="Flow" /> -->
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import type { Flow, FlowModule } from '$lib/gen'
|
||||
import { FlowModuleValue, type Flow, type FlowModule } from '$lib/gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { loadSchema as UloadSchema } from '$lib/scripts'
|
||||
import { addPreviewResult, previewResults } from '$lib/stores'
|
||||
import { DENO_INIT_CODE, PYTHON_INIT_CODE } from '$lib/script_helpers'
|
||||
import { addPreviewResult, previewResults, workspaceStore } from '$lib/stores'
|
||||
import {
|
||||
buildExtraLib,
|
||||
emptySchema,
|
||||
@@ -10,7 +12,9 @@
|
||||
schemaToObject,
|
||||
schemaToTsType
|
||||
} from '$lib/utils'
|
||||
import Editor from './Editor.svelte'
|
||||
import FlowPreview from './FlowPreview.svelte'
|
||||
import RadioButton from './RadioButton.svelte'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import ScriptPicker from './ScriptPicker.svelte'
|
||||
|
||||
@@ -22,15 +26,39 @@
|
||||
export let schemas: Schema[] = []
|
||||
export let schemaForms: (SchemaForm | undefined)[] = []
|
||||
|
||||
let editor: Editor
|
||||
|
||||
$: previousSchema = i === 0 ? schemaToObject(flow.schema) : $previewResults[i]
|
||||
|
||||
$: extraLib = buildExtraLib(
|
||||
i == 0 ? schemaToTsType(flow.schema) : objectToTsType($previewResults[i])
|
||||
)
|
||||
|
||||
function initContent(lang: string) {
|
||||
const newStart = lang == 'deno' ? DENO_INIT_CODE : PYTHON_INIT_CODE
|
||||
if (editor) {
|
||||
editor.setCode(newStart)
|
||||
} else {
|
||||
mod.value.content = newStart
|
||||
}
|
||||
}
|
||||
|
||||
$: mod.value.type == 'rawscript' &&
|
||||
mod.value.language == undefined &&
|
||||
(mod.value.language = FlowModuleValue.language.DENO)
|
||||
|
||||
$: mod.value.type == 'rawscript' && mod.value.language && initContent(mod.value.language)
|
||||
|
||||
export async function loadSchema() {
|
||||
if (mod.value.path) {
|
||||
let schema = await UloadSchema(mod.value.path)
|
||||
let isRaw = mod.value.type == 'rawscript'
|
||||
if ((!isRaw && mod.value.path) || isRaw) {
|
||||
let schema: Schema
|
||||
if (isRaw) {
|
||||
schema = emptySchema()
|
||||
await inferArgs(mod.value.language!, mod.value.content!, schema)
|
||||
} else {
|
||||
schema = await UloadSchema(mod.value.path!)
|
||||
}
|
||||
if (
|
||||
JSON.stringify(Object.keys(schema?.properties ?? {}).sort()) !=
|
||||
JSON.stringify(Object.keys(mod.input_transform).sort())
|
||||
@@ -50,9 +78,10 @@
|
||||
schemaForms[i]?.setArgs({})
|
||||
schemas[i] = emptySchema()
|
||||
}
|
||||
|
||||
schemas = schemas
|
||||
}
|
||||
|
||||
$: $workspaceStore && loadSchema()
|
||||
</script>
|
||||
|
||||
<li class="flex flex-row flex-shrink max-w-full mx-auto mt-20">
|
||||
@@ -74,7 +103,32 @@
|
||||
</div>
|
||||
<div class="p-10">
|
||||
<h2 class="mb-4">Step script</h2>
|
||||
<ScriptPicker allowHub={true} bind:scriptPath={mod.value.path} on:select={loadSchema} />
|
||||
<RadioButton
|
||||
small={true}
|
||||
options={[
|
||||
['Pick from an existing script', 'script'],
|
||||
['Edit in-place', 'rawscript']
|
||||
]}
|
||||
bind:value={mod.value.type}
|
||||
/>
|
||||
{#if mod.value.type == 'script'}
|
||||
<ScriptPicker allowHub={true} bind:scriptPath={mod.value.path} on:select={loadSchema} />
|
||||
{:else}
|
||||
<div class="mt-2" />
|
||||
<RadioButton
|
||||
label="Language"
|
||||
small={true}
|
||||
options={[
|
||||
['Python 3.10', 'python3'],
|
||||
['Typescript (Deno)', 'deno']
|
||||
]}
|
||||
bind:value={mod.value.language}
|
||||
/>
|
||||
<div class="h-96 mt-4">
|
||||
<Editor bind:this={editor} class="h-full" bind:code={mod.value.content} />
|
||||
</div>
|
||||
<button class="default-button w-full p-1 mt-4" on:click={loadSchema}>Infer schema</button>
|
||||
{/if}
|
||||
<div class="my-4" />
|
||||
<h2 class="mb-4">Step inputs</h2>
|
||||
<SchemaForm
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
let args: Record<string, any> = {}
|
||||
|
||||
if (!isString(value) && value) {
|
||||
console.log(value)
|
||||
args = value
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +1,3 @@
|
||||
<script context="module">
|
||||
const PYTHON_INIT_CODE = `import os
|
||||
import wmill
|
||||
from datetime import datetime
|
||||
# Our webeditor includes a syntax, type checker through a language server running pyright
|
||||
# and the autoformatter Black in our servers. Use Cmd/Ctrl + S to autoformat the code.
|
||||
# Beware that the code is only saved when you click Save and not across reload.
|
||||
# You can however navigate to any steps safely.
|
||||
"""
|
||||
The client is used to interact with windmill itself through its standard API.
|
||||
One can explore the methods available through autocompletion of \`client.XXX\`.
|
||||
Only the most common methods are included for ease of use. Request more as
|
||||
feedback if you feel you are missing important ones.
|
||||
"""
|
||||
def main(name: str = "Nicolas Bourbaki",
|
||||
age: int = 42,
|
||||
obj: dict = {"even": "dicts"},
|
||||
l: list = ["or", "lists!"],
|
||||
file_: bytes = bytes(0),
|
||||
dtime: datetime = datetime.now()):
|
||||
"""A main function is required for the script to be able to accept arguments.
|
||||
Types are recommended."""
|
||||
print(f"Hello World and a warm welcome especially to {name}")
|
||||
print("and its acolytes..", age, obj, l, len(file_), dtime)
|
||||
# retrieve variables, including secrets by querying the windmill platform.
|
||||
# secret fetching is audited by windmill.
|
||||
secret = wmill.get_variable("g/all/pretty_secret")
|
||||
print(f"The env variable at \`g_all/pretty_secret\`: {secret}")
|
||||
# interact with the windmill platform to get the version
|
||||
version = wmill.get_version()
|
||||
# fetch reserved variables as environment variables
|
||||
user = os.environ.get("WM_USERNAME")
|
||||
# the return value is then parsed and can be retrieved by other scripts conveniently
|
||||
return {"version": version, "splitted": name.split(), "user": user}
|
||||
`
|
||||
const DENO_INIT_CODE = `
|
||||
// only do the following import if you require your script to interact with the windmill
|
||||
// for instance to get a variable or resource
|
||||
// import * as wmill from 'https://deno.land/x/windmill@v${__pkg__.version}/mod.ts'
|
||||
|
||||
export async function main(x: string, y: string = 'default arg') {
|
||||
// let x = await wmill.getVariable('u/user/foo');
|
||||
// let y = await wmill.getResource('u/user/foo')
|
||||
return { foo: x }
|
||||
}
|
||||
`
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { ScriptService, type Script } from '$lib/gen'
|
||||
|
||||
@@ -61,6 +13,7 @@ export async function main(x: string, y: string = 'default arg') {
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import Required from './Required.svelte'
|
||||
import RadioButton from './RadioButton.svelte'
|
||||
import { DENO_INIT_CODE, PYTHON_INIT_CODE } from '$lib/script_helpers'
|
||||
|
||||
let editor: ScriptEditor
|
||||
let scriptSchema: ScriptSchema
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { Schema } from '$lib/common'
|
||||
import { ScriptService, type Flow, type FlowModule } from '$lib/gen'
|
||||
import { emptySchema } from '$lib/utils'
|
||||
|
||||
export async function loadFlowSchemas(flow: Flow, workspace: string): Promise<Schema[]> {
|
||||
const schemas = await Promise.all(
|
||||
flow.value.modules.map(async (flowModule: FlowModule) => {
|
||||
if (flowModule.value.path) {
|
||||
const script = await ScriptService.getScriptByPath({
|
||||
workspace: workspace,
|
||||
path: flowModule.value.path
|
||||
})
|
||||
return script.schema ?? emptySchema()
|
||||
} else {
|
||||
return emptySchema()
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
if (schemas.length === 0) {
|
||||
return [emptySchema()]
|
||||
}
|
||||
|
||||
return schemas
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
export const PYTHON_INIT_CODE = `import os
|
||||
import wmill
|
||||
from datetime import datetime
|
||||
# Our webeditor includes a syntax, type checker through a language server running pyright
|
||||
# and the autoformatter Black in our servers. Use Cmd/Ctrl + S to autoformat the code.
|
||||
# Beware that the code is only saved when you click Save and not across reload.
|
||||
# You can however navigate to any steps safely.
|
||||
"""
|
||||
The client is used to interact with windmill itself through its standard API.
|
||||
One can explore the methods available through autocompletion of \`client.XXX\`.
|
||||
Only the most common methods are included for ease of use. Request more as
|
||||
feedback if you feel you are missing important ones.
|
||||
"""
|
||||
def main(name: str = "Nicolas Bourbaki",
|
||||
age: int = 42,
|
||||
obj: dict = {"even": "dicts"},
|
||||
l: list = ["or", "lists!"],
|
||||
file_: bytes = bytes(0),
|
||||
dtime: datetime = datetime.now()):
|
||||
"""A main function is required for the script to be able to accept arguments.
|
||||
Types are recommended."""
|
||||
print(f"Hello World and a warm welcome especially to {name}")
|
||||
print("and its acolytes..", age, obj, l, len(file_), dtime)
|
||||
# retrieve variables, including secrets by querying the windmill platform.
|
||||
# secret fetching is audited by windmill.
|
||||
secret = wmill.get_variable("g/all/pretty_secret")
|
||||
print(f"The env variable at \`g_all/pretty_secret\`: {secret}")
|
||||
# interact with the windmill platform to get the version
|
||||
version = wmill.get_version()
|
||||
# fetch reserved variables as environment variables
|
||||
user = os.environ.get("WM_USERNAME")
|
||||
# the return value is then parsed and can be retrieved by other scripts conveniently
|
||||
return {"version": version, "splitted": name.split(), "user": user}
|
||||
`
|
||||
export const DENO_INIT_CODE = `
|
||||
// only do the following import if you require your script to interact with the windmill
|
||||
// for instance to get a variable or resource
|
||||
// import * as wmill from 'https://deno.land/x/windmill@v${__pkg__.version}/mod.ts'
|
||||
|
||||
export async function main(x: string, y: string = 'default arg') {
|
||||
// let x = await wmill.getVariable('u/user/foo');
|
||||
// let y = await wmill.getResource('u/user/foo')
|
||||
return { foo: x }
|
||||
}
|
||||
`
|
||||
@@ -1,10 +1,11 @@
|
||||
import { get } from 'svelte/store'
|
||||
import type { Schema } from './common'
|
||||
import { ScriptService } from './gen'
|
||||
import { inferArgs } from './infer'
|
||||
import { workspaceStore } from './stores'
|
||||
import { emptySchema } from './utils'
|
||||
|
||||
export async function loadSchema(path: string) {
|
||||
export async function loadSchema(path: string): Promise<Schema> {
|
||||
if (path.startsWith('hub/')) {
|
||||
const code = await ScriptService.getHubScriptContentByPath({ path })
|
||||
const schema = emptySchema()
|
||||
|
||||
Reference in New Issue
Block a user