Merge remote-tracking branch 'upstream/main' into appeditor-multiselect-input-type

This commit is contained in:
Clement Zhang
2024-11-07 10:32:48 +01:00
27 changed files with 516 additions and 213 deletions
+18
View File
@@ -1,5 +1,23 @@
# Changelog
## [1.419.0](https://github.com/windmill-labs/windmill/compare/v1.418.0...v1.419.0) (2024-11-06)
### Features
* Add full-text search on windmill service logs ([#4576](https://github.com/windmill-labs/windmill/issues/4576)) ([77735d8](https://github.com/windmill-labs/windmill/commit/77735d859cfee9204f67a8e4f9885228d657a41d))
* show path in flow script picker ([#4574](https://github.com/windmill-labs/windmill/issues/4574)) ([8e27392](https://github.com/windmill-labs/windmill/commit/8e27392afacbb725aaf9f9f892ab8a6171b59ce5))
* websocket authentication ([#4635](https://github.com/windmill-labs/windmill/issues/4635)) ([4dda0fb](https://github.com/windmill-labs/windmill/commit/4dda0fb8cd8262ad3a2ab2b9d27e7043ac3bb891))
### Bug Fixes
* clarify error messages when job timeout or cancelled with more details ([771d740](https://github.com/windmill-labs/windmill/commit/771d740701902166f8b4e3f77aa9c5579237cb15))
* **cli:** improve handling of deleted items on windows ([9a8dcc9](https://github.com/windmill-labs/windmill/commit/9a8dcc9a250caefa0b7c9523e1321599b7471c8b))
* display logs in native mode when script fails ([#4655](https://github.com/windmill-labs/windmill/issues/4655)) ([7578ceb](https://github.com/windmill-labs/windmill/commit/7578cebaf92e729a26fd665e4b6f8357d34f59eb))
* **frontend:** arg input json handling when the value is not of the same type as schema ([#4479](https://github.com/windmill-labs/windmill/issues/4479)) ([8d8156b](https://github.com/windmill-labs/windmill/commit/8d8156bd0773da3ddec81c46ad5fda114ecd3dda))
* **frontend:** improve flow prop picker design ([323912c](https://github.com/windmill-labs/windmill/commit/323912c73c18d3bb6d136f2e6458389270364a0e))
## [1.418.0](https://github.com/windmill-labs/windmill/compare/v1.417.3...v1.418.0) (2024-11-04)
+41 -41
View File
@@ -166,9 +166,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.92"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13"
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
[[package]]
name = "arc-swap"
@@ -966,9 +966,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime-api"
version = "1.7.2"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96"
checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
@@ -983,9 +983,9 @@ dependencies = [
[[package]]
name = "aws-smithy-types"
version = "1.2.8"
version = "1.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07c9cdc179e6afbf5d391ab08c85eac817b51c87e1892a5edb5f7bbdc64314b4"
checksum = "4fbd94a32b3a7d55d3806fe27d98d3ad393050439dd05eb53ece36ec5e3d3510"
dependencies = [
"base64-simd 0.8.0",
"bytes",
@@ -1599,9 +1599,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.1.34"
version = "1.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9"
checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70"
dependencies = [
"jobserver",
"libc",
@@ -2554,7 +2554,7 @@ dependencies = [
"once_cell",
"percent-encoding",
"serde",
"sourcemap 9.0.0",
"sourcemap 9.0.1",
"swc_atoms",
"swc_common",
"swc_config",
@@ -6537,9 +6537,9 @@ dependencies = [
[[package]]
name = "quinn-udp"
version = "0.5.6"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e346e016eacfff12233c243718197ca12f148c84e1e84268a896699b41c71780"
checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da"
dependencies = [
"cfg_aliases",
"libc",
@@ -7173,9 +7173,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.38"
version = "0.38.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a"
checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee"
dependencies = [
"bitflags 2.6.0",
"errno",
@@ -8016,9 +8016,9 @@ dependencies = [
[[package]]
name = "sourcemap"
version = "9.0.0"
version = "9.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab08a862c70980b8e23698b507e272317ae52a608a164a844111f5372374f1f"
checksum = "86721155737d89818020dc35713595c6e7b21016073c101e6590c47ca58c2d16"
dependencies = [
"base64-simd 0.7.0",
"bitvec",
@@ -8506,7 +8506,7 @@ dependencies = [
"rustc-hash 1.1.0",
"serde",
"siphasher",
"sourcemap 9.0.0",
"sourcemap 9.0.1",
"swc_allocator",
"swc_atoms",
"swc_eq_ignore_macros",
@@ -8570,7 +8570,7 @@ dependencies = [
"num-bigint",
"once_cell",
"serde",
"sourcemap 9.0.0",
"sourcemap 9.0.1",
"swc_allocator",
"swc_atoms",
"swc_common",
@@ -10563,7 +10563,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"axum",
@@ -10605,7 +10605,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"argon2",
@@ -10690,7 +10690,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -10708,7 +10708,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"chrono",
"serde",
@@ -10721,7 +10721,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"rsmq_async",
@@ -10736,7 +10736,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"async-stream",
@@ -10782,7 +10782,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"regex",
"rsmq_async",
@@ -10797,7 +10797,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"bytes",
@@ -10819,7 +10819,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"itertools 0.13.0",
"lazy_static",
@@ -10831,7 +10831,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -10840,7 +10840,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -10852,7 +10852,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"gosyn",
@@ -10864,7 +10864,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -10876,7 +10876,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"itertools 0.13.0",
@@ -10887,7 +10887,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"itertools 0.13.0",
@@ -10898,7 +10898,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -10916,7 +10916,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -10933,7 +10933,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -10945,7 +10945,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -10963,7 +10963,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"getrandom 0.2.15",
@@ -10984,7 +10984,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"serde_json",
@@ -10994,7 +10994,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -11027,7 +11027,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -11037,7 +11037,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.418.0"
version = "1.419.0"
dependencies = [
"anyhow",
"async-recursion",
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.418.0"
version = "1.419.0"
authors.workspace = true
edition.workspace = true
@@ -29,7 +29,7 @@ members = [
]
[workspace.package]
version = "1.418.0"
version = "1.419.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.418.0
version: 1.419.0
title: Windmill API
contact:
+4 -4
View File
@@ -562,7 +562,7 @@ pub async fn resolve_job_timeout(
_w_id: &str,
_job_id: Uuid,
custom_timeout_secs: Option<i32>,
) -> (Duration, Option<String>) {
) -> (Duration, Option<String>, bool) {
let mut warn_msg: Option<String> = None;
#[cfg(feature = "cloud")]
let cloud_premium_workspace = *CLOUD_HOSTED
@@ -587,12 +587,12 @@ pub async fn resolve_job_timeout(
Some(timeout_secs)
if Duration::from_secs(timeout_secs as u64) < global_max_timeout_duration =>
{
(Duration::from_secs(timeout_secs as u64), warn_msg)
(Duration::from_secs(timeout_secs as u64), warn_msg, true)
}
Some(timeout_secs) => {
warn_msg = Some(format!("WARNING: Custom job timeout of {timeout_secs} seconds was greater than the maximum timeout. It will be ignored and the max timeout will be used instead"));
tracing::warn!(warn_msg);
(global_max_timeout_duration, warn_msg)
(global_max_timeout_duration, warn_msg, false)
}
None => {
// fallback to default timeout or max if not set
@@ -610,7 +610,7 @@ pub async fn resolve_job_timeout(
global_max_timeout_duration
}
};
(default_timeout, warn_msg)
(default_timeout, warn_msg, false)
}
}
}
+39 -11
View File
@@ -143,15 +143,40 @@ pub async fn handle_child(
occupancy_metrics,
);
#[derive(PartialEq, Debug)]
enum KillReason {
TooManyLogs,
Timeout,
Cancelled,
Timeout { is_job_specific: bool },
Cancelled(Option<CanceledBy>),
AlreadyCompleted,
}
let (timeout_duration, timeout_warn_msg) =
impl std::fmt::Debug for KillReason {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
KillReason::TooManyLogs => f.write_str("too many logs (max size: 2MB)"),
KillReason::Timeout { is_job_specific } => f.write_str(if *is_job_specific {
"timeout after exceeding job-specific duration limit"
} else {
"timeout after exceeding instance-wide job duration limit"
}),
KillReason::Cancelled(canceled_by) => {
let mut reason = "cancelled".to_string();
if let Some(canceled_by) = canceled_by {
if let Some(by) = canceled_by.username.as_ref() {
reason.push_str(&format!(" by {}", by));
}
if let Some(rsn) = canceled_by.reason.as_ref() {
reason.push_str(&format!(" (reason: {})", rsn));
}
}
f.write_str(&reason)
}
KillReason::AlreadyCompleted => f.write_str("already completed"),
}
}
}
let (timeout_duration, timeout_warn_msg, is_job_specific) =
resolve_job_timeout(&db, w_id, job_id, custom_timeout).await;
if let Some(msg) = timeout_warn_msg {
append_logs(&job_id, w_id, msg.as_str(), db).await;
@@ -165,9 +190,9 @@ pub async fn handle_child(
biased;
result = child.wait() => return result.map(Ok),
Ok(()) = too_many_logs.changed() => KillReason::TooManyLogs,
_ = sleep(timeout_duration) => KillReason::Timeout,
_ = sleep(timeout_duration) => KillReason::Timeout { is_job_specific },
ex = update_job, if job_id != Uuid::nil() => match ex {
UpdateJobPollingExit::Done => KillReason::Cancelled,
UpdateJobPollingExit::Done(canceled_by) => KillReason::Cancelled(canceled_by),
UpdateJobPollingExit::AlreadyCompleted => KillReason::AlreadyCompleted,
},
};
@@ -175,7 +200,7 @@ pub async fn handle_child(
drop(tx);
let set_reason = async {
if kill_reason == KillReason::Timeout {
if matches!(kill_reason, KillReason::Timeout { .. }) {
if let Err(err) = sqlx::query(
r#"
UPDATE queue
@@ -402,7 +427,7 @@ pub async fn handle_child(
Err(Error::AlreadyCompleted("Job already completed".to_string()))
}
_ => Err(Error::ExecutionErr(format!(
"job process killed because {kill_reason:#?}"
"job process terminated due to {kill_reason:#?}"
))),
},
Err(err) => Err(Error::ExecutionErr(format!("job process io error: {err}"))),
@@ -505,7 +530,10 @@ where
})?,
ex = update_job, if job_id != Uuid::nil() => {
match ex {
UpdateJobPollingExit::Done => Err(Error::ExecutionErr("Job cancelled".to_string())).map_err(to_anyhow)?,
UpdateJobPollingExit::Done(canceled_by) => {
let (by, reason) = canceled_by.as_ref().map_or(("unknown".to_string(), "unknown".to_string()), |x| (x.username.clone().unwrap_or("".to_string()), x.reason.clone().unwrap_or("".to_string())));
Err(Error::ExecutionErr(format!("Job cancelled by {by} (reason: {reason})",))).map_err(to_anyhow)?
},
UpdateJobPollingExit::AlreadyCompleted => Err(Error::AlreadyCompleted("Job already completed".to_string())).map_err(to_anyhow)?,
}
}
@@ -515,7 +543,7 @@ where
}
pub enum UpdateJobPollingExit {
Done,
Done(Option<CanceledBy>),
AlreadyCompleted,
}
@@ -640,7 +668,7 @@ where
}
tracing::info!("job {job_id} finished");
UpdateJobPollingExit::Done
UpdateJobPollingExit::Done(canceled_by_ref.clone())
}
/// takes stdout and stderr from Child, panics if either are not present
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.418.0";
export const VERSION = "v1.419.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+1 -1
View File
@@ -60,7 +60,7 @@ export {
// }
// });
export const VERSION = "1.418.0";
export const VERSION = "1.419.0";
const command = new Command()
.name("wmill")
+8 -7
View File
@@ -1398,11 +1398,12 @@ export async function push(opts: GlobalOptions & SyncOptions) {
log.info(`Deleting ${typ} ${change.path}`);
}
const workspaceId = workspace.workspaceId;
const target = change.path.replaceAll(SEP, "/");
switch (typ) {
case "script": {
const script = await wmill.getScriptByPath({
workspace: workspaceId,
path: removeExtensionToPath(change.path),
path: removeExtensionToPath(target),
});
await wmill.archiveScriptByHash({
workspace: workspaceId,
@@ -1419,37 +1420,37 @@ export async function push(opts: GlobalOptions & SyncOptions) {
case "resource":
await wmill.deleteResource({
workspace: workspaceId,
path: removeSuffix(change.path, ".resource.json"),
path: removeSuffix(target, ".resource.json"),
});
break;
case "resource-type":
await wmill.deleteResourceType({
workspace: workspaceId,
path: removeSuffix(change.path, ".resource-type.json"),
path: removeSuffix(target, ".resource-type.json"),
});
break;
case "flow":
await wmill.deleteFlowByPath({
workspace: workspaceId,
path: removeSuffix(change.path, ".flow/flow.json"),
path: removeSuffix(target, ".flow/flow.json"),
});
break;
case "app":
await wmill.deleteApp({
workspace: workspaceId,
path: removeSuffix(change.path, ".app/app.json"),
path: removeSuffix(target, ".app/app.json"),
});
break;
case "schedule":
await wmill.deleteSchedule({
workspace: workspaceId,
path: removeSuffix(change.path, ".schedule.json"),
path: removeSuffix(target, ".schedule.json"),
});
break;
case "variable":
await wmill.deleteVariable({
workspace: workspaceId,
path: removeSuffix(change.path, ".variable.json"),
path: removeSuffix(target, ".variable.json"),
});
break;
case "user": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.418.0",
"version": "1.419.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.418.0",
"version": "1.419.0",
"license": "AGPL-3.0",
"dependencies": {
"@aws-crypto/sha256-js": "^4.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.418.0",
"version": "1.419.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -1,3 +1,12 @@
<script context="module">
const dynamicTemplateRegexPairs = buildPrefixRegex([
'flow_input',
'results',
'resource',
'variable'
])
</script>
<script lang="ts">
import type { Schema } from '$lib/common'
import type { InputCat } from '$lib/utils'
@@ -13,8 +22,9 @@
import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import { tick } from 'svelte'
import { fade } from 'svelte/transition'
import { buildPrefixRegex } from './flows/previousResults'
import type VariableEditor from './VariableEditor.svelte'
import type ItemPicker from './ItemPicker.svelte'
import type { InputTransform } from '$lib/gen'
@@ -43,6 +53,7 @@
let monaco: SimpleEditor | undefined = undefined
let monacoTemplate: TemplateEditor | undefined = undefined
let argInput: ArgInput | undefined = undefined
let focusedPrev = false
const dispatch = createEventDispatcher()
@@ -59,6 +70,9 @@
const { shouldUpdatePropertyType, exprsToSet } =
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
const { inputMatches, focusProp, propPickerConfig } =
getContext<PropPickerWrapperContext>('PropPickerWrapper')
function setExpr() {
const newArg = $exprsToSet?.[argName]
if (newArg) {
@@ -133,6 +147,50 @@
}
}
let codeInjectionDetected = false
function checkCodeInjection(rawValue: string) {
if (!arg || !rawValue || rawValue.length < 3 || !dynamicTemplateRegexPairs) {
return undefined
}
if (rawValue.trim() !== rawValue) {
return undefined
}
const matches = dynamicTemplateRegexPairs.filter(({ regex }) => regex.test(rawValue))
if (matches.length > 0) {
return matches.map((m) => ({ word: m.word, value: rawValue }))
}
return undefined
}
async function setJavaScriptExpr(rawValue: string) {
arg = {
type: 'javascript',
expr: rawValue
}
propertyType = 'javascript'
monaco?.setCode('')
monaco?.insertAtCursor(rawValue)
await tick()
monaco?.focus()
await tick()
monaco?.setCursorToEnd()
}
function handleKeyUp(e: KeyboardEvent) {
if (
e.key === 'Tab' &&
isStaticTemplate(inputCat) &&
propertyType == 'static' &&
!noDynamicToggle &&
codeInjectionDetected
) {
setJavaScriptExpr(arg.value)
} else {
stepInputGen?.onKeyUp?.(e)
}
}
function isStaticTemplate(inputCat: InputCat) {
return inputCat === 'string' || inputCat === 'sql' || inputCat == 'yaml'
}
@@ -166,9 +224,24 @@
}
}
const { focusProp, propPickerConfig } = getContext<PropPickerWrapperContext>('PropPickerWrapper')
$: updateStaticInput(inputCat, propertyType, arg)
$: isStaticTemplate(inputCat) && propertyType == 'static' && setPropertyType(arg?.value)
function updateStaticInput(
inputCat: InputCat,
propertyType: 'static' | 'javascript',
arg: InputTransform | any
) {
if (!isStaticTemplate(inputCat)) {
return
}
if (propertyType == 'static') {
setPropertyType(arg?.value)
codeInjectionDetected = checkCodeInjection(arg?.value) != undefined
} else if (propertyType == 'javascript' && focused) {
setPropertyType(arg?.expr)
$inputMatches = checkCodeInjection(arg?.expr)
}
}
function setDefaultCode() {
if (!arg?.value) {
@@ -176,6 +249,14 @@
}
}
function updateFocused(newFocused: boolean) {
if (focusedPrev && !newFocused) {
$inputMatches = undefined
}
focusedPrev = focused
}
$: updateFocused(focused)
$: schema?.properties?.[argName].default && setDefaultCode()
let resourceTypes: string[] | undefined = undefined
@@ -325,13 +406,27 @@
<ToggleButton small label="Static" value="static" />
{/if}
<ToggleButton
small
light
tooltip="JavaScript expression ('flow_input' or 'results')."
value="javascript"
icon={FunctionSquare}
/>
{#if codeInjectionDetected && propertyType == 'static'}
<Button
size="xs2"
color="light"
btnClasses="font-normal text-xs w-fit bg-green-100 text-green-800 hover:bg-green-100 dark:text-green-300 dark:bg-green-700 dark:hover:bg-green-600"
on:click={() => setJavaScriptExpr(arg.value)}
>
<span class="font-normal whitespace-nowrap flex gap-2 items-center"
><FunctionSquare size={14} /> detected -
<span class="font-bold">TAB</span>
</span>
</Button>
{:else}
<ToggleButton
small
light
tooltip="JavaScript expression ('flow_input' or 'results')."
value="javascript"
icon={FunctionSquare}
/>
{/if}
</ToggleButtonGroup>
</div>
@@ -363,7 +458,7 @@
<div class="max-w-xs" />
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="relative" on:keyup={stepInputGen?.onKeyUp}>
<div class="relative" on:keyup={handleKeyUp}>
<!-- {#if $propPickerConfig?.propName == argName && $propPickerConfig?.insertionMode == 'connect'}
<span
class={'text-white z-50 px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute top-0 right-0 bg-blue-500'}
@@ -405,6 +405,15 @@
editor && editor.dispose()
} catch (err) {}
})
export function setCursorToEnd(): void {
if (editor) {
const lastLine = editor.getModel()?.getLineCount() ?? 1
const lastColumn = editor.getModel()?.getLineMaxColumn(lastLine) ?? 1
editor.setPosition({ lineNumber: lastLine, column: lastColumn })
editor.focus()
}
}
</script>
<EditorTheme />
@@ -77,6 +77,8 @@
.filter(([i, f, m]) => f.length > 0)
setContext<PropPickerWrapperContext>('PropPickerWrapper', {
inputMatches: writable(undefined),
filteredPickableProperties: writable(undefined),
focusProp: () => {},
propPickerConfig: writable(undefined),
clearFocus: () => {}
@@ -260,3 +260,40 @@ declare const approvers: string
}
`
}
export function buildPrefixRegex(words: string[]): Array<{ regex: RegExp; word: string }> {
return words.map((word) => {
const prefixes: string[] = []
for (let i = 1; i <= word.length; i++) {
prefixes.push(word.slice(0, i) + '$')
}
prefixes.push(word + '\\.')
prefixes.push(word + '\\[')
return {
regex: new RegExp(`^(${prefixes.join('|')}).*`),
word
}
})
}
export function filterNestedObject(obj: any, nestedKeys: string[]) {
if (nestedKeys.length === 0) return {}
if (nestedKeys.length === 1) {
if (nestedKeys[0] === '') {
return obj
}
const regexes = buildPrefixRegex(Object.keys(obj))
const matches = regexes.filter(({ regex }) => regex.test(nestedKeys[0]))
return Object.fromEntries(
Object.entries(obj).filter(([key]) => matches.some(({ word }) => word === key))
)
}
const [key, ...rest] = nestedKeys
if (obj && typeof obj === 'object' && key in obj) {
const result = {}
result[key] = filterNestedObject(obj[key], rest)
return result
}
return {}
}
@@ -11,6 +11,8 @@
export type PropPickerWrapperContext = {
propPickerConfig: Writable<PropPickerConfig | undefined>
inputMatches: Writable<{ word: string; value: string }[] | undefined>
filteredPickableProperties: Writable<PickableProperties | undefined>
focusProp: (propName: string, insertionMode: InsertionMode, onSelect: SelectCallback) => void
clearFocus: () => void
}
@@ -37,10 +39,14 @@
export let noPadding: boolean = false
const propPickerConfig = writable<PropPickerConfig | undefined>(undefined)
const filteredPickableProperties = writable<PickableProperties | undefined>(undefined)
const inputMatches = writable<{ word: string; value: string }[] | undefined>(undefined)
const dispatch = createEventDispatcher()
setContext<PropPickerWrapperContext>('PropPickerWrapper', {
propPickerConfig,
inputMatches,
filteredPickableProperties,
focusProp: (propName, insertionMode, onSelect) => {
propPickerConfig.set({
propName,
@@ -274,7 +274,7 @@ export const settings: Record<string, Setting[]> = {
label: 'Keep job directories',
key: 'keep_job_dir',
fieldType: 'boolean',
description: 'Keep Job directories after execution at /tmp/windmill/<worker>/<job_id>',
description: 'Keep Job directories after execution at /tmp/windmill/WORKER/JOB_ID',
storage: 'setting'
},
{
@@ -9,6 +9,7 @@
import { keepByKey } from './utils'
import type { PickableProperties } from '../flows/previousResults'
import ClearableInput from '../common/clearableInput/ClearableInput.svelte'
import { filterNestedObject } from '../flows/previousResults'
export let pickableProperties: PickableProperties
export let displayContext = true
@@ -20,18 +21,31 @@
let resources: Record<string, any> = {}
let displayVariable = false
let displayResources = false
let allResultsCollapsed = true
let collapsableInitialState:
| {
allResultsCollapsed: boolean
displayVariable: boolean
displayResources: boolean
}
| undefined
let filterActive = false
const EMPTY_STRING = ''
let search = ''
const { propPickerConfig } = getContext<PropPickerWrapperContext>('PropPickerWrapper')
const { propPickerConfig, filteredPickableProperties, inputMatches } =
getContext<PropPickerWrapperContext>('PropPickerWrapper')
$filteredPickableProperties = { ...pickableProperties }
let flowInputsFiltered = pickableProperties.flow_input
let resultByIdFiltered = pickableProperties.priorIds
let timeout: NodeJS.Timeout
function onSearch(search: string) {
filterActive = false
clearTimeout(timeout)
setTimeout(() => {
flowInputsFiltered =
@@ -72,29 +86,124 @@
).map((resource) => [resource.path, resource.description ?? ''])
)
}
async function filterPickableProperties() {
if (!filterActive) {
return
}
if (!$inputMatches?.some((match) => match.word === 'flow_input')) {
flowInputsFiltered = []
}
if (!$inputMatches?.some((match) => match.word === 'results')) {
resultByIdFiltered = []
}
if ($inputMatches?.length == 1) {
if ($inputMatches[0].word === 'flow_input') {
flowInputsFiltered = pickableProperties.flow_input
let [, ...nestedKeys] = $inputMatches[0].value.split('.')
let filtered = filterNestedObject(flowInputsFiltered, nestedKeys)
if (Object.keys(filtered).length > 0) {
flowInputsFiltered = filtered
}
} else if ($inputMatches[0].word === 'results') {
resultByIdFiltered = pickableProperties.priorIds
let [, ...nestedKeys] = $inputMatches[0].value.split('.')
let filtered = filterNestedObject(resultByIdFiltered, nestedKeys)
if (Object.keys(filtered).length > 0) {
resultByIdFiltered = filtered
}
}
}
if ($filteredPickableProperties) {
resultByIdFiltered && ($filteredPickableProperties.priorIds = resultByIdFiltered)
flowInputsFiltered && ($filteredPickableProperties.flow_input = flowInputsFiltered)
}
}
async function updateCollapsable() {
if (!$inputMatches || $inputMatches.length !== 1) {
resetCollapsable()
return
}
if (!collapsableInitialState) {
collapsableInitialState = { allResultsCollapsed, displayVariable, displayResources }
}
if ($inputMatches[0].word === 'variable') {
await loadVariables()
displayVariable = true
return
}
if ($inputMatches[0].word === 'resource') {
await loadResources()
displayResources = true
return
}
if ($inputMatches[0].word === 'results') {
allResultsCollapsed = false
return
}
}
function resetCollapsable() {
if (!collapsableInitialState) {
return
}
;({ allResultsCollapsed, displayVariable, displayResources } = collapsableInitialState)
collapsableInitialState = undefined
}
async function updateFilterActive() {
const prev = filterActive
filterActive = Boolean(
$inputMatches &&
$inputMatches?.length > 0 &&
$propPickerConfig?.insertionMode === 'insert' &&
search === EMPTY_STRING
)
if (prev && !filterActive) {
flowInputsFiltered = pickableProperties.flow_input
resultByIdFiltered = pickableProperties.priorIds
}
}
async function updateState() {
await updateFilterActive()
await filterPickableProperties()
await updateCollapsable()
}
$: search, $inputMatches, $propPickerConfig, updateState()
</script>
<div class="flex flex-col h-full rounded overflow-hidden">
<div class="px-2 py-2">
<ClearableInput bind:value={search} placeholder="Search prop..." />
</div>
<div class="overflow-y-auto px-2 pt-2 grow">
<div class="flex justify-between items-center space-x-1">
<span class="font-normal text-sm text-secondary">Flow Input</span>
<div class="flex space-x-2 items-center" />
</div>
<div class="overflow-y-auto mb-2">
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
json={flowInputsFiltered}
prefix="flow_input"
on:select
/>
</div>
<div class="overflow-y-auto px-2 pt-2 grow" class:bg-surface-secondary={!$propPickerConfig}>
{#if flowInputsFiltered && (Object.keys(flowInputsFiltered).length > 0 || !filterActive)}
<div class="flex justify-between items-center space-x-1">
<span class="font-normal text-sm text-secondary">Flow Input</span>
<div class="flex space-x-2 items-center" />
</div>
<div class="overflow-y-auto pb-2">
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
json={flowInputsFiltered}
prefix="flow_input"
on:select
/>
</div>
{/if}
{#if error}
<span class="font-normal text-sm text-secondary">Error</span>
<div class="overflow-y-auto mb-2">
<div class="overflow-y-auto pb-2">
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
@@ -112,7 +221,7 @@
{#if Object.keys(pickableProperties.priorIds).length > 0}
{#if suggestedPropsFiltered && Object.keys(suggestedPropsFiltered).length > 0}
<span class="font-normal text-sm text-secondary">Suggested Results</span>
<div class="overflow-y-auto mb-2">
<div class="overflow-y-auto pb-2">
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
@@ -124,11 +233,11 @@
</div>
{/if}
<span class="font-normal text-sm text-secondary">All Results</span>
<div class="overflow-y-auto mb-2">
<div class="overflow-y-auto pb-2">
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
collapsed={true}
collapseLevel={allResultsCollapsed ? 1 : undefined}
json={resultByIdFiltered}
prefix="results"
on:select
@@ -136,9 +245,12 @@
</div>
{/if}
{:else}
{#if previousId}
{@const json = Object.fromEntries(
Object.entries(resultByIdFiltered).filter(([k, v]) => k == previousId)
)}
{#if previousId && Object.keys(json).length > 0}
<span class="font-normal text-sm text-secondary">Previous Result</span>
<div class="overflow-y-auto mb-2">
<div class="overflow-y-auto pb-2">
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
@@ -152,7 +264,7 @@
{/if}
{#if pickableProperties.hasResume}
<span class="font-normal text-sm text-secondary">Resume payloads</span>
<div class="overflow-y-auto mb-2">
<div class="overflow-y-auto pb-2">
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
@@ -166,9 +278,9 @@
</div>
{/if}
{#if Object.keys(pickableProperties.priorIds).length > 0}
{#if suggestedPropsFiltered && Object.keys(suggestedPropsFiltered).length > 0}
{#if !filterActive && suggestedPropsFiltered && Object.keys(suggestedPropsFiltered).length > 0}
<span class="font-normal text-sm text-secondary">Suggested Results</span>
<div class="overflow-y-auto mb-2">
<div class="overflow-y-auto pb-2">
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
@@ -179,112 +291,105 @@
/>
</div>
{/if}
<div class="overflow-y-auto mb-2">
<span class="font-normal text-sm text-secondary">All Results</span>
{#if !allResultsCollapsed}
{#if Object.keys(resultByIdFiltered).length > 0}
<div class="overflow-y-auto pb-2">
<span class="font-normal text-sm text-secondary">All Results</span>
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
collapseLevel={allResultsCollapsed ? 1 : undefined}
json={resultByIdFiltered}
prefix="results"
on:select
/>
</div>
{/if}
{/if}
{/if}
{#if displayContext}
{#if !filterActive || $inputMatches?.some((match) => match.word === 'variable')}
<div class="overflow-y-auto pb-2">
<span class="font-normal text-sm text-secondary">Variables:</span>
{#if displayVariable}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
allResultsCollapsed = true
displayVariable = false
}}
wrapperClasses="inline-flex whitespace-nowrap w-fit"
btnClasses="font-mono h-4 text-2xs font-thin px-1 rounded-[0.275rem]">-</Button
>
{/if}
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
bind:collapsed={allResultsCollapsed}
json={resultByIdFiltered}
prefix="results"
on:select
/>
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
rawKey={true}
json={variables}
prefix="variable"
on:select
/>
{:else}
<Button
color="light"
size="xs2"
variant="border"
on:click={async () => {
await loadVariables()
displayVariable = true
}}
wrapperClasses="inline-flex w-fit"
btnClasses="font-normal text-2xs rounded-[0.275rem] h-4 px-1"
>
{'{...}'}
</Button>
{/if}
</div>
{/if}
{#if !filterActive || $inputMatches?.some((match) => match.word === 'resource')}
<div class="overflow-y-auto pb-2">
<span class="font-normal text-sm text-secondary">Resources:</span>
{#if displayResources}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
displayResources = false
}}
wrapperClasses="inline-flex whitespace-nowrap w-fit"
btnClasses="font-mono h-4 text-2xs font-thin px-1 rounded-[0.275rem]">-</Button
>
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
rawKey={true}
json={resources}
prefix="resource"
on:select
/>
{:else}
<Button
color="light"
size="xs2"
variant="border"
on:click={async () => {
await loadResources()
displayResources = true
}}
wrapperClasses="inline-flex whitespace-nowrap w-fit"
btnClasses="font-normal text-2xs rounded-[0.275rem] h-4 px-1"
>
{'{...}'}
</Button>
{/if}
</div>
{/if}
{/if}
{#if displayContext}
<div class="overflow-y-auto mb-2">
<span class="font-normal text-sm text-secondary">Variables:</span>
{#if displayVariable}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
displayVariable = false
}}
wrapperClasses="inline-flex whitespace-nowrap w-fit"
btnClasses="font-mono h-4 text-2xs font-thin px-1 rounded-[0.275rem]">-</Button
>
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
rawKey={true}
json={variables}
prefix="variable"
on:select
/>
{:else}
<Button
color="light"
size="xs2"
variant="border"
on:click={async () => {
await loadVariables()
displayVariable = true
}}
wrapperClasses="inline-flex w-fit"
btnClasses="font-normal text-2xs rounded-[0.275rem] h-4 px-1"
>
{'{...}'}
</Button>
{/if}
</div>
<div class="overflow-y-auto mb-2">
<span class="font-normal text-sm text-secondary">Resources:</span>
{#if displayResources}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
displayResources = false
}}
wrapperClasses="inline-flex whitespace-nowrap w-fit"
btnClasses="font-mono h-4 text-2xs font-thin px-1 rounded-[0.275rem]">-</Button
>
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
rawKey={true}
json={resources}
prefix="resource"
on:select
/>
{:else}
<Button
color="light"
size="xs2"
variant="border"
on:click={async () => {
await loadResources()
displayResources = true
}}
wrapperClasses="inline-flex whitespace-nowrap w-fit"
btnClasses="font-normal text-2xs rounded-[0.275rem] h-4 px-1"
>
{'{...}'}
</Button>
{/if}
</div>
{/if}
</div>
</div>
@@ -32,9 +32,11 @@
<Tab value="routes" selectedClass="text-primary text-sm font-semibold">Routes</Tab>
<Tab value="websockets" selectedClass="text-primary text-sm font-semibold">Websockets</Tab>
<Tab value="emails" selectedClass="text-primary text-sm font-semibold">Email</Tab>
<Tab value="scheduledPoll" selectedClass="text-primary text-sm font-semibold"
>Scheduled Poll</Tab
>
{#if isFlow}
<Tab value="scheduledPoll" selectedClass="text-primary text-sm font-semibold"
>Scheduled Poll</Tab
>
{/if}
<svelte:fragment slot="content">
{#if $selectedTrigger === 'webhooks'}
+2 -2
View File
@@ -4,8 +4,8 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.418.0"
wmill_pg = ">=1.418.0"
wmill = ">=1.419.0"
wmill_pg = ">=1.419.0"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.418.0
version: 1.419.0
title: OpenFlow Spec
contact:
name: Ruben Fiszel
@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.418.0'
ModuleVersion = '1.419.0'
# Supported PSEditions
# CompatiblePSEditions = @()
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.418.0"
version = "1.419.0"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill-pg"
version = "1.418.0"
version = "1.419.0"
description = "An extension client for the wmill client library focused on pg"
license = "Apache-2.0"
homepage = "https://windmill.dev"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.418.0",
"version": "1.419.0",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./client.ts"]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.418.0",
"version": "1.419.0",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"devDependencies": {
+1 -1
View File
@@ -1 +1 @@
1.418.0
1.419.0