mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat: support bash as 4th language (#865)
* bash support * bash backend working * frontend part * bash backend working
This commit is contained in:
Generated
+15
@@ -4086,6 +4086,7 @@ dependencies = [
|
||||
"windmill-audit",
|
||||
"windmill-common",
|
||||
"windmill-parser",
|
||||
"windmill-parser-bash",
|
||||
"windmill-parser-go",
|
||||
"windmill-parser-py",
|
||||
"windmill-parser-ts",
|
||||
@@ -4152,6 +4153,19 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.44.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
"phf 0.11.1",
|
||||
"regex",
|
||||
"unicode-general-category",
|
||||
"windmill-common",
|
||||
"windmill-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.44.0"
|
||||
@@ -4240,6 +4254,7 @@ dependencies = [
|
||||
"windmill-audit",
|
||||
"windmill-common",
|
||||
"windmill-parser",
|
||||
"windmill-parser-bash",
|
||||
"windmill-parser-go",
|
||||
"windmill-parser-py",
|
||||
"windmill-parser-ts",
|
||||
|
||||
@@ -59,6 +59,7 @@ windmill-parser = { path = "./parsers/windmill-parser" }
|
||||
windmill-parser-ts = { path = "./parsers/windmill-parser-ts" }
|
||||
windmill-parser-py = { path = "./parsers/windmill-parser-py" }
|
||||
windmill-parser-go = { path = "./parsers/windmill-parser-go" }
|
||||
windmill-parser-bash = { path = "./parsers/windmill-parser-bash" }
|
||||
axum = { version = "^0", features = ["headers"] }
|
||||
headers = "^0"
|
||||
hyper = { version = "^0", features = ["full"] }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TYPE SCRIPT_LANG ADD VALUE 'bash';
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "windmill-parser-bash"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "windmill_parser_bash"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
windmill-parser.workspace = true
|
||||
windmill-common.workspace = true
|
||||
phf.workspace = true
|
||||
unicode-general-category.workspace = true
|
||||
itertools.workspace = true
|
||||
anyhow.workspace = true
|
||||
regex.workspace = true
|
||||
@@ -0,0 +1,84 @@
|
||||
#![allow(non_snake_case)] // TODO: switch to parse_* function naming
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use windmill_parser::{Arg, MainArgSignature, Typ};
|
||||
|
||||
pub fn parse_bash_sig(code: &str) -> windmill_common::error::Result<MainArgSignature> {
|
||||
let parsed = parse_file(&code)?;
|
||||
if let Some(x) = parsed {
|
||||
let args = x;
|
||||
Ok(MainArgSignature { star_args: false, star_kwargs: false, args })
|
||||
} else {
|
||||
Err(windmill_common::error::Error::BadRequest(
|
||||
"Error parsing bash script".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
|
||||
let mut hm = HashMap::new();
|
||||
let re = Regex::new(r#"(?m)^(\w+)="\$(\d+)"$"#).unwrap();
|
||||
for cap in re.captures_iter(code) {
|
||||
hm.insert(cap[2].parse::<i32>()?, cap[1].to_string());
|
||||
}
|
||||
let mut args = vec![];
|
||||
for i in 1..20 {
|
||||
if hm.contains_key(&i) {
|
||||
args.push(Arg {
|
||||
name: hm[&i].clone(),
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
otyp: None,
|
||||
has_default: false,
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(Some(args))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_bash_sig() -> anyhow::Result<()> {
|
||||
let code = r#"
|
||||
token="$1"
|
||||
image="$2"
|
||||
digest="${3:-latest}"
|
||||
foo="$4"
|
||||
|
||||
"#;
|
||||
//println!("{}", serde_json::to_string()?);
|
||||
assert_eq!(
|
||||
parse_bash_sig(code)?,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![
|
||||
Arg {
|
||||
otyp: None,
|
||||
name: "token".to_string(),
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
has_default: false
|
||||
},
|
||||
Arg {
|
||||
otyp: None,
|
||||
name: "image".to_string(),
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
has_default: false
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1515,6 +1515,30 @@ func main(derp string) (string, error) {
|
||||
assert_eq!(result, serde_json::json!("hello world"));
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_bash_job(db: Pool<Postgres>) {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await;
|
||||
let port = server.addr.port();
|
||||
|
||||
let content = r#"
|
||||
msg="$1"
|
||||
echo "hello $msg"
|
||||
"#
|
||||
.to_owned();
|
||||
|
||||
let job = RunJob::from(JobPayload::Code(RawCode {
|
||||
content,
|
||||
path: None,
|
||||
language: ScriptLang::Bash,
|
||||
}))
|
||||
.arg("msg", json!("world"))
|
||||
.run_until_complete(&db, port)
|
||||
.await;
|
||||
|
||||
assert_eq!(job.result, Some(json!("hello world")));
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_python_job(db: Pool<Postgres>) {
|
||||
initialize_tracing().await;
|
||||
|
||||
@@ -28,6 +28,7 @@ windmill-parser.workspace = true
|
||||
windmill-parser-ts.workspace = true
|
||||
windmill-parser-go.workspace = true
|
||||
windmill-parser-py.workspace = true
|
||||
windmill-parser-bash.workspace = true
|
||||
tokio.workspace = true
|
||||
anyhow.workspace = true
|
||||
argon2.workspace = true
|
||||
|
||||
@@ -1671,7 +1671,7 @@ paths:
|
||||
schema: {}
|
||||
language:
|
||||
type: string
|
||||
enum: [deno, python3, go]
|
||||
enum: [deno, python3, go, bash]
|
||||
summary:
|
||||
type: string
|
||||
required:
|
||||
@@ -1794,7 +1794,7 @@ paths:
|
||||
type: string
|
||||
language:
|
||||
type: string
|
||||
enum: [python3, deno, go]
|
||||
enum: [python3, deno, go, bash]
|
||||
kind:
|
||||
type: string
|
||||
enum: [script, failure, trigger, command, approval]
|
||||
@@ -1854,6 +1854,27 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MainArgSignature"
|
||||
|
||||
/scripts/bash/tojsonschema:
|
||||
post:
|
||||
summary: inspect bash code to infer jsonschema of arguments
|
||||
operationId: bashToJsonschema
|
||||
tags:
|
||||
- script
|
||||
requestBody:
|
||||
description: bash code with the main function
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: parsed args
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MainArgSignature"
|
||||
|
||||
/scripts/go/tojsonschema:
|
||||
post:
|
||||
summary: inspect go code to infer jsonschema of arguments
|
||||
@@ -3585,7 +3606,7 @@ components:
|
||||
type: string
|
||||
language:
|
||||
type: string
|
||||
enum: [python3, deno, go]
|
||||
enum: [python3, deno, go, bash]
|
||||
kind:
|
||||
type: string
|
||||
enum: [script, failure, trigger, command, approval]
|
||||
@@ -3677,7 +3698,7 @@ components:
|
||||
type: boolean
|
||||
language:
|
||||
type: string
|
||||
enum: [python3, deno, go]
|
||||
enum: [python3, deno, go, bash]
|
||||
required:
|
||||
- id
|
||||
- running
|
||||
@@ -3755,7 +3776,7 @@ components:
|
||||
type: boolean
|
||||
language:
|
||||
type: string
|
||||
enum: [python3, deno, go]
|
||||
enum: [python3, deno, go, bash]
|
||||
is_skipped:
|
||||
type: boolean
|
||||
required:
|
||||
@@ -4126,7 +4147,7 @@ components:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
language:
|
||||
type: string
|
||||
enum: [python3, deno, go]
|
||||
enum: [python3, deno, go, bash]
|
||||
|
||||
required:
|
||||
- content
|
||||
|
||||
@@ -50,6 +50,7 @@ pub fn global_service() -> Router {
|
||||
)
|
||||
.route("/deno/tojsonschema", post(parse_deno_code_to_jsonschema))
|
||||
.route("/go/tojsonschema", post(parse_go_code_to_jsonschema))
|
||||
.route("/bash/tojsonschema", post(parse_bash_code_to_jsonschema))
|
||||
.route("/hub/list", get(list_hub_scripts))
|
||||
.route("/hub/get/*path", get(get_hub_script_by_path))
|
||||
.route("/hub/get_full/*path", get(get_full_hub_script_by_path))
|
||||
@@ -661,3 +662,9 @@ async fn parse_go_code_to_jsonschema(
|
||||
) -> JsonResult<windmill_parser::MainArgSignature> {
|
||||
windmill_parser_go::parse_go_sig(&code).map(Json)
|
||||
}
|
||||
|
||||
async fn parse_bash_code_to_jsonschema(
|
||||
Json(code): Json<String>,
|
||||
) -> JsonResult<windmill_parser::MainArgSignature> {
|
||||
windmill_parser_bash::parse_bash_sig(&code).map(Json)
|
||||
}
|
||||
|
||||
@@ -562,6 +562,7 @@ async fn tarball_workspace(
|
||||
ScriptLang::Python3 => "py",
|
||||
ScriptLang::Deno => "ts",
|
||||
ScriptLang::Go => "go",
|
||||
ScriptLang::Bash => "sh",
|
||||
};
|
||||
write_to_archive(
|
||||
script.content,
|
||||
|
||||
@@ -28,6 +28,7 @@ pub enum ScriptLang {
|
||||
Deno,
|
||||
Python3,
|
||||
Go,
|
||||
Bash,
|
||||
}
|
||||
|
||||
impl ScriptLang {
|
||||
@@ -36,6 +37,7 @@ impl ScriptLang {
|
||||
ScriptLang::Deno => "deno",
|
||||
ScriptLang::Python3 => "python3",
|
||||
ScriptLang::Go => "go",
|
||||
ScriptLang::Bash => "bash",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ windmill-parser.workspace = true
|
||||
windmill-parser-ts.workspace = true
|
||||
windmill-parser-go.workspace = true
|
||||
windmill-parser-py.workspace = true
|
||||
windmill-parser-bash.workspace = true
|
||||
sqlx.workspace = true
|
||||
uuid.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
name: "bash run script"
|
||||
|
||||
mode: ONCE
|
||||
hostname: "bash"
|
||||
log_level: ERROR
|
||||
time_limit: 300
|
||||
|
||||
rlimit_as: 2048
|
||||
rlimit_cpu: 1000
|
||||
rlimit_fsize: 1024
|
||||
rlimit_nofile: 64
|
||||
|
||||
cwd: "/tmp"
|
||||
|
||||
clone_newnet: false
|
||||
clone_newuser: {CLONE_NEWUSER}
|
||||
|
||||
keep_caps: false
|
||||
keep_env: true
|
||||
|
||||
mount {
|
||||
src: "/bin"
|
||||
dst: "/bin"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/lib"
|
||||
dst: "/lib"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
|
||||
mount {
|
||||
src: "/lib64"
|
||||
dst: "/lib64"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
|
||||
mount {
|
||||
src: "/usr"
|
||||
dst: "/usr"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/dev/null"
|
||||
dst: "/dev/null"
|
||||
is_bind: true
|
||||
rw: true
|
||||
}
|
||||
|
||||
mount {
|
||||
dst: "/tmp"
|
||||
fstype: "tmpfs"
|
||||
rw: true
|
||||
options: "size=500000000"
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "{JOB_DIR}/main.sh"
|
||||
dst: "/tmp/main.sh"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/etc/ssl"
|
||||
dst: "/etc/ssl"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/etc/pki"
|
||||
dst: "/etc/pki"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/etc/resolv.conf"
|
||||
dst: "/etc/resolv.conf"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/dev/random"
|
||||
dst: "/dev/random"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
iface_no_lo: true
|
||||
|
||||
mount {
|
||||
src: "{CACHE_DIR}"
|
||||
dst: "/tmp/.cache/go"
|
||||
is_bind: true
|
||||
rw: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
{SHARED_MOUNT}
|
||||
|
||||
|
||||
@@ -139,8 +139,8 @@ const DEFAULT_HEAVY_DEPS: [&str; 18] = [
|
||||
const INCLUDE_DEPS_PY_SH_CONTENT: &str = include_str!("../nsjail/download_deps.py.sh");
|
||||
const NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT: &str = include_str!("../nsjail/download.py.config.proto");
|
||||
const NSJAIL_CONFIG_RUN_PYTHON3_CONTENT: &str = include_str!("../nsjail/run.python3.config.proto");
|
||||
|
||||
const NSJAIL_CONFIG_RUN_GO_CONTENT: &str = include_str!("../nsjail/run.go.config.proto");
|
||||
const NSJAIL_CONFIG_RUN_BASH_CONTENT: &str = include_str!("../nsjail/run.bash.config.proto");
|
||||
|
||||
const NSJAIL_CONFIG_RUN_DENO_CONTENT: &str = include_str!("../nsjail/run.deno.config.proto");
|
||||
const MAX_LOG_SIZE: u32 = 200000;
|
||||
@@ -904,6 +904,21 @@ mount {{
|
||||
)
|
||||
.await
|
||||
}
|
||||
Some(ScriptLang::Bash) => {
|
||||
handle_bash_job(
|
||||
worker_config,
|
||||
envs,
|
||||
logs,
|
||||
job,
|
||||
db,
|
||||
token,
|
||||
&inner_content,
|
||||
timeout,
|
||||
job_dir,
|
||||
&shared_mount,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
tracing::info!(
|
||||
worker_name = %worker_name,
|
||||
@@ -1100,6 +1115,90 @@ func Run(req Req) (interface{{}}, error){{
|
||||
read_result(job_dir).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn handle_bash_job(
|
||||
WorkerConfig { base_internal_url, disable_nuser, disable_nsjail, base_url, .. }: &WorkerConfig,
|
||||
Envs { nsjail_path, path_env, home_env, .. }: &Envs,
|
||||
logs: &mut String,
|
||||
job: &QueuedJob,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
token: String,
|
||||
content: &str,
|
||||
timeout: i32,
|
||||
job_dir: &str,
|
||||
shared_mount: &str,
|
||||
) -> Result<serde_json::Value, Error> {
|
||||
logs.push_str("\n\n--- BASH CODE EXECUTION ---\n");
|
||||
set_logs(logs, job.id, db).await;
|
||||
write_file(job_dir, "main.sh", content).await?;
|
||||
|
||||
let mut reserved_variables = get_reserved_variables(job, &token, &base_url, db).await?;
|
||||
reserved_variables.insert("RUST_LOG".to_string(), "info".to_string());
|
||||
|
||||
let hm = match job.args {
|
||||
Some(Value::Object(ref hm)) => hm.clone(),
|
||||
_ => serde_json::Map::new(),
|
||||
};
|
||||
let args_owned = windmill_parser_bash::parse_bash_sig(&content)?
|
||||
.args
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
hm.get(&arg.name)
|
||||
.and_then(|v| match v {
|
||||
Value::String(s) => Some(s.clone()),
|
||||
_ => serde_json::to_string(v).ok(),
|
||||
})
|
||||
.unwrap_or_else(String::new)
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
let args = args_owned.iter().map(|s| &s[..]).collect::<Vec<&str>>();
|
||||
|
||||
let child = if !disable_nsjail {
|
||||
let _ = write_file(
|
||||
job_dir,
|
||||
"run.config.proto",
|
||||
&NSJAIL_CONFIG_RUN_BASH_CONTENT
|
||||
.replace("{JOB_DIR}", job_dir)
|
||||
.replace("{CLONE_NEWUSER}", &(!disable_nuser).to_string())
|
||||
.replace("{SHARED_MOUNT}", shared_mount),
|
||||
)
|
||||
.await?;
|
||||
let mut cmd_args = vec!["--config", "run.config.proto", "--", "/bin/sh", "main.sh"];
|
||||
cmd_args.extend(args);
|
||||
Command::new(nsjail_path)
|
||||
.current_dir(job_dir)
|
||||
.env_clear()
|
||||
.envs(reserved_variables)
|
||||
.env("PATH", path_env)
|
||||
.env("BASE_INTERNAL_URL", base_internal_url)
|
||||
.args(cmd_args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?
|
||||
} else {
|
||||
let mut cmd_args = vec!["main.sh"];
|
||||
cmd_args.extend(&args);
|
||||
Command::new("/bin/sh")
|
||||
.current_dir(job_dir)
|
||||
.env_clear()
|
||||
.envs(reserved_variables)
|
||||
.env("PATH", path_env)
|
||||
.env("BASE_INTERNAL_URL", base_internal_url)
|
||||
.env("HOME", home_env)
|
||||
.args(cmd_args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?
|
||||
};
|
||||
handle_child(&job.id, db, logs, timeout, child).await?;
|
||||
//for now bash jobs have an empty result object
|
||||
Ok(serde_json::json!(logs
|
||||
.lines()
|
||||
.last()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(String::new)))
|
||||
}
|
||||
|
||||
fn capitalize(s: &str) -> String {
|
||||
let mut c = s.chars();
|
||||
match c.next() {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
import getMessageServiceOverride from 'vscode/service-override/messages'
|
||||
import { StandaloneServices } from 'vscode/services'
|
||||
import {
|
||||
DENO_INIT_CODE,
|
||||
BASH_INIT_CODE,
|
||||
DENO_INIT_CODE_CLEAR,
|
||||
GO_INIT_CODE,
|
||||
PYTHON_INIT_CODE_CLEAR
|
||||
@@ -57,7 +57,7 @@
|
||||
let divEl: HTMLDivElement | null = null
|
||||
let editor: monaco.editor.IStandaloneCodeEditor
|
||||
|
||||
export let lang: 'typescript' | 'python' | 'go'
|
||||
export let lang: 'typescript' | 'python' | 'go' | 'shell'
|
||||
export let code: string = ''
|
||||
export let hash: string = randomHash()
|
||||
export let cmdEnterAction: (() => void) | undefined = undefined
|
||||
@@ -147,6 +147,8 @@
|
||||
setCode(PYTHON_INIT_CODE_CLEAR)
|
||||
} else if (lang == 'go') {
|
||||
setCode(GO_INIT_CODE)
|
||||
} else if (lang == 'shell') {
|
||||
setCode(BASH_INIT_CODE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,8 @@
|
||||
import HighlightCode from './HighlightCode.svelte'
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
import { Drawer } from './common'
|
||||
import Icon from 'svelte-awesome'
|
||||
import Popover from './Popover.svelte'
|
||||
|
||||
export let lang: 'python3' | 'deno' | 'go'
|
||||
export let lang: 'python3' | 'deno' | 'go' | 'bash'
|
||||
export let editor: Editor
|
||||
export let websocketAlive: { pyright: boolean; black: boolean; deno: boolean; go: boolean }
|
||||
export let iconOnly: boolean = false
|
||||
@@ -42,7 +40,7 @@
|
||||
let resourceEditor: ResourceEditor
|
||||
|
||||
let codeViewer: Drawer
|
||||
let codeLang: 'python3' | 'deno' | 'go' = 'deno'
|
||||
let codeLang: 'python3' | 'deno' | 'go' | 'bash' = 'deno'
|
||||
let codeContent: string = ''
|
||||
|
||||
function addEditorActions() {
|
||||
@@ -109,6 +107,13 @@
|
||||
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}`)
|
||||
}
|
||||
sendUserToast(`${name} inserted at cursor`)
|
||||
}}
|
||||
@@ -137,6 +142,8 @@
|
||||
editor.insertAtLine('import wmill "github.com/windmill-labs/windmill-go-client"\n\n', 3)
|
||||
}
|
||||
editor.insertAtCursor(`v, _ := wmill.GetVariable("${path}")`)
|
||||
} else if (lang == 'bash') {
|
||||
sendUserToast('Not supported yet', true)
|
||||
}
|
||||
sendUserToast(`${name} inserted at cursor`)
|
||||
}}
|
||||
@@ -181,6 +188,8 @@
|
||||
editor.insertAtLine('import wmill "github.com/windmill-labs/windmill-go-client"\n\n', 3)
|
||||
}
|
||||
editor.insertAtCursor(`r, _ := wmill.GetResource("${path}")`)
|
||||
} else if (lang == 'bash') {
|
||||
sendUserToast('Not supported yet', true)
|
||||
}
|
||||
sendUserToast(`${path} inserted at cursor`)
|
||||
}}
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import python from 'svelte-highlight/languages/python'
|
||||
import typescript from 'svelte-highlight/languages/typescript'
|
||||
import go from 'svelte-highlight/languages/go'
|
||||
import shell from 'svelte-highlight/languages/shell'
|
||||
|
||||
export let code: string = ''
|
||||
export let language: 'python3' | 'deno' | 'go' | undefined
|
||||
export let language: 'python3' | 'deno' | 'go' | 'bash' | undefined
|
||||
|
||||
function getLang(lang: string | undefined) {
|
||||
switch (lang) {
|
||||
@@ -15,8 +16,10 @@
|
||||
return typescript
|
||||
case 'go':
|
||||
return go
|
||||
case 'bash':
|
||||
return shell
|
||||
default:
|
||||
return python
|
||||
return typescript
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
}
|
||||
|
||||
function initContent(
|
||||
language: 'deno' | 'python3' | 'go',
|
||||
language: 'deno' | 'python3' | 'go' | 'bash',
|
||||
kind: Script.kind,
|
||||
template: 'pgsql' | 'script'
|
||||
) {
|
||||
@@ -193,7 +193,8 @@
|
||||
options={[
|
||||
['Typescript (Deno)', 'deno'],
|
||||
['Python 3.10', 'python3'],
|
||||
['Go', 'go']
|
||||
['Go', 'go'],
|
||||
['Bash', 'bash']
|
||||
]}
|
||||
on:change={(e) => initContent(e.detail, script.kind, template)}
|
||||
bind:value={script.language}
|
||||
@@ -275,11 +276,12 @@
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<Toggle
|
||||
bind:checked={script.is_template}
|
||||
options={{ right: 'Save as a workspace template' }}
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<Toggle
|
||||
bind:checked={script.is_template}
|
||||
options={{ right: 'Save as a workspace template' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CenteredPage>
|
||||
{:else if step === 2}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
let itemPicker: ItemPicker
|
||||
let drawerViewer: Drawer
|
||||
let code: string = ''
|
||||
let lang: 'deno' | 'python3' | 'go' | undefined
|
||||
let lang: 'deno' | 'python3' | 'go' | 'bash' | undefined
|
||||
|
||||
let options: [[string, any]] = [['Script', 'script']]
|
||||
allowHub && options.unshift(['Hub', 'hub'])
|
||||
|
||||
@@ -10,9 +10,17 @@
|
||||
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
|
||||
|
||||
let drawer: Drawer
|
||||
|
||||
export function openDrawer() {
|
||||
loadVersion()
|
||||
listUsers()
|
||||
drawer?.openDrawer()
|
||||
}
|
||||
|
||||
export function toggleDrawer() {
|
||||
drawer?.toggleDrawer()
|
||||
}
|
||||
|
||||
let version: string | undefined
|
||||
let users: GlobalUserInfo[] = []
|
||||
let filteredUsers: GlobalUserInfo[] | undefined
|
||||
@@ -37,9 +45,6 @@
|
||||
users = await UserService.listUsersAsSuperAdmin({ perPage: 100000 })
|
||||
fuse?.setCollection(users)
|
||||
}
|
||||
|
||||
loadVersion()
|
||||
listUsers()
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={drawer} on:open={listUsers} size="900px">
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
export async function runPreview(
|
||||
path: string | undefined,
|
||||
code: string,
|
||||
lang: 'deno' | 'go' | 'python3',
|
||||
lang: 'deno' | 'go' | 'python3' | 'bash',
|
||||
args: Record<string, any>
|
||||
): Promise<void> {
|
||||
try {
|
||||
|
||||
@@ -23,12 +23,15 @@
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
|
||||
let drawer: Drawer
|
||||
|
||||
export function openDrawer() {
|
||||
loadVersion()
|
||||
loadLoginType()
|
||||
listTokens()
|
||||
drawer?.openDrawer()
|
||||
}
|
||||
|
||||
export function toggleDrawer() {
|
||||
if (!drawer || !drawer?.isOpen()) {
|
||||
loadVersion()
|
||||
loadLoginType()
|
||||
listTokens()
|
||||
}
|
||||
drawer?.toggleDrawer()
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
export let path: string
|
||||
|
||||
let code: string
|
||||
let language: 'deno' | 'python3' | 'go'
|
||||
let language: 'deno' | 'python3' | 'go' | 'bash'
|
||||
|
||||
async function loadCode(path: string) {
|
||||
const script = await getScriptByPath(path!)
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import DrawerContent from '../common/drawer/DrawerContent.svelte'
|
||||
import HighlightCode from '../HighlightCode.svelte'
|
||||
import LogViewer from '../LogViewer.svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { Pane } from 'svelte-splitpanes'
|
||||
import SplitPanesWrapper from '../splitPanes/SplitPanesWrapper.svelte'
|
||||
|
||||
export let path: string | undefined
|
||||
@@ -59,7 +59,7 @@
|
||||
class="overflow-x-auto break-words relative h-full m-2 text-xs bg-white shadow-inner p-2">
|
||||
{drawerContent?.content}
|
||||
</pre>
|
||||
{:else if drawerContent?.mode === 'deno' || drawerContent?.mode === 'python3' || drawerContent?.mode === 'go'}
|
||||
{:else if drawerContent?.mode === 'deno' || drawerContent?.mode === 'python3' || drawerContent?.mode === 'go' || drawerContent?.mode === 'bash'}
|
||||
<HighlightCode language={drawerContent?.mode} code={drawerContent?.content} />
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
@@ -82,8 +82,8 @@
|
||||
</Pane>
|
||||
<Pane class="!duration-[0ms]">
|
||||
{#if previewJob != undefined && 'result' in previewJob && previewJob.result != undefined}
|
||||
<pre class="overflow-x-auto break-words relative h-full px-2">
|
||||
<DisplayResult result={previewJob.result} />
|
||||
<pre class="overflow-x-auto break-words relative h-full p-2"
|
||||
><DisplayResult result={previewJob.result} />
|
||||
</pre>
|
||||
{:else}
|
||||
<div class="text-sm text-gray-600 p-2">
|
||||
|
||||
@@ -43,6 +43,8 @@ export function langToExt(lang: string): string {
|
||||
return 'py'
|
||||
case 'go':
|
||||
return 'go'
|
||||
case 'bash':
|
||||
return 'sh'
|
||||
default:
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ const loadSchemaLastRun = writable<[string | undefined, MainArgSignature | undef
|
||||
|
||||
|
||||
export async function inferArgs(
|
||||
language: 'python3' | 'deno' | 'go',
|
||||
language: 'python3' | 'deno' | 'go' | 'bash',
|
||||
code: string,
|
||||
schema: Schema
|
||||
): Promise<void> {
|
||||
@@ -28,6 +28,10 @@ export async function inferArgs(
|
||||
inferedSchema = await ScriptService.goToJsonschema({
|
||||
requestBody: code
|
||||
})
|
||||
} else if (language == 'bash') {
|
||||
inferedSchema = await ScriptService.bashToJsonschema({
|
||||
requestBody: code
|
||||
})
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -143,6 +143,15 @@ export async function main(
|
||||
return query.rows;
|
||||
}`
|
||||
|
||||
export const BASH_INIT_CODE = `
|
||||
# arguments of the form X="$I" are parsed as parameters X of type string
|
||||
msg="$1"
|
||||
|
||||
# the last line of the stdout is the return value
|
||||
echo "Hello $msg"
|
||||
`
|
||||
|
||||
|
||||
export const DENO_INIT_CODE_TRIGGER = `import * as wmill from "https://deno.land/x/windmill@v${__pkg__.version}/mod.ts"
|
||||
|
||||
export async function main() {
|
||||
@@ -181,7 +190,7 @@ export function isInitialCode(content: string): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
export function initialCode(language: 'deno' | 'python3' | 'go', kind: Script.kind, subkind: 'pgsql' | 'flow' | 'script' | undefined): string {
|
||||
export function initialCode(language: 'deno' | 'python3' | 'go' | 'bash', kind: Script.kind, subkind: 'pgsql' | 'flow' | 'script' | undefined): string {
|
||||
if (language === 'deno') {
|
||||
if (kind === 'trigger') {
|
||||
return DENO_INIT_CODE_TRIGGER
|
||||
@@ -210,6 +219,8 @@ export function initialCode(language: 'deno' | 'python3' | 'go', kind: Script.ki
|
||||
} else {
|
||||
return PYTHON_INIT_CODE
|
||||
}
|
||||
} else if (language == 'bash') {
|
||||
return BASH_INIT_CODE
|
||||
} else {
|
||||
if (kind === 'failure') {
|
||||
return GO_FAILURE_MODULE_CODE
|
||||
|
||||
@@ -490,7 +490,7 @@ export function scriptPathToHref(path: string): string {
|
||||
|
||||
export async function getScriptByPath(path: string): Promise<{
|
||||
content: string
|
||||
language: 'deno' | 'python3' | 'go',
|
||||
language: 'deno' | 'python3' | 'go' | 'bash',
|
||||
schema: any
|
||||
}> {
|
||||
if (path.startsWith('hub/')) {
|
||||
@@ -594,11 +594,13 @@ export function classNames(...classes: Array<string | undefined>): string {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export function scriptLangToEditorLang(lang: Script.language): 'typescript' | 'python' | 'go' {
|
||||
export function scriptLangToEditorLang(lang: Script.language): 'typescript' | 'python' | 'go' | 'shell' {
|
||||
if (lang == 'deno') {
|
||||
return 'typescript'
|
||||
} else if (lang == 'python3') {
|
||||
return 'python'
|
||||
} else if (lang == 'bash') {
|
||||
return 'shell'
|
||||
} else {
|
||||
return lang
|
||||
}
|
||||
|
||||
@@ -111,8 +111,8 @@
|
||||
<div class="px-2 py-4 space-y-2 border-y border-blue-400">
|
||||
<WorkspaceMenu />
|
||||
<UserMenu
|
||||
on:user-settings={() => userSettings.toggleDrawer()}
|
||||
on:superadmin-settings={() => superadminSettings.toggleDrawer()}
|
||||
on:user-settings={() => userSettings.openDrawer()}
|
||||
on:superadmin-settings={() => superadminSettings.openDrawer()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -146,8 +146,8 @@
|
||||
<div class="px-2 py-4 space-y-2 border-y border-blue-400">
|
||||
<WorkspaceMenu {isCollapsed} />
|
||||
<UserMenu
|
||||
on:user-settings={userSettings.toggleDrawer}
|
||||
on:superadmin-settings={() => superadminSettings.toggleDrawer()}
|
||||
on:user-settings={userSettings.openDrawer}
|
||||
on:superadmin-settings={() => superadminSettings.openDrawer()}
|
||||
{isCollapsed}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -246,7 +246,7 @@
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
<Button size="xs" on:click={userSettings.toggleDrawer}>Create token</Button>
|
||||
<Button size="xs" on:click={userSettings.openDrawer}>Create token</Button>
|
||||
</div>
|
||||
</div>
|
||||
{#if schedule}
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
|
||||
let codeViewer: Drawer
|
||||
let codeViewerContent: string = ''
|
||||
let codeViewerLanguage: 'deno' | 'python3' | 'go' = 'deno'
|
||||
let codeViewerLanguage: 'deno' | 'python3' | 'go' | 'bash' = 'deno'
|
||||
let codeViewerPath: string = ''
|
||||
|
||||
$: filteredScripts =
|
||||
|
||||
@@ -432,7 +432,7 @@
|
||||
{/each}
|
||||
</ul>
|
||||
<div class="flex flex-row-reverse mt-2">
|
||||
<Button size="xs" on:click={userSettings.toggleDrawer}>Create token</Button>
|
||||
<Button size="xs" on:click={userSettings.openDrawer}>Create token</Button>
|
||||
</div>
|
||||
</TabContent>
|
||||
{/each}
|
||||
|
||||
@@ -160,11 +160,11 @@
|
||||
{/each}
|
||||
<div class="flex justify-between items-center mt-10">
|
||||
{#if $superadmin}
|
||||
<Button variant="border" size="sm" on:click={superadminSettings.toggleDrawer}>
|
||||
<Button variant="border" size="sm" on:click={superadminSettings.openDrawer}>
|
||||
<Icon data={faCrown} class="mr-1" scale={1} />Superadmin settings</Button
|
||||
>
|
||||
{/if}
|
||||
<Button variant="border" size="sm" on:click={userSettings.toggleDrawer}>
|
||||
<Button variant="border" size="sm" on:click={userSettings.openDrawer}>
|
||||
<Icon data={faUserCog} class="mr-1" scale={1} />User settings</Button
|
||||
>
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user