feat: in-flow editor mvp

This commit is contained in:
Ruben Fiszel
2022-07-05 11:26:06 +02:00
parent 5aa862bf32
commit c2e9ef1ca0
11 changed files with 142 additions and 99 deletions
+8 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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!(
+4 -1
View File
@@ -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" /> -->
+60 -6
View File
@@ -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
}
+45
View File
@@ -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 }
}
`
+2 -1
View File
@@ -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()