Compare commits

...
Author SHA1 Message Date
claude[bot]andDiego Imbert 065b0efa85 Replace all structuredClone patterns with clone() utility
- Replace structuredClone($state.snapshot(x)) with clone(x)
- Replace structuredClone(stateSnapshot(x)) with clone(x)
- Replace structuredClone(x) with clone(x)
- Add clone import to 40+ files
- Remove unused stateSnapshot imports
- Fix syntax errors and import conflicts

Total: 101 structuredClone patterns replaced across codebase

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Diego Imbert <diegoimbert@users.noreply.github.com>
2025-06-26 13:52:00 +00:00
Diego Imbert 83e5bfbc17 clone impl 2025-06-26 11:36:36 +02:00
Alexander Petric 835f1d2ec9 fix: add windows paths to uv install to find git/ssh (#6063) 2025-06-26 08:26:59 +02:00
d933648d36 fix: optionally enable CSP headers (#6033)
* feat: add optional CSP headers when ENABLE_CSP_HEADERS is set

- Add middleware to conditionally set Content-Security-Policy headers
- Check for ENABLE_CSP_HEADERS environment variable  
- Apply secure default CSP policy suitable for web applications
- Follows existing middleware patterns in codebase

Resolves #6031

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* perf: optimize CSP headers with lazy_static and add configurability

- Cache CSP_ENABLED and CSP_POLICY using lazy_static to avoid env var lookups on every request
- Add comprehensive documentation to add_csp_headers middleware function
- Make CSP policy configurable via CSP_POLICY environment variable
- Maintain backward compatibility with secure default policy
- Implement proper error handling for custom CSP policy values

Co-authored-by: Alexander Petric &lt;alpetric@users.noreply.github.com&gt;

* refactor: conditionally add CSP middleware layer instead of NO-OP

- Use .option_layer() to only add CSP middleware when ENABLE_CSP_HEADERS is set
- Remove runtime conditional check from add_csp_headers function
- Improve performance by avoiding unnecessary middleware execution when disabled

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* refactor: conditionally add CSP middleware layer instead of NO-OP

Remove CSP_ENABLED variable and instead check if CSP_POLICY is not empty.
This eliminates the need for a separate enable flag and simplifies the logic
to just check if a meaningful CSP policy is configured.

- Remove CSP_ENABLED lazy_static variable
- Simplify CSP_POLICY to use unwrap_or_default()
- Update conditional middleware logic to check !CSP_POLICY.is_empty()
- Update documentation to reflect new behavior

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

* refactor: conditionally add CSP middleware layer instead of NO-OP

Move CSP headers from general middleware stack to static assets only.
CSP headers are only relevant for browsers consuming static content,
not for API endpoints.

Changes:
- Add CSP headers directly to static asset responses in serve_path()
- Remove CSP middleware from general middleware stack
- Remove unused add_csp_headers middleware function
- Maintain same CSP_POLICY environment variable configuration

Co-authored-by: Alexander Petric <alpetric@users.noreply.github.com>

* fix: conditionally compile CSP header imports with static_frontend feature

Fixes unused import errors by wrapping HeaderValue and CSP_POLICY imports 
with #[cfg(feature = "static_frontend")] to match their usage context.

Co-authored-by: Alexander Petric <alpetric@users.noreply.github.com>

* refactor: move CSP lazy static block to static_assets.rs

- Move CSP_POLICY lazy static from lib.rs to static_assets.rs for better encapsulation
- Remove crate-level import since CSP_POLICY is now local to static_assets module
- Maintain same functionality with proper feature gating for static_frontend

Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
Co-authored-by: Alexander Petric <alpetric@users.noreply.github.com>
2025-06-25 21:15:22 +02:00
Ruben Fiszelandrubenfiszel 2039c93d4c chore(main): release 1.501.3 (#6051)
* chore(main): release 1.501.3

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-06-25 21:14:43 +02:00
Ruben Fiszel 18ee03a323 fix: improve ordering of forms for non complete ordering + array schema fix 2025-06-25 20:58:58 +02:00
Ruben Fiszel 4f6ad58e41 nit logout 2025-06-25 19:06:56 +02:00
HugoCasa 395f1ff8ba fix: missing static_asset_config from api call (#6058) 2025-06-25 19:05:08 +02:00
Guilhem 7f02e8020a remove duplicate instance of FlowPreviewContent (#6056) 2025-06-25 19:02:53 +02:00
HugoCasa 7a8c6d9dbb upgrade hf hub to prevent error on pull (#6057) 2025-06-25 18:34:47 +02:00
Tom Deckers 44457c72cf fix(backend): return correct content-type for openapi spec (#6045) 2025-06-25 16:41:35 +02:00
claude[bot]andRuben Fiszel 91a5a549c3 remove BETA label from Ansible language (#6053)
Removes the BETA designation from Ansible language in the script builder
while keeping it for Nu language as requested.

Closes #6052

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
2025-06-25 14:32:03 +02:00
Ruben Fiszel 900c8edd7b fix: ignore type only imports when computing ts lockfiles 2025-06-25 14:29:52 +02:00
Guilhem dc5e764d9d fix(frontend): load all flow jobs on page load (#6029)
* Add initial loading status for flow steps

* Add loading state

* reset module initial if test flow initial is reset

* ensure all jobs are loaded before unmouning the preview

* only use loadIndividualStepsStates when no history

* Revert "only use loadIndividualStepsStates when no history"

This reverts commit bfc37b7e7f.

* wait for all flow child to be loaded befor loading flowSteps

* Revert "wait for all flow child to be loaded befor loading flowSteps"

This reverts commit 7da81756b0.

* Load individual steps on flow load

* Add loading status in graph

* Use a context to manage initial state and save to local storage

* Handle new flow

* nit

* nit

* nit

* Prevent loading step when mock is enabled

* Load jobs based on last flow run

* Revert "Load jobs based on last flow run"

This reverts commit 212cb7f785.

* Change step initial display status

* nit

* Add parallelisation limit on step job loading
2025-06-25 13:07:49 +02:00
Diego Imbert 3f23198385 Select subtitle (#6043) 2025-06-25 13:07:31 +02:00
90 changed files with 813 additions and 411 deletions
+11
View File
@@ -1,5 +1,16 @@
# Changelog
## [1.501.3](https://github.com/windmill-labs/windmill/compare/v1.501.2...v1.501.3) (2025-06-25)
### Bug Fixes
* **backend:** return correct content-type for openapi spec ([#6045](https://github.com/windmill-labs/windmill/issues/6045)) ([44457c7](https://github.com/windmill-labs/windmill/commit/44457c72cf75c969de97c39bb23f57acad268e10))
* **frontend:** load all flow jobs on page load ([#6029](https://github.com/windmill-labs/windmill/issues/6029)) ([dc5e764](https://github.com/windmill-labs/windmill/commit/dc5e764d9db9251dc356094d6ac47c45fdf72c74))
* ignore type only imports when computing ts lockfiles ([900c8ed](https://github.com/windmill-labs/windmill/commit/900c8edd7b35802e23a1359029da8ddbfb783753))
* improve ordering of forms for non complete ordering + array schema fix ([18ee03a](https://github.com/windmill-labs/windmill/commit/18ee03a32371885f5e608cb306b5ccbccc31dac5))
* missing static_asset_config from api call ([#6058](https://github.com/windmill-labs/windmill/issues/6058)) ([395f1ff](https://github.com/windmill-labs/windmill/commit/395f1ff8ba05020d72d1d8b34bd6bb32517b7aec))
## [1.501.2](https://github.com/windmill-labs/windmill/compare/v1.501.1...v1.501.2) (2025-06-24)
+94 -43
View File
@@ -2364,9 +2364,9 @@ dependencies = [
[[package]]
name = "crunchy"
version = "0.2.3"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-bigint"
@@ -4502,6 +4502,15 @@ dependencies = [
"dirs-sys 0.4.1",
]
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys 0.5.0",
]
[[package]]
name = "dirs-next"
version = "2.0.0"
@@ -4519,7 +4528,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users",
"redox_users 0.4.6",
"winapi",
]
@@ -4531,10 +4540,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"redox_users 0.4.6",
"windows-sys 0.48.0",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users 0.5.0",
"windows-sys 0.60.2",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
@@ -4542,7 +4563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"redox_users 0.4.6",
"winapi",
]
@@ -6149,19 +6170,26 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "hf-hub"
version = "0.3.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b780635574b3d92f036890d8373433d6f9fc7abb320ee42a5c25897fc8ed732"
checksum = "629d8f3bbeda9d148036d6b0de0a3ab947abd08ce90626327fc3547a49d59d97"
dependencies = [
"dirs 5.0.1",
"dirs 6.0.0",
"futures",
"http 1.3.1",
"indicatif",
"libc",
"log",
"native-tls",
"rand 0.8.5",
"num_cpus",
"rand 0.9.0",
"reqwest 0.12.20",
"serde",
"serde_json",
"thiserror 1.0.69",
"thiserror 2.0.12",
"tokio",
"ureq",
"windows-sys 0.60.2",
]
[[package]]
@@ -7266,9 +7294,9 @@ dependencies = [
[[package]]
name = "libredox"
version = "0.1.3"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638"
dependencies = [
"bitflags 2.9.1",
"libc",
@@ -10103,6 +10131,17 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "redox_users"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror 2.0.12",
]
[[package]]
name = "ref-cast"
version = "1.0.24"
@@ -10411,7 +10450,7 @@ dependencies = [
[[package]]
name = "rmcp"
version = "0.1.5"
source = "git+https://github.com/modelcontextprotocol/rust-sdk#893969ac7780897bbb804d41fb98347553386f33"
source = "git+https://github.com/modelcontextprotocol/rust-sdk#e6effb327883d0a8a91f5efbb527194747eee57b"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -10440,7 +10479,7 @@ dependencies = [
[[package]]
name = "rmcp-macros"
version = "0.1.5"
source = "git+https://github.com/modelcontextprotocol/rust-sdk#893969ac7780897bbb804d41fb98347553386f33"
source = "git+https://github.com/modelcontextprotocol/rust-sdk#e6effb327883d0a8a91f5efbb527194747eee57b"
dependencies = [
"darling 0.20.11",
"proc-macro2",
@@ -11599,6 +11638,17 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "socks"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b"
dependencies = [
"byteorder",
"libc",
"winapi",
]
[[package]]
name = "sourcemap"
version = "8.0.1"
@@ -14026,6 +14076,7 @@ dependencies = [
"rustls-pki-types",
"serde",
"serde_json",
"socks",
"url",
"webpki-roots 0.26.11",
]
@@ -14565,7 +14616,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"axum",
@@ -14617,7 +14668,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"argon2",
@@ -14728,7 +14779,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -14743,7 +14794,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"chrono",
"serde",
@@ -14756,7 +14807,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"serde",
@@ -14770,7 +14821,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"async-stream",
@@ -14847,7 +14898,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"regex",
"serde",
@@ -14861,7 +14912,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"bytes",
@@ -14884,7 +14935,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -14896,7 +14947,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -14905,7 +14956,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"lazy_static",
@@ -14917,7 +14968,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"serde_json",
@@ -14929,7 +14980,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"gosyn",
@@ -14941,7 +14992,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"lazy_static",
@@ -14953,7 +15004,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"serde_json",
@@ -14965,7 +15016,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"nu-parser",
@@ -14976,7 +15027,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -14987,7 +15038,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -14998,7 +15049,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"async-recursion",
@@ -15021,7 +15072,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -15038,7 +15089,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"lazy_static",
@@ -15050,7 +15101,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"lazy_static",
@@ -15068,7 +15119,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"getrandom 0.2.16",
@@ -15092,7 +15143,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"serde_json",
@@ -15102,7 +15153,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"async-recursion",
@@ -15135,7 +15186,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -15145,7 +15196,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.501.2"
version = "1.501.3"
dependencies = [
"anyhow",
"async-recursion",
@@ -15781,9 +15832,9 @@ dependencies = [
[[package]]
name = "xattr"
version = "1.5.0"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e"
checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909"
dependencies = [
"libc",
"rustix 1.0.7",
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.501.2"
version = "1.501.3"
authors.workspace = true
edition.workspace = true
@@ -32,7 +32,7 @@ members = [
]
[workspace.package]
version = "1.501.2"
version = "1.501.3"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -329,7 +329,7 @@ jsonwebtoken = "8.3.0"
pem = "3.0.1"
nix = { version = "0.27.1", features = ["process", "signal"] }
tinyvector = { git = "https://github.com/windmill-labs/tinyvector", rev = "20823b94c20f2b9093f318badd24026cf54dcc85" }
hf-hub = "0.3.2"
hf-hub = "0.4.3"
tokenizers = "0.14.1"
candle-core = "0.9.1"
candle-transformers = "0.9.1"
+27 -3
View File
@@ -29,12 +29,36 @@ use wasm_bindgen::prelude::*;
struct ImportsFinder {
imports: HashSet<String>,
skip_type_only: bool,
}
impl Visit for ImportsFinder {
noop_visit_type!();
fn visit_import_decl(&mut self, n: &swc_ecma_ast::ImportDecl) {
if self.skip_type_only {
if n.type_only {
return;
}
if n.specifiers.len() > 0 {
let mut is_type_only = true;
for specifier in n.specifiers.iter() {
match specifier {
swc_ecma_ast::ImportSpecifier::Named(
swc_ecma_ast::ImportNamedSpecifier { is_type_only, .. },
) if *is_type_only => (),
_ => {
is_type_only = false;
break;
}
}
}
if is_type_only {
return;
}
}
}
if let Some(ref s) = n.src.raw {
let s = s.to_string();
if s.starts_with("'") && s.ends_with("'") {
@@ -46,7 +70,7 @@ impl Visit for ImportsFinder {
}
}
pub fn parse_expr_for_imports(code: &str) -> anyhow::Result<Vec<String>> {
pub fn parse_expr_for_imports(code: &str, skip_type_only: bool) -> anyhow::Result<Vec<String>> {
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(FileName::Custom("main.d.ts".into()).into(), code.into());
let mut tss = TsSyntax::default();
@@ -72,7 +96,7 @@ pub fn parse_expr_for_imports(code: &str) -> anyhow::Result<Vec<String>> {
anyhow::anyhow!("Error while parsing code, it is invalid TypeScript: {err_s}, {e:?}")
})?;
let mut visitor = ImportsFinder { imports: HashSet::new() };
let mut visitor = ImportsFinder { imports: HashSet::new(), skip_type_only };
visitor.visit_module(&expr);
let mut imports: Vec<_> = visitor.imports.into_iter().collect();
@@ -318,7 +342,7 @@ lazy_static::lazy_static! {
}
pub fn remove_pinned_imports(code: &str) -> anyhow::Result<String> {
let mut imports = parse_expr_for_imports(code)?;
let mut imports = parse_expr_for_imports(code, false)?;
imports.sort_by_key(|f| 0 - (f.len() as i32));
let mut content = code.to_string();
for import in imports {
@@ -0,0 +1,19 @@
mod tests {
use windmill_parser_ts::parse_expr_for_imports;
#[test]
fn test_imports() {
let code = r#"
import { foo } from "bar";
import type { foo } from "bar2";
import { type foo, bar } from "bar3";
import { bar, type foo } from "bar7";
import { type foo, type bar } from "bar4";
import * as foo from "bar5";
import foo from "bar6";
"#;
let imports = parse_expr_for_imports(code, true).unwrap();
assert_eq!(imports, vec!["bar", "bar3", "bar5", "bar6", "bar7"]);
}
}
@@ -41,7 +41,7 @@ pub fn parse_outputs(code: &str) -> String {
#[cfg(feature = "ts-parser")]
#[wasm_bindgen]
pub fn parse_ts_imports(code: &str) -> String {
let parsed = parse_expr_for_imports(code);
let parsed = parse_expr_for_imports(code, false);
let r = if let Ok(parsed) = parsed {
json!({ "imports": parsed })
} else {
@@ -339,7 +339,7 @@ fn test_parse_imports() -> anyhow::Result<()> {
import { bar } from \"bar/foo/d\";
import { bar as baroof } from \"bar\";
";
let mut l = parse_expr_for_imports(code)?;
let mut l = parse_expr_for_imports(code, false)?;
l.sort();
assert_eq!(
l,
@@ -360,7 +360,7 @@ fn test_parse_imports_dts() -> anyhow::Result<()> {
let code = "
export type foo = number
";
let mut l = parse_expr_for_imports(code)?;
let mut l = parse_expr_for_imports(code, false)?;
l.sort();
assert_eq!(l, vec![] as Vec<String>);
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.501.2
version: 1.501.3
title: Windmill API
contact:
+15 -56
View File
@@ -24,7 +24,7 @@ use candle_nn::VarBuilder;
#[cfg(feature = "embedding")]
use candle_transformers::models::bert::{BertModel, Config, DTYPE};
#[cfg(feature = "embedding")]
use hf_hub::{api::sync::Api, Cache, Repo};
use hf_hub::api::tokio::Api;
#[cfg(feature = "embedding")]
use serde::Deserialize;
#[cfg(feature = "embedding")]
@@ -158,63 +158,22 @@ pub struct ModelInstance {
#[cfg(feature = "embedding")]
impl ModelInstance {
pub async fn load_model_files() -> Result<(PathBuf, PathBuf, PathBuf)> {
let repo = Repo::model("thenlper/gte-small".to_string());
let cache = Cache::default().repo(repo.clone());
let api = Api::new()?;
let api = api.repo(repo);
let repo_api = api.model("thenlper/gte-small".to_string());
let (config_filename, tokenizer_filename, weights_filename) = (
cache
.get("config.json")
.or_else(|| {
api.get("config.json")
.or_else(|e| {
tracing::error!("Failed to get config.json from hugging face: {}", e);
return Err(e);
})
.ok()
})
.ok_or(Error::msg("could not get config.json"))?,
cache
.get("tokenizer.json")
.or_else(|| {
api.get("tokenizer.json")
.or_else(|e| {
tracing::error!(
"Failed to get tokenizer.json from hugging face: {}",
e
);
return Err(e);
})
.ok()
})
.ok_or(Error::msg("could not get tokenizer.json"))?,
cache
.get("model.safetensors")
.and_then(|p| {
tracing::info!("Found embedding model in cache");
Some(p)
})
.or_else(|| {
tracing::info!("Downloading embedding model...");
api.get("model.safetensors")
.or_else(|e| {
tracing::error!(
"Failed to get model.safetensors from hugging face: {}",
e
);
return Err(e);
})
.ok()
.and_then(|p| {
tracing::info!("Downloaded embedding model");
Some(p)
})
})
.ok_or(Error::msg("could not get model.safetensors"))?,
);
let (config_filename, tokenizer_filename, weights_filename) =
(
repo_api
.get("config.json")
.await
.map_err(|e| anyhow!("Failed to get config.json from hugging face: {}", e))?,
repo_api.get("tokenizer.json").await.map_err(|e| {
anyhow!("Failed to get tokenizer.json from hugging face: {}", e)
})?,
repo_api.get("model.safetensors").await.map_err(|e| {
anyhow!("Failed to get model.safetensors from hugging face: {}", e)
})?,
);
Ok((config_filename, tokenizer_filename, weights_filename))
}
+15 -5
View File
@@ -36,8 +36,10 @@ use anyhow::Context;
use argon2::Argon2;
use axum::extract::DefaultBodyLimit;
use axum::{middleware::from_extractor, routing::get, routing::post, Extension, Router};
use axum::response::Response;
use axum::http::HeaderValue;
use axum::body::Body;
use db::DB;
use http::HeaderValue;
use reqwest::Client;
#[cfg(feature = "oauth2")]
use std::collections::HashMap;
@@ -236,6 +238,7 @@ lazy_static::lazy_static! {
}
// Compliance with cloud events spec.
pub async fn add_webhook_allowed_origin(
req: axum::extract::Request,
@@ -258,6 +261,7 @@ pub async fn add_webhook_allowed_origin(
next.run(req).await
}
#[cfg(not(feature = "tantivy"))]
type IndexReader = ();
@@ -890,12 +894,18 @@ async fn ee_license() -> String {
}
}
async fn openapi() -> &'static str {
include_str!("../openapi-deref.yaml")
async fn openapi() -> Response {
Response::builder()
.header("content-type", "application/yaml")
.body(Body::from(include_str!("../openapi-deref.yaml")))
.unwrap()
}
async fn openapi_json() -> &'static str {
include_str!("../openapi-deref.json")
async fn openapi_json() -> Response {
Response::builder()
.header("content-type", "application/json")
.body(Body::from(include_str!("../openapi-deref.json")))
.unwrap()
}
pub async fn migrate_db(db: &DB) -> anyhow::Result<Option<JoinHandle<()>>> {
+15
View File
@@ -10,6 +10,8 @@ use axum::{body::Body, extract::OriginalUri, http::Response, response::IntoRespo
#[cfg(feature = "static_frontend")]
use axum::http::header;
#[cfg(feature = "static_frontend")]
use http::HeaderValue;
use hyper::Uri;
#[cfg(feature = "static_frontend")]
@@ -17,6 +19,12 @@ use mime_guess::mime;
#[cfg(feature = "static_frontend")]
use rust_embed::RustEmbed;
// Content Security Policy configuration
#[cfg(feature = "static_frontend")]
lazy_static::lazy_static! {
static ref CSP_POLICY: String = std::env::var("CSP_POLICY").unwrap_or_default();
}
// static_handler is a handler that serves static files from the
pub async fn static_handler(OriginalUri(original_uri): OriginalUri) -> StaticFile {
StaticFile(original_uri)
@@ -51,6 +59,13 @@ fn serve_path(path: &str) -> Response<Body> {
let mut res = Response::builder()
.header(header::CONTENT_TYPE, mime.as_ref())
.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*");
// Add Content-Security-Policy header for static assets when policy is set
if !CSP_POLICY.is_empty() {
if let Ok(header_value) = HeaderValue::try_from(CSP_POLICY.as_str()) {
res = res.header("Content-Security-Policy", header_value);
}
}
if mime.as_ref() == mime::APPLICATION_JAVASCRIPT
|| mime.as_ref() == mime::TEXT_JAVASCRIPT
|| path.ends_with(".wasm")
+56 -8
View File
@@ -66,10 +66,10 @@ lazy_static::lazy_static! {
lazy_static::lazy_static! {
static ref PIPTAR_UPLOAD_CHANNEL: tokio::sync::mpsc::UnboundedSender<PiptarUploadTask> = {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
// Spawn background task to handle uploads sequentially
tokio::spawn(handle_piptar_uploads(rx));
tx
};
}
@@ -85,7 +85,7 @@ struct PiptarUploadTask {
async fn handle_piptar_uploads(mut rx: tokio::sync::mpsc::UnboundedReceiver<PiptarUploadTask>) {
use crate::global_cache::build_tar_and_push;
use windmill_common::s3_helpers::get_object_store;
while let Some(task) = rx.recv().await {
if let Some(os) = get_object_store().await {
match build_tar_and_push(os, task.venv_path.clone(), task.cache_dir, None, false).await {
@@ -324,6 +324,7 @@ pub async fn uv_pip_compile(
child_cmd
.env("SystemRoot", SYSTEM_ROOT.as_str())
.env("USERPROFILE", crate::USERPROFILE_ENV.as_str())
.env("HOME", crate::USERPROFILE_ENV.as_str())
.env(
"LOCALAPPDATA",
std::env::var("LOCALAPPDATA")
@@ -332,6 +333,29 @@ pub async fn uv_pip_compile(
.env(
"TMP",
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
)
.env(
"APPDATA",
std::env::var("APPDATA")
.unwrap_or_else(|_| format!("{}\\AppData\\Roaming", crate::USERPROFILE_ENV.as_str())),
)
.env(
"ComSpec",
std::env::var("ComSpec").unwrap_or_else(|_| String::from("C:\\Windows\\System32\\cmd.exe")),
)
.env(
"PATHEXT",
std::env::var("PATHEXT").unwrap_or_else(|_|
String::from(".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL")
),
)
.env(
"ProgramData",
std::env::var("ProgramData").unwrap_or_else(|_| String::from("C:\\ProgramData")),
)
.env(
"ProgramFiles",
std::env::var("ProgramFiles").unwrap_or_else(|_| String::from("C:\\Program Files")),
);
}
@@ -602,7 +626,7 @@ pub async fn handle_python_job(
if v == '<function call>':
del pre_args[k]
kwargs = inner_script.preprocessor(**pre_args)
kwrags_json = res_to_json(kwargs)
kwrags_json = res_to_json(kwargs)
with open("args.json", 'w') as f:
f.write(kwrags_json)"#
)
@@ -678,7 +702,7 @@ except BaseException as e:
tb = traceback.format_tb(exc_traceback)
with open(result_json, 'w') as f:
err = {{ "message": str(e), "name": e.__class__.__name__, "stack": '\n'.join(tb[1:]) }}
extra = e.__dict__
extra = e.__dict__
if extra and len(extra) > 0:
err['extra'] = extra
flow_node_id = os.environ.get('WM_FLOW_STEP_ID')
@@ -1417,6 +1441,7 @@ async fn spawn_uv_install(
.envs(PROXY_ENVS.clone())
.env("SystemRoot", SYSTEM_ROOT.as_str())
.env("USERPROFILE", crate::USERPROFILE_ENV.as_str())
.env("HOME", HOME_ENV.as_str())
.env(
"TMP",
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
@@ -1426,6 +1451,29 @@ async fn spawn_uv_install(
std::env::var("LOCALAPPDATA")
.unwrap_or_else(|_| format!("{}\\AppData\\Local", HOME_ENV.as_str())),
)
.env(
"APPDATA",
std::env::var("APPDATA")
.unwrap_or_else(|_| format!("{}\\AppData\\Roaming", crate::USERPROFILE_ENV.as_str())),
)
.env(
"ComSpec",
std::env::var("ComSpec").unwrap_or_else(|_| String::from("C:\\Windows\\System32\\cmd.exe")),
)
.env(
"PATHEXT",
std::env::var("PATHEXT").unwrap_or_else(|_|
String::from(".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL")
),
)
.env(
"ProgramData",
std::env::var("ProgramData").unwrap_or_else(|_| String::from("C:\\ProgramData")),
)
.env(
"ProgramFiles",
std::env::var("ProgramFiles").unwrap_or_else(|_| String::from("C:\\Program Files")),
)
.args(&command_args[1..])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
@@ -1810,7 +1858,7 @@ pub async fn handle_python_reqs(
// Create a file to indicate that installation was successfull
let valid_path = venv_p.clone() + "/.valid.windmill";
// This is atomic operation, meaning, that it either completes and wheel is valid,
// This is atomic operation, meaning, that it either completes and wheel is valid,
// or it does not and wheel is invalid and will be reinstalled next run
if let Err(e) = File::create(&valid_path).await{
tracing::error!(
@@ -1942,7 +1990,7 @@ pub async fn handle_python_reqs(
venv_path: venv_p.clone(),
cache_dir: py_version.to_cache_dir_top_level(false),
};
if let Err(e) = PIPTAR_UPLOAD_CHANNEL.send(upload_task) {
tracing::warn!("Failed to queue piptar upload for {venv_p}: {e}");
} else {
@@ -1961,7 +2009,7 @@ pub async fn handle_python_reqs(
pids.lock().await.get_mut(i).and_then(|e| e.take());
// Create a file to indicate that installation was successfull
let valid_path = venv_p.clone() + "/.valid.windmill";
// This is atomic operation, meaning, that it either completes and wheel is valid,
// This is atomic operation, meaning, that it either completes and wheel is valid,
// or it does not and wheel is invalid and will be reinstalled next run
if let Err(e) = File::create(&valid_path).await{
tracing::error!(
@@ -188,7 +188,7 @@ fn try_normalize(path: &Path) -> Option<PathBuf> {
fn parse_ts_relative_imports(raw_code: &str, script_path: &str) -> error::Result<Vec<String>> {
let mut relative_imports = vec![];
let r = parse_expr_for_imports(raw_code)?;
let r = parse_expr_for_imports(raw_code, true)?;
for import in r {
let import = import.trim_end_matches(".ts");
if import.starts_with("/") {
+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.501.2";
export const VERSION = "v1.501.3";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+1 -1
View File
@@ -63,7 +63,7 @@ export {
// }
// });
export const VERSION = "1.501.2";
export const VERSION = "1.501.3";
const command = new Command()
.name("wmill")
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.501.2",
"version": "1.501.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.501.2",
"version": "1.501.3",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.501.2",
"version": "1.501.3",
"scripts": {
"dev": "vite dev",
"build": "vite build",
+5 -4
View File
@@ -7,7 +7,8 @@
setInputCat as computeInputCat,
debounce,
emptyString,
getSchemaFromProperties
getSchemaFromProperties,
clone
} from '$lib/utils'
import { DollarSign, Pipette, Plus, X, Check, Loader2 } from 'lucide-svelte'
import { createEventDispatcher, onDestroy, onMount, tick, untrack } from 'svelte'
@@ -244,7 +245,7 @@
nvalue = undefined
}
if ((value == undefined || value == null) && !ignoreValueUndefined) {
nvalue = structuredClone($state.snapshot(defaultValue))
nvalue = clone(defaultValue)
if (defaultValue === undefined || defaultValue === null) {
if (inputCat === 'string') {
nvalue = nullable ? null : ''
@@ -296,7 +297,7 @@
lastValue = value
}
let oldDefaultValue = structuredClone($state.snapshot(defaultValue))
let oldDefaultValue = clone(defaultValue)
function handleDefaultValueChange() {
if (
deepEqual(value, oldDefaultValue) &&
@@ -305,7 +306,7 @@
) {
value = defaultValue
}
oldDefaultValue = structuredClone($state.snapshot(defaultValue))
oldDefaultValue = clone(defaultValue)
}
function isObjectCat(inputCat?: string) {
@@ -8,6 +8,8 @@
import Alert from './common/alert/Alert.svelte'
import EditableSchemaDrawer from './schema/EditableSchemaDrawer.svelte'
import type { SchemaProperty } from '$lib/common'
import Toggle from './Toggle.svelte'
import { tick } from 'svelte'
interface Props {
canEditResourceType?: boolean
@@ -48,20 +50,6 @@
? 'bytes'
: 'string'
)
let schema = $state({
properties: itemsType?.properties || {},
order: Object.keys(itemsType?.properties || {}),
required: Object.values(itemsType?.properties || {}).map((p) => p.required)
})
function updateItemsType() {
itemsType = {
...itemsType,
properties: schema.properties,
type: 'object'
}
}
</script>
{#if canEditResourceType || originalType == 'string[]' || originalType == 'object[]'}
@@ -180,10 +168,40 @@
{/if}
{#if selected === 'object'}
<EditableSchemaDrawer
bind:schema
on:change={() => {
updateItemsType()
}}
<Toggle
bind:checked={
() => {
return itemsType?.properties != undefined
},
async (v) => {
await tick()
if (v) {
itemsType = { type: 'object', properties: {} }
} else {
itemsType = { type: 'object', properties: undefined }
}
}
}
options={{ left: 'JSON', right: 'Custom Object' }}
/>
{#if itemsType?.properties != undefined}
<EditableSchemaDrawer
bind:schema={
() => {
return {
properties: itemsType?.properties || {},
order: Object.keys(itemsType?.properties || {}),
required: Object.values(itemsType?.properties || {}).map((p) => p.required)
}
},
(schema) => {
itemsType = {
...itemsType,
properties: schema.properties,
type: 'object'
}
}
}
/>
{/if}
{/if}
@@ -9,7 +9,8 @@
orderedJsonStringify,
orderedYamlStringify,
replaceFalseWithUndefined,
type Value
type Value,
clone
} from '$lib/utils'
import type { Script } from '$lib/gen'
@@ -68,7 +69,7 @@
}
function prepareDiff(data: Value) {
const metadata = structuredClone(cleanValueProperties(replaceFalseWithUndefined(data)))
const metadata = clone(cleanValueProperties(replaceFalseWithUndefined(data)))
const content = metadata['content']
if (metadata['content'] !== undefined) {
metadata['content'] = 'check content diff'
+2 -2
View File
@@ -23,7 +23,7 @@
import Tooltip from './Tooltip.svelte'
import { Loader2 } from 'lucide-svelte'
import { untrack } from 'svelte'
import { readFieldsRecursively } from '$lib/utils'
import { readFieldsRecursively, clone} from '$lib/utils'
interface Props {
value?: any
@@ -37,7 +37,7 @@
let { value = $bindable(), helperScript, entrypoint, args: _args, name }: Props = $props()
let args = $state(structuredClone($state.snapshot(_args)))
let args = $state(clone(_args))
$effect(() => {
readFieldsRecursively(_args, { excludeField: [name] })
untrack(() => !deepEqual(args, _args) && (args = $state.snapshot(_args)))
@@ -20,7 +20,7 @@
import Label from './Label.svelte'
import { sendUserToast } from '$lib/toast'
import Toggle from './Toggle.svelte'
import { emptyString } from '$lib/utils'
import { emptyString, clone} from '$lib/utils'
import Popover from './meltComponents/Popover.svelte'
import SchemaFormDnd from './schema/SchemaFormDND.svelte'
import { deepEqual } from 'fast-equals'
@@ -114,7 +114,7 @@
Object.keys(schema?.properties ?? {}).forEach((key) => {
if (schema?.properties[key].default != undefined && args?.[key] == undefined) {
let value = schema?.properties[key].default
nargs[key] = value === 'object' ? structuredClone($state.snapshot(value)) : value
nargs[key] = value === 'object' ? clone(value) : value
}
})
args = nargs
+64 -10
View File
@@ -27,7 +27,8 @@
readFieldsRecursively,
replaceFalseWithUndefined,
type StateStore,
type Value
type Value,
clone
} from '$lib/utils'
import { sendUserToast } from '$lib/toast'
import { Drawer } from '$lib/components/common'
@@ -75,6 +76,11 @@
import { Triggers } from './triggers/triggers.svelte'
import { TestSteps } from './flows/testSteps.svelte'
import { aiChatManager } from './copilot/chat/AIChatManager.svelte'
import {
setStepHistoryLoaderContext,
StepHistoryLoader,
type stepState
} from './stepHistoryLoader.svelte'
interface Props {
initialPath?: string
@@ -96,6 +102,11 @@
draftTriggersFromUrl?: Trigger[] | undefined
selectedTriggerIndexFromUrl?: number | undefined
children?: import('svelte').Snippet
loadedFromHistoryFromUrl?: {
flowJobInitial: boolean | undefined
stepsState: Record<string, stepState>
}
noInitial?: boolean
}
let {
@@ -117,7 +128,9 @@
setSavedraftCb = undefined,
draftTriggersFromUrl = undefined,
selectedTriggerIndexFromUrl = undefined,
children
children,
loadedFromHistoryFromUrl,
noInitial = false
}: Props = $props()
let initialPathStore = writable(initialPath)
@@ -173,7 +186,7 @@
savedValue: savedFlow,
modifiedValue: {
...flowStore.val,
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
draft_triggers: clone(triggersState.getDraftTriggersSnapshot())
}
}
}
@@ -238,7 +251,7 @@
if (savedFlow) {
const draftOrDeployed = cleanValueProperties(savedFlow.draft || savedFlow)
const currentDraftTriggers = structuredClone(triggersState.getDraftTriggersSnapshot())
const currentDraftTriggers = clone(triggersState.getDraftTriggersSnapshot())
const current = cleanValueProperties(
$state.snapshot({
...flowStore.val,
@@ -317,15 +330,15 @@
savedFlow = {
...(newFlow || savedFlow?.draft_only
? {
...structuredClone($state.snapshot(flowStore.val)),
...clone(flowStore.val),
path: $pathStore,
draft_only: true
}
: savedFlow),
draft: {
...structuredClone($state.snapshot(flowStore.val)),
...clone(flowStore.val),
path: $pathStore,
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
draft_triggers: clone(triggersState.getDraftTriggersSnapshot())
}
} as FlowWithDraftAndDraftTriggers
@@ -507,7 +520,7 @@
draft_triggers: Trigger[]
}
savedFlow = {
...structuredClone($state.snapshot(newSavedFlow)),
...clone(newSavedFlow),
path: $pathStore
} as Flow
setDraftTriggers([])
@@ -533,7 +546,11 @@
path: $pathStore,
selectedId: $selectedIdStore,
draft_triggers: triggersState.getDraftTriggersSnapshot(),
selected_trigger: triggersState.getSelectedTriggerSnapshot()
selected_trigger: triggersState.getSelectedTriggerSnapshot(),
loadedFromHistory: {
flowJobInitial: stepHistoryLoader.flowJobInitial,
stepsState: stepHistoryLoader.stepStates
}
})
)
} catch (err) {
@@ -833,6 +850,40 @@
const hasAiDiff = aiChatManager.flowAiChatHelpers?.hasDiff() ?? false
customUi && untrack(() => onCustomUiChange(customUi, hasAiDiff))
})
export async function loadFlowState() {
await stepHistoryLoader.loadIndividualStepsStates(
flowStore.val as Flow,
flowStateStore,
$workspaceStore!,
$initialPathStore,
$pathStore
)
}
let stepHistoryLoader = new StepHistoryLoader(
loadedFromHistoryFromUrl?.stepsState ?? {},
loadedFromHistoryFromUrl?.flowJobInitial,
saveSessionDraft,
noInitial
)
setStepHistoryLoaderContext(stepHistoryLoader)
export function setLoadedFromHistory(
loadedFromHistoryUrl:
| {
flowJobInitial: boolean | undefined
stepsState: Record<string, stepState>
}
| undefined
) {
if (!loadedFromHistoryUrl) {
return
}
stepHistoryLoader.setFlowJobInitial(loadedFromHistoryUrl.flowJobInitial)
stepHistoryLoader.stepStates = loadedFromHistoryUrl.stepsState
}
</script>
<svelte:window onkeydown={onKeyDown} />
@@ -992,7 +1043,7 @@
await syncWithDeployed()
const currentDraftTriggers = structuredClone(
const currentDraftTriggers = clone(
triggersState.getDraftTriggersSnapshot()
)
@@ -1093,6 +1144,9 @@
}}
{forceTestTab}
{highlightArg}
onRunPreview={() => {
flowPreviewButtons?.openPreview(true)
}}
/>
{:else}
<CenteredPage>Loading...</CenteredPage>
@@ -18,9 +18,9 @@
import Toggle from './Toggle.svelte'
import JsonInputs from './JsonInputs.svelte'
import FlowHistoryJobPicker from './FlowHistoryJobPicker.svelte'
import { NEVER_TESTED_THIS_FAR } from './flows/models'
import { writable, type Writable } from 'svelte/store'
import type { DurationStatus, GraphModuleState } from './graph'
import { getStepHistoryLoaderContext } from './stepHistoryLoader.svelte'
import { aiChatManager } from './copilot/chat/AIChatManager.svelte'
import { stateSnapshot } from '$lib/svelte5Utils.svelte'
@@ -30,7 +30,6 @@
export let jobId: string | undefined = undefined
export let job: Job | undefined = undefined
export let initial: boolean = false
export let selectedJobStep: string | undefined = undefined
export let selectedJobStepIsTopLevel: boolean | undefined = undefined
@@ -71,6 +70,8 @@
} = getContext<FlowEditorContext>('FlowEditorContext')
const dispatch = createEventDispatcher()
let stepHistoryLoader = getStepHistoryLoaderContext()
let renderCount: number = 0
let schemaFormWithArgPicker: SchemaFormWithArgPicker | undefined = undefined
let currentJobId: string | undefined = undefined
@@ -96,8 +97,8 @@
args: Record<string, any>,
restartedFrom: RestartedFrom | undefined
) {
if (initial) {
initial = false
if (stepHistoryLoader?.flowJobInitial) {
stepHistoryLoader?.setFlowJobInitial(false)
}
try {
lastPreviewFlow = JSON.stringify(flowStore.val)
@@ -186,44 +187,6 @@
$: selectedJobStep !== undefined && onSelectedJobStepChange()
async function loadIndividualStepsStates() {
// console.log('loadIndividualStepsStates')
dfs(flowStore.val.value.modules, async (module) => {
// console.log('module', $flowStateStore[module.id], module.id)
const prev = $flowStateStore[module.id]?.previewResult
if (prev && prev != NEVER_TESTED_THIS_FAR) {
return
}
const previousJobId = await JobService.listJobs({
workspace: $workspaceStore!,
scriptPathExact:
`path` in module.value
? module.value.path
: ($initialPathStore == '' ? $pathStore : $initialPathStore) + '/' + module.id,
jobKinds: ['preview', 'script', 'flowpreview', 'flow', 'flowscript'].join(','),
page: 1,
perPage: 1
})
// console.log('previousJobId', previousJobId, module.id)
if (previousJobId.length > 0) {
const getJobResult = await JobService.getCompletedJobResultMaybe({
workspace: $workspaceStore!,
id: previousJobId[0].id
})
if ('result' in getJobResult) {
$flowStateStore[module.id] = {
...($flowStateStore[module.id] ?? {}),
previewResult: getJobResult.result,
previewJobId: previousJobId[0].id,
previewWorkspaceId: previousJobId[0].workspace_id,
previewSuccess: getJobResult.success
}
}
}
})
}
let scrollableDiv: HTMLDivElement | undefined = undefined
function handleScroll() {
scrollTop = scrollableDiv?.scrollTop ?? 0
@@ -486,16 +449,15 @@
>
<FlowHistoryJobPicker
selectInitial={jobId == undefined}
on:nohistory={() => {
loadIndividualStepsStates()
}}
on:select={(e) => {
if (!currentJobId) {
currentJobId = jobId
}
const detail = e.detail
initial = detail.initial
jobId = detail.jobId
if (detail.initial && stepHistoryLoader?.flowJobInitial === undefined) {
stepHistoryLoader?.setFlowJobInitial(detail.initial)
}
}}
on:unselect={() => {
jobId = currentJobId
@@ -505,12 +467,12 @@
/>
</div>
{#if jobId}
{#if initial}
{#if stepHistoryLoader?.flowJobInitial}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
on:click={() => {
initial = false
stepHistoryLoader?.setFlowJobInitial(false)
}}
class="cursor-pointer h-full hover:bg-gray-500/20 dark:hover:bg-gray-500/20 dark:bg-gray-500/80 rounded bg-gray-500/40 absolute top-0 left-0 w-full z-50"
>
@@ -532,12 +494,6 @@
on:done={() => {
$executionCount = $executionCount + 1
}}
on:jobsLoaded={() => {
if (initial) {
console.log('loading initial steps after initial job loaded')
loadIndividualStepsStates()
}
}}
bind:selectedJobStep
bind:rightColumnSelect
/>
@@ -1,4 +1,5 @@
<script lang="ts">
import { clone } from '$lib/utils'
import { type RunnableType, type Job } from '$lib/gen/index.js'
import { sendUserToast } from '$lib/utils.js'
import RunningJobSchemaPicker from '$lib/components/schema/RunningJobSchemaPicker.svelte'
@@ -48,7 +49,7 @@
dispatch('select', { args: fullPayload, jobId: data.id })
} else {
dispatch('select', {
args: structuredClone($state.snapshot(data.payloadData)),
args: clone(data.payloadData),
jobId: data.id
})
}
@@ -28,7 +28,7 @@
import type ItemPicker from './ItemPicker.svelte'
import type { InputTransform } from '$lib/gen'
import TemplateEditor from './TemplateEditor.svelte'
import { setInputCat as computeInputCat, isCodeInjection } from '$lib/utils'
import { setInputCat as computeInputCat, isCodeInjection, clone} from '$lib/utils'
import { FunctionSquare, InfoIcon } from 'lucide-svelte'
import { getResourceTypes } from './resourceTypesStore'
import type { FlowCopilotContext } from './copilot/flow'
@@ -280,7 +280,7 @@
function onArgChange() {
const newArg = { arg, propertyType, inputCat }
if (!deepEqual(newArg, prevArg)) {
prevArg = structuredClone($state.snapshot(newArg))
prevArg = clone(newArg)
updateStaticInput(inputCat, propertyType, arg)
}
}
@@ -24,6 +24,7 @@
disableMock?: boolean
disableHistory?: boolean
onUpdateMock?: (mock: { enabled: boolean; return_value?: unknown }) => void
loadingHistory?: boolean
}
let {
@@ -38,7 +39,8 @@
testIsLoading = false,
disableMock = false,
disableHistory = false,
onUpdateMock
onUpdateMock,
loadingHistory = false
}: Props = $props()
const { testSteps } = getContext<FlowEditorContext>('FlowEditorContext')
@@ -82,7 +84,7 @@
mock={mod.mock}
bind:forceJson
bind:selectedJob
isLoading={(testIsLoading && !scriptProgress) || fetchingLastJob}
isLoading={(testIsLoading && !scriptProgress) || fetchingLastJob || loadingHistory}
bind:preview
path={`path` in mod.value ? mod.value.path : ''}
{loopStatus}
@@ -112,7 +114,9 @@
duration={selectedJob?.['duration_ms']}
mem={selectedJob?.['mem_peak']}
content={selectedJob?.logs}
isLoading={(testIsLoading && selectedJob?.['running'] == false) || fetchingLastJob}
isLoading={(testIsLoading && selectedJob?.['running'] == false) ||
fetchingLastJob ||
loadingHistory}
tag={selectedJob?.tag}
/>
{/if}
@@ -15,6 +15,7 @@
import { getContext, onMount } from 'svelte'
import type { FlowEditorContext } from './flows/types'
import TestJobLoader from './TestJobLoader.svelte'
import { getStepHistoryLoaderContext } from './stepHistoryLoader.svelte'
interface Props {
mod: FlowModule
@@ -37,6 +38,7 @@
let testJobLoader: TestJobLoader | undefined = $state(undefined)
let jobProgressReset: () => void = () => {}
let stepHistoryLoader = getStepHistoryLoaderContext()
export function runTestWithStepArgs() {
runTest(testSteps.getStepArgs(mod.id)?.value)
@@ -92,6 +94,7 @@
$flowStateStore[mod.id].previewWorkspaceId = testJob.workspace_id
$flowStateStore = $flowStateStore
}
stepHistoryLoader?.resetInitial(mod.id)
}
testJob = undefined
}
@@ -1,4 +1,5 @@
<script lang="ts">
import { clone } from '$lib/utils'
import { Button } from '$lib/components/common'
import { InputService, type Input, type RunnableType } from '$lib/gen/index.js'
import { userStore, workspaceStore } from '$lib/stores.js'
@@ -137,7 +138,7 @@
const fullPayload = await input.getFullPayload?.()
dispatch('select', fullPayload)
} else {
selectedArgs = structuredClone($state.snapshot(input.payloadData) ?? {})
selectedArgs = clone($state.snapshot(input.payloadData) ?? {})
dispatch('select', selectedArgs)
}
}
@@ -1,4 +1,5 @@
<script lang="ts">
import { clone } from '$lib/utils'
import SavedInputsPicker from '$lib/components/SavedInputsPicker.svelte'
import { createEventDispatcher, tick } from 'svelte'
import HistoricInputs from '$lib/components/HistoricInputs.svelte'
@@ -162,7 +163,7 @@
{/if}
<Button
on:click={async () => {
appliedSearchArgs = structuredClone($state.snapshot(searchArgs))
appliedSearchArgs = clone(searchArgs)
await tick()
historicInputs?.refresh(true)
}}
+9 -10
View File
@@ -5,7 +5,7 @@
import type { Schema } from '$lib/common'
import { VariableService, type Script } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { allTrue, computeShow } from '$lib/utils'
import { allTrue, computeShow, clone } from '$lib/utils'
import { Button } from './common'
import ItemPicker from './ItemPicker.svelte'
import VariableEditor from './VariableEditor.svelte'
@@ -119,12 +119,12 @@
let inputCheck: { [id: string]: boolean } = $state({})
export function setDefaults() {
const nargs = structuredClone($state.snapshot(defaultValues))
const nargs = clone(defaultValues)
Object.keys(schema?.properties ?? {}).forEach((key) => {
if (schema?.properties[key].default != undefined && args && args[key] == undefined) {
let value = schema?.properties[key].default
nargs[key] = value === 'object' ? structuredClone($state.snapshot(value)) : value
nargs[key] = value === 'object' ? clone(value) : value
}
})
args = nargs
@@ -163,23 +163,21 @@
if (!deepEqual(schema?.order, lkeys) || !deepEqual(keys, lkeys)) {
if (schema?.order && Array.isArray(schema.order)) {
const n = {}
;(schema.order as string[]).forEach((x) => {
if (schema.properties && schema.properties[x] != undefined) {
n[x] = schema.properties[x]
}
})
Object.keys(schema.properties ?? {})
.filter((x) => !schema.order?.includes(x))
.forEach((x) => {
n[x] = schema.properties[x]
})
if (!deepEqual(schema.properties, n)) {
if (!deepEqual(schema.properties, n) || !deepEqual(schema.properties, Object.keys(n))) {
schema.properties = n
}
}
let nkeys = [...new Set(Object.keys(schema.properties ?? {}))]
let nkeys = Object.keys(schema.properties ?? {})
if (!deepEqual(keys, nkeys)) {
keys = nkeys
@@ -261,7 +259,8 @@
</Button>
</div>
{/if}
<!-- {JSON.stringify(schema.order)} -->
<!-- {JSON.stringify(schema)} -->
<div
class="w-full {className} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 ' : ''} {nestedClasses}"
use:dragHandleZone={dndConfig ?? { items: [], dragDisabled: true }}
@@ -295,7 +294,7 @@
pattern={formerProperty?.pattern}
valid={inputCheck[argName]}
defaultValue={defaultValues?.[argName] ??
structuredClone($state.snapshot(formerProperty?.default))}
clone(formerProperty?.default)}
enum_={dynamicEnums?.[argName] ?? formerProperty?.enum}
format={formerProperty?.format}
contentEncoding={formerProperty?.contentEncoding}
@@ -366,7 +365,7 @@
pattern={schema.properties[argName].pattern}
bind:valid={inputCheck[argName]}
defaultValue={defaultValues?.[argName] ??
structuredClone($state.snapshot(schema.properties[argName].default))}
clone(schema.properties[argName].default)}
enum_={dynamicEnums?.[argName] ?? schema.properties[argName].enum}
format={schema.properties[argName].format}
contentEncoding={schema.properties[argName].contentEncoding}
@@ -31,7 +31,8 @@
generateRandomString,
orderedJsonStringify,
replaceFalseWithUndefined,
type Value
type Value,
clone
} from '$lib/utils'
import Path from './Path.svelte'
import ScriptEditor from './ScriptEditor.svelte'
@@ -143,7 +144,7 @@
savedValue: savedScript,
modifiedValue: {
...script,
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
draft_triggers: clone(triggersState.getDraftTriggersSnapshot())
}
}
}
@@ -336,7 +337,7 @@
'#' +
encodeState({
...script,
draft_triggers: structuredClone(triggersState.getDraftTriggersSnapshot())
draft_triggers: clone(triggersState.getDraftTriggersSnapshot())
})
)
}
@@ -562,8 +563,8 @@
)
}
const { draft_triggers: _, ...newScript } = structuredClone($state.snapshot(script))
savedScript = structuredClone($state.snapshot(newScript)) as NewScriptWithDraft
const { draft_triggers: _, ...newScript } = clone(script)
savedScript = clone(newScript) as NewScriptWithDraft
setDraftTriggers([])
if (!disableHistoryChange) {
@@ -589,7 +590,7 @@
if (savedScript) {
const draftOrDeployed = cleanValueProperties(savedScript.draft || savedScript)
const currentTriggers = structuredClone(triggersState.getDraftTriggersSnapshot())
const currentTriggers = clone(triggersState.getDraftTriggersSnapshot())
const current = cleanValueProperties({ ...script, draft_triggers: currentTriggers })
if (!forceSave && orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(current)) {
sendUserToast('No changes detected, ignoring', false, [
@@ -694,7 +695,7 @@
}
})
const clonedScript = structuredClone($state.snapshot(script))
const clonedScript = clone(script)
savedScript = {
...(initialPath == '' || savedScript?.draft_only
? { ...clonedScript, draft_only: true }
@@ -754,7 +755,7 @@
}
await syncWithDeployed()
const currentDraftTriggers = structuredClone(
const currentDraftTriggers = clone(
triggersState.getDraftTriggersSnapshot()
)
@@ -1125,7 +1126,7 @@
>
<LanguageIcon {lang} />
<span class="ml-2 py-2 truncate">{label}</span>
{#if lang === 'ansible' || lang === 'nu'}
{#if lang === 'nu'}
<span class="text-tertiary !text-xs"> BETA </span>
{/if}
</Button>
@@ -1,6 +1,6 @@
<script lang="ts">
import { Button } from '$lib/components/common'
import { getContext, onDestroy, onMount, untrack } from 'svelte'
import { getContext, onDestroy, untrack } from 'svelte'
import type { AppInput } from '../../inputType'
import type {
AppViewerContext,
@@ -190,15 +190,6 @@
.filter((x) => x != '')
.join('\n')
)
// $inspect(result)
const rid = Math.random()
onMount(() => {
console.log('onMount', rid, id)
})
onDestroy(() => {
console.log('onDestroy', rid, id)
})
</script>
{#each Object.entries(components['buttoncomponent'].initialData.configuration) as [key, initialConfig] (key)}
@@ -20,7 +20,7 @@
import ResolveStyle from '../helpers/ResolveStyle.svelte'
import { deepEqual } from 'fast-equals'
import { computeWorkspaceS3FileInputPolicy } from '../../editor/appUtilsS3'
import { defaultIfEmptyString } from '$lib/utils'
import { defaultIfEmptyString, clone} from '$lib/utils'
import { userStore } from '$lib/stores'
import SchemaForm from '$lib/components/SchemaForm.svelte'
@@ -112,7 +112,7 @@
let previousDefault = $state(resolvedConfig.defaultValues)
function onDefaultChange() {
previousDefault = structuredClone($state.snapshot(resolvedConfig.defaultValues))
previousDefault = clone(resolvedConfig.defaultValues)
args = previousDefault ?? {}
}
@@ -1,6 +1,6 @@
<script lang="ts">
import { GridApi, createGrid, type IDatasource } from 'ag-grid-community'
import { sendUserToast } from '$lib/utils'
import { sendUserToast, clone} from '$lib/utils'
import { createEventDispatcher, getContext, mount, unmount } from 'svelte'
import type { AppViewerContext, ComponentCustomCSS, ContextPanelContext } from '../../../types'
@@ -111,7 +111,7 @@
function refreshActions(actions: TableAction[]) {
if (!deepEqual(actions, lastActions)) {
lastActions = structuredClone(stateSnapshot(actions))
lastActions = clone(actions)
updateOptions()
}
}
@@ -1,6 +1,6 @@
<script lang="ts">
import { GridApi, createGrid } from 'ag-grid-community'
import { isObject, sendUserToast } from '$lib/utils'
import { isObject, sendUserToast, clone} from '$lib/utils'
import { getContext, mount, onDestroy, unmount } from 'svelte'
import type { AppInput } from '../../../inputType'
import type {
@@ -206,7 +206,7 @@
function refreshActions(actions: TableAction[]) {
if (!deepEqual(actions, lastActions)) {
lastActions = structuredClone(stateSnapshot(actions))
lastActions = clone(actions)
updateOptions()
}
}
@@ -1,4 +1,5 @@
<script lang="ts">
import { clone } from '$lib/utils'
import { createBubbler, stopPropagation } from 'svelte/legacy'
const bubble = createBubbler()
@@ -133,7 +134,7 @@
tabs
)
) {
lastTabs = structuredClone($state.snapshot(tabs))
lastTabs = clone(tabs)
} else {
return
}
@@ -35,7 +35,8 @@
cleanValueProperties,
orderedJsonStringify,
type Value,
replaceFalseWithUndefined
replaceFalseWithUndefined,
clone
} from '../../../utils'
import type { AppInput, Runnable } from '../inputType'
import type { App, AppEditorContext, AppViewerContext } from '../types'
@@ -392,7 +393,7 @@
})
savedApp = {
summary: $summary,
value: structuredClone($state.snapshot($app)),
value: clone($app),
path: path,
policy: policy,
custom_path: customPath
@@ -488,7 +489,7 @@
})
savedApp = {
summary: $summary,
value: structuredClone($state.snapshot($app)),
value: clone($app),
path: npath,
policy,
custom_path: customPath
@@ -572,13 +573,13 @@
})
savedApp = {
summary: $summary,
value: structuredClone($state.snapshot($app)),
value: clone($app),
path: newEditedPath,
policy,
draft_only: true,
draft: {
summary: $summary,
value: structuredClone($state.snapshot($app)),
value: clone($app),
path: newEditedPath,
policy,
custom_path: customPath
@@ -660,7 +661,7 @@
...(savedApp?.draft_only
? {
summary: $summary,
value: structuredClone($state.snapshot($app)),
value: clone($app),
path: savedApp.draft_only ? newEditedPath || path : path,
policy,
draft_only: true,
@@ -669,7 +670,7 @@
: savedApp),
draft: {
summary: $summary,
value: structuredClone($state.snapshot($app)),
value: clone($app),
path: newEditedPath || path,
policy,
custom_path: customPath
@@ -16,7 +16,7 @@
} from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { base } from '$lib/base'
import { emptyString, formatCron, sendUserToast, tryEvery } from '$lib/utils'
import { emptyString, formatCron, sendUserToast, tryEvery, clone} from '$lib/utils'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import Toggle from '$lib/components/Toggle.svelte'
@@ -127,7 +127,7 @@
)
: {}
setTimeout(() => {
args = structuredClone($state.snapshot(nargs))
args = clone(nargs)
})
customPath =
@@ -1,4 +1,5 @@
<script lang="ts">
import { clone } from '$lib/utils'
import { Code, Copy, MoveLeft, MoveRight, Paintbrush2 } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import { slide } from 'svelte/transition'
@@ -53,10 +54,10 @@
const dispatch = createEventDispatcher()
let isQuickMenuOpen = $state(false)
let prevValue = structuredClone($state.snapshot(value))
let prevValue = clone(value)
$effect(() => {
if (deepEqual(prevValue, value)) return
prevValue = structuredClone($state.snapshot(value))
prevValue = clone(value)
dispatch('change', value)
})
@@ -36,6 +36,7 @@
onEditInput?: ((moduleId: string, key: string) => void) | undefined
forceTestTab?: Record<string, boolean>
highlightArg?: Record<string, string | undefined>
onRunPreview?: () => void
}
let {
@@ -52,7 +53,8 @@
onTestUpTo = undefined,
onEditInput = undefined,
forceTestTab,
highlightArg
highlightArg,
onRunPreview = () => {}
}: Props = $props()
let flowModuleSchemaMap: FlowModuleSchemaMap | undefined = $state()
@@ -128,6 +130,7 @@
{onDeployTrigger}
{forceTestTab}
{highlightArg}
{onRunPreview}
/>
{/if}
</Pane>
@@ -27,6 +27,7 @@
onDeployTrigger?: (trigger: Trigger) => void
forceTestTab?: Record<string, boolean>
highlightArg?: Record<string, string | undefined>
onRunPreview?: () => void
}
let {
@@ -37,7 +38,8 @@
savedFlow = undefined,
onDeployTrigger = () => {},
forceTestTab,
highlightArg
highlightArg,
onRunPreview
}: Props = $props()
const {
@@ -82,6 +84,7 @@
showCaptureHint.set(true)
}}
on:applyArgs
{onRunPreview}
/>
{:else if $selectedId === 'Result'}
<p class="p-4 text-secondary">The result of the flow will be the result of the last node.</p>
@@ -4,7 +4,6 @@
import { getContext, tick, untrack } from 'svelte'
import FlowCard from '../common/FlowCard.svelte'
import type { FlowEditorContext } from '../types'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import JsonInputs from '$lib/components/JsonInputs.svelte'
import { convert } from '@redocly/json-to-json-schema'
import { sendUserToast } from '$lib/toast'
@@ -27,7 +26,6 @@
Check
} from 'lucide-svelte'
import CaptureIcon from '$lib/components/triggers/CaptureIcon.svelte'
import FlowPreviewContent from '$lib/components/FlowPreviewContent.svelte'
import FlowInputEditor from './FlowInputEditor.svelte'
import { twMerge } from 'tailwind-merge'
import CaptureButton from '$lib/components/triggers/CaptureButton.svelte'
@@ -42,15 +40,16 @@
} from '$lib/components/schema/schemaUtils.svelte'
import SideBarTab from '$lib/components/meltComponents/SideBarTab.svelte'
import CaptureTable from '$lib/components/triggers/CaptureTable.svelte'
import { isObjectTooBig } from '$lib/utils'
import { isObjectTooBig, clone } from '$lib/utils'
import { refreshStateStore } from '$lib/svelte5Utils.svelte'
interface Props {
noEditor: boolean
disabled: boolean
onRunPreview?: () => void
}
let { noEditor, disabled }: Props = $props()
let { noEditor, disabled, onRunPreview }: Props = $props()
const {
flowStore,
@@ -76,7 +75,6 @@
let runDisabled: boolean = $state(false)
let editableSchemaForm: EditableSchemaForm | undefined = $state(undefined)
let savedPreviewArgs: Record<string, any> | undefined = $state(undefined)
let preventEscape = $state(false)
let isValid = $state(true)
function updateEditPanelSize(size: number | undefined) {
@@ -172,7 +170,7 @@
}
function schemaFromPayload(payloadData: any) {
const payload = structuredClone($state.snapshot(payloadData))
const payload = clone(payloadData)
const parsed = JSON.parse(JSON.stringify(payload))
if (!parsed) {
@@ -193,9 +191,6 @@
return { required: [], properties: {}, ...convert(parsed) }
}
let flowPreviewContent: FlowPreviewContent | undefined = $state()
let previewOpen = $state(false)
function handleKeydown(event: KeyboardEvent) {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
runPreview()
@@ -209,10 +204,9 @@
function runPreview() {
if (previewArguments) {
previewArgs.val = structuredClone($state.snapshot(previewArguments))
previewArgs.val = clone(previewArguments)
}
previewOpen = true
flowPreviewContent?.test()
onRunPreview?.()
}
function updatePreviewSchemaAndArgs(payload: any) {
@@ -227,7 +221,7 @@
updatePreviewSchema(undefined)
return
}
payloadData = structuredClone($state.snapshot(payload))
payloadData = clone(payload)
selectedSchema = schemaFromPayload(payloadData)
updatePreviewSchema(selectedSchema)
updatePreviewArguments(payloadData)
@@ -253,7 +247,7 @@
async function applySchemaAndArgs() {
flowStore.val.schema = applyDiff(flowStore.val.schema, diff)
if (previewArguments) {
savedPreviewArgs = structuredClone($state.snapshot(previewArguments))
savedPreviewArgs = clone(previewArguments)
}
updatePreviewSchemaAndArgs(undefined)
if ($flowInputEditorState) {
@@ -266,8 +260,8 @@
previewArguments = savedPreviewArgs
return
}
savedPreviewArgs = structuredClone($state.snapshot(previewArguments))
previewArguments = structuredClone($state.snapshot(payloadData))
savedPreviewArgs = clone(previewArguments)
previewArguments = clone(payloadData)
}
let tabButtonWidth = 0
@@ -359,18 +353,6 @@
<!-- Add svelte:window to listen for keyboard events -->
<svelte:window onkeydown={handleKeydown} />
<Drawer bind:open={previewOpen} alwaysOpen size="75%" {preventEscape}>
<FlowPreviewContent
bind:this={flowPreviewContent}
open={previewOpen}
previewMode="whole"
on:close={() => {
previewOpen = false
}}
bind:preventEscape
/>
</Drawer>
<FlowCard {noEditor} title="Flow Input">
{#if !disabled}
<div class="py-2 px-4 h-full">
@@ -416,7 +398,7 @@
on:change={() => {
previewArguments = previewArguments
if (!previewSchema) {
savedPreviewArgs = structuredClone($state.snapshot(previewArguments))
savedPreviewArgs = clone(previewArguments)
}
refreshStateStore(flowStore)
}}
@@ -53,6 +53,7 @@
import ModulePreviewResultViewer from '$lib/components/ModulePreviewResultViewer.svelte'
import { aiChatManager } from '$lib/components/copilot/chat/AIChatManager.svelte'
import { refreshStateStore } from '$lib/svelte5Utils.svelte'
import { getStepHistoryLoaderContext } from '$lib/components/stepHistoryLoader.svelte'
const {
selectedId,
@@ -186,6 +187,7 @@
let forceReload = $state(0)
let editorPanelSize = $state(noEditor ? 0 : flowModule.value.type == 'script' ? 30 : 50)
let editorSettingsPanelSize = $state(100 - untrack(() => editorPanelSize))
let stepHistoryLoader = getStepHistoryLoaderContext()
function onSelectedIdChange() {
if (!$flowStateStore?.[$selectedId]?.schema && flowModule) {
@@ -820,7 +822,23 @@
</div>
</Pane>
{#if selected === 'test'}
<Pane minSize={20}>
<Pane minSize={20} class="relative">
{#if stepHistoryLoader?.stepStates[flowModule.id]?.initial && !flowModule.mock?.enabled}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
onclick={() => {
stepHistoryLoader?.resetInitial(flowModule.id)
}}
class="cursor-pointer h-full hover:bg-gray-500/20 dark:hover:bg-gray-500/20 dark:bg-gray-500/80 bg-gray-500/40 absolute top-0 left-0 w-full z-50"
>
<div class="text-center text-primary text-sm py-2 pt-20"
><span class="font-bold border p-2 bg-surface-secondary rounded-md"
>Run loaded from history</span
></div
>
</div>
{/if}
<ModulePreviewResultViewer
lang={flowModule.value['language'] ?? 'deno'}
{editor}
@@ -840,6 +858,7 @@
{testIsLoading}
disableMock={preprocessorModule || failureModule}
disableHistory={failureModule}
loadingHistory={stepHistoryLoader?.stepStates[flowModule.id]?.loadingJobs}
/>
</Pane>
{/if}
@@ -11,7 +11,8 @@
cleanValueProperties,
emptySchema,
orderedJsonStringify,
sendUserToast
sendUserToast,
clone
} from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { fade } from 'svelte/transition'
@@ -28,7 +29,7 @@
workspace: $workspaceStore!,
hash
})
savedScript = structuredClone($state.snapshot(script))
savedScript = clone(script)
callback = cb
}
@@ -100,7 +101,7 @@
lock: undefined
}
})
savedScript = structuredClone($state.snapshot(script))
savedScript = clone(script)
callback?.()
} catch (error) {
sendUserToast(`Impossible to save the script: ${error.body}`, true)
@@ -246,7 +247,7 @@
button: {
text: 'Restore to saved',
onClick: () => {
script = structuredClone($state.snapshot(savedScript))
script = clone(savedScript)
diffDrawer?.closeDrawer()
}
}
@@ -24,7 +24,7 @@ export async function copyFirstStepSchema(
const firstModuleId = flowStore.val.value.modules[0]?.id
if (flowState[firstModuleId] && firstModuleId) {
flowStore.val.schema = structuredClone($state.snapshot(flowState[firstModuleId].schema))
flowStore.val.schema = clone(flowState[firstModuleId].schema)
const v = flowStore.val.value.modules[0].value
if (v.type == 'rawscript' || v.type == 'script') {
Object.keys(v.input_transforms ?? {}).forEach((key) => {
@@ -47,7 +47,7 @@ export async function getFirstStepSchema(flowState: FlowState, flow: OpenFlow) {
throw new Error('no first step found')
}
const schema = structuredClone(flowState[firstModuleId].schema)
const schema = clone(flowState[firstModuleId].schema)
const v = flow.value.modules[0].value
if (v.type !== 'rawscript' && v.type !== 'script') {
@@ -23,7 +23,7 @@ export async function copyFirstStepSchema(flowState: FlowState, flowStore: Writa
const firstModuleId = flow.value.modules[0]?.id
if (flowState[firstModuleId] && firstModuleId) {
flow.schema = structuredClone(stateSnapshot(flowState[firstModuleId].schema))
flow.schema = clone(flowState[firstModuleId].schema)
const v = flow.value.modules[0].value
if (v.type == 'rawscript' || v.type == 'script') {
Object.keys(v.input_transforms ?? {}).forEach((key) => {
@@ -50,7 +50,7 @@ export async function getFirstStepSchema(flowState: FlowState, flowStore: StateS
throw new Error('no first step found')
}
const schema = structuredClone(stateSnapshot(flowState[firstModuleId].schema))
const schema = clone(flowState[firstModuleId].schema)
const v = flow.value.modules[0].value
if (v.type !== 'rawscript' && v.type !== 'script') {
@@ -4,7 +4,7 @@
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import FlowPreviewContent from '$lib/components/FlowPreviewContent.svelte'
import type { Job } from '$lib/gen'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, tick } from 'svelte'
import { getContext } from 'svelte'
import type { FlowEditorContext } from '../types'
@@ -26,6 +26,7 @@
export async function openPreview(test: boolean = false) {
if (!previewOpen) {
previewOpen = true
await tick()
flowPreviewContent?.refresh()
if (!test) return
}
@@ -38,7 +39,6 @@
let jobId: string | undefined = $state(undefined)
let job: Job | undefined = $state(undefined)
let preventEscape = $state(false)
let initial = $state(false)
let selectedJobStep: string | undefined = $state(undefined)
let selectedJobStepIsTopLevel: boolean | undefined = $state(undefined)
let selectedJobStepType: 'single' | 'forloop' | 'branchall' = $state('single')
@@ -120,7 +120,6 @@
bind:previewMode
bind:job
bind:jobId
bind:initial
bind:selectedJobStep
bind:selectedJobStepIsTopLevel
bind:selectedJobStepType
@@ -40,6 +40,7 @@
} from '$lib/components/copilot/chat/flow/ModuleAcceptReject.svelte'
import { Button } from '$lib/components/common'
import ModuleTest from '$lib/components/ModuleTest.svelte'
import { getStepHistoryLoaderContext } from '$lib/components/stepHistoryLoader.svelte'
interface Props {
selected?: boolean
@@ -139,6 +140,8 @@
let flowStateStore = $derived(flowEditorContext?.flowStateStore)
let stepHistoryLoader = getStepHistoryLoaderContext()
function updateConnectingData(
id: string | undefined,
pickableIds: Record<string, any> | undefined,
@@ -449,7 +452,9 @@
bind:derivedHistoryOpen={historyOpen}
historyOffset={{ mainAxis: 12, crossAxis: -9 }}
clazz="p-1"
isLoading={testIsLoading}
isLoading={testIsLoading ||
(id ? stepHistoryLoader?.stepStates[id]?.loadingJobs : false)}
initial={id ? stepHistoryLoader?.stepStates[id]?.initial : undefined}
/>
{/snippet}
</OutputPicker>
@@ -38,6 +38,7 @@
import { dfsByModule } from '../previousResults'
import type { InlineScript, InsertKind } from '$lib/components/graph/graphBuilder.svelte'
import { refreshStateStore } from '$lib/svelte5Utils.svelte'
import { getStepHistoryLoaderContext } from '$lib/components/stepHistoryLoader.svelte'
interface Props {
sidebarSize?: number | undefined
@@ -239,10 +240,18 @@
}
}
let stepHistoryLoader = getStepHistoryLoaderContext()
async function loadLastJob(path: string, moduleId: string) {
if (!path) {
return
}
if (stepHistoryLoader) {
stepHistoryLoader.stepStates[moduleId] = {
initial: true,
loadingJobs: true
}
}
const previousJobId = await JobService.listJobs({
workspace: $workspaceStore!,
scriptPathExact: path,
@@ -263,6 +272,9 @@
previewWorkspaceId: previousJobId[0].workspace_id,
previewSuccess: getJobResult.success
}
if (stepHistoryLoader) {
stepHistoryLoader.stepStates[moduleId].loadingJobs = false
}
}
$flowStateStore = $flowStateStore
}
@@ -20,6 +20,8 @@
bottomBarOpen?: boolean
loopStatus?: { type: 'inside' | 'self'; flow: 'forloopflow' | 'whileloopflow' } | undefined
onEditInput?: (moduleId: string, key: string) => void
initial?: boolean
onResetInitial?: () => void
}
let {
@@ -184,7 +186,7 @@
}}
bind:this={popover}
allowFullScreen
contentClasses="overflow-hidden resize"
contentClasses="overflow-hidden resize relative"
contentStyle={`width: calc(${MIN_WIDTH}px); min-width: calc(${MIN_WIDTH}px); height: calc(${MIN_HEIGHT}px); min-height: calc(${MIN_HEIGHT}px); `}
extraProps={{ 'data-prop-picker': true }}
closeOnOtherPopoverOpen
@@ -15,6 +15,7 @@
</script>
<script lang="ts">
import { clone } from '$lib/utils'
import Button from '$lib/components/common/button/Button.svelte'
import { Pin, History, Pen, Check, X, Loader2, Pencil } from 'lucide-svelte'
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
@@ -28,6 +29,7 @@
import { twMerge } from 'tailwind-merge'
import DisplayResultControlBar from '$lib/components/DisplayResultControlBar.svelte'
import { base } from '$lib/base'
import { fade } from 'svelte/transition'
interface Props {
prefix?: string
@@ -64,6 +66,8 @@
onUpdateMock?: (mock: { enabled: boolean; return_value?: unknown }) => void
onEditInput?: (moduleId: string, key: string) => void
selectionId?: string
initial?: boolean
onResetInitial?: () => void
}
let {
@@ -95,7 +99,8 @@
onSelect,
onUpdateMock,
onEditInput,
selectionId
selectionId,
initial
}: Props = $props()
type SelectedJob =
@@ -191,7 +196,7 @@
})
} else if (selectedJob && 'result' in selectedJob) {
// Pin the job
let mockValue: any = structuredClone($state.snapshot(selectedJob.result))
let mockValue: any = clone(selectedJob.result)
if (selectedJob.result === 'never tested this far') {
mockValue = { example: 'value' }
}
@@ -252,6 +257,8 @@
let popoverHeight = $derived(customHeight ?? (clientHeight > 0 ? clientHeight : 0))
const isLoadingAndNotMock = $derived(isLoading && !mock?.enabled)
const copilot_fix_render = $derived(copilot_fix)
</script>
@@ -353,7 +360,7 @@
{/snippet}
</Popover>
{/if}
{#if !isLoading}
{#if !isLoadingAndNotMock || mock?.enabled}
<div
class={twMerge(
'w-grow min-w-0 flex gap-1 items-center h-[27px] rounded-md group',
@@ -413,7 +420,7 @@
</div>
{/if}
{#if !disableMock && !isLoading}
{#if !disableMock && !isLoadingAndNotMock}
<Tooltip disablePopup={mock?.enabled}>
<Button
color="light"
@@ -578,7 +585,7 @@
hoveringResult = false
}}
>
{#if isLoading}
{#if isLoadingAndNotMock}
<div class="flex flex-col items-center justify-center">
<Loader2 class="animate-spin" />
</div>
@@ -609,7 +616,7 @@
if (mock?.enabled) {
const newMock = {
enabled: true,
return_value: structuredClone($state.snapshot(detail))
return_value: clone(detail)
}
tmpMock = newMock
}
@@ -700,6 +707,13 @@
{/if}
</div>
</div>
{#if initial && !mock?.enabled}
<span
in:fade
class="-mb-1 -mt-0.5 w-full text-right pr-4 dark:text-gray-500 text-gray-400 font-normal text-2xs py-0"
>Run loaded from history</span
>
{/if}
</div>
{#snippet editKey(key: string)}
@@ -3,8 +3,7 @@ import type { ScheduleTrigger } from '../triggers'
import type { Writable } from 'svelte/store'
import { writable } from 'svelte/store'
import { get } from 'svelte/store'
import { sendUserToast } from '$lib/utils'
import { stateSnapshot } from '$lib/svelte5Utils.svelte'
import { sendUserToast, clone} from '$lib/utils'
// Load the schedule of a flow given its path and the workspace
export async function loadSchedule(path: string, workspace: string): Promise<ScheduleTrigger> {
@@ -46,7 +45,7 @@ export async function loadSchedules(
if (!path || path == '') {
schedules.set([])
primarySchedule.update((ps) => (ps === undefined ? false : ps))
initialPrimarySchedule.set(structuredClone(stateSnapshot(get(primarySchedule))))
initialPrimarySchedule.set(clone(get(primarySchedule)))
return
}
try {
@@ -74,7 +73,7 @@ export async function loadSchedules(
: false
}
primarySchedule.update((ps) => (ps === undefined || forceRefresh ? remotePrimarySchedule : ps))
initialPrimarySchedule.set(structuredClone(stateSnapshot(remotePrimarySchedule)))
initialPrimarySchedule.set(clone(remotePrimarySchedule))
triggersCount.update((tc) => {
const primary = get(primarySchedule)
@@ -7,6 +7,7 @@ import {
type PickableProperties
} from './previousResults'
import { evalValue, type ModuleArgs } from './utils'
import { clone } from '$lib/utils'
export class TestSteps {
#stepsEvaluated = $state<Record<string, ModuleArgs>>({})
@@ -102,7 +103,7 @@ export class TestSteps {
const argSnapshot = $state.snapshot(evalValue(argName, modules[0], pickableProperties, false))
this.#stepsEvaluated[moduleId].value[argName] = argSnapshot
this.#steps[moduleId].value[argName] = structuredClone(argSnapshot)
this.#steps[moduleId].value[argName] = clone(argSnapshot)
}
initializeFromSchema(
@@ -36,7 +36,7 @@
import { Expand } from 'lucide-svelte'
import Toggle from '../Toggle.svelte'
import DataflowEdge from './renderers/edges/DataflowEdge.svelte'
import { encodeState } from '$lib/utils'
import { encodeState, clone} from '$lib/utils'
import BranchOneStart from './renderers/nodes/BranchOneStart.svelte'
import NoBranchNode from './renderers/nodes/NoBranchNode.svelte'
import HiddenBaseEdge from './renderers/edges/HiddenBaseEdge.svelte'
@@ -310,12 +310,12 @@
}
}
let lastModules = structuredClone($state.snapshot(modules))
let lastModules = clone(modules)
let moduleCounter = $state(0)
function onModulesChange2(modules) {
if (!deepEqual(modules, lastModules)) {
console.log('modules changed', modules)
lastModules = structuredClone($state.snapshot(modules))
lastModules = clone(modules)
moduleCounter++
}
}
@@ -1,4 +1,5 @@
<script lang="ts">
import { clone } from '$lib/utils'
import { Alert, Badge, Drawer, DrawerContent } from '$lib/components/common'
import Button from '$lib/components/common/button/Button.svelte'
@@ -48,7 +49,6 @@
import { collectStaticFields, hash, type TriggerableV2 } from '../apps/editor/commonAppUtils'
import type { SavedAndModifiedValue } from '../common/confirmationModal/unsavedTypes'
import DropdownV2 from '../DropdownV2.svelte'
import { stateSnapshot } from '$lib/svelte5Utils.svelte'
// async function hash(message) {
// try {
@@ -205,7 +205,7 @@
})
savedApp = {
summary: summary,
value: structuredClone(stateSnapshot(app)),
value: clone(app),
path: path,
policy: policy,
custom_path: customPath
@@ -310,7 +310,7 @@
})
savedApp = {
summary: summary,
value: structuredClone(stateSnapshot(app)),
value: clone(app),
path: npath,
policy,
custom_path: customPath
@@ -402,13 +402,13 @@
})
savedApp = {
summary: summary,
value: structuredClone(stateSnapshot(app)),
value: clone(app),
path: newEditedPath,
policy,
draft_only: true,
draft: {
summary: summary,
value: structuredClone(stateSnapshot(app)),
value: clone(app),
path: newEditedPath,
policy,
custom_path: customPath
@@ -500,7 +500,7 @@
...(savedApp?.draft_only
? {
summary: summary,
value: structuredClone(stateSnapshot(app)),
value: clone(app),
path: savedApp.draft_only ? newEditedPath || path : path,
policy,
draft_only: true,
@@ -509,7 +509,7 @@
: savedApp),
draft: {
summary: summary,
value: structuredClone(stateSnapshot(app)),
value: clone(app),
path: newEditedPath || path,
policy,
custom_path: customPath
@@ -7,7 +7,7 @@
import AddProperty from './AddProperty.svelte'
import { dragHandle, dragHandleZone } from '@windmill-labs/svelte-dnd-action'
import { flip } from 'svelte/animate'
import { emptyString } from '$lib/utils'
import { emptyString, clone} from '$lib/utils'
import Button from '$lib/components/common/button/Button.svelte'
import { createEventDispatcher, tick } from 'svelte'
import Label from '../Label.svelte'
@@ -56,7 +56,7 @@
let { schema = $bindable(), parentId = undefined, jsonView = $bindable(false) }: Props = $props()
// let schema = $state(structuredClone($state.snapshot(schema)))
// let schema = $state(clone(schema))
let schemaString: string = $state(JSON.stringify(schema, null, '\t'))
let editor: SimpleEditor | undefined = $state(undefined)
@@ -1,8 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import autosize from '$lib/autosize'
import { shouldDisplayPlaceholder } from '$lib/utils'
import { shouldDisplayPlaceholder, clone} from '$lib/utils'
import { twMerge } from 'tailwind-merge'
import ArrayTypeNarrowing from '../ArrayTypeNarrowing.svelte'
import Label from '../Label.svelte'
@@ -120,7 +118,7 @@
order
})
let initialExtra: any = structuredClone({
let initialExtra: any = clone({
order: undefined,
properties: undefined,
...$state.snapshot(extra)
@@ -140,7 +138,7 @@
return
}
if (!deepEqual(extra, initialExtra)) {
initialExtra = structuredClone($state.snapshot(extra))
initialExtra = clone(extra)
console.debug('property content updated')
dispatchIfMounted('change')
}
@@ -160,10 +158,10 @@
;[oneOf]
untrack(() => updateOneOfSchemas(oneOf))
})
run(() => {
$effect(() => {
extra && mounted && untrack(() => onContentChange())
})
run(() => {
$effect(() => {
;(properties || order) && untrack(() => updateSchema())
})
</script>
@@ -1,3 +1,4 @@
import { clone } from '$lib/utils'
export type SchemaDiff = {
diff: 'same' | 'added' | 'removed' | 'modified' | Record<string, SchemaDiff>
fullSchema: { [key: string]: any } | undefined
@@ -133,7 +134,7 @@ export function schemaFromDiff(
if (!schema) {
return undefined
}
const newSchema = structuredClone($state.snapshot(schema))
const newSchema = clone(schema)
Object.keys(diff).forEach((key) => {
const diffValue = diff[key].diff
if (diffValue === 'added' || diffValue === 'modified') {
@@ -179,7 +180,7 @@ export function setNestedProperty(
return curr[field][key]
}, obj)
if (lastKey && value) {
const newValue = structuredClone($state.snapshot(value))
const newValue = clone(value)
target[field][lastKey] = newValue
return
} else if (lastKey && !value) {
@@ -199,7 +200,7 @@ export function applyDiff(
return
}
let newSchema = structuredClone($state.snapshot(schema))
let newSchema = clone(schema)
Object.keys(diff).forEach((key) => {
const diffValue = diff[key].diff
@@ -1,4 +1,4 @@
<script lang="ts" generics="Item extends { label?: string; value: any; }">
<script lang="ts" generics="Item extends { label?: string; value: any; subtitle?: string }">
import { clickOutside } from '$lib/utils'
import { twMerge } from 'tailwind-merge'
import CloseButton from '../common/CloseButton.svelte'
@@ -138,6 +138,9 @@
}}
>
{item.label || '\xa0'}
{#if item.subtitle}
<div class="text-xs text-tertiary">{item.subtitle}</div>
{/if}
</button>
</li>
{/each}
@@ -43,6 +43,7 @@ export type ProcessedItem<T> = {
__is_create?: true
label: string
value: T
subtitle?: string
}
export function getLabel<T>(item: { label?: string; value: T } | undefined): string {
@@ -0,0 +1,170 @@
import { type Writable, get } from 'svelte/store'
import type { FlowState } from './flows/flowState'
import { NEVER_TESTED_THIS_FAR } from './flows/models'
import { JobService, type Flow, type FlowModule } from '$lib/gen'
import { dfs } from './flows/dfs'
import { getContext, setContext } from 'svelte'
import pLimit from 'p-limit'
export type stepState = {
initial?: boolean
loadingJobs?: boolean
}
const STEP_HISTORY_LOADER_CONTEXT_KEY = 'stepHistoryLoader'
export class StepHistoryLoader {
#stepStates = $state<Record<string, stepState>>({})
#flowJobInitial = $state<boolean | undefined>(undefined)
#saveCb = $state<(() => void) | undefined>(undefined)
#noInitial = $state<boolean>(false)
get stepStates() {
return this.#stepStates
}
set stepStates(value: Record<string, stepState>) {
this.#stepStates = value
}
get flowJobInitial(): boolean | undefined {
return this.#flowJobInitial
}
setFlowJobInitial(value: boolean | undefined) {
if (this.#noInitial) {
return
}
this.#flowJobInitial = value
if (value === false) {
// Set all step initials to false
Object.keys(this.#stepStates).forEach((stepId) => {
this.#stepStates[stepId].initial = false
})
}
this.#saveCb?.()
}
constructor(
stepStates: Record<string, stepState>,
flowJobInitial?: boolean,
saveCb?: () => void,
noInitial?: boolean
) {
this.#stepStates = stepStates
this.#flowJobInitial = flowJobInitial
this.#saveCb = saveCb
this.#noInitial = noInitial ?? false
}
resetInitial(stepId: string | undefined) {
if (!stepId) {
return
}
if (stepId in this.#stepStates && this.#stepStates[stepId]) {
this.#stepStates[stepId].initial = false
}
// Check if all initial states are false
const allInitialFalse = Object.values(this.#stepStates).every((state) => !state.initial)
if (allInitialFalse) {
this.#flowJobInitial = false
}
this.#saveCb?.()
}
async loadIndividualStepsStates(
flow: Flow,
flowStateStore: Writable<FlowState>,
workspaceId: string,
initialPath: string,
path: string
): Promise<void> {
// Collect all modules that need loading
const modulesToLoad: FlowModule[] = []
dfs(flow.value.modules, (module) => {
const prev = get(flowStateStore)[module.id]?.previewResult
if (!prev || prev === NEVER_TESTED_THIS_FAR) {
modulesToLoad.push(module)
// Initialize step state if it doesn't exist
if (!this.#stepStates[module.id]) {
this.#stepStates[module.id] = {
initial: this.#noInitial ? false : undefined,
loadingJobs: undefined
}
}
}
})
// Load all modules in parallel and wait for completion
const limit = pLimit(5)
const loadPromises = modulesToLoad.map((module) =>
limit(async () => {
try {
this.#stepStates[module.id].loadingJobs = true
const scriptPath =
`path` in module.value
? module.value.path
: (initialPath === '' ? path : initialPath) + '/' + module.id
const previousJobId = await JobService.listJobs({
workspace: workspaceId,
scriptPathExact: scriptPath,
jobKinds: ['preview', 'script', 'flowpreview', 'flow', 'flowscript'].join(','),
page: 1,
perPage: 1
})
if (previousJobId.length === 0) {
this.#stepStates[module.id].loadingJobs = false
return
}
const getJobResult = await JobService.getCompletedJobResultMaybe({
workspace: workspaceId,
id: previousJobId[0].id
})
if ('result' in getJobResult) {
flowStateStore.update((state) => ({
...state,
[module.id]: {
...(state[module.id] ?? {}),
previewResult: getJobResult.result,
previewJobId: previousJobId[0].id,
previewWorkspaceId: previousJobId[0].workspace_id,
previewSuccess: getJobResult.success
}
}))
this.#stepStates[module.id].initial =
this.#stepStates[module.id].initial !== undefined
? this.#stepStates[module.id].initial
: true
}
} catch (error) {
console.error(`Failed to load history for module ${module.id}:`, error)
this.#stepStates[module.id] = {
initial: false,
loadingJobs: false
}
} finally {
// Ensure loading state is always cleaned up
this.#stepStates[module.id].loadingJobs = false
}
})
)
// Wait for all loading operations to complete
await Promise.all(loadPromises)
}
}
export function getStepHistoryLoaderContext() {
return getContext<StepHistoryLoader>(STEP_HISTORY_LOADER_CONTEXT_KEY)
}
export function setStepHistoryLoaderContext(stepHistoryLoader: StepHistoryLoader) {
setContext<StepHistoryLoader>(STEP_HISTORY_LOADER_CONTEXT_KEY, stepHistoryLoader)
}
@@ -27,9 +27,8 @@
import { Popover } from '$lib/components/meltComponents'
import { CaptureService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { isObject, sendUserToast } from '$lib/utils'
import { isObject, sendUserToast, clone, formatDateShort } from '$lib/utils'
import { triggerIconMap } from './utils'
import { formatDateShort } from '$lib/utils'
import DisplayResult from '$lib/components/DisplayResult.svelte'
import DisplayResultControlBar from '$lib/components/DisplayResultControlBar.svelte'
import { base } from '$lib/base'
@@ -432,7 +431,7 @@
dispatch('applyArgs', {
kind: testKind,
args: { ...structuredClone($state.snapshot(payloadData)), ...trigger_extra }
args: { ...clone(payloadData), ...trigger_extra }
})
}
}}
@@ -14,7 +14,7 @@
import { type CaptureTriggerKind } from '$lib/gen'
import CaptureButton from '$lib/components/triggers/CaptureButton.svelte'
import InfiniteList from '../InfiniteList.svelte'
import { isObject, sendUserToast } from '$lib/utils'
import { isObject, sendUserToast, clone} from '$lib/utils'
import SchemaPickerRow from '$lib/components/schema/SchemaPickerRow.svelte'
import type { Capture } from '$lib/gen'
import { AwsIcon, MqttIcon } from '../icons'
@@ -161,7 +161,7 @@
} else {
const payloadData = await getPayload(capture)
selected = capture.id
dispatch('select', structuredClone($state.snapshot(payloadData)))
dispatch('select', clone(payloadData))
dispatch('selectCapture', capture)
}
}
@@ -188,7 +188,7 @@
testKind === 'preprocessor' ? fullCapture.preprocessor_args : fullCapture.main_args
}
} else {
payloadData = structuredClone($state.snapshot(capture.payloadData))
payloadData = clone(capture.payloadData)
}
return payloadData
}
@@ -4,7 +4,7 @@
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import Path from '$lib/components/Path.svelte'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
import { canWrite, emptyString, sendUserToast, clone} from '$lib/utils'
import { Loader2 } from 'lucide-svelte'
import Label from '$lib/components/Label.svelte'
import {
@@ -106,7 +106,7 @@
} finally {
drawerLoading = false
if (!defaultValues) {
initialConfig = structuredClone($state.snapshot(getGcpConfig()))
initialConfig = clone(getGcpConfig())
}
}
}
@@ -7,7 +7,7 @@
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import { HttpTriggerService, VariableService, type AuthenticationMethod } from '$lib/gen'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
import { canWrite, emptyString, sendUserToast, clone} from '$lib/utils'
import Section from '$lib/components/Section.svelte'
import { Loader2, Pipette, Plus } from 'lucide-svelte'
import Label from '$lib/components/Label.svelte'
@@ -183,7 +183,7 @@
} finally {
if (!defaultConfig) {
// If the route is loaded from the backend, we to set the initial config
initialConfig = structuredClone($state.snapshot(getRouteConfig()))
initialConfig = clone(getRouteConfig())
}
clearTimeout(loader)
drawerLoading = false
@@ -51,6 +51,7 @@ export async function saveHttpRouteFromCfg(
route_path: isAdmin || !edit ? routeCfg.route_path : undefined,
http_method: routeCfg.http_method,
is_static_website: routeCfg.is_static_website,
static_asset_config: routeCfg.static_asset_config,
workspaced_route: routeCfg.workspaced_route,
authentication_resource_path: routeCfg.authentication_resource_path,
wrap_body: routeCfg.wrap_body,
@@ -7,7 +7,7 @@
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import { KafkaTriggerService } from '$lib/gen'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
import { canWrite, emptyString, sendUserToast, clone} from '$lib/utils'
import Section from '$lib/components/Section.svelte'
import { Loader2 } from 'lucide-svelte'
import Label from '$lib/components/Label.svelte'
@@ -119,7 +119,7 @@
sendUserToast(`Could not load Kafka trigger: ${err}`, true)
} finally {
if (!defaultConfig) {
initialConfig = structuredClone($state.snapshot(getSaveCfg()))
initialConfig = clone(getSaveCfg())
}
clearTimeout(loadingTimeout)
drawerLoading = false
@@ -221,7 +221,7 @@
return {
kafka_resource_path: kafkaResourcePath,
group_id: kafkaCfg.group_id,
topics: structuredClone($state.snapshot(kafkaCfg.topics)),
topics: clone(kafkaCfg.topics),
path
}
}
@@ -6,7 +6,7 @@
import Required from '$lib/components/Required.svelte'
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
import { canWrite, emptyString, sendUserToast, clone} from '$lib/utils'
import Section from '$lib/components/Section.svelte'
import { Loader2 } from 'lucide-svelte'
import Label from '$lib/components/Label.svelte'
@@ -114,7 +114,7 @@
sendUserToast(`Could not load mqtt trigger: ${err.body}`, true)
} finally {
if (!defaultConfig) {
initialConfig = structuredClone($state.snapshot(getSaveCfg()))
initialConfig = clone(getSaveCfg())
}
clearTimeout(loadingTimeout)
drawerLoading = false
@@ -7,7 +7,7 @@
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import { NatsTriggerService } from '$lib/gen'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
import { canWrite, emptyString, sendUserToast, clone} from '$lib/utils'
import Section from '$lib/components/Section.svelte'
import { Loader2 } from 'lucide-svelte'
import Label from '$lib/components/Label.svelte'
@@ -112,7 +112,7 @@
drawerLoading = false
showLoading = false
if (!defaultConfig) {
initialConfig = structuredClone($state.snapshot(getSaveCfg()))
initialConfig = clone(getSaveCfg())
}
}
}
@@ -7,7 +7,7 @@
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import { PostgresTriggerService, type Language, type Relations } from '$lib/gen'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, emptyStringTrimmed, sendUserToast } from '$lib/utils'
import { canWrite, emptyString, emptyStringTrimmed, sendUserToast, clone} from '$lib/utils'
import Section from '$lib/components/Section.svelte'
import { Loader2 } from 'lucide-svelte'
import Label from '$lib/components/Label.svelte'
@@ -210,7 +210,7 @@
sendUserToast(`Could not load postgres trigger: ${err.body}`, true)
} finally {
if (!defaultConfig) {
initialConfig = structuredClone($state.snapshot(getSaveCfg()))
initialConfig = clone(getSaveCfg())
}
clearTimeout(loadingTimeout)
drawerLoading = false
@@ -21,7 +21,7 @@
type Schedule
} from '$lib/gen'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, formatCron, sendUserToast, cronV1toV2 } from '$lib/utils'
import { canWrite, emptyString, formatCron, sendUserToast, cronV1toV2, clone } from '$lib/utils'
import { base } from '$lib/base'
import Section from '$lib/components/Section.svelte'
import { List, Loader2, Save, AlertTriangle } from 'lucide-svelte'
@@ -132,7 +132,7 @@
edit = true
} finally {
if (!defaultCfg) {
initialConfig = structuredClone($state.snapshot(getScheduleCfg()))
initialConfig = clone(getScheduleCfg())
}
clearTimeout(loadingTimeout)
drawerLoading = false
@@ -608,21 +608,21 @@
}
function getScheduleCfg(): Record<string, any> {
let errorHadlerExtraArgsDerived = structuredClone($state.snapshot(errorHandlerExtraArgs))
let errorHadlerExtraArgsDerived = clone(errorHandlerExtraArgs)
if (errorHandlerPath !== undefined && isSlackHandler('error', errorHandlerPath)) {
errorHadlerExtraArgsDerived['slack'] = '$res:f/slack_bot/bot_token'
} else {
errorHadlerExtraArgsDerived['slack'] = undefined
}
let recoveryHandlerExtraArgsDerived = structuredClone($state.snapshot(recoveryHandlerExtraArgs))
let recoveryHandlerExtraArgsDerived = clone(recoveryHandlerExtraArgs)
if (recoveryHandlerPath !== undefined && isSlackHandler('recovery', recoveryHandlerPath)) {
recoveryHandlerExtraArgsDerived['slack'] = '$res:f/slack_bot/bot_token'
} else {
recoveryHandlerExtraArgsDerived['slack'] = undefined
}
let successHandlerExtraArgsDerived = structuredClone($state.snapshot(successHandlerExtraArgs))
let successHandlerExtraArgsDerived = clone(successHandlerExtraArgs)
if (successHandlerPath !== undefined && isSlackHandler('success', successHandlerPath)) {
successHandlerExtraArgsDerived['slack'] = '$res:f/slack_bot/bot_token'
} else {
@@ -4,7 +4,7 @@
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import Path from '$lib/components/Path.svelte'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
import { canWrite, emptyString, sendUserToast, clone} from '$lib/utils'
import { Loader2 } from 'lucide-svelte'
import Label from '$lib/components/Label.svelte'
import { SqsTriggerService, type AwsAuthResourceType } from '$lib/gen'
@@ -136,7 +136,7 @@
dirtyPath = false
enabled = defaultValues?.enabled ?? false
} finally {
initialConfig = structuredClone($state.snapshot(getSaveCfg()))
initialConfig = clone(getSaveCfg())
clearTimeout(loadingTimeout)
drawerLoading = false
showLoading = false
@@ -15,7 +15,7 @@
type WebsocketTriggerInitialMessage
} from '$lib/gen'
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptySchema, emptyString, sendUserToast } from '$lib/utils'
import { canWrite, emptySchema, emptyString, sendUserToast, clone} from '$lib/utils'
import Section from '$lib/components/Section.svelte'
import { Loader2, X, Plus } from 'lucide-svelte'
import Label from '$lib/components/Label.svelte'
@@ -139,7 +139,7 @@
sendUserToast(`Could not load websocket trigger: ${err}`, true)
} finally {
if (!defaultConfig) {
initialConfig = structuredClone($state.snapshot(getSaveCfg()))
initialConfig = clone(getSaveCfg())
}
clearTimeout(loadingTimeout)
drawerLoading = false
+8 -2
View File
@@ -1023,7 +1023,7 @@ export type Value = {
}
export function replaceFalseWithUndefined(obj: any) {
return replaceFalseWithUndefinedRec(structuredClone(stateSnapshot(obj)))
return replaceFalseWithUndefinedRec(clone(obj))
}
function replaceFalseWithUndefinedRec(obj: any) {
@@ -1052,7 +1052,7 @@ export function cleanValueProperties(obj: Value) {
let newObj: any = {}
for (const key of Object.keys(obj)) {
if (key !== 'parent_hash' && key !== 'draft' && key !== 'draft_only') {
newObj[key] = structuredClone(stateSnapshot(obj[key]))
newObj[key] = clone(obj[key])
}
}
return newObj
@@ -1432,4 +1432,10 @@ export function scroll_into_view_if_needed_polyfill(elem: Element, centerIfNeede
return observer // return for testing
}
export function clone<T>(t: T): T {
if (typeof t === 'function') throw new Error('Cannot clone a function')
if (typeof t === 'object') return stateSnapshot(t) as T
return t
}
export const editorPositionMap: Record<string, IPosition> = {}
@@ -8,14 +8,13 @@
} from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { page } from '$app/stores'
import { cleanValueProperties, decodeState, type Value } from '$lib/utils'
import { cleanValueProperties, decodeState, type Value, clone} from '$lib/utils'
import { afterNavigate, replaceState } from '$app/navigation'
import { goto } from '$lib/navigation'
import { sendUserToast, type ToastAction } from '$lib/toast'
import DiffDrawer from '$lib/components/DiffDrawer.svelte'
import type { App } from '$lib/components/apps/types'
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
import { stateSnapshot } from '$lib/svelte5Utils.svelte'
import { untrack } from 'svelte'
let app = $state(
@@ -53,7 +52,7 @@
path,
workspace: $workspaceStore!
})
const app_w_draft_: AppWithLastVersionWDraft = structuredClone(stateSnapshot(app_w_draft))
const app_w_draft_: AppWithLastVersionWDraft = clone(app_w_draft)
savedApp = {
summary: app_w_draft_.summary,
value: app_w_draft_.value as App,
@@ -201,7 +200,7 @@
function onRestore(ev: any) {
sendUserToast('App restored from previous deployment')
app = ev.detail
const app_ = structuredClone(stateSnapshot(app!))
const app_ = clone(app!)
savedApp = {
summary: app_.summary,
value: app_.value as App,
@@ -2,14 +2,13 @@
import { AppService, DraftService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { page } from '$app/stores'
import { cleanValueProperties, decodeState, type Value } from '$lib/utils'
import { cleanValueProperties, decodeState, type Value, clone} from '$lib/utils'
import { afterNavigate, replaceState } from '$app/navigation'
import { goto } from '$lib/navigation'
import { sendUserToast, type ToastAction } from '$lib/toast'
import DiffDrawer from '$lib/components/DiffDrawer.svelte'
import type { HiddenRunnable } from '$lib/components/apps/types'
import RawAppEditor from '$lib/components/raw_apps/RawAppEditor.svelte'
import { stateSnapshot } from '$lib/svelte5Utils.svelte'
let files: Record<string, string> | undefined = undefined
let runnables = {}
@@ -63,7 +62,7 @@
path,
workspace: $workspaceStore!
})
const app_w_draft_ = structuredClone(stateSnapshot(app_w_draft))
const app_w_draft_ = clone(app_w_draft)
savedApp = {
summary: app_w_draft_.summary,
value: app_w_draft_.value as any,
@@ -191,9 +190,9 @@
extractRawApp(prev)
savedApp = {
summary: prev.summary,
value: structuredClone(stateSnapshot(prev.value)),
value: clone(prev.value),
path: prev.path,
policy: structuredClone(stateSnapshot(policy)),
policy: clone(policy),
custom_path: prev.custom_path
}
redraw++
@@ -153,6 +153,7 @@
}
}
await initFlow(flow, flowStore, flowStateStore)
flowBuilder?.loadFlowState()
loading = false
}
@@ -182,6 +183,7 @@
{loading}
{draftTriggersFromUrl}
{selectedTriggerIndexFromUrl}
noInitial
>
<UnsavedConfirmationModal
getInitialAndModifiedValues={flowBuilder?.getInitialAndModifiedValues}
@@ -9,7 +9,8 @@
decodeState,
emptySchema,
orderedJsonStringify,
type StateStore
type StateStore,
clone
} from '$lib/utils'
import { initFlow } from '$lib/components/flows/flowStore.svelte'
import { goto } from '$lib/navigation'
@@ -22,6 +23,7 @@
import type { ScheduleTrigger } from '$lib/components/triggers'
import type { Trigger } from '$lib/components/triggers/utils'
import { untrack } from 'svelte'
import type { stepState } from '$lib/components/stepHistoryLoader.svelte'
let version: undefined | number = $state(undefined)
let nodraft = $page.url.searchParams.get('nodraft')
@@ -73,6 +75,9 @@
let draftTriggersFromUrl: Trigger[] | undefined = $state(undefined)
let selectedTriggerIndexFromUrl: number | undefined = $state(undefined)
let loadedFromHistoryFromUrl:
| { flowJobInitial: boolean | undefined; stepsState: Record<string, stepState> }
| undefined = $state(undefined)
let flowBuilder: FlowBuilder | undefined = $state(undefined)
@@ -106,8 +111,10 @@
flow = stateLoadedFromUrl.flow
draftTriggersFromUrl = stateLoadedFromUrl.draft_triggers
selectedTriggerIndexFromUrl = stateLoadedFromUrl.selected_trigger
loadedFromHistoryFromUrl = stateLoadedFromUrl.loadedFromHistory
flowBuilder?.setDraftTriggers(draftTriggersFromUrl)
flowBuilder?.setSelectedTriggerIndex(selectedTriggerIndexFromUrl)
flowBuilder?.setLoadedFromHistory(loadedFromHistoryFromUrl)
const selectedId = stateLoadedFromUrl?.selectedId ?? 'settings-metadata'
const reloadAction = () => {
stateLoadedFromUrl = undefined
@@ -152,10 +159,10 @@
path: $page.params.path
})
savedFlow = {
...structuredClone($state.snapshot(flowWithDraft)),
...clone(flowWithDraft),
draft: flowWithDraft.draft
? {
...structuredClone($state.snapshot(flowWithDraft.draft)),
...clone(flowWithDraft.draft),
path: flowWithDraft.draft.path ?? flowWithDraft.path // backward compatibility for old drafts missing path
}
: undefined
@@ -212,6 +219,7 @@
await initFlow(flow, flowStore, flowStateStore)
loading = false
selectedId = stateLoadedFromUrl?.selectedId ?? $page.url.searchParams.get('selected')
flowBuilder?.loadFlowState()
}
$effect(() => {
@@ -283,6 +291,7 @@
{draftTriggersFromUrl}
{selectedTriggerIndexFromUrl}
{version}
{loadedFromHistoryFromUrl}
>
<UnsavedConfirmationModal
{diffDrawer}
@@ -4,7 +4,7 @@
import { page } from '$app/stores'
import { initialArgsStore, workspaceStore } from '$lib/stores'
import ScriptBuilder from '$lib/components/ScriptBuilder.svelte'
import { decodeState, cleanValueProperties, orderedJsonStringify } from '$lib/utils'
import { decodeState, cleanValueProperties, orderedJsonStringify, clone} from '$lib/utils'
import { goto } from '$lib/navigation'
import { replaceState } from '$app/navigation'
import { sendUserToast } from '$lib/toast'
@@ -87,14 +87,14 @@
workspace: $workspaceStore!,
hash
})
savedScript = structuredClone($state.snapshot(scriptByHash)) as NewScriptWithDraft
savedScript = clone(scriptByHash) as NewScriptWithDraft
script = { ...scriptByHash, parent_hash: hash, lock: undefined }
} else {
const scriptWithDraft = await ScriptService.getScriptByPathWithDraft({
workspace: $workspaceStore!,
path: $page.params.path
})
savedScript = structuredClone($state.snapshot(scriptWithDraft))
savedScript = clone(scriptWithDraft)
if (scriptWithDraft.draft != undefined) {
script = scriptWithDraft.draft
scriptBuilder?.setDraftTriggers(script.draft_triggers)
@@ -18,8 +18,11 @@
return
}
console.log('logout 4', rd)
goto(rd ?? '/user/login')
if (rd?.startsWith('https://') || rd?.startsWith('http://')) {
window.location.href = rd
} else {
goto(rd ?? '/user/login')
}
})
</script>
+2 -2
View File
@@ -4,8 +4,8 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.501.2"
wmill_pg = ">=1.501.2"
wmill = ">=1.501.3"
wmill_pg = ">=1.501.3"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.501.2
version: 1.501.3
title: OpenFlow Spec
contact:
name: Ruben Fiszel
@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.501.2'
ModuleVersion = '1.501.3'
# Supported PSEditions
# CompatiblePSEditions = @()
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.501.2"
version = "1.501.3"
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.501.2"
version = "1.501.3"
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.501.2",
"version": "1.501.3",
"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.501.2",
"version": "1.501.3",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"devDependencies": {
+1 -1
View File
@@ -1 +1 @@
1.501.2
1.501.3