Compare commits

..

26 Commits

Author SHA1 Message Date
Faton Ramadani
a3373edddf Merge branch 'main' into app-components-outputs 2023-03-13 15:25:03 +01:00
Faton Ramadani
71da2f67e6 feat(frontend): improving connection 2023-03-13 15:24:47 +01:00
Ruben Fiszel
75f87e7e11 feat(frontend): disabled for action buttons can now depend on row 2023-03-13 14:36:42 +01:00
Ádám Kovács
3e5a179eb8 fix(frontend): General fixes and updates (#1281)
* fix(frontend): App icon select double click issue

* fix(frontend): Update script metadata page

* fix(frontend): Set dropdown default icon to vertical dots

* fix(frontend): Clean up

* fix(frontend): Update table styles

* fix(frontend): Add spacing to secondary menu items

* fix(frontend): Scale down full path

* fix(frontend): Table loading state

* fix(frontend): Hide script kind setting by default
2023-03-13 14:17:57 +01:00
Ruben Fiszel
c082c6350e fix runs reload 2023-03-13 13:00:23 +01:00
Ruben Fiszel
cfd489a550 feat(frontend): improve drag-n-drop behavior 2023-03-13 12:44:39 +01:00
Ruben Fiszel
1f4ae53fb4 integrate svelte-grid in codebase 2023-03-13 10:01:02 +01:00
Faton Ramadani
0f4da49886 Merge branch 'main' into app-components-outputs 2023-03-13 09:01:48 +01:00
Faton Ramadani
ae81d7ee2c feat(frontend): wip 2023-03-13 09:01:36 +01:00
Ruben Fiszel
0dcbf270da small app fixes 2023-03-12 21:32:41 +01:00
Faton Ramadani
82c139ed09 feat(frontend): Copy, Cut and Paste (#1279)
* feat(frontend): add copy, paste and cut

* feat(frontend): simplify code

* feat(frontend): add apple modifiers

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-03-12 20:58:06 +01:00
Faton Ramadani
98f8b845a6 feat(frontend): wip 2023-03-12 19:12:54 +01:00
Faton Ramadani
f521fb63fa feat(frontend): working animations 2023-03-12 14:17:25 +01:00
Faton Ramadani
fce464434b feat(frontend): working animations 2023-03-12 14:09:27 +01:00
Faton Ramadani
ee18804c87 feat(frontend): wip 2023-03-12 11:25:03 +01:00
Ruben Fiszel
0789bef120 chore(main): release 1.75.0 (#1278)
* chore(main): release 1.75.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-11 20:54:54 +01:00
Ruben Fiszel
1a7dc0a3bd more cli fixes 2023-03-11 20:53:34 +01:00
Ruben Fiszel
ce323709a9 fix(cli): many small fixes 2023-03-11 20:16:56 +01:00
Ruben Fiszel
61a5e1f1ac feat(frontend): make runs filters synced with query args 2023-03-11 17:13:49 +01:00
Ruben Fiszel
3b44f9a72c feat: add filter jobs by args or result 2023-03-11 15:30:46 +01:00
Ruben Fiszel
b349308ff7 handle better linked variables renaming 2023-03-11 11:25:47 +01:00
Faton Ramadani
6b5963f9d2 feat(frontend): merge main 2023-03-11 00:14:49 +01:00
Ruben Fiszel
f87b722a21 apps improvements 2023-03-10 23:27:55 +01:00
Faton Ramadani
d00a80a8f6 feat(frontend): hierarchical output panel WIP 2023-03-10 21:59:13 +01:00
Ruben Fiszel
0e9be7f300 fix for app viewer 2023-03-10 20:26:49 +01:00
Ruben Fiszel
8681e83b57 feat(apps): add resource picker 2023-03-10 20:01:00 +01:00
155 changed files with 2931 additions and 2445 deletions

View File

@@ -1,6 +1,23 @@
# Changelog
## [1.75.0](https://github.com/windmill-labs/windmill/compare/v1.74.2...v1.75.0) (2023-03-11)
### Features
* add filter jobs by args or result ([3b44f9a](https://github.com/windmill-labs/windmill/commit/3b44f9a72ca0466a44963a4b9657a0ee59b44753))
* **apps:** add resource picker ([8681e83](https://github.com/windmill-labs/windmill/commit/8681e83b574141acbf7e5a389a9e8a4f340336d1))
* **bash:** add default argument handling for bash ([1d5c194](https://github.com/windmill-labs/windmill/commit/1d5c194f09ffba963d52e418c5954843d84ae337))
* **frontend-apps:** add variable picker for static string input on apps ([bc440f8](https://github.com/windmill-labs/windmill/commit/bc440f8d4154ce464c0e027d93b7a0a3b76d782e))
* **frontend:** make runs filters synced with query args ([61a5e1f](https://github.com/windmill-labs/windmill/commit/61a5e1f1accc988628b785b3b9be04c4ea719874))
### Bug Fixes
* **backend:** add killpill for lines reading ([7c825c2](https://github.com/windmill-labs/windmill/commit/7c825c212dd0f1e8be427eabd9a9756303241d1b))
* **cli:** many small fixes ([ce32370](https://github.com/windmill-labs/windmill/commit/ce323709a94d27fb24214719180ea1aafc66d646))
## [1.74.2](https://github.com/windmill-labs/windmill/compare/v1.74.1...v1.74.2) (2023-03-09)

84
backend/Cargo.lock generated
View File

@@ -1123,9 +1123,9 @@ dependencies = [
[[package]]
name = "futures"
version = "0.3.26"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84"
checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549"
dependencies = [
"futures-channel",
"futures-core",
@@ -1138,9 +1138,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.26"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5"
checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac"
dependencies = [
"futures-core",
"futures-sink",
@@ -1148,15 +1148,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.26"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608"
checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd"
[[package]]
name = "futures-executor"
version = "0.3.26"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e"
checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83"
dependencies = [
"futures-core",
"futures-task",
@@ -1176,9 +1176,9 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.26"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531"
checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91"
[[package]]
name = "futures-lite"
@@ -1197,9 +1197,9 @@ dependencies = [
[[package]]
name = "futures-macro"
version = "0.3.26"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70"
checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6"
dependencies = [
"proc-macro2",
"quote",
@@ -1208,21 +1208,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.26"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364"
checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2"
[[package]]
name = "futures-task"
version = "0.3.26"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366"
checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879"
[[package]]
name = "futures-util"
version = "0.3.26"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1"
checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab"
dependencies = [
"futures-channel",
"futures-core",
@@ -1503,9 +1503,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "hyper"
version = "0.14.24"
version = "0.14.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c"
checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899"
dependencies = [
"bytes",
"futures-channel",
@@ -2953,7 +2953,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#87728c44527272ece739cddbb4942ad7e176dd79"
source = "git+https://github.com/RustPython/RustPython#415cdb1ef9b832fbca858a05f6894087e133dd31"
dependencies = [
"num-bigint",
"rustpython-compiler-core",
@@ -2962,7 +2962,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#87728c44527272ece739cddbb4942ad7e176dd79"
source = "git+https://github.com/RustPython/RustPython#415cdb1ef9b832fbca858a05f6894087e133dd31"
dependencies = [
"bitflags",
"bstr",
@@ -2975,7 +2975,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#87728c44527272ece739cddbb4942ad7e176dd79"
source = "git+https://github.com/RustPython/RustPython#415cdb1ef9b832fbca858a05f6894087e133dd31"
dependencies = [
"ahash",
"anyhow",
@@ -3667,9 +3667,9 @@ dependencies = [
[[package]]
name = "swc_common"
version = "0.29.35"
version = "0.29.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d515be281f603cb97afaa89896aca5e5c748fde11c7f926e35cdaa8ff8da705"
checksum = "5005cd73617e18592faa31298225b26f1c407b84a681d67efb735c3d3458e101"
dependencies = [
"ahash",
"ast_node",
@@ -4052,9 +4052,9 @@ checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
[[package]]
name = "toml_edit"
version = "0.19.4"
version = "0.19.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825"
checksum = "7082a95d48029677a28f181e5f6422d0c8339ad8396a39d3f33d62a90c1f6c30"
dependencies = [
"indexmap",
"toml_datetime",
@@ -4274,7 +4274,7 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "typify"
version = "0.0.11-dev"
source = "git+https://github.com/oxidecomputer/typify#18e7faf1957812626e3bf459e20fbe16785de2d4"
source = "git+https://github.com/oxidecomputer/typify#d1cae0f7d5115782c9557a46396dae786ad5f592"
dependencies = [
"typify-impl",
"typify-macro",
@@ -4283,7 +4283,7 @@ dependencies = [
[[package]]
name = "typify-impl"
version = "0.0.11-dev"
source = "git+https://github.com/oxidecomputer/typify#18e7faf1957812626e3bf459e20fbe16785de2d4"
source = "git+https://github.com/oxidecomputer/typify#d1cae0f7d5115782c9557a46396dae786ad5f592"
dependencies = [
"heck",
"log",
@@ -4301,7 +4301,7 @@ dependencies = [
[[package]]
name = "typify-macro"
version = "0.0.11-dev"
source = "git+https://github.com/oxidecomputer/typify#18e7faf1957812626e3bf459e20fbe16785de2d4"
source = "git+https://github.com/oxidecomputer/typify#d1cae0f7d5115782c9557a46396dae786ad5f592"
dependencies = [
"proc-macro2",
"quote",
@@ -4742,7 +4742,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.74.2"
version = "1.75.0"
dependencies = [
"anyhow",
"axum",
@@ -4769,7 +4769,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.74.2"
version = "1.75.0"
dependencies = [
"anyhow",
"argon2",
@@ -4824,7 +4824,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.74.2"
version = "1.75.0"
dependencies = [
"base64 0.21.0",
"chrono",
@@ -4839,7 +4839,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.74.2"
version = "1.75.0"
dependencies = [
"chrono",
"serde",
@@ -4852,7 +4852,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.74.2"
version = "1.75.0"
dependencies = [
"anyhow",
"axum",
@@ -4877,7 +4877,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.74.2"
version = "1.75.0"
dependencies = [
"serde",
"serde_json",
@@ -4885,7 +4885,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.74.2"
version = "1.75.0"
dependencies = [
"anyhow",
"itertools",
@@ -4900,7 +4900,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.74.2"
version = "1.75.0"
dependencies = [
"anyhow",
"itertools",
@@ -4912,7 +4912,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.74.2"
version = "1.75.0"
dependencies = [
"anyhow",
"itertools",
@@ -4927,7 +4927,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.74.2"
version = "1.75.0"
dependencies = [
"anyhow",
"deno_core",
@@ -4941,7 +4941,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.74.2"
version = "1.75.0"
dependencies = [
"anyhow",
"chrono",
@@ -4964,7 +4964,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.74.2"
version = "1.75.0"
dependencies = [
"anyhow",
"async-recursion",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.74.2"
version = "1.75.0"
authors.workspace = true
edition.workspace = true
@@ -19,7 +19,7 @@ members = [
]
[workspace.package]
version = "1.74.2"
version = "1.75.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"

View File

@@ -2984,20 +2984,6 @@
},
"query": "UPDATE group_ SET summary = $1 WHERE name = $2 AND workspace_id = $3"
},
"83fc9bf32b9b40987feff61fea82e20613e74efb7bf8ba52485566667ccf2284": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
}
},
"query": "UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3"
},
"8543f029d9784234e4c6a6dcd7b03e62d544b98be261334ee210594e0bb839f2": {
"describe": {
"columns": [
@@ -3168,6 +3154,21 @@
},
"query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, false, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage"
},
"8c0131a9cc61f2daa258d49767242bcaab6bb34a977ff7fb0c18aa9202d11f47": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Jsonb",
"Text",
"Text"
]
}
},
"query": "UPDATE resource SET path = $1, value = $2 WHERE path = $3 AND workspace_id = $4"
},
"8c11511a74a41a65f448249a00ebe6964a61d00c2f7b4875a55e64741bf1f0ca": {
"describe": {
"columns": [
@@ -4697,6 +4698,27 @@
},
"query": "SELECT null FROM queue WHERE id = $1 FOR UPDATE"
},
"be1de9116b18a40681cb8c5ec7578fd7c5f7ce77f7af63f766c2899d85fe0bef": {
"describe": {
"columns": [
{
"name": "value",
"ordinal": 0,
"type_info": "Jsonb"
}
],
"nullable": [
true
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT value FROM resource WHERE path = $1 AND workspace_id = $2"
},
"be7a99a5bb6858323ca61dd51077010f51ba58ae76b9a413339255024dcb524d": {
"describe": {
"columns": [],

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.74.2
version: 1.75.0
title: Windmill API
contact:
@@ -3168,12 +3168,14 @@ paths:
- $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath"
- $ref: "#/components/parameters/ScriptExactHash"
- $ref: "#/components/parameters/CreatedBefore"
- $ref: "#/components/parameters/CreatedAfter"
- $ref: "#/components/parameters/StartedBefore"
- $ref: "#/components/parameters/StartedAfter"
- $ref: "#/components/parameters/Success"
- $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/Suspended"
- $ref: "#/components/parameters/Running"
- $ref: "#/components/parameters/ArgsFilter"
- $ref: "#/components/parameters/ResultFilter"
responses:
"200":
description: All available queued jobs
@@ -3198,10 +3200,12 @@ paths:
- $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath"
- $ref: "#/components/parameters/ScriptExactHash"
- $ref: "#/components/parameters/CreatedBefore"
- $ref: "#/components/parameters/CreatedAfter"
- $ref: "#/components/parameters/StartedBefore"
- $ref: "#/components/parameters/StartedAfter"
- $ref: "#/components/parameters/Success"
- $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/ArgsFilter"
- $ref: "#/components/parameters/ResultFilter"
- name: is_skipped
description: is the job skipped
in: query
@@ -3235,9 +3239,11 @@ paths:
- $ref: "#/components/parameters/ScriptExactPath"
- $ref: "#/components/parameters/ScriptStartPath"
- $ref: "#/components/parameters/ScriptExactHash"
- $ref: "#/components/parameters/CreatedBefore"
- $ref: "#/components/parameters/CreatedAfter"
- $ref: "#/components/parameters/StartedBefore"
- $ref: "#/components/parameters/StartedAfter"
- $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/ArgsFilter"
- $ref: "#/components/parameters/ResultFilter"
- name: is_skipped
description: is the job skipped
in: query
@@ -4614,15 +4620,15 @@ components:
in: query
schema:
type: string
CreatedBefore:
name: created_before
StartedBefore:
name: started_before
description: filter on created before (inclusive) timestamp
in: query
schema:
type: string
format: date-time
CreatedAfter:
name: created_after
StartedAfter:
name: started_after
description: filter on created after (exclusive) timestamp
in: query
schema:
@@ -4646,6 +4652,19 @@ components:
in: query
schema:
type: boolean
ArgsFilter:
name: args
description: filter on jobs containing those args as a json subset (@> in postgres)
in: query
schema:
type: string
ResultFilter:
name: result
description: filter on jobs containing those result as a json subset (@> in postgres)
in: query
schema:
type: string
After:
name: after
description: filter on created after (exclusive) timestamp

View File

@@ -16,10 +16,9 @@ use axum::{
routing::{delete, get, post},
Json, Router,
};
use itertools::Itertools;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{self, Error, JsonResult, Result},
error::{self, to_anyhow, Error, JsonResult, Result},
users::username_to_permissioned_as,
utils::{not_found_if_none, paginate, Pagination},
};
@@ -262,13 +261,26 @@ async fn update_folder(
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
if let Some(display_name) = ng.display_name {
sqlb.set("display_name", display_name);
sqlb.set("display_name", "?".bind(&display_name));
}
if let Some(owners) = ng.owners {
sqlb.set_str("owners", format!("{{{}}}", owners.into_iter().join(",")));
sqlb.set(
"owners",
"?".bind(&format!(
"{{{}}}",
owners
.iter()
.map(|x| format!("\"{x}\""))
.collect::<Vec<_>>()
.join(","),
)),
);
}
if let Some(extra_perms) = ng.extra_perms {
sqlb.set_str("extra_perms", extra_perms.to_string());
sqlb.set(
"extra_perms",
"?".bind(&serde_json::to_string(&extra_perms).map_err(to_anyhow)?),
);
}
sqlb.returning("*");

View File

@@ -327,13 +327,15 @@ pub struct ListQueueQuery {
pub script_path_exact: Option<String>,
pub script_hash: Option<String>,
pub created_by: Option<String>,
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
pub running: Option<bool>,
pub parent_job: Option<String>,
pub order_desc: Option<bool>,
pub job_kinds: Option<String>,
pub suspended: Option<bool>,
// filter by matching a subset of the args using base64 encoded json subset
pub args: Option<String>,
}
fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder {
@@ -362,11 +364,11 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
if let Some(pj) = &lq.parent_job {
sqlb.and_where_eq("parent_job", "?".bind(pj));
}
if let Some(dt) = &lq.created_before {
sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp()));
if let Some(dt) = &lq.started_before {
sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp()));
}
if let Some(dt) = &lq.created_after {
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
if let Some(dt) = &lq.started_after {
sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp()));
}
if let Some(s) = &lq.suspended {
@@ -376,6 +378,7 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
sqlb.and_where_eq("suspend", 0);
}
}
if let Some(jk) = &lq.job_kinds {
sqlb.and_where_in(
"job_kind",
@@ -383,6 +386,10 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
);
}
if let Some(args) = &lq.args {
sqlb.and_where("args @> ?".bind(&args.replace("'", "''")));
}
sqlb
}
@@ -455,13 +462,14 @@ async fn list_jobs(
script_path_exact: lq.script_path_exact,
script_hash: lq.script_hash,
created_by: lq.created_by,
created_before: lq.created_before,
created_after: lq.created_after,
started_before: lq.started_before,
started_after: lq.started_after,
running: None,
parent_job: lq.parent_job,
order_desc: Some(true),
job_kinds: lq.job_kinds,
suspended: lq.suspended,
args: lq.args,
},
&[
"'QueuedJob' as typ",
@@ -1136,15 +1144,15 @@ where
struct InPayload {
payload: Option<String>,
}
fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
let vec = base64::engine::general_purpose::URL_SAFE
.decode(t)
.context("invalid base64")?;
serde_json::from_slice(vec.as_slice()).context("invalid json")
}
}
}
fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
let vec = base64::engine::general_purpose::URL_SAFE
.decode(t)
.context("invalid base64")?;
serde_json::from_slice(vec.as_slice()).context("invalid json")
}
pub async fn run_flow_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
@@ -1687,11 +1695,11 @@ fn list_completed_jobs_query(
if let Some(pj) = &lq.parent_job {
sqlb.and_where_eq("parent_job", "?".bind(pj));
}
if let Some(dt) = &lq.created_before {
sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp()));
if let Some(dt) = &lq.started_before {
sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp()));
}
if let Some(dt) = &lq.created_after {
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
if let Some(dt) = &lq.started_after {
sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp()));
}
if let Some(sk) = &lq.is_skipped {
sqlb.and_where_eq("is_skipped", sk);
@@ -1706,6 +1714,15 @@ fn list_completed_jobs_query(
);
}
if let Some(args) = &lq.args {
sqlb.and_where("args @> ?".bind(&args.replace("'", "''")));
}
if let Some(result) = &lq.result {
sqlb.and_where("result @> ?".bind(&result.replace("'", "''")));
}
tracing::info!("{:?}", sqlb.sql());
sqlb
}
#[derive(Deserialize, Clone)]
@@ -1714,8 +1731,8 @@ pub struct ListCompletedQuery {
pub script_path_exact: Option<String>,
pub script_hash: Option<String>,
pub created_by: Option<String>,
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
pub success: Option<bool>,
pub parent_job: Option<String>,
pub order_desc: Option<bool>,
@@ -1723,6 +1740,10 @@ pub struct ListCompletedQuery {
pub is_skipped: Option<bool>,
pub is_flow_step: Option<bool>,
pub suspended: Option<bool>,
// filter by matching a subset of the args using base64 encoded json subset
pub args: Option<String>,
// filter by matching a subset of the result using base64 encoded json subset
pub result: Option<String>,
}
async fn list_completed_jobs(

View File

@@ -26,6 +26,7 @@ use axum::{
Json, Router,
};
use hyper::StatusCode;
use serde_json::Value;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{Error, JsonResult, Result},
@@ -378,9 +379,26 @@ async fn update_variable(
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
}
let mut v = sqlx::query_scalar!(
"SELECT value FROM resource WHERE path = $1 AND workspace_id = $2",
path,
w_id
)
.fetch_one(&mut tx)
.await?;
if let Some(old_v) = v {
v = Some(replace_path(
old_v,
&format!("$var:{path}"),
&format!("$var:{npath}"),
))
}
sqlx::query!(
"UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3",
"UPDATE resource SET path = $1, value = $2 WHERE path = $3 AND workspace_id = $4",
npath,
v,
path,
w_id
)
@@ -419,6 +437,23 @@ async fn update_variable(
Ok(format!("variable {} updated (npath: {:?})", path, npath))
}
fn replace_path(v: serde_json::Value, path: &str, npath: &str) -> Value {
match v {
Value::Object(v) => Value::Object(
v.into_iter()
.map(|(k, v)| (k, replace_path(v, path, npath)))
.collect(),
),
Value::Array(arr) => Value::Array(
arr.into_iter()
.map(|v| replace_path(v, path, npath))
.collect(),
),
Value::String(s) if s == path => Value::String(npath.to_owned()),
_ => v,
}
}
pub async fn build_crypt<'c>(
db: &mut Transaction<'c, Postgres>,
w_id: &str,

View File

@@ -1073,6 +1073,7 @@ struct ScriptMetadata {
schema: Option<Schema>,
is_template: bool,
lock: Vec<String>,
kind: String,
}
enum ArchiveImpl {
@@ -1235,6 +1236,7 @@ async fn tarball_workspace(
description: script.description,
schema: script.schema,
is_template: script.is_template,
kind: script.kind.to_string(),
lock,
};
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();

View File

@@ -7,7 +7,7 @@
*/
use std::{
fmt::Display,
fmt::{self, Display},
hash::{Hash, Hasher},
};
@@ -103,6 +103,18 @@ pub enum ScriptKind {
Approval,
}
impl Display for ScriptKind {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str(match self {
ScriptKind::Trigger => "trigger",
ScriptKind::Failure => "failure",
ScriptKind::Script => "script",
ScriptKind::Approval => "approval",
})?;
Ok(())
}
}
#[derive(Serialize)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct Script {

View File

@@ -28,14 +28,18 @@ export class AppFile implements Resource, PushDiffs {
remotePath: string,
diffs: Difference[],
): Promise<void> {
if (await AppService.existsApp({ workspace, path: remotePath })) {
let app: AppWithLastVersion | undefined = undefined;
try {
app = await AppService.getAppByPath({ workspace, path: remotePath });
} catch (e) {}
if (app) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing app...`,
`Applying ${diffs.length} diffs to existing app... ${remotePath}`,
),
);
const changeset: {
path?: string | undefined;
summary?: string | undefined;
value?: any;
policy?: Policy | undefined;
@@ -46,7 +50,7 @@ export class AppFile implements Resource, PushDiffs {
(
diff.path[0] !== "value" && diff.path[0] !== "policy" && (
diff.path.length !== 1 ||
!["path", "summary"].includes(
!["summary"].includes(
diff.path[0] as string,
)
)
@@ -61,6 +65,13 @@ export class AppFile implements Resource, PushDiffs {
}
}
if ((!changeset?.policy || JSON.stringify(changeset?.policy) == JSON.stringify(app.policy))
&& (!changeset?.value || JSON.stringify(changeset?.value) == JSON.stringify(app.value))
&& (!changeset?.summary || changeset.summary == app.summary)) {
console.log(colors.yellow(`No changes to push for app ${remotePath}, skipping`))
return;
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
);
@@ -87,19 +98,10 @@ export class AppFile implements Resource, PushDiffs {
}
}
async push(workspace: string, remotePath: string): Promise<void> {
let existing: AppWithLastVersion | undefined;
try {
existing = await AppService.getAppByPath({
workspace: workspace,
path: remotePath,
});
} catch {
existing = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
microdiff({}, this, { cyclesFix: false }),
);
}
}

View File

@@ -94,6 +94,7 @@ export class FlowFile implements Resource, PushDiffs {
...changeset,
...base_changeset,
}
await FlowService.updateFlow({
workspace: workspace,
path: remotePath,
@@ -114,20 +115,11 @@ export class FlowFile implements Resource, PushDiffs {
}
}
async push(workspace: string, remotePath: string): Promise<void> {
let remote: Flow | undefined;
try {
remote = await FlowService.getFlowByPath({
workspace,
path: remotePath,
});
} catch {
remote = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff(remote ?? {}, this, { cyclesFix: false }),
microdiff({}, this, { cyclesFix: false }),
);
}
}

View File

@@ -1,4 +1,4 @@
import { colors, Command, Folder, FolderService, microdiff } from "./deps.ts";
import { colors, Command, FolderService, microdiff } from "./deps.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import {
Difference,
@@ -22,7 +22,9 @@ export class FolderFile implements Resource, PushDiffs {
owners: Array<string> | undefined;
@property(map(() => String, () => Boolean, { shape: MapShape.Object }))
extra_perms: Map<string, boolean> | undefined;
@property(() => String)
display_name: string| undefined;
async push(workspace: string, remotePath: string): Promise<void> {
if (remotePath.startsWith("/")) {
remotePath = remotePath.substring(1);
@@ -31,16 +33,10 @@ export class FolderFile implements Resource, PushDiffs {
remotePath = remotePath.substring(2);
}
let existing: Folder | undefined;
try {
existing = await FolderService.getFolder({ workspace, name: remotePath });
} catch {
existing = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
microdiff({}, this, { cyclesFix: false }),
);
}
@@ -66,22 +62,24 @@ export class FolderFile implements Resource, PushDiffs {
if (exists) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing folder...`,
`Applying ${diffs.length} diffs to existing folder... ${remotePath}`,
),
);
const changeset: {
owners?: string[] | undefined;
extra_perms?: any;
display_name?: string | undefined;
} = {};
for (const diff of diffs) {
if (
diff.type !== "REMOVE" &&
(
diff.path.length !== 1 ||
!["owners", "extra_perms"].includes(diff.path[0] as string)
!["owners", "extra_perms", "display_name"].includes(diff.path[0] as string)
)
) {
console.log(diff.path)
throw new Error("Invalid folder diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
@@ -97,11 +95,10 @@ export class FolderFile implements Resource, PushDiffs {
if (!hasChanges) {
return;
}
await FolderService.updateFolder({
workspace: workspace,
name: remotePath,
requestBody: changeset,
requestBody: {...changeset, extra_perms: changeset.extra_perms ? Object.fromEntries(this.extra_perms?.entries() ?? []) : undefined}
});
} else {
console.log(colors.bold.yellow("Creating new folder: " + remotePath));

View File

@@ -13,7 +13,7 @@ import sync from "./sync.ts";
import { tryResolveVersion } from "./context.ts";
import { GlobalOptions } from "./types.ts";
const VERSION = "v1.74.2";
const VERSION = "v1.75.0";
let command: any = new Command()
.name("wmill")

View File

@@ -13,7 +13,6 @@ import {
EditResourceType,
microdiff,
ResourceService,
ResourceType,
Table,
} from "./deps.ts";
import { Any, decoverto, model, property } from "./decoverto.ts";
@@ -26,19 +25,10 @@ export class ResourceTypeFile implements ResourceI, PushDiffs {
description?: string;
async push(workspace: string, remotePath: string): Promise<void> {
let existing: ResourceType | undefined;
try {
existing = await ResourceService.getResourceType({
workspace,
path: remotePath,
});
} catch {
existing = undefined;
}
this.pushDiffs(
await this.pushDiffs(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
microdiff({}, this, { cyclesFix: false }),
);
}
@@ -65,8 +55,8 @@ export class ResourceTypeFile implements ResourceI, PushDiffs {
return;
}
console.log(
colors.yellow(
`Applying ${diffs.length} diffs to existing resource type...`,
colors.yellow.bold(
`Applying ${diffs.length} diffs to existing resource type... ${remotePath}`,
),
);
const changeset: EditResourceType = {};

View File

@@ -43,7 +43,7 @@ export class ResourceFile implements Resource2, PushDiffs {
})
) {
console.log(
colors.yellow(`Applying ${diffs.length} diffs to existing resource...`),
colors.yellow.bold(`Applying ${diffs.length} diffs to existing resource... ${remotePath}`),
);
const changeset: EditResource = {
@@ -51,11 +51,7 @@ export class ResourceFile implements Resource2, PushDiffs {
};
for (const diff of diffs) {
if (diff.path[0] === "is_oauth") {
console.log(
colors.yellow(
"! is_oauth has been removed in newer versions. Ignoring.",
),
);
//is_oauth is not updatable
continue;
}
if (
@@ -64,9 +60,10 @@ export class ResourceFile implements Resource2, PushDiffs {
diff.path[0] !== "value" && (
diff.path.length !== 1 ||
diff.path[0] !== "description"
)
) && diff.path[0] !== "resource_type"
)
) {
console.log(colors.red("Invalid variable diff with path " + diff.path));
throw new Error("Invalid folder diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
@@ -110,19 +107,10 @@ export class ResourceFile implements Resource2, PushDiffs {
}
}
async push(workspace: string, remotePath: string): Promise<void> {
let existing: Resource | undefined;
try {
existing = await ResourceService.getResource({
workspace,
path: remotePath,
});
} catch {
existing = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
microdiff({}, this, { cyclesFix: false }),
);
}
}

View File

@@ -42,6 +42,7 @@ export class ScriptFile {
},
toPlain: (data) => data,
})
@property(() => String)
kind?: "script" | "failure" | "trigger" | "command" | "approval";
constructor(summary: string, description: string) {
@@ -105,11 +106,21 @@ export async function handleFile(path: string, content: string, workspace: strin
} catch { }
const language = inferContentTypeFromFilePath(path);
let remote = undefined
try {
const remote = await ScriptService.getScriptByPath({
remote = await ScriptService.getScriptByPath({
workspace,
path: remotePath,
});
} catch { }
if (remote) {
if (typed.description === remote.description && content === remote.content && typed.summary === remote.summary && typed.is_template === remote.is_template && typed.kind == remote.kind && remote?.lock == typed.lock?.join('\n') && JSON.stringify(typed.schema) == JSON.stringify(remote.schema)) {
console.log(colors.yellow(`No change to push for script ${remotePath}, skipping`))
return true
}
await ScriptService.createScript({
workspace,
requestBody: {
@@ -123,10 +134,11 @@ export async function handleFile(path: string, content: string, workspace: strin
lock: typed.lock,
parent_hash: remote.hash,
schema: typed.schema,
},
}
});
console.log(colors.yellow.bold(`Creating script with a parent ${remotePath}`))
} catch {
} else {
// no parent hash
await ScriptService.createScript({
workspace: workspace,
@@ -144,7 +156,6 @@ export async function handleFile(path: string, content: string, workspace: strin
},
});
console.log(colors.yellow.bold(`Creating script without parent ${remotePath}`))
}
return true
}

View File

@@ -33,8 +33,8 @@ import { ResourceFile } from "./resource.ts";
import { FlowFile } from "./flow.ts";
import { VariableFile } from "./variable.ts";
import { handleFile } from "./script.ts";
import { equal } from "https://deno.land/x/equal/mod.ts";
import { diffCharacters } from "https://deno.land/x/diff/mod.ts";
import { equal } from "https://deno.land/x/equal@v1.5.0/mod.ts";
import { diffCharacters } from "https://deno.land/x/diff@v0.3.5/mod.ts";
type DynFSElement = {
isDirectory: boolean;
path: string;
@@ -179,13 +179,13 @@ async function elementsToMap(els: DynFSElement, ignore: (path: string, isDirecto
return map;
}
async function compareDynFSElement(
els1: DynFSElement, els2: DynFSElement,
els1: DynFSElement, els2: DynFSElement | undefined,
ignore: (path: string, isDirectory: boolean) => boolean,
raw: boolean
): Promise<Change[]> {
const [m1, m2] = raw ? [await elementsToMap(els1, ignore), {}] :
await Promise.all([elementsToMap(els1, ignore), elementsToMap(els2, ignore)]);
const [m1, m2] = els2
? await Promise.all([elementsToMap(els1, ignore), elementsToMap(els2, ignore)])
: [await elementsToMap(els1, ignore), {}];
const changes: Change[] = [];
@@ -266,8 +266,8 @@ async function pull(
console.log(colors.gray("Computing the files to update locally to match remote (taking .wmillignore into account)"));
const remote = ZipFSElement((await downloadZip(workspace))!)
const local = await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill"))
const changes = await compareDynFSElement(remote, local, await ignoreF(), opts.raw)
const local = opts.raw ? undefined : await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill"))
const changes = await compareDynFSElement(remote, local, await ignoreF())
console.log(`remote -> local: ${changes.length} changes to apply`);
@@ -289,7 +289,7 @@ async function pull(
try {
const currentLocal = await Deno.readTextFile(target)
if (currentLocal !== change.before) {
if (currentLocal !== change.before && currentLocal !== change.after) {
console.log(colors.red(`Conflict detected on ${change.path}\nBoth local and remote have been modified.`))
if (opts.failConflicts) {
conflicts.push({ local: currentLocal, change, path: change.path })
@@ -459,7 +459,6 @@ function removeSuffix(str: string, suffix: string) {
async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull: boolean, failConflicts: boolean }) {
if (!opts.raw) {
if (!opts.skipPull) {
console.log(colors.gray("You need to be up-to-date before pushing, pulling first."))
@@ -474,9 +473,9 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull
console.log(colors.gray("Computing the files to update on the remote to match local (taking .wmillignore into account)"));
const remote = ZipFSElement((await downloadZip(workspace))!)
const remote = opts.raw ? undefined : ZipFSElement((await downloadZip(workspace))!)
const local = await FSFSElement(path.join(Deno.cwd(), ""))
const changes = await compareDynFSElement(local, remote, await ignoreF(), opts.raw)
const changes = await compareDynFSElement(local, remote, await ignoreF())
console.log(`remote <- local: ${changes.length} changes to apply`);
if (changes.length > 0) {
@@ -575,7 +574,7 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull
break;
}
try {
Deno.remove(stateTarget)
await Deno.remove(stateTarget)
} catch { }
}
}
@@ -616,7 +615,7 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull
await file.pushDiffs(workspace, remotePath, diffs);
} catch (e) {
console.error("Failing to apply diffs to " + remotePath)
console.error(e.body)
console.error(JSON.stringify(e))
}
}
}
@@ -635,7 +634,7 @@ const command = new Command()
"Push any local changes and apply them remotely. Use --raw for usage without local state tracking.",
)
.option("--fail-conflicts", "Error on conflicts (both remote and local have changes on the same item)")
.option("--skip-pull", "Push without pulling first")
.option("--skip-pull", "Push without pulling first (you have pulled prior)")
.option("--yes", "Push without needing confirmation")
.option("--raw", "Push without using state, just overwrite.")
.action(push as any);

View File

@@ -11,7 +11,6 @@ import {
colors,
Command,
EditVariable,
ListableVariable,
microdiff,
Table,
VariableService,
@@ -67,7 +66,7 @@ export class VariableFile implements Resource, PushDiffs {
if (await VariableService.existsVariable({ workspace, path: remotePath })) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing variable...`,
`Applying ${diffs.length} diffs to existing variable... ${remotePath}`,
),
);
const changeset: EditVariable = {};
@@ -76,11 +75,12 @@ export class VariableFile implements Resource, PushDiffs {
diff.type !== "REMOVE" &&
(
diff.path.length !== 1 ||
!["path", "value", "is_secret", "description"].includes(
!["path", "value", "is_secret", "description", "account", "is_oauth"].includes(
diff.path[0] as string,
)
)
) {
console.log(colors.red("Invalid variable diff with path " + diff.path));
throw new Error("Invalid variable diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
@@ -96,6 +96,7 @@ export class VariableFile implements Resource, PushDiffs {
if (!hasChanges) {
return;
}
await VariableService.updateVariable({
workspace,
path: remotePath,
@@ -103,7 +104,6 @@ export class VariableFile implements Resource, PushDiffs {
requestBody: changeset,
});
console.log(changeset);
} else {
console.log(colors.yellow.bold("Creating new variable..."));
await VariableService.createVariable({
@@ -121,19 +121,10 @@ export class VariableFile implements Resource, PushDiffs {
}
}
async push(workspace: string, remotePath: string): Promise<void> {
let existing: ListableVariable | undefined;
try {
existing = await VariableService.getVariable({
workspace: workspace,
path: remotePath,
});
} catch {
existing = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
microdiff({}, this, { cyclesFix: false }),
);
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "windmill",
"version": "1.74.2",
"version": "1.75.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "windmill",
"version": "1.74.2",
"version": "1.75.0",
"dependencies": {
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
@@ -48,7 +48,6 @@
"@types/vscode": "~1.74.0",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.48.0",
"@windmill-labs/svelte-grid": "^5.1.6",
"@zerodevx/svelte-toast": "^0.8.1",
"autoprefixer": "^10.4.13",
"cssnano": "^5.1.14",
@@ -64,7 +63,6 @@
"postcss-load-config": "^4.0.1",
"prettier": "^2.8.3",
"prettier-plugin-svelte": "^2.9.0",
"shepherd.js": "^10.0.1",
"simple-svelte-autocomplete": "^2.5.1",
"stylelint-config-recommended": "^9.0.0",
"svelte": "^3.55.1",
@@ -1602,12 +1600,6 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@windmill-labs/svelte-grid": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/@windmill-labs/svelte-grid/-/svelte-grid-5.1.6.tgz",
"integrity": "sha512-wXHIG7XeMxwxnfaHajhjvz9c0u5uGgiQtTk/QxAVyViGdshsclfIfIWZF113QQBKCKp2jY/zP1cj64BrqioRoQ==",
"dev": true
},
"node_modules/@zeit/schemas": {
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.21.0.tgz",
@@ -6654,24 +6646,6 @@
"node": ">=8"
}
},
"node_modules/shepherd.js": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/shepherd.js/-/shepherd.js-10.0.1.tgz",
"integrity": "sha512-R32v4b4b0N1gK/vxvRtdhty+SBZlBPcPTSYCwcaAQvFd0n6Xki7cRH6Sx0oD9WB/HCkqCNM1msyIyylXmq635w==",
"dev": true,
"dependencies": {
"@popperjs/core": "^2.11.5",
"deepmerge": "^4.2.2",
"smoothscroll-polyfill": "^0.4.4"
},
"engines": {
"node": "12.* || 14.* || >= 16"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/rwwagner90"
}
},
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@@ -6758,12 +6732,6 @@
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/smoothscroll-polyfill": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/smoothscroll-polyfill/-/smoothscroll-polyfill-0.4.4.tgz",
"integrity": "sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==",
"dev": true
},
"node_modules/sorcery": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
@@ -9309,12 +9277,6 @@
"eslint-visitor-keys": "^3.3.0"
}
},
"@windmill-labs/svelte-grid": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/@windmill-labs/svelte-grid/-/svelte-grid-5.1.6.tgz",
"integrity": "sha512-wXHIG7XeMxwxnfaHajhjvz9c0u5uGgiQtTk/QxAVyViGdshsclfIfIWZF113QQBKCKp2jY/zP1cj64BrqioRoQ==",
"dev": true
},
"@zeit/schemas": {
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.21.0.tgz",
@@ -13014,17 +12976,6 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true
},
"shepherd.js": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/shepherd.js/-/shepherd.js-10.0.1.tgz",
"integrity": "sha512-R32v4b4b0N1gK/vxvRtdhty+SBZlBPcPTSYCwcaAQvFd0n6Xki7cRH6Sx0oD9WB/HCkqCNM1msyIyylXmq635w==",
"dev": true,
"requires": {
"@popperjs/core": "^2.11.5",
"deepmerge": "^4.2.2",
"smoothscroll-polyfill": "^0.4.4"
}
},
"signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@@ -13085,12 +13036,6 @@
"is-fullwidth-code-point": "^3.0.0"
}
},
"smoothscroll-polyfill": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/smoothscroll-polyfill/-/smoothscroll-polyfill-0.4.4.tgz",
"integrity": "sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==",
"dev": true
},
"sorcery": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "windmill",
"version": "1.74.2",
"version": "1.75.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -27,7 +27,6 @@
"@types/vscode": "~1.74.0",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.48.0",
"@windmill-labs/svelte-grid": "^5.1.6",
"@zerodevx/svelte-toast": "^0.8.1",
"autoprefixer": "^10.4.13",
"cssnano": "^5.1.14",
@@ -43,7 +42,6 @@
"postcss-load-config": "^4.0.1",
"prettier": "^2.8.3",
"prettier-plugin-svelte": "^2.9.0",
"shepherd.js": "^10.0.1",
"simple-svelte-autocomplete": "^2.5.1",
"stylelint-config-recommended": "^9.0.0",
"svelte": "^3.55.1",

View File

@@ -1,79 +1,2 @@
/// <reference types="@sveltejs/kit" />
declare type Item = import('svelte-dnd-action').Item
declare type DndEvent<ItemType = Item> = import('svelte-dnd-action').DndEvent<ItemType>
declare namespace svelte.JSX {
interface HTMLAttributes<T> {
onconsider?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void
onfinalize?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void
}
}
declare module '@windmill-labs/svelte-grid' {
import type { SvelteComponentTyped } from 'svelte'
export interface Size {
w: number
h: number
}
export interface Positon {
x: number
y: number
}
interface ItemLayout extends Size, Positon {
fixed?: boolean
resizable?: boolean
draggable?: boolean
customDragger?: boolean
customResizer?: boolean
min?: Size
max?: Size
}
export type Item<T> = T & { [width: number]: ItemLayout; data: any }
export type FilledItem<T> = T & { [width: number]: Required<ItemLayout>; data: any }
export interface Props<T> {
fillSpace?: boolean
items: FilledItem<T>[]
rowHeight: number
cols: [number, number][]
gap?: [number, number]
fastStart?: boolean
throttleUpdate?: number
throttleResize?: number
onTopId?: string
scroller?: undefined
sensor?: number
parentWidth?: number
}
export interface Slots<T> {
default: { item: ItemLayout; dataItem: Item<T> }
}
export default class Grid<T = {}> extends SvelteComponentTyped<
Props<T>,
{
pointerup: CustomEvent<{ id: string }>
mount: CustomEvent<>
},
Slots<T>
> { }
}
declare module '@windmill-labs/svelte-grid/build/helper/index.mjs' {
import { ItemLayout } from '@windmill-labs/svelte-grid'
const x: {
normalize(items: any[], col: any): unknown[]
adjust(items: any[], col: any): unknown[]
findSpace(item: any, items: any, cols: any): unknown
item<T>(obj: ItemLayout): Required<ItemLayout>
}
export default x
}

View File

@@ -81,7 +81,7 @@
$: key =
apiTokenApps[resource_type]?.key ??
(args != undefined
? Object.keys(args).filter((x) => ['token', 'password', 'api_key'].includes(x))[0]
? Object.keys(args).filter((x) => ['token', 'password', 'api_key', 'key'].includes(x))[0]
: undefined)
let scopes: string[] = []
@@ -259,6 +259,7 @@
args['token'] == '' &&
args['password'] == '' &&
args['api_key'] == '' &&
args['key'] == '' &&
key != undefined) ||
(step == 3 && pathError != '') ||
!isValid
@@ -396,7 +397,7 @@
bind:error={pathError}
bind:path
initialPath=""
namePlaceholder="{resource_type}"
namePlaceholder={resource_type}
kind="resource"
/>
@@ -433,7 +434,7 @@
{:else}
<Path
initialPath=""
namePlaceholder="{resource_type}"
namePlaceholder={resource_type}
bind:error={pathError}
bind:path
kind="resource"

View File

@@ -2,9 +2,10 @@
import type { DropdownItem } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import { MoreHorizontal } from 'lucide-svelte'
import { MoreVertical } from 'lucide-svelte'
import { Button, Menu } from './common'
import { goto } from '$app/navigation'
import { twMerge } from 'tailwind-merge'
type Alignment = 'start' | 'end'
type Side = 'top' | 'bottom'
@@ -13,15 +14,24 @@
export let dropdownItems: DropdownItem[]
export let name: string | undefined = undefined
export let placement: Placement = 'bottom-start'
export let btnClasses = '!text-blue-500 bg-transparent'
export let btnClasses = ''
$: buttonClass = twMerge('!border-0 bg-transparent !p-[6px]', btnClasses)
const dispatch = createEventDispatcher()
</script>
<Menu {placement} let:close>
<Button nonCaptureEvent color="light" size="xs" {btnClasses} {...$$restProps} slot="trigger">
<Button
nonCaptureEvent
color="dark"
variant="border"
size="xs"
btnClasses={buttonClass}
{...$$restProps}
slot="trigger"
>
{#if !$$slots.default}
<MoreHorizontal size={20} />
<MoreVertical size={20} />
{:else}
<slot />
{/if}

View File

@@ -232,14 +232,8 @@
<Badge color={validCode ? 'green' : 'red'} class="min-w-[60px] mr-3">
{validCode ? 'Valid' : 'Invalid'}
</Badge>
<div id="script-tutorial-3" class="flex items-center divide-x">
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="pr-1"
disablePopup={!iconOnly}
>
<div class="flex items-center divide-x">
<Popover notClickable placement="bottom" disapperTimoout={0} class="pr-1" disablePopup={!iconOnly}>
<Button
color="light"
btnClasses="!font-medium !h-full"
@@ -251,15 +245,11 @@
>
+Context Var
</Button>
<svelte:fragment slot="text">Add context variable</svelte:fragment>
<svelte:fragment slot="text">
Add context variable
</svelte:fragment>
</Popover>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
color="light"
btnClasses="!font-medium !h-full"
@@ -271,15 +261,11 @@
>
+Variable
</Button>
<svelte:fragment slot="text">Add variable</svelte:fragment>
<svelte:fragment slot="text">
Add variable
</svelte:fragment>
</Popover>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
btnClasses="!font-medium !h-full"
size="xs"
@@ -291,15 +277,11 @@
>
+Resource
</Button>
<svelte:fragment slot="text">Add resource</svelte:fragment>
<svelte:fragment slot="text">
Add resource
</svelte:fragment>
</Popover>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
btnClasses="!font-medium !h-full"
size="xs"
@@ -311,15 +293,11 @@
>
Reset
</Button>
<svelte:fragment slot="text">Reset</svelte:fragment>
<svelte:fragment slot="text">
Reset
</svelte:fragment>
</Popover>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
btnClasses="!font-medium !h-full"
size="xs"
@@ -342,15 +320,11 @@
{/if}
</span>
</Button>
<svelte:fragment slot="text">Reload assistant</svelte:fragment>
<svelte:fragment slot="text">
Reload assistant
</svelte:fragment>
</Popover>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
btnClasses="!font-medium"
size="xs"
@@ -368,13 +342,7 @@
</Popover>
</div>
</div>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
btnClasses="!font-medium"
size="xs"
@@ -386,7 +354,9 @@
>
Script
</Button>
<svelte:fragment slot="text">Script</svelte:fragment>
<svelte:fragment slot="text">
Script
</svelte:fragment>
</Popover>
</div>

View File

@@ -20,7 +20,6 @@
import { loadFlowSchedule, type Schedule } from './flows/scheduleUtils'
import type { FlowEditorContext } from './flows/types'
import { cleanInputs } from './flows/utils'
import { Tour } from './tutorial'
export let initialPath: string = ''
export let selectedId: string | undefined
@@ -28,7 +27,6 @@
export let loading = false
export let flowStore: Writable<Flow>
export let flowStateStore: Writable<FlowState>
export let tour = false
async function createSchedule(path: string) {
const { cron, args, enabled } = $scheduleStore
@@ -248,10 +246,6 @@
<svelte:window on:keydown={onKeyDown} />
{#if tour}
<Tour tutorial="flow" />
{/if}
{#if !$userStore?.operator}
<ScriptEditorDrawer bind:this={$scriptEditorDrawer} />

View File

@@ -3,6 +3,8 @@
import { workspaceStore } from '$lib/stores'
export let name: string
export let tabular = false
export let order = ['scripts', 'flows', 'apps', 'schedules', 'variables', 'resources']
$: $workspaceStore && loadUsage()
@@ -13,10 +15,18 @@
}
</script>
<div class="flex flex-col text-xs text-gray-600">
{#each Object.entries(usage) as [k, v]}
<div>
{k}: {v}
</div>
{#if tabular}
{#each order as key}
<td class="text-center">
{usage[key] ?? ''}
</td>
{/each}
</div>
{:else}
<div class="flex flex-col text-xs text-gray-600">
{#each Object.entries(usage) as [k, v]}
<div>
{k}: {v}
</div>
{/each}
</div>
{/if}

View File

@@ -234,6 +234,14 @@
name,
requestBody: { username: member_name }
})
await GranularAclService.removeGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'group_',
requestBody: {
owner: 'u/' + member_name
}
})
loadGroup()
}}>remove</button
>

View File

@@ -11,7 +11,7 @@
<span class="flex items-center space-x-2 mb-2">
<h1>{title}</h1>
{#if tooltip}
<Tooltip>{tooltip}</Tooltip>
<Tooltip scale={0.9}>{tooltip}</Tooltip>
{/if}
</span>
{:else}

View File

@@ -23,6 +23,7 @@
import FolderEditor from './FolderEditor.svelte'
import GroupEditor from './GroupEditor.svelte'
import { random_adj } from './random_positive_adjetive'
import Badge from './common/badge/Badge.svelte'
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app'
let meta: Meta | undefined = undefined
@@ -306,7 +307,7 @@
{#if meta != undefined}
<div class="flex gap-4 shrink">
<label class="block">
<span class="text-gray-700 text-sm whitespace-nowrap">&nbsp;</span>
<span class="text-gray-700 text-sm whitespace-nowrap">Owner</span>
<ToggleButtonGroup
class="mt-0.5"
@@ -409,8 +410,26 @@
{/if}
</div>
<div class="flex-row flex justify-between w-full">
<div><span class="font-mono text-sm break-all">{path}</span></div>
<div class="flex flex-col w-full mt-4">
<div class="flex justify-start w-full">
<Badge
color="gray"
class="center-center !bg-gray-300 !text-gray-600 !w-[70px] !h-[24px] rounded-r-none"
>
Full path
</Badge>
<input
type="text"
readonly
value={path}
size={path?.length || 50}
class="font-mono !text-xs max-w-[calc(100%-70px)] !w-auto !h-[24px] !py-0 !border-l-0 !rounded-l-none"
on:focus={({ currentTarget }) => {
currentTarget.select()
}}
/>
<!-- <span class="font-mono text-sm break-all">{path}</span> -->
</div>
<div class="text-red-600 text-2xs">{error}</div>
</div>
</div>

View File

@@ -88,7 +88,7 @@
}}
items={collection}
class="text-clip grow min-w-0"
placeholder="{resourceType} resource"
placeholder="{resourceType ?? 'any'} resource"
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
containerStyles={SELECT_INPUT_DEFAULT_STYLE.containerStyles}
/>

View File

@@ -45,7 +45,7 @@
label: 'Failed',
data:
failed?.map((job) => ({
x: job.created_at as any,
x: job.started_at as any,
y: job.duration_ms,
id: job.id,
path: job.script_path
@@ -57,7 +57,7 @@
label: 'Successful',
data:
success?.map((job) => ({
x: job.created_at as any,
x: job.started_at as any,
y: job.duration_ms,
id: job.id,
path: job.script_path
@@ -71,7 +71,10 @@
enabled: true,
modifierKey: 'ctrl' as 'ctrl',
onPanComplete: ({ chart }) => {
dispatch('zoom', { min: new Date(chart.scales.x.min), max: new Date(chart.scales.x.max) })
dispatch('zoom', {
min: addSeconds(new Date(chart.scales.x.min), -1),
max: addSeconds(new Date(chart.scales.x.max), 1)
})
}
},
zoom: {
@@ -80,7 +83,10 @@
},
mode: 'x' as 'x',
onZoom: ({ chart }) => {
dispatch('zoom', { min: new Date(chart.scales.x.min), max: new Date(chart.scales.x.max) })
dispatch('zoom', {
min: addSeconds(new Date(chart.scales.x.min), -1),
max: addSeconds(new Date(chart.scales.x.max), 1)
})
}
}
}
@@ -88,6 +94,22 @@
function getPath(x: any): string {
return x.path
}
$: minTime = getMinTime(jobs)
function addSeconds(date: Date, seconds: number): Date {
date.setTime(date.getTime() + seconds * 1000)
return date
}
function getMinTime(jobs: CompletedJob[] | undefined): Date {
return addSeconds(new Date(jobs?.[jobs?.length - 1]?.started_at ?? new Date().toString()), -15)
}
$: maxTime = getMaxTime(jobs)
function getMaxTime(jobs: CompletedJob[] | undefined): Date {
return addSeconds(new Date(jobs?.[0]?.started_at ?? new Date().toString()), 15)
}
</script>
<Scatter
@@ -115,7 +137,8 @@
display: false
},
type: 'time',
min: jobs?.[jobs?.length - 1]?.created_at ?? new Date().toString()
min: minTime,
max: maxTime
},
y: {
grid: {

View File

@@ -1,23 +1,23 @@
<script lang="ts">
import { Script, ScriptService } from '$lib/gen'
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { inferArgs } from '$lib/infer'
import { initialCode, isInitialCode } from '$lib/script_helpers'
import { initialCode } from '$lib/script_helpers'
import { userStore, workspaceStore } from '$lib/stores'
import { emptySchema, encodeState, sendUserToast, setQueryWithoutLoad } from '$lib/utils'
import Path from './Path.svelte'
import RadioButton from './RadioButton.svelte'
import ScriptEditor from './ScriptEditor.svelte'
import ScriptSchema from './ScriptSchema.svelte'
import CenteredPage from './CenteredPage.svelte'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import { Button, ButtonPopup, ButtonPopupItem, Kbd } from './common'
import { faChevronDown, faChevronUp, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import { faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import Breadcrumb from './common/breadcrumb/Breadcrumb.svelte'
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
import type { SupportedLanguage } from '$lib/common'
import Tooltip from './Tooltip.svelte'
import SettingSection from './SettingSection.svelte'
export let script: Script
export let initialPath: string = ''
@@ -31,14 +31,34 @@
['Go', Script.language.GO],
['Bash', Script.language.BASH]
]
let viewScriptKind = script.kind !== Script.kind.SCRIPT
const scriptKindOptions: { value: Script.kind; title: string; desc?: string }[] = [
{
value: Script.kind.SCRIPT,
title: 'Action'
},
{
value: Script.kind.TRIGGER,
title: 'Trigger',
desc: 'First module of flows to trigger them based on external changes. These kind of scripts are usually running on a schedule to periodically look for changes.'
},
{
value: Script.kind.APPROVAL,
title: 'Approval',
desc: 'Send notifications externally to ask for approval to continue a flow.'
},
{
value: Script.kind.FAILURE,
title: 'Error Handler',
desc: 'Handle errors in flows after all retry attempts have been exhausted.'
}
]
let pathError = ''
let summaryC: HTMLInputElement | undefined = undefined
let pathC: Path | undefined = undefined
let loadingSave = false
$: setQueryWithoutLoad($page.url, 'state', encodeState(script))
$: setQueryWithoutLoad($page.url, [{ key: 'state', value: encodeState(script) }])
$: step = Number($page.url.searchParams.get('step')) || 1
if (script.content == '') {
@@ -53,7 +73,6 @@
script.content = initialCode(language, kind, template)
}
let loadingSave = false
async function editScript(leave: boolean): Promise<void> {
loadingSave = true
try {
@@ -250,7 +269,8 @@
}}
disabled={lockedLanguage}
>
<LanguageIcon {lang} /><span class="ml-2 py-4">{label}</span>
<LanguageIcon {lang} />
<span class="ml-2 py-2">{label}</span>
</Button>
{/each}
<Button
@@ -265,7 +285,7 @@
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="pgsql" /><span class="ml-2 py-4">PostgreSQL</span>
<LanguageIcon lang="pgsql" /><span class="ml-2 py-2">PostgreSQL</span>
</Button>
<!-- <Button
size="sm"
@@ -278,65 +298,41 @@
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="mysql" /><span class="ml-2 py-4">MySQL</span>
<LanguageIcon lang="mysql" /><span class="ml-2 py-2">MySQL</span>
</Button> -->
</div>
<div class="mt-16 mb-4">
<Button
color="light"
size="sm"
endIcon={{ icon: viewScriptKind ? faChevronUp : faChevronDown }}
on:click={() => (viewScriptKind = !viewScriptKind)}
>
Tag this script as having a specific purpose inside flows
</Button>
</div>
{#if viewScriptKind}
<div class="max-w-lg">
<RadioButton
label="Script Type"
options={[
['Action', Script.kind.SCRIPT],
[
{
title: 'Trigger',
desc: `First module of flows to trigger them based on watching changes external periodically using an internal state`
},
Script.kind.TRIGGER
],
[
{
title: 'Approval',
desc: `Send notification externally to ask for approval to continue a flow`
},
Script.kind.APPROVAL
],
[
{
title: 'Error Handler',
desc: `Handle errors for flows after all retries attempts have been exhausted`
},
Script.kind.FAILURE
]
// ['Command Handler', Script.kind.COMMAND]
]}
on:change={(e) => {
if (isInitialCode(script.content)) {
<SettingSection
title="Script kind"
element="h3"
tooltip="Tag this script as having a specific purpose inside flows. If it won't be used in flows,
you don't have to worry about this."
accordion
>
<div class="flex flex-wrap gap-2">
{#each scriptKindOptions as { value, title, desc }}
{@const isPicked = script.kind === value}
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'dark'}
btnClasses="font-medium {isPicked ? '!bg-blue-50/75' : ''}"
on:click={() => {
template = 'script'
initContent(script.language, e.detail, template)
}
}}
bind:value={script.kind}
/>
script.kind = value
initContent(script.language, value, template)
}}
disabled={lockedLanguage}
>
{title}
{#if desc}
<Tooltip class="mb-0.5 ml-1">
{desc}
</Tooltip>
{/if}
</Button>
{/each}
</div>
{/if}
<!-- <div class="ml-3">
<Toggle
bind:checked={script.is_template}
options={{ right: 'Save as a workspace template' }}
/>
</div> -->
</SettingSection>
</CenteredPage>
{:else if step === 2}
<ScriptEditor
@@ -346,7 +342,6 @@
lang={script.language}
{initialArgs}
{kind}
tour
/>
{:else if step === 3}
<CenteredPage>

View File

@@ -17,7 +17,6 @@
import { Button, Kbd } from './common'
import SplitPanesWrapper from './splitPanes/SplitPanesWrapper.svelte'
import WindmillIcon from './icons/WindmillIcon.svelte'
import { Tour } from './tutorial'
// Exported
export let schema: Schema = emptySchema()
@@ -28,7 +27,6 @@
export let initialArgs: Record<string, any> = {}
export let fixedOverflowWidgets = true
export let noSyncFromGithub = false
export let tour = false
let websocketAlive = { pyright: false, black: false, deno: false, go: false }
@@ -116,10 +114,6 @@
<svelte:window on:keydown={onKeyDown} />
{#if tour}
<Tour tutorial="script" />
{/if}
<div class="border-b-2 shadow-sm px-1 pr-4" bind:clientWidth={width}>
<div class="flex justify-between space-x-2">
<EditorBar
@@ -152,7 +146,6 @@
<Splitpanes class="!overflow-visible">
<Pane size={60} minSize={10} class="!overflow-visible">
<div
id="script-tutorial-1"
class="pl-2 h-full !overflow-visible"
on:mouseleave={() => {
inferSchema(code)
@@ -187,7 +180,7 @@
</Pane>
<Pane size={40} minSize={10}>
<div class="flex flex-col h-full">
<div id="script-tutorial-4" class="px-2 w-full border-b py-1">
<div class="px-2 w-full border-b py-1">
{#if testIsLoading}
<Button on:click={testJobLoader?.cancelJob} btnClasses="w-full" color="red" size="xs">
<WindmillIcon
@@ -218,7 +211,7 @@
</div>
<Splitpanes horizontal class="!max-h-[calc(100%-43px)]">
<Pane size={33}>
<div id="script-tutorial-2" class="px-2">
<div class="px-2">
<div class="break-words relative font-sans">
<SchemaForm compact {schema} bind:args bind:isValid />
</div>

View File

@@ -0,0 +1,43 @@
<script lang="ts">
import { ChevronDown } from 'lucide-svelte'
import { slide } from 'svelte/transition'
import type { IntRange } from '../common'
import Tooltip from './Tooltip.svelte'
export let title: string
export let tooltip: string
export let element: `h${IntRange<1, 6>}` = 'h2'
export let accordion = false
let showContent = !accordion
</script>
<div class="border-b [&:has(button:hover)]:border-gray-400 duration-200 pb-1 mt-8 mb-2">
{#if accordion}
<button
class="flex w-full justify-between items-center"
on:click={() => (showContent = !showContent)}
>
<svelte:element this={element}>
{title}
<Tooltip scale={0.9} class="mb-0.5">
{tooltip}
</Tooltip>
</svelte:element>
<span class="rounded-full hover:bg-gray-100 focus:bg-gray-100 p-1">
<ChevronDown size={22} class="rotate-0 duration-300 {showContent ? '!rotate-180' : ''}" />
</span>
</button>
{:else}
<svelte:element this={element}>
{title}
<Tooltip scale={0.9} class="mb-0.5">
{tooltip}
</Tooltip>
</svelte:element>
{/if}
</div>
{#if showContent}
<div transition:slide={{ duration: 300 }}>
<slot />
</div>
{/if}

View File

@@ -2,13 +2,18 @@
import { ChevronDown, ChevronUp } from 'lucide-svelte'
import { slide } from 'svelte/transition'
import Button from './common/button/Button.svelte'
import Tooltip from './Tooltip.svelte'
export let text: string
export let tooltip: string | undefined = undefined
export let view = false
</script>
<Button color="light" on:click={() => (view = !view)} variant="border"
>{text}
{#if tooltip}
<Tooltip wrapperClass="mx-1">{tooltip}</Tooltip>
{/if}
{#if !view}<ChevronDown />{:else}<ChevronUp />{/if}</Button
>
{#if view}

View File

@@ -16,7 +16,7 @@
const dispatch = createEventDispatcher()
</script>
<span class="{$$props.class ?? ''} z-auto">
<span class="{$$props.class} z-auto">
<label
for={id}
class="inline-flex items-center mt-2 duration-200 {disabled

View File

@@ -18,5 +18,7 @@
data={faInfoCircle}
{scale}
/>
<span slot="text"> <slot /> </span>
<svelte:fragment slot="text">
<slot />
</svelte:fragment>
</Popover>

View File

@@ -127,6 +127,7 @@
<InputValue
{id}
input={configuration.disabled}
extraContext={extraQueryParams}
bind:value={disabled}
bind:error={errors.disabled}
/>

View File

@@ -12,6 +12,7 @@
export let value: T
export let id: string | undefined = undefined
export let error: string = ''
export let extraContext: Record<string, any> = {}
let lastInput = input ? JSON.parse(JSON.stringify(input)) : undefined
@@ -72,16 +73,19 @@
}
function computeGlobalContext() {
return Object.fromEntries(
Object.entries($worldStore?.outputsById ?? {})
.filter(([k, _]) => k != id)
.map(([key, value]) => {
return [
key,
Object.fromEntries(Object.entries(value ?? {}).map((x) => [x[0], x[1].peak()]))
]
})
)
return {
...Object.fromEntries(
Object.entries($worldStore?.outputsById ?? {})
.filter(([k, _]) => k != id)
.map(([key, value]) => {
return [
key,
Object.fromEntries(Object.entries(value ?? {}).map((x) => [x[0], x[1].peak()]))
]
})
),
...extraContext
}
}
export function getValue(input: AppInput) {

View File

@@ -43,6 +43,8 @@
: []
if (defaultValue) {
value = JSON.stringify(defaultValue)
} else if (listItems.length > 0) {
value = listItems[0]?.value
}
}

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { Tab, Tabs } from '$lib/components/common'
import { getContext, onMount } from 'svelte'
import { initOutput } from '../../editor/appUtils'
import SubGridEditor from '../../editor/SubGridEditor.svelte'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
@@ -27,6 +28,12 @@
let noPadding: boolean | undefined = undefined
let tabHeight: number = 0
$: outputs = $worldStore
? initOutput($worldStore, id, {
selectedTabIndex: 0
})
: undefined
function handleTabSelection() {
const selectedIndex = tabs?.indexOf(selected)
outputs?.selectedTabIndex.set(selectedIndex, true)
@@ -76,10 +83,6 @@
$: selected && handleTabSelection()
$: outputs = $worldStore?.outputsById[id] as {
selectedTabIndex: Output<number>
}
$: selectedIndex = tabs?.indexOf(selected) ?? -1
$: css = concatCustomCss($app.css?.tabscomponent, customCss)
@@ -88,7 +91,7 @@
<InputValue {id} input={configuration.noPadding} bind:value={noPadding} />
<div>
<div bind:clientHeight={tabHeight} on:pointerdown|stopPropagation>
<div bind:clientHeight={tabHeight}>
<Tabs bind:selected class={css?.tabRow?.class} style={css?.tabRow?.style}>
{#each tabs ?? [] as res}
<Tab

View File

@@ -37,7 +37,6 @@
import { page } from '$app/stores'
import CssSettings from './componentsPanel/CssSettings.svelte'
import { initHistory } from '$lib/history'
import { Tour } from '../../tutorial'
import ComponentNavigation from './component/ComponentNavigation.svelte'
import ItemPicker from '$lib/components/ItemPicker.svelte'
import VariableEditor from '$lib/components/VariableEditor.svelte'
@@ -48,7 +47,6 @@
export let policy: Policy
export let summary: string
export let fromHub: boolean = false
export let tour: boolean = false
const appStore = writable<App>(app)
const worldStore = writable<World | undefined>(undefined)
@@ -154,155 +152,154 @@
<svelte:window on:hashchange={hashchange} />
{#if tour}
<Tour tutorial="app" />
{/if}
{#if $connectingInput.opened}
<div
class="absolute w-full h-screen bg-black border-2 bg-opacity-25 z-20 flex justify-center items-center"
/>
{/if}
{#if !$userStore?.operator}
{#if initialMode !== 'preview'}
<AppEditorHeader {policy} {fromHub} />
{/if}
{#if $appStore}
{#if initialMode !== 'preview'}
<AppEditorHeader {policy} {fromHub} />
{/if}
{#if previewing}
<SplitPanesWrapper>
<div
class={twMerge('h-full w-full', $appStore.css?.['app']?.['viewer']?.class)}
style={$appStore.css?.['app']?.['viewer']?.style}
>
<AppPreview
workspace={$workspaceStore ?? ''}
summary={$summaryStore}
app={$appStore}
appPath={path}
{breakpoint}
{policy}
isEditor
{context}
noBackend={false}
/>
</div>
</SplitPanesWrapper>
{:else}
<SplitPanesWrapper>
<Splitpanes class="max-w-full overflow-hidden">
<Pane size={$connectingInput?.opened ? 40 : 15} minSize={5} maxSize={33}>
<ContextPanel />
</Pane>
<Pane size={64}>
<SplitPanesWrapper>
<Splitpanes horizontal>
<Pane size={$connectingInput?.opened ? 100 : 70}>
<div
on:pointerdown={(e) => {
$selectedComponent = undefined
$focusedGrid = undefined
}}
id="app-tutorial-3"
class={twMerge(
'bg-gray-100 h-full w-full',
$appStore.css?.['app']?.['viewer']?.class
)}
style={$appStore.css?.['app']?.['viewer']?.style}
>
{#if previewing}
<SplitPanesWrapper>
<div
class={twMerge('h-full w-full', $appStore.css?.['app']?.['viewer']?.class)}
style={$appStore.css?.['app']?.['viewer']?.style}
>
<AppPreview
workspace={$workspaceStore ?? ''}
summary={$summaryStore}
app={$appStore}
appPath={path}
{breakpoint}
{policy}
isEditor
{context}
noBackend={false}
/>
</div>
</SplitPanesWrapper>
{:else}
<SplitPanesWrapper>
<Splitpanes class="max-w-full overflow-hidden">
<Pane size={15} minSize={5} maxSize={33}>
<ContextPanel />
</Pane>
<Pane size={64}>
<SplitPanesWrapper>
<Splitpanes horizontal>
<Pane size={70}>
<div
class={classNames(
'relative mx-auto w-full h-full overflow-auto',
app.fullscreen ? '' : 'max-w-6xl'
on:pointerdown={(e) => {
$selectedComponent = undefined
$focusedGrid = undefined
}}
class={twMerge(
'bg-gray-100 h-full w-full',
$appStore.css?.['app']?.['viewer']?.class
)}
style={$appStore.css?.['app']?.['viewer']?.style}
>
{#if $appStore.grid}
<ComponentNavigation />
<div
class={classNames(
'relative mx-auto w-full h-full overflow-auto',
app.fullscreen ? '' : 'max-w-6xl'
)}
>
{#if $appStore.grid}
<ComponentNavigation />
<div on:pointerdown|stopPropagation class={width}>
<GridEditor {policy} />
<div on:pointerdown|stopPropagation class={width}>
<GridEditor {policy} />
</div>
<div id="app-editor-top-level-drawer" />
{/if}
</div>
</div>
</Pane>
<Pane size={$connectingInput?.opened ? 0 : 30}>
<div class="relative h-full w-full">
<InlineScriptsPanel />
</div>
</Pane>
</Splitpanes>
</SplitPanesWrapper>
</Pane>
<Pane size={21} minSize={5} maxSize={33}>
<div class="relative flex flex-col h-full">
<Tabs bind:selected={selectedTab} wrapperClass="!h-[40px]" class="!h-full">
<Tab value="insert" size="xs">
<div class="m-1 center-center gap-2">
<Icon data={faPlus} />
<span>Insert</span>
</div>
</Tab>
<Tab value="settings" size="xs">
<div class="m-1 center-center gap-2">
<Icon data={faSliders} />
<span>Settings</span>
</div>
</Tab>
<Tab value="css" size="xs">
<div class="m-1 center-center gap-2">
<Icon data={faCode} />
<span>CSS</span>
</div>
</Tab>
<div slot="content" class="h-full overflow-y-auto pb-4">
<TabContent class="overflow-auto h-full" value="settings">
{#if $selectedComponent !== undefined}
<SettingsPanel />
{:else}
<div class="min-w-[150px] text-sm text-gray-500 text-center py-8 px-2">
Select a component to see the settings&nbsp;for&nbsp;it
</div>
<div id="app-editor-top-level-drawer" />
{/if}
</div>
</TabContent>
<TabContent value="insert">
<ComponentList />
</TabContent>
<TabContent value="css">
<CssSettings />
</TabContent>
</div>
</Pane>
<Pane size={$connectingInput?.opened ? 0 : 30}>
<div id="app-tutorial-4" class="relative h-full w-full">
<InlineScriptsPanel />
</div>
</Pane>
</Splitpanes>
</SplitPanesWrapper>
</Pane>
<Pane size={21} minSize={5} maxSize={33}>
<div class="relative flex flex-col h-full">
<Tabs bind:selected={selectedTab} wrapperClass="!h-[40px]" class="!h-full">
<Tab value="insert" size="xs">
<div class="m-1 center-center gap-2">
<Icon data={faPlus} />
<span>Insert</span>
</div>
</Tab>
<Tab value="settings" size="xs">
<div class="m-1 center-center gap-2">
<Icon data={faSliders} />
<span>Settings</span>
</div>
</Tab>
<Tab value="css" size="xs">
<div class="m-1 center-center gap-2">
<Icon data={faCode} />
<span>CSS</span>
</div>
</Tab>
<div slot="content" class="h-full overflow-y-auto pb-4">
<TabContent class="overflow-auto" value="settings">
{#if $selectedComponent !== undefined}
<SettingsPanel />
{:else}
<div class="min-w-[150px] text-sm text-gray-500 text-center py-8 px-2">
Select a component to see the settings&nbsp;for&nbsp;it
</Tabs>
{#if $connectingInput.opened}
<div
class="fixed top-32 p-2 z-50 flex justify-center items-center"
transition:fly|local={{ duration: 100, y: -100 }}
>
<Alert title="Connecting" type="info">
<div class="flex gap-2 flex-col">
Click on the output of the component you want to connect to on the left panel.
<div>
<Button
color="blue"
variant="border"
size="xs"
on:click={() => {
$connectingInput.opened = false
$connectingInput.input = undefined
}}
>
Stop connecting
</Button>
</div>
</div>
{/if}
</TabContent>
<TabContent value="insert">
<ComponentList />
</TabContent>
<TabContent value="css">
<CssSettings />
</TabContent>
</div>
</Tabs>
{#if $connectingInput.opened}
<div
class="fixed top-32 p-2 z-50 flex justify-center items-center"
transition:fly|local={{ duration: 100, y: -100 }}
>
<Alert title="Connecting" type="info">
<div class="flex gap-2 flex-col">
Click on the output of the component you want to connect to on the left panel.
<div>
<Button
color="blue"
variant="border"
size="xs"
on:click={() => {
$connectingInput.opened = false
$connectingInput.input = undefined
}}
>
Stop connecting
</Button>
</div>
</div>
</Alert>
</div>
{/if}
</div>
</Pane>
</Splitpanes>
</SplitPanesWrapper>
</Alert>
</div>
{/if}
</div>
</Pane>
</Splitpanes>
</SplitPanesWrapper>
{/if}
{:else}
App is null
{/if}
{:else}
App editor not available to operators

View File

@@ -18,6 +18,7 @@
import DisplayResult from '$lib/components/DisplayResult.svelte'
import Dropdown from '$lib/components/Dropdown.svelte'
import FlowProgressBar from '$lib/components/flows/FlowProgressBar.svelte'
import { idMutex } from '$lib/components/flows/flowStateUtils'
import FlowStatusViewer from '$lib/components/FlowStatusViewer.svelte'
import JobArgs from '$lib/components/JobArgs.svelte'
import LogViewer from '$lib/components/LogViewer.svelte'
@@ -220,18 +221,23 @@
return
}
loading.save = true
await computeTriggerables()
await AppService.updateApp({
workspace: $workspaceStore!,
path: $page.params.path,
requestBody: {
value: $app!,
summary: $summary,
policy
}
})
loading.save = false
sendUserToast('App saved')
try {
await computeTriggerables()
await AppService.updateApp({
workspace: $workspaceStore!,
path: $page.params.path,
requestBody: {
value: $app!,
summary: $summary,
policy
}
})
sendUserToast('App saved')
loading.save = false
} catch (e) {
loading.save = false
throw e
}
}
let selectedJobId: string | undefined = undefined
@@ -242,7 +248,10 @@
$: selectedJobId && testJobLoader?.watchJob(selectedJobId)
$: hasErrors = Object.keys($errorByComponent).length > 0
let lock = false
function onKeyDown(event: KeyboardEvent) {
if (lock) return
lock = true
switch (event.key) {
case 'Z':
if (event.ctrlKey) {
@@ -253,6 +262,7 @@
case 'z':
if (event.ctrlKey) {
$app = undo(history, $app)
event.preventDefault()
}
break
@@ -281,6 +291,7 @@
// break
// }
}
lock = false
}
</script>
@@ -471,8 +482,8 @@
<input type="text" placeholder="App summary" class="text-sm w-full" bind:value={$summary} />
</div>
<UndoRedo
undoProps={{ disabled: $history.index === 0 }}
redoProps={{ disabled: $history.index === $history.history.length - 1 }}
undoProps={{ disabled: $history?.index === 0 }}
redoProps={{ disabled: $history && $history?.index === $history.history.length - 1 }}
on:undo={() => {
$app = undo(history, $app)
}}
@@ -486,12 +497,12 @@
<ToggleButton position="left" value="dnd" size="xs">
<div class="inline-flex gap-1 items-center">
<Pencil size={14} />
<span class="hidden md:inline">Editor</span>
<span class="hidden lg:inline">Editor</span>
</div>
</ToggleButton>
<ToggleButton position="right" value="preview" size="xs">
<div class="inline-flex gap-1 items-center">
<Eye size={14} /> <span class="hidden md:inline">Preview</span>
<Eye size={14} /> <span class="hidden lg:inline">Preview</span>
</div>
</ToggleButton>
</ToggleButtonGroup>
@@ -508,42 +519,46 @@
</div>
<div class="hidden lg:block">
<ToggleButtonGroup bind:selected={$app.fullscreen}>
<ToggleButton position="left" value={false} size="xs">
<div class="flex gap-1 justify-start items-center">
<AlignHorizontalSpaceAround size={14} />
<Tooltip light class="mb-0.5">
The max width is 1168px and the content stay centered instead of taking the full page
width
</Tooltip>
</div>
</ToggleButton>
<ToggleButton position="right" value={true} size="xs">
<Expand size={14} />
</ToggleButton>
</ToggleButtonGroup>
{#if $app}
<ToggleButtonGroup bind:selected={$app.fullscreen}>
<ToggleButton position="left" value={false} size="xs">
<div class="flex gap-1 justify-start items-center">
<AlignHorizontalSpaceAround size={14} />
<Tooltip light class="mb-0.5">
The max width is 1168px and the content stay centered instead of taking the full
page width
</Tooltip>
</div>
</ToggleButton>
<ToggleButton position="right" value={true} size="xs">
<Expand size={14} />
</ToggleButton>
</ToggleButtonGroup>
{/if}
</div>
</div>
{#if $focusedGrid !== undefined}
<Badge color="indigo">
<div class="flex flex-row gap-2 justify-center items-center">
<div>{`Sub grid: ${$focusedGrid.parentComponentId} (${$focusedGrid.subGridIndex})`}</div>
<button
on:click={() => {
$selectedComponent = undefined
$focusedGrid = undefined
}}
>
<X size={14} />
</button>
</div>
</Badge>
<div class="hidden lg:block">
<Badge color="indigo">
<div class="flex flex-row gap-2 justify-center items-center">
<div>{`Sub grid: ${$focusedGrid.parentComponentId} (${$focusedGrid.subGridIndex})`}</div>
<button
on:click={() => {
$selectedComponent = undefined
$focusedGrid = undefined
}}
>
<X size={14} />
</button>
</div>
</Badge>
</div>
{/if}
<div class="flex flex-row gap-2 justify-end items-center overflow-visible">
<Dropdown
placement="bottom-end"
btnClasses="!text-gray-700 !bg-transparent hover:!bg-gray-400/20 !p-[6px] hidden lg:block"
btnClasses="hidden lg:block"
dropdownItems={[
{
displayName: 'JSON',
@@ -568,9 +583,7 @@
}
}
]}
>
<MoreVertical size={20} />
</Dropdown>
/>
<span class="hidden md:inline">
<Button on:click={() => (inputsDrawerOpen = true)} color="light" size="xs" variant="border">
<span class="flex gap-2">
@@ -587,7 +600,7 @@
variant="border"
startIcon={{ icon: faBug }}
>
<span class="hidden md:inline">Debug Runs</span>
<span class="hidden xl:inline">Debug Runs</span>
</Button>
</span>
<AppExportButton bind:this={appExport} />
@@ -598,7 +611,7 @@
variant="border"
startIcon={{ icon: faExternalLink }}
>
<span class="hidden md:inline">Publish</span>
<span class="hidden xl:inline">Publish</span>
</Button>
{#if appPath == ''}
<Button

View File

@@ -4,6 +4,7 @@
import { buildWorld, type World } from '../rx'
import type {
App,
AppEditorContext,
AppViewerContext,
ConnectingInput,
EditorBreakpoint,
@@ -64,6 +65,12 @@
parentWidth: writable(0)
})
setContext<AppEditorContext>('AppEditorContext', {
history: undefined,
componentControl: writable({}),
pickVariableCallback: writable(undefined)
})
let mounted = false
onMount(() => {

View File

@@ -41,13 +41,14 @@
{#if pointerdown || selected || hover}
<button
title="Position locking"
title="Lock Position"
class={classNames(
'text-gray-800 px-1 text-2xs py-0.5 font-bold w-fit shadow border border-gray-300 absolute -top-1 right-[2.5rem] z-50 cursor-pointer',
' hover:bg-gray-300',
selected ? 'bg-gray-200/80' : 'bg-gray-200/80'
)}
on:click={() => dispatch('lock')}
on:pointerdown|stopPropagation
>
{#if locked}
<Anchor aria-label="Unlock position" size={14} class="text-orange-500" />
@@ -56,15 +57,16 @@
{/if}
</button>
<button
title="Position locking"
title="Expand"
class={classNames(
'text-gray-800 px-1 text-2xs py-0.5 font-bold w-fit shadow border border-gray-300 absolute -top-1 right-[0.5rem] z-50 cursor-pointer',
'text-gray-800 px-1 text-2xs py-0.5 font-bold w-fit shadow border border-gray-300 absolute -top-1 right-[4.5rem] z-50 cursor-pointer',
' hover:bg-gray-300',
selected ? 'bg-gray-200/80' : 'bg-gray-200/80'
)}
on:click={() => dispatch('expand')}
on:pointerdown|stopPropagation
>
<Expand aria-label="Lock position" size={14} />
<Expand aria-label="Expand position" size={14} />
</button>
{/if}
@@ -73,7 +75,7 @@
title="Move"
on:mousedown|stopPropagation|capture
class={classNames(
'text-gray-600 px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute border border-gray-300 -top-1 shadow right-[4.5rem] z-50 cursor-move',
'text-gray-600 px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute border border-gray-300 -top-1 shadow right-[0.5rem] z-50 cursor-move',
'bg-gray-200/80'
)}
>

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import { getContext, afterUpdate } from 'svelte'
import type { App, AppEditorContext, AppViewerContext, GridItem } from '../types'
import Grid from '@windmill-labs/svelte-grid'
import { classNames } from '$lib/utils'
import { columnConfiguration, disableDrag, enableDrag, isFixed, toggleFixed } from '../gridUtils'
import { twMerge } from 'tailwind-merge'
@@ -13,6 +12,7 @@
import { deepEqual } from 'fast-equals'
import { push } from '$lib/history'
import { expandGriditem, findGridItem } from './appUtils'
import Grid from '../svelte-grid/Grid.svelte'
export let policy: Policy
@@ -108,13 +108,17 @@
let pointerdown = false
let lastapp: App | undefined = undefined
const onpointerdown = () => {
lastapp = JSON.parse(JSON.stringify($app))
pointerdown = true
if (history) {
lastapp = JSON.parse(JSON.stringify($app))
pointerdown = true
}
}
const onpointerup = () => {
pointerdown = false
if (!deepEqual(lastapp, $app)) {
push(history, lastapp, false, true)
if (history) {
pointerdown = false
if (!deepEqual(lastapp, $app)) {
push(history, lastapp, false, true)
}
}
}
@@ -211,11 +215,13 @@
if (gridItem) {
toggleFixed(gridItem)
}
$app = $app
}}
on:expand={() => {
push(history, $app)
$selectedComponent = gridComponent.data.id
expandGriditem($app.grid, gridComponent, $breakpoint)
$app = { ...$app }
$app = $app
}}
/>
</div>

View File

@@ -1,13 +1,13 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import { createEventDispatcher, getContext } from 'svelte'
import Grid from '@windmill-labs/svelte-grid'
import { twMerge } from 'tailwind-merge'
import { columnConfiguration, isFixed, toggleFixed } from '../gridUtils'
import type { AppEditorContext, AppViewerContext, GridItem } from '../types'
import Component from './component/Component.svelte'
import { expandGriditem, findGridItem } from './appUtils'
import { push } from '$lib/history'
import Grid from '../svelte-grid/Grid.svelte'
export let containerHeight: number
let classes = ''
@@ -63,6 +63,7 @@
if (fComponent) {
fComponent = toggleFixed(fComponent)
}
$app = $app
}
// @ts-ignore
@@ -136,11 +137,11 @@
if (!parentGridItem) {
return
}
$selectedComponent = gridComponent.data.id
push(history, $app)
expandGriditem(subGrid, gridComponent, $breakpoint, parentGridItem)
$app = { ...$app }
$app = $app
}}
/>
</div>

View File

@@ -1,9 +1,10 @@
import { getNextId } from '$lib/components/flows/flowStateUtils'
import type { App, EditorBreakpoint, FocusedGrid, GridItem } from '../types'
import type { App, ConnectingInput, EditorBreakpoint, FocusedGrid, GridItem } from '../types'
import { getRecommendedDimensionsByComponent, type AppComponent } from './component'
import gridHelp from '@windmill-labs/svelte-grid/src/utils/helper'
import { gridColumns } from '../gridUtils'
import { allItems } from '../utils'
import type { Output, World } from '../rx'
import gridHelp from '../svelte-grid/utils/helper'
function findGridItemById(
root: GridItem[],
@@ -18,7 +19,7 @@ function findGridItemById(
return undefined
}
export function findGridItemParentId(app: App, id: string): string | undefined {
export function findGridItemParentGrid(app: App, id: string): string | undefined {
const gridItem = app.grid.find((x) => x.id === id)
if (gridItem) {
return undefined
@@ -27,12 +28,23 @@ export function findGridItemParentId(app: App, id: string): string | undefined {
const subGrid = app.subgrids[key]
const gridItem = subGrid.find((x) => x.id === id)
if (gridItem) {
return key.split('-')[0]
return key
}
}
}
}
export function isIdInsideGriditem(app: App, gridItem: GridItem, id: string | undefined): boolean {
const path: string[] = []
let currentId = id
while (currentId) {
path.push(currentId)
currentId = findGridItemParentGrid(app, currentId)?.split('-')[0]
}
return path.includes(gridItem.id)
}
export function findGridItem(app: App, id: string): GridItem | undefined {
return findGridItemById(app.grid, app.subgrids, id)
}
@@ -46,10 +58,6 @@ export function getNextGridItemId(app: App): string {
}
export function createNewGridItem(grid: GridItem[], id: string, data: AppComponent): GridItem {
const appComponent = data
appComponent.id = id
const newComponent = {
resizable: true,
draggable: true,
@@ -57,7 +65,8 @@ export function createNewGridItem(grid: GridItem[], id: string, data: AppCompone
y: 0
}
let newData: AppComponent = JSON.parse(JSON.stringify(appComponent))
let newData: AppComponent = JSON.parse(JSON.stringify(data))
newData.id = id
const newItem: GridItem = {
data: newData,
@@ -65,7 +74,7 @@ export function createNewGridItem(grid: GridItem[], id: string, data: AppCompone
}
gridColumns.forEach((column) => {
const rec = getRecommendedDimensionsByComponent(appComponent.type, column)
const rec = getRecommendedDimensionsByComponent(newData.type, column)
newItem[column] = {
...newComponent,
@@ -84,6 +93,16 @@ export function createNewGridItem(grid: GridItem[], id: string, data: AppCompone
return newItem
}
export function getGridItems(app: App, focusedGrid: FocusedGrid | undefined): GridItem[] {
if (!focusedGrid) {
return app.grid
} else {
const { parentComponentId, subGridIndex } = focusedGrid
const key = `${parentComponentId}-${subGridIndex ?? 0}`
return app?.subgrids?.[key] ?? []
}
}
export function insertNewGridItem(
app: App,
data: AppComponent,
@@ -96,18 +115,6 @@ export function insertNewGridItem(
app.subgrids = {}
}
if (!focusedGrid) {
const newItem = createNewGridItem(app.grid, id, data)
app.grid.push(newItem)
} else {
const { parentComponentId, subGridIndex } = focusedGrid
const key = `${parentComponentId}-${subGridIndex ?? 0}`
const subGrid = app.subgrids[key] ?? []
subGrid.push(createNewGridItem(subGrid, id, data))
app.subgrids[key] = subGrid
}
// We only want to set subgrids when we are not moving
if (!keepId) {
for (let i = 0; i < (data.numberOfSubgrids ?? 0); i++) {
@@ -115,6 +122,17 @@ export function insertNewGridItem(
}
}
const key = focusedGrid
? `${focusedGrid?.parentComponentId}-${focusedGrid?.subGridIndex ?? 0}`
: undefined
let grid = focusedGrid ? app.subgrids[key!] : app.grid
const newItem = createNewGridItem(grid, id, data)
grid.push(newItem)
if (focusedGrid) {
app.subgrids[key!] = grid
}
return id
}
@@ -169,28 +187,6 @@ export function deleteGridItem(
return components
}
export function duplicateGridItem(
app: App,
parent: string | undefined,
id: string
): string | undefined {
const gridItem = findGridItem(app, id)
if (gridItem) {
const newId = getNextGridItemId(app)
const newItem = JSON.parse(JSON.stringify(gridItem))
newItem.id = newId
newItem.data.id = newId
let focusedGrid = parent
? { parentComponentId: parent.split('-')[0], subGridIndex: Number(parent.split('-')[1]) }
: undefined
return insertNewGridItem(app, newItem.data, focusedGrid)
}
return undefined
}
type AvailableSpace = {
left: number
right: number
@@ -206,7 +202,7 @@ export function findAvailableSpace(
): AvailableSpace | undefined {
if (gridItem) {
const breakpoint = editorBreakpoint === 'sm' ? 3 : 12
const maxHeight = parentGridItem ? parentGridItem[breakpoint].h - 1 : 12
const maxHeight = parentGridItem ? parentGridItem[breakpoint].h - 1 : 16
const maxWidth = 12
const availableSpace = {
@@ -297,6 +293,25 @@ function isOverlapping(item1: any, item2: any) {
)
}
type Outputtable<Type> = {
-readonly [Property in keyof Type]: Output<Type[Property]>
}
export function initOutput<I extends Record<string, any>>(
world: World,
id: string,
init: I
): Outputtable<I> {
const output = world.outputsById[id] as Outputtable<I>
if (init) {
for (const key in init) {
if (output && output[key] && output[key].peak() == undefined) {
output[key].set(init[key] as any)
}
}
}
return output
}
export function expandGriditem(
grid: GridItem[],
gridComponent: GridItem,
@@ -311,10 +326,60 @@ export function expandGriditem(
const { left, right, top, bottom } = availableSpace
const width = $breakpoint === 'sm' ? 3 : 12
const previousGridItem = JSON.parse(JSON.stringify(gridComponent[width]))
const item = gridComponent[width]
gridComponent[width].x = previousGridItem.x - left
gridComponent[width].y = previousGridItem.y - top
gridComponent[width].w = previousGridItem.w + left + right
gridComponent[width].h = previousGridItem.h + top + bottom
item.x = item.x - left
item.y = item.y - top
item.w = item.w + left + right
item.h = item.h + top + bottom
}
export function sortGridItemsPosition(
gridItems: GridItem[],
breakpoint: EditorBreakpoint
): GridItem[] {
return gridItems.sort((a: GridItem, b: GridItem) => {
const width = breakpoint === 'lg' ? 12 : 3
const aX = a[width].x
const aY = a[width].y
const bX = b[width].x
const bY = b[width].y
if (aY < bY) {
return -1
} else if (aY > bY) {
return 1
} else {
if (aX < bX) {
return -1
} else if (aX > bX) {
return 1
} else {
return 0
}
}
})
}
export function connectInput(
connectingInput: ConnectingInput,
componentId: string,
path: string
): ConnectingInput {
if (connectingInput) {
connectingInput = {
opened: false,
input: {
connection: {
componentId,
path
},
type: 'connected'
},
hoveredComponent: undefined
}
}
return connectingInput
}

View File

@@ -222,7 +222,7 @@
recomputeIds={component.recomputeIds}
{render}
/>
{:else if component.type === 'selectcomponent'}
{:else if component.type === 'selectcomponent' || component.type === 'resourceselectcomponent'}
<AppSelect
id={component.id}
verticalAlignment={component.verticalAlignment}

View File

@@ -1,23 +1,34 @@
<script lang="ts">
import { getContext } from 'svelte'
import {
findGridItem,
findGridItemParentGrid,
getAllSubgridsAndComponentIds,
insertNewGridItem,
sortGridItemsPosition
} from '../appUtils'
import type { AppEditorContext, AppViewerContext, EditorBreakpoint, GridItem } from '../../types'
import { findGridItemParentId } from '../appUtils'
import { push } from '$lib/history'
import { sendUserToast } from '$lib/utils'
const { app, selectedComponent, breakpoint, focusedGrid } =
getContext<AppViewerContext>('AppViewerContext')
const { componentControl } = getContext<AppEditorContext>('AppEditorContext')
const { componentControl, history } = getContext<AppEditorContext>('AppEditorContext')
let tempGridItem: GridItem | undefined = undefined
let copiedGridItem: GridItem | undefined = undefined
function getSortedGridItemsOfChildren(): GridItem[] {
if (!$focusedGrid) {
return sortGridItems($app.grid, $breakpoint)
return sortGridItemsPosition($app.grid, $breakpoint)
}
if (!$app.subgrids) {
return []
}
return sortGridItems(
return sortGridItemsPosition(
$app.subgrids[`${$focusedGrid.parentComponentId}-${$focusedGrid.subGridIndex}`],
$breakpoint
)
@@ -25,14 +36,14 @@
function getSortedGridItemsOfGrid(): GridItem[] {
if ($app.grid.find((item) => item.id === $selectedComponent)) {
return sortGridItems($app.grid, $breakpoint)
return sortGridItemsPosition($app.grid, $breakpoint)
}
if (!$app.subgrids) {
return []
}
return sortGridItems(
return sortGridItemsPosition(
Object.values($app.subgrids ?? {}).find((grid) =>
grid.find((item) => item.id === $selectedComponent)
) ?? [],
@@ -79,7 +90,7 @@
return
}
const sortedGridItems = sortGridItems(subgrid, $breakpoint)
const sortedGridItems = sortGridItemsPosition(subgrid, $breakpoint)
if (sortedGridItems) {
$selectedComponent = sortedGridItems[0].id
@@ -88,22 +99,89 @@
}
}
function handleEscape(event: KeyboardEvent) {
$selectedComponent = undefined
$focusedGrid = undefined
event.preventDefault()
}
function handleArrowUp(event: KeyboardEvent) {
if (!$selectedComponent) return
let parentId = findGridItemParentGrid($app, $selectedComponent)?.split('-')[0]
if (parentId) {
$selectedComponent = parentId
} else {
$selectedComponent = undefined
$focusedGrid = undefined
}
}
function handleCopy(event: KeyboardEvent) {
if (!$selectedComponent) {
return
}
tempGridItem = undefined
copiedGridItem = findGridItem($app, $selectedComponent)
}
function handleCut(event: KeyboardEvent) {
if (!$selectedComponent) {
return
}
push(history, $app)
const gridItem = findGridItem($app, $selectedComponent)
copiedGridItem = gridItem
if (!gridItem) {
return
}
// Store the grid item in a temp variable so we can paste it later
tempGridItem = gridItem
}
function handlePaste(event: KeyboardEvent) {
push(history, $app)
if (tempGridItem) {
if (
$focusedGrid &&
getAllSubgridsAndComponentIds($app, tempGridItem.data)[0].includes(
`${$focusedGrid.parentComponentId}-${$focusedGrid.subGridIndex}`
)
) {
sendUserToast('Cannot paste a component into itself', true)
return
}
let parentGrid = findGridItemParentGrid($app, tempGridItem.data.id)
const grid = parentGrid ? $app.subgrids![parentGrid] : $app.grid
let idx = grid.findIndex((item) => {
return item.id == tempGridItem!.data.id
})
if (idx > -1) {
grid.splice(idx, 1)
}
insertNewGridItem($app, tempGridItem.data, $focusedGrid, true)
copiedGridItem = tempGridItem
tempGridItem = undefined
} else if (copiedGridItem) {
insertNewGridItem($app, copiedGridItem.data, $focusedGrid, false)
}
$app = $app
}
function keydown(event: KeyboardEvent) {
switch (event.key) {
case 'Escape':
$selectedComponent = undefined
event.preventDefault()
handleEscape(event)
break
case 'ArrowUp': {
if (!$selectedComponent) return
let parentId = findGridItemParentId($app, $selectedComponent)
if (parentId) {
$selectedComponent = parentId
} else {
$selectedComponent = undefined
$focusedGrid = undefined
}
handleArrowUp(event)
break
}
@@ -122,35 +200,28 @@
break
}
case 'c':
if (event.ctrlKey || event.metaKey) {
handleCopy(event)
}
break
case 'v':
if (event.ctrlKey || event.metaKey) {
handlePaste(event)
}
break
case 'x':
if (event.ctrlKey || event.metaKey) {
handleCut(event)
}
break
default:
break
}
}
function sortGridItems(gridItems: GridItem[], breakpoint: EditorBreakpoint): GridItem[] {
return gridItems.sort((a: GridItem, b: GridItem) => {
const width = breakpoint === 'lg' ? 12 : 3
const aX = a[width].x
const aY = a[width].y
const bX = b[width].x
const bY = b[width].y
if (aY < bY) {
return -1
} else if (aY > bY) {
return 1
} else {
if (aX < bX) {
return -1
} else if (aX > bX) {
return 1
} else {
return 0
}
}
})
}
</script>
<svelte:window on:keydown={keydown} />

View File

@@ -1,4 +1,3 @@
import type { Size } from '@windmill-labs/svelte-grid'
import type { IntRange } from '../../../../common'
import type { NARROW_GRID_COLUMNS, WIDE_GRID_COLUMNS } from '../../gridUtils'
import { defaultAlignement } from '../componentsPanel/componentDefaultProps'
@@ -36,6 +35,7 @@ import {
AtSignIcon
} from 'lucide-svelte'
import type { BaseAppComponent } from '../../types'
import type { Size } from '../../svelte-grid/types'
type BaseComponent<T extends string> = {
type: T
@@ -74,7 +74,8 @@ export type AggridComponent = BaseComponent<'aggridcomponent'>
export type DisplayComponent = BaseComponent<'displaycomponent'>
export type ImageComponent = BaseComponent<'imagecomponent'>
export type InputComponent = BaseComponent<'inputcomponent'>
export type SelectComponent = BaseComponent<'selectcomponent'>
export type SelectComponent = BaseComponent<'resourceselectcomponent'>
export type ResourceSelectComponent = BaseComponent<'selectcomponent'>
export type MultiSelectComponent = BaseComponent<'multiselectcomponent'>
export type CheckboxComponent = BaseComponent<'checkboxcomponent'>
export type RadioComponent = BaseComponent<'radiocomponent'>
@@ -116,6 +117,7 @@ export type AppComponent = BaseAppComponent &
| PieChartComponent
| ScatterChartComponent
| SelectComponent
| ResourceSelectComponent
| MultiSelectComponent
| CheckboxComponent
| FormComponent
@@ -911,7 +913,7 @@ Hello \${ctx.username}
}
},
multiselectcomponent: {
name: 'MultiSelect',
name: 'Multi Select',
icon: List,
dims: '2:1-3:1',
data: {
@@ -940,6 +942,36 @@ Hello \${ctx.username}
softWrap: true
}
},
resourceselectcomponent: {
name: 'Resource Select',
icon: List,
dims: '2:1-3:1',
data: {
verticalAlignment: 'center',
id: '',
type: 'resourceselectcomponent',
componentInput: undefined,
configuration: {
items: {
type: 'static',
fieldType: 'array',
subFieldType: 'labeledresource',
value: []
},
placeholder: {
type: 'static',
fieldType: 'text',
value: 'Select an item',
onlyStatic: true
}
},
customCss: {
input: { style: '' }
} as const,
card: false,
softWrap: true
}
},
numberinputcomponent: {
name: 'Number',
icon: Binary,

View File

@@ -32,6 +32,7 @@ const inputs: ComponentSet = {
'fileinputcomponent',
'checkboxcomponent',
'selectcomponent',
'resourceselectcomponent',
'multiselectcomponent',
]
} as const

View File

@@ -65,11 +65,7 @@
No components found
</div>
{:else}
<div
id="app-tutorial-1"
in:fade|local={{ duration: 50, delay: 50 }}
out:fade|local={{ duration: 50 }}
>
<div in:fade|local={{ duration: 50, delay: 50 }} out:fade|local={{ duration: 50 }}>
{#each componentsFiltered as { title, components }, index (index)}
{#if components.length}
<div transition:slide|local={{ duration: 300 }}>

View File

@@ -23,7 +23,13 @@
<label class="block pb-2">
<div class="text-xs font-medium pb-0.5"> Style </div>
<div class="relative">
<input type="text" class="!pr-7" bind:value={value.style} on:focus />
<input
on:keydown|stopPropagation
type="text"
class="!pr-7"
bind:value={value.style}
on:focus
/>
{#if value?.style}
<button
transition:fade|local={{ duration: 100 }}
@@ -41,7 +47,13 @@
<label class="block">
<div class="text-xs font-medium pb-0.5"> Tailwind classes </div>
<div class="relative">
<input type="text" class="!pr-7" bind:value={value.class} on:focus />
<input
on:keydown|stopPropagation
type="text"
class="!pr-7"
bind:value={value.class}
on:focus
/>
{#if value?.class}
<button
transition:fade|local={{ duration: 100 }}

View File

@@ -0,0 +1,113 @@
<script lang="ts">
import { components } from '../component'
import { getContext } from 'svelte'
import type { AppViewerContext, GridItem } from '../../types'
import ComponentOutputViewer from './ComponentOutputViewer.svelte'
import { classNames } from '$lib/utils'
import { ChevronDown, ChevronRight, Lock } from 'lucide-svelte'
import { connectInput, isIdInsideGriditem } from '../appUtils'
import { slide } from 'svelte/transition'
import SubGridOutput from './SubGridOutput.svelte'
export let gridItem: GridItem
export let first: boolean = false
export let nested: boolean = false
export let parentId: string | undefined = undefined
export let expanded: boolean = false
const { app, staticOutputs, selectedComponent, connectingInput } =
getContext<AppViewerContext>('AppViewerContext')
const name = getComponentNameById(gridItem.id)
function getComponentNameById(componentId: string) {
if (gridItem?.data?.type) {
return components[gridItem?.data.type].name
} else if (componentId == 'ctx') {
return 'Context'
} else if (componentId.startsWith('bg_')) {
return 'Background'
} else {
return 'Table action'
}
}
let manuallyOpened = false
$: if (expanded) {
manuallyOpened = true
} else {
manuallyOpened = false
}
$: subGrids = Array.from({ length: gridItem.data.numberOfSubgrids }).map(
(_, i) => `${gridItem.id}-${i}`
)
$: insideGrid = isIdInsideGriditem($app, gridItem, $selectedComponent)
$: isSelected = $selectedComponent === gridItem.id
$: opened = insideGrid || isSelected || manuallyOpened
function onHeaderClick() {
if (manuallyOpened) {
if (parentId) {
$selectedComponent = parentId
} else {
$selectedComponent = undefined
}
manuallyOpened = false
} else {
$selectedComponent = gridItem.id
manuallyOpened = true
}
}
</script>
{#if $staticOutputs[gridItem.id] || gridItem.data.numberOfSubgrids > 1}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class={classNames(
'flex items-center justify-between p-1 cursor-pointer hover:bg-indigo-100 hover:text-indigo-500 border-b',
isSelected ? 'bg-indigo-200' : 'bg-white',
first ? 'border-t' : '',
nested ? 'border-l' : ''
)}
on:click={onHeaderClick}
>
<div
class={classNames(
'text-2xs ml-0.5 font-bold px-2 py-0.5 rounded-sm',
isSelected ? 'bg-indigo-500 text-white' : ' bg-indigo-50'
)}
>
{gridItem.id}
</div>
<div class="text-2xs font-bold flex flex-row gap-2 items-center">
{getComponentNameById(gridItem.id)}
{#if !opened && !manuallyOpened}
<ChevronRight size={14} />
{:else if manuallyOpened}
<Lock size={14} class="text-orange-600" />
{:else}
<ChevronDown size={14} />
{/if}
</div>
</div>
{#if opened}
<div class={classNames('border-b', nested ? 'border-l' : '')} transition:slide|local>
<div class="py-1">
<ComponentOutputViewer
componentId={gridItem.id}
outputs={$staticOutputs[gridItem.id]}
on:select={({ detail }) => {
$connectingInput = connectInput($connectingInput, gridItem.id, detail)
}}
/>
</div>
<div>
<SubGridOutput {name} {expanded} {subGrids} parentId={gridItem.id} />
</div>
</div>
{/if}
{/if}

View File

@@ -30,6 +30,4 @@
{#if Object.keys(object).length > 0}
<ObjectViewer json={object} on:select topBrackets={false} />
{:else}
<div class="text-xs text-gray-500 px-4">No outputs</div>
{/if}

View File

@@ -1,34 +1,19 @@
<script lang="ts">
import Button from '$lib/components/common/button/Button.svelte'
import { classNames } from '$lib/utils'
import { X } from 'lucide-svelte'
import { Maximize, Minimize, X } from 'lucide-svelte'
import { getContext } from 'svelte'
import { flip } from 'svelte/animate'
import { fade } from 'svelte/transition'
import type { AppViewerContext } from '../../types'
import { findGridItem } from '../appUtils'
import { connectInput, findGridItem, sortGridItemsPosition } from '../appUtils'
import { components } from '../component'
import PanelSection from '../settingsPanel/common/PanelSection.svelte'
import ComponentOutput from './ComponentOutput.svelte'
import ComponentOutputViewer from './ComponentOutputViewer.svelte'
const { connectingInput, staticOutputs, worldStore, selectedComponent, app } =
const { staticOutputs, app, breakpoint, connectingInput, selectedComponent } =
getContext<AppViewerContext>('AppViewerContext')
function connectInput(componentId: string, path: string) {
if ($connectingInput) {
$connectingInput = {
opened: false,
input: {
connection: {
componentId,
path
},
type: 'connected'
},
hoveredComponent: undefined
}
}
}
function getComponentNameById(componentId: string) {
const component = findGridItem($app, componentId)
@@ -42,12 +27,18 @@
return 'Table action'
}
}
function toggleExpanded() {
expanded = !expanded
}
let search = ''
let expanded = false
$: panels = [['ctx', ['email', 'username', 'query', 'hash']] as [string, string[]]].concat(
Object.entries($staticOutputs)
)
let search = ''
// filter out outputs that don't match the search by name (computed by getComponentNameById) and id
// The output should be [string, string[]][]
$: filteredPanels = panels.filter(([componentId, outputs]) => {
@@ -60,99 +51,54 @@
</script>
<PanelSection noPadding titlePadding="px-4 pt-2 pb-0.5" title="Outputs">
<div class="overflow-auto h-full min-w-[150px] w-full relative flex flex-col">
<div class="sticky z-50 top-0 left-0 w-full bg-white px-2 pb-2">
<div class="relative">
<input
bind:value={search}
class="px-2 py-1 border border-gray-300 rounded-sm {search ? 'pr-8' : ''}"
placeholder="Search outputs..."
/>
{#if search}
<button
class="absolute right-2 top-1/2 transform -translate-y-1/2 hover:bg-gray-200 rounded-full p-0.5"
on:click|stopPropagation|preventDefault={() => (search = '')}
>
<X size="14" />
</button>
{/if}
</div>
</div>
<div id="app-tutorial-2" class="relative p-2">
{#each filteredPanels as [componentId, outputs] (componentId)}
<div
animate:flip={{ duration: 300 }}
in:fade|local={{ duration: 100, delay: 50 }}
out:fade|local={{ duration: 100 }}
class="pb-2"
>
{#if outputs.length > 0 && $worldStore?.outputsById[componentId]}
{@const name = getComponentNameById(componentId)}
<div>
<div
class="flex {$connectingInput?.opened
? 'bg-white z-50'
: ''} flex-row justify-between w-full"
>
<button
on:click|stopPropagation|preventDefault={$connectingInput.opened
? undefined
: () => ($selectedComponent = componentId)}
class={classNames(
'px-2 text-2xs py-0.5 border-t border-x font-bold rounded-t-sm w-fit',
$selectedComponent === componentId
? ' bg-indigo-500/90 text-white border-indigo-500/90'
: 'bg-gray-100 text-gray-500 border-gray-200'
)}
>
{componentId}
</button>
<span
class={classNames(
'px-1 text-2xs py-0.5 font-semibold rounded-t-sm w-fit',
'bg-gray-700 text-white'
)}
>
{name}
</span>
</div>
<div
class={classNames(
$connectingInput?.opened ? 'bg-white z-50' : '',
`w-full py-2 grow border relative break-all `,
$selectedComponent === componentId ? 'border border-indigo-500/90 ' : '',
$connectingInput.hoveredComponent === componentId
? 'outline outline-indigo-500/90'
: ''
)}
>
{#key $selectedComponent}
{#key $connectingInput?.opened}
<ComponentOutputViewer
outputs={$connectingInput?.opened && $selectedComponent === componentId
? name == 'Table'
? ['search']
: []
: outputs}
{componentId}
on:select={({ detail }) => {
connectInput(componentId, detail)
}}
/>
{/key}
{/key}
</div>
</div>
<div style="z-index:1000;" class="bg-white">
<div class="overflow-auto h-full min-w-[150px] w-full relative flex flex-col">
<div class="sticky z-50 top-0 left-0 w-full bg-white px-2 pb-2">
<div class="relative">
<input
bind:value={search}
class="px-2 py-1 border border-gray-300 rounded-sm {search ? 'pr-8' : ''}"
placeholder="Search outputs..."
/>
{#if search}
<button
class="absolute right-2 top-1/2 transform -translate-y-1/2 hover:bg-gray-200 rounded-full p-0.5"
on:click|stopPropagation|preventDefault={() => (search = '')}
>
<X size="14" />
</button>
{/if}
</div>
{:else}
<div
in:fade|local={{ duration: 50, delay: 100 }}
out:fade|local={{ duration: 50 }}
class="absolute left-0 top-0 w-full text-sm text-gray-500 text-center py-4 px-2"
>
No outputs found
</div>
</div>
<div class="p-1 ">
<Button on:click={toggleExpanded} color="light" size="xs">
{#if !expanded}
<Maximize size="14" />
{:else}
<Minimize size="14" />
{/if}
</Button>
</div>
<div
class={classNames(
'text-2xs ml-0.5 font-bold px-2 py-0.5 rounded-sm',
$selectedComponent === 'ctx' ? 'bg-indigo-500 text-white' : ' bg-indigo-50'
)}
>
ctx
</div>
<ComponentOutputViewer
componentId={'ctx'}
outputs={['email', 'username', 'query', 'hash']}
on:select={({ detail }) => {
$connectingInput = connectInput($connectingInput, 'ctx', detail)
}}
/>
{#each sortGridItemsPosition($app.grid, $breakpoint) as gridItem, index}
<ComponentOutput {gridItem} first={index === 0} {expanded} />
{/each}
</div>
</div>

View File

@@ -0,0 +1,73 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import { getContext } from 'svelte'
import { slide } from 'svelte/transition'
import type { Output } from '../../rx'
import type { AppViewerContext } from '../../types'
import { connectInput, sortGridItemsPosition } from '../appUtils'
import ComponentOutput from './ComponentOutput.svelte'
export let name: string | undefined = undefined
export let parentId: string
export let expanded: boolean = false
export let subGrids: string[]
const { app, connectingInput, breakpoint, worldStore } =
getContext<AppViewerContext>('AppViewerContext')
let selected = 0
$: outputs = $worldStore?.outputsById[parentId] as {
selectedTabIndex: Output<number>
}
$: if (outputs?.selectedTabIndex) {
outputs.selectedTabIndex.subscribe({
next: (value) => {
selected = value
}
})
}
</script>
{#each subGrids as subGridId, index}
<div class="ml-2 my-2">
{#if subGrids.length > 1}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class={classNames(
'px-1 py-0.5 flex justify-between items-center font-semibold text-xs border-l border-y w-full cursor-pointer',
selected === index ? 'bg-gray-200' : 'bg-gray-50'
)}
on:click={() => {
selected = index
}}
>
<div class="text-xs">
{name ? name : 'Should implement'}
{index + 1}
</div>
</div>
{/if}
{#if selected === index || name !== 'Tabs'}
<div transition:slide|local>
{#if $app.subgrids && $app.subgrids[subGridId].length > 0}
{#each sortGridItemsPosition($app.subgrids[subGridId], $breakpoint) as subGridItem, index}
<ComponentOutput
gridItem={subGridItem}
first={index === 0}
nested
{expanded}
on:select={({ detail }) => {
$connectingInput = connectInput($connectingInput, subGridItem.id, detail)
}}
/>
{/each}
{:else}
<div class="text-xs text-gray-500 border-y border-l p-1">No components</div>
{/if}
</div>
{/if}
</div>
{/each}

View File

@@ -8,7 +8,6 @@
import { inferArgs } from '$lib/infer'
import type { Schema } from '$lib/common'
import Badge from '$lib/components/common/badge/Badge.svelte'
import { fly } from 'svelte/transition'
import Editor from '$lib/components/Editor.svelte'
import { emptySchema, scriptLangToEditorLang } from '$lib/utils'
import Popover from '../../../Popover.svelte'
@@ -82,7 +81,12 @@
<div class="h-full flex flex-col gap-1">
<div class="flex justify-between w-full gap-2 px-2 pt-1 flex-row items-center">
{#if name !== undefined}
<input bind:value={name} placeholder="Inline script name" class="!text-xs !rounded-xs" />
<input
on:keydown|stopPropagation
bind:value={name}
placeholder="Inline script name"
class="!text-xs !rounded-xs"
/>
{/if}
<div class="flex w-full flex-row gap-2 items-center justify-end">
{#if validCode}

View File

@@ -22,6 +22,8 @@
componentInput.value.push(0)
} else if (componentInput.subFieldType === 'object') {
componentInput.value.push({})
} else if (componentInput.subFieldType === 'labeledresource') {
componentInput.value.push({ value: '', label: '' })
} else if (
componentInput.subFieldType === 'text' ||
componentInput.subFieldType === 'textarea' ||

View File

@@ -19,14 +19,12 @@
import TemplateEditor from '$lib/components/TemplateEditor.svelte'
import type { AppComponent } from '../component'
import CssProperty from '../componentsPanel/CssProperty.svelte'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
import GridTab from './GridTab.svelte'
import { duplicateGridItem } from '../appUtils'
import { deleteGridItem } from '../appUtils'
import MoveToOtherGrid from './MoveToOtherGrid.svelte'
import GridPane from './GridPane.svelte'
import { slide } from 'svelte/transition'
import { push } from '$lib/history'
import Kbd from '$lib/components/common/kbd/Kbd.svelte'
export let component: AppComponent
export let rowColumns = false
@@ -47,13 +45,6 @@
const { history } = getContext<AppEditorContext>('AppEditorContext')
function duplicateElement(id: string) {
push(history, $app)
$dirtyStore = true
const newId = duplicateGridItem($app, parent, id)
$selectedComponent = newId
}
function removeGridElement() {
push(history, $app)
$selectedComponent = undefined
@@ -96,7 +87,7 @@
<svelte:window on:keydown={keydown} />
{#if component}
<div class="flex flex-col min-w-[150px] w-full divide-y">
<div class="flex min-h-full flex-col min-w-[150px] w-full divide-y">
{#if component.componentInput}
<PanelSection
smallPadding
@@ -195,6 +186,8 @@
<Recompute bind:recomputeIds={component.recomputeIds} ownId={component.id} />
{/if}
<div class="grow shrink" />
{#if Object.keys(component.customCss ?? {}).length > 0}
<PanelSection title="Custom CSS">
<div slot="action">
@@ -222,8 +215,20 @@
{/if}
{#if duplicateMoveAllowed}
<PanelSection title="Duplicate">
<Button
<PanelSection title="Copy/Move">
<div slot="action">
<Button size="xs" color="red" variant="border" on:click={removeGridElement}>
Delete&nbsp;&nbsp;
<Kbd>Del</Kbd>
</Button>
</div>
<div class="flex flex-col">
<div
><span class="text-gray-600 text-xs mr-2">Copy:</span><Kbd>CTRL+C</Kbd> + <Kbd
>CTRL+V</Kbd
></div
>
<!-- <Button
size="xs"
color="dark"
startIcon={{ icon: faCopy }}
@@ -234,24 +239,19 @@
}}
>
Duplicate component: {component.id}
</Button>
</PanelSection>
<PanelSection title="Move to other grid">
<MoveToOtherGrid bind:component {parent} />
</Button> -->
<div
><span class="text-gray-600 text-xs mr-2">Move:</span><Kbd>CTRL+X</Kbd> + <Kbd
>CTRL+V</Kbd
></div
>
<div
><span class="text-gray-600 text-xs mr-2">Navigate:</span><Kbd>&leftarrow;</Kbd><Kbd
>&uparrow;</Kbd
><Kbd>&rightarrow;</Kbd><Kbd>ESC</Kbd></div
>
</div>
</PanelSection>
{/if}
<PanelSection title="Danger zone">
<Button
size="xs"
color="red"
variant="border"
startIcon={{ icon: faTrashAlt }}
on:click={removeGridElement}
>
Delete component: {component.id}
</Button>
</PanelSection>
</div>
{/if}

View File

@@ -56,7 +56,7 @@
<div class="w-full flex gap-2 flex-col mt-2">
{#each panes as value, index (index)}
<div class="w-full flex flex-row gap-2 items-center relative">
<input type="number" bind:value />
<input on:keydown|stopPropagation type="number" bind:value />
<div class="absolute top-1 right-1">
<Button

View File

@@ -1,6 +1,8 @@
<script lang="ts">
import Button from '$lib/components/common/button/Button.svelte'
import CloseButton from '$lib/components/common/CloseButton.svelte'
import { faPlus, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { CrossIcon } from 'lucide-svelte'
import { getContext } from 'svelte'
import type { AppViewerContext } from '../../types'
import { deleteGridItem } from '../appUtils'
@@ -54,20 +56,10 @@
<div class="w-full flex gap-2 flex-col mt-2">
{#each tabs as value, index (index)}
<div class="w-full flex flex-row gap-2 items-center relative">
<input type="text" bind:value />
<input on:keydown|stopPropagation type="text" bind:value />
<div class="absolute top-1 right-1">
<Button
size="xs"
color="light"
variant="border"
on:click={() => {
deleteSubgrid(index)
}}
iconOnly
btnClasses="!text-red-500"
startIcon={{ icon: faTrashAlt }}
/>
<div class="absolute right-1">
<CloseButton noBg on:close={() => deleteSubgrid(index)} />
</div>
</div>
{/each}

View File

@@ -1,123 +0,0 @@
<script lang="ts">
import { Button } from '$lib/components/common'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
import { push } from '$lib/history'
import { faCopy } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { App, AppEditorContext, AppViewerContext } from '../../types'
import {
createNewGridItem,
deleteGridItem,
findGridItem,
getAllSubgridsAndComponentIds,
insertNewGridItem
} from '../appUtils'
import type { AppComponent } from '../component'
export let component: AppComponent | undefined
export let parent: string | undefined
let selectedOption: string
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { history } = getContext<AppEditorContext>('AppEditorContext')
function listAllSubGrids(app: App) {
return app.subgrids ? Object.keys(app.subgrids) : []
}
function findAndDelete(id: string) {
const node = findGridItem($app, id)
if (!node) {
return
}
const data: AppComponent = JSON.parse(JSON.stringify(node.data))
const shouldKeepSubGrid = data.numberOfSubgrids ? data.numberOfSubgrids >= 1 : false
$dirtyStore = true
deleteGridItem($app, data, parent, shouldKeepSubGrid)
return data
}
function insertComponentInSubGrid(
data: AppComponent,
targetId: string,
targetSubGridIndex: number
) {
push(history, $app)
const focusedGrid = {
parentComponentId: targetId,
subGridIndex: targetSubGridIndex
}
insertNewGridItem($app, data, focusedGrid, true)
$app.grid = [...$app.grid]
}
function insertComponentInMainGrid(data: AppComponent) {
const newComponent = createNewGridItem($app.grid, data.id, data)
$app.grid = [...$app.grid, newComponent]
}
function onMove(component: AppComponent) {
push(history, $app)
const data = findAndDelete(component.id)
if (!data) {
return
}
if (selectedOption !== 'main-grid') {
const [targetId, targetSubGridIndex] = selectedOption.split('-')
insertComponentInSubGrid(data, targetId, parseInt(targetSubGridIndex))
} else {
insertComponentInMainGrid(data)
}
}
const defaultOption = {
label: 'Main grid',
value: 'main-grid',
disabled: parent === undefined
}
$: [subgrids] = component ? getAllSubgridsAndComponentIds($app, component) : [[], []]
$: availableGrids = listAllSubGrids($app)
$: options = availableGrids
? [
defaultOption,
...availableGrids?.map((grid) => ({
label: grid,
value: grid,
disabled: grid === parent || subgrids.includes(grid)
}))
]
: [defaultOption]
</script>
{#if component && !options.every((option) => option.disabled)}
<select bind:value={selectedOption} class="w-full">
{#each options as option}
<option value={option.value} disabled={option.disabled}>{option.label}</option>
{/each}
</select>
<Button
size="xs"
color="dark"
startIcon={{ icon: faCopy }}
on:click={() => {
if (component) {
onMove(component)
}
}}
>
Move component: {component.id}
</Button>
{:else}
<p class="text-gray-500 text-sm">No grids available</p>
{/if}

View File

@@ -43,7 +43,8 @@
disabled: {
fieldType: 'boolean',
type: 'eval',
expr: 'false'
expr: 'false',
tooltip: "Disabled based on a predicate. Use `row` to access the current row's data"
},
goto: {
tooltip: 'Go to an url on success if not empty',

View File

@@ -2,6 +2,7 @@
import type { AppInput, StaticInput } from '../../../inputType'
import { Loader2, X } from 'lucide-svelte'
import { Popup } from '../../../../common'
import { fade } from 'svelte/transition'
export let componentInput: StaticInput<string> & Extract<AppInput, { fieldType: 'icon-select' }>
let anchor: HTMLElement
@@ -9,6 +10,7 @@
let items: { label: string; icon: any }[]
let filteredItems: { label: string; icon: any }[]
let search = ''
let openPopup: () => void
$: if (search) {
filteredItems = items.filter((item) => {
@@ -19,13 +21,16 @@
}
async function getData() {
if (items) return
if (items) {
return openPopup()
}
loading = true
const data = await import('lucide-svelte/dist/svelte/icons')
filteredItems = items = Object.entries(data)
.filter(([key]) => !(key.endsWith('Icon') || key.startsWith('Lucide')))
.map(([key, icon]) => ({ label: key, icon }))
loading = false
openPopup()
}
function formatName(name?: string) {
@@ -54,7 +59,11 @@
on:focus={getData}
class="pr-8"
/>
{#if componentInput.value}
{#if loading}
<div class="center-center absolute right-2 top-1/2 transform -translate-y-1/2 p-0.5">
<Loader2 class="animate-spin" size={16} />
</div>
{:else if componentInput.value}
<button
class="absolute right-2 top-1/2 transform -translate-y-1/2 hover:bg-gray-200 rounded-full p-0.5"
on:click|stopPropagation|preventDefault={() => (componentInput.value = undefined)}
@@ -66,42 +75,52 @@
{/if}
</div>
{#if anchor}
<Popup ref={anchor} options={{ placement: 'bottom' }} let:close>
<div class="max-w-xs shadow-[0_10px_40px_-5px_rgba(0,0,0,0.25)] bg-white rounded-md p-2">
{#if loading}
<div class="center-center p-2">
<Loader2 class="animate-spin" size={18} />
</div>
{:else if filteredItems}
<input bind:value={search} type="text" placeholder="Search" class="col-span-4 mb-2" />
<div class="grid gap-1 grid-cols-4 max-h-[300px] overflow-auto">
{#each filteredItems as { label, icon }}
{@const formatedLabel = formatName(label)}
<button
type="button"
title={formatedLabel}
on:click={() => {
select(label)
close()
}}
class="w-full center-center flex-col font-normal p-1
hover:bg-gray-100 focus:bg-gray-100 rounded duration-200
{label === componentInput.value ? 'text-blue-600 bg-blue-50 pointer-events-none' : ''}"
>
<svelte:component this={icon} size={22} />
<span class="inline-block w-full text-[10px] ellipsize pt-0.5">
{formatedLabel}
</span>
</button>
{:else}
<div class="col-span-4 text-center text-gray-700 text-sm p-2">
No icons match your search
</div>
{/each}
</div>
{:else}
<div class="text-center text-sm text-gray-600 p-2"> Couldn't load options </div>
{/if}
</div>
<Popup
ref={anchor}
options={{ placement: 'bottom' }}
bind:open={openPopup}
let:close
transition={fade}
>
{#if !loading}
<div class="max-w-xs shadow-[0_10px_40px_-5px_rgba(0,0,0,0.25)] bg-white rounded-md p-2">
{#if filteredItems}
<input
on:keydown|stopPropagation
bind:value={search}
type="text"
placeholder="Search"
class="col-span-4 mb-2"
/>
<div class="grid gap-1 grid-cols-4 max-h-[300px] overflow-auto">
{#each filteredItems as { label, icon }}
{@const formatedLabel = formatName(label)}
<button
type="button"
title={formatedLabel}
on:click={() => {
select(label)
close()
}}
class="w-full center-center flex-col font-normal p-1
hover:bg-gray-100 focus:bg-gray-100 rounded duration-200
{label === componentInput.value ? 'text-blue-600 bg-blue-50 pointer-events-none' : ''}"
>
<svelte:component this={icon} size={22} />
<span class="inline-block w-full text-[10px] ellipsize pt-0.5">
{formatedLabel}
</span>
</button>
{:else}
<div class="col-span-4 text-center text-gray-700 text-sm p-2">
No icons match your search
</div>
{/each}
</div>
{:else}
<div class="text-center text-sm text-gray-600 p-2"> Couldn't load options </div>
{/if}
</div>
{/if}
</Popup>
{/if}

View File

@@ -1,7 +1,8 @@
<script lang="ts">
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
export let code: string
export let value: any
export let value: any = undefined
export let error = ''
function parseJson() {
try {
@@ -11,13 +12,12 @@
error = e.message
}
}
let error = ''
$: code && parseJson()
</script>
<div class="flex flex-col w-full">
<div class="border border-gray-300 w-full">
<SimpleEditor autoHeight lang="json" bind:code />
<SimpleEditor on:change autoHeight lang="json" bind:code />
</div>
{#if error != ''}
<span class="text-red-600 text-xs">{error}</span>

View File

@@ -39,6 +39,29 @@
</select>
{:else if componentInput.fieldType === 'icon-select'}
<IconSelectInput bind:componentInput />
{:else if componentInput.fieldType === 'labeledresource'}
{#if componentInput?.value && typeof componentInput?.value == 'object' && 'label' in componentInput?.value}
<div class="flex flex-col gap-1 w-full">
<input
on:keydown|stopPropagation
placeholder="Label"
type="text"
bind:value={componentInput.value['label']}
/>
<ResourcePicker
initialValue={componentInput.value?.['value']?.split('$res:')[1] || ''}
on:change={(e) => {
let path = e.detail
if (componentInput && path) {
componentInput.value['value'] = `$res:${path}`
}
}}
/>
</div>
{:else}
Inconsistent labeled resource object
{/if}
{:else if componentInput.fieldType === 'color'}
<ColorInput bind:componentInput />
{:else if componentInput.fieldType === 'object'}

View File

@@ -19,6 +19,7 @@
<div class="w-full flex mt-1 items-center gap-2">
<slot />
<input
on:keydown|stopPropagation
type="text"
placeholder="Search inline scripts"
bind:value={filter}

View File

@@ -17,6 +17,7 @@ export type InputType =
| 'object'
| 'array'
| 'any'
| 'labeledresource'
// Connection to an output of another component
// defined by the id of the component and the path of the output
@@ -139,11 +140,11 @@ export type AppInput =
| AppInputSpec<'object', Record<string | number, any>>
| AppInputSpec<'object', string>
| (AppInputSpec<'select', string> & {
/**
* One of the keys of `staticValues` from `lib/components/apps/editor/componentsPanel/componentStaticValues`
*/
optionValuesKey: keyof typeof staticValues
})
/**
* One of the keys of `staticValues` from `lib/components/apps/editor/componentsPanel/componentStaticValues`
*/
optionValuesKey: keyof typeof staticValues
})
| (AppInputSpec<'select', string> & StaticOptions)
| AppInputSpec<'icon-select', string>
| AppInputSpec<'color', string>
@@ -156,9 +157,12 @@ export type AppInput =
| AppInputSpec<'array', string[], 'datetime'>
| AppInputSpec<'array', object[], 'object'>
| (AppInputSpec<'array', string[], 'select'> & {
optionValuesKey: keyof typeof staticValues
})
optionValuesKey: keyof typeof staticValues
})
| (AppInputSpec<'array', string[], 'select'> & StaticOptions)
| AppInputSpec<'array', object[], 'labeledresource'>
| AppInputSpec<'labeledresource', object>
export type RowAppInput = Extract<AppInput, { type: 'row' }>
export type StaticAppInput = Extract<AppInput, { type: 'static' }>

View File

@@ -0,0 +1,196 @@
<script lang="ts">
import { getContainerHeight } from './utils/container'
import { moveItemsAroundItem, moveItem, getItemById, specifyUndefinedColumns } from './utils/item'
import { onMount, createEventDispatcher } from 'svelte'
import { getColumn, throttle } from './utils/other'
import MoveResize from './MoveResize.svelte'
import type { FilledItem } from './types'
const dispatch = createEventDispatcher()
type T = $$Generic
export let fillSpace = false
export let items: FilledItem<T>[]
export let rowHeight: number
export let cols: [number, number][]
export let gap = [10, 10]
export let fastStart = false
export let throttleUpdate = 100
export let throttleResize = 100
export let onTopId: string | undefined = undefined
export let scroller = undefined
export let sensor = 20
export let parentWidth: number | undefined = undefined
let getComputedCols
let container
$: [gapX, gapY] = gap
let xPerPx = 0
let yPerPx = rowHeight
let containerWidth
$: containerHeight = getContainerHeight(items, yPerPx, getComputedCols)
const pointerup = (ev) => {
dispatch('pointerup', {
id: ev.detail.id,
cols: getComputedCols
})
}
const onResize = throttle(() => {
items = specifyUndefinedColumns(items, getComputedCols, cols)
dispatch('resize', {
cols: getComputedCols,
xPerPx,
yPerPx,
width: containerWidth
})
}, throttleUpdate)
onMount(() => {
const sizeObserver = new ResizeObserver((entries) => {
requestAnimationFrame(() => {
let width = entries[0].contentRect.width
if (width === containerWidth) return
getComputedCols = getColumn(parentWidth ?? width, cols)
xPerPx = width / getComputedCols
if (!containerWidth) {
items = specifyUndefinedColumns(items, getComputedCols, cols)
dispatch('mount', {
cols: getComputedCols,
xPerPx,
yPerPx // same as rowHeight
})
} else {
onResize()
}
containerWidth = width
})
})
sizeObserver.observe(container)
return () => sizeObserver.disconnect()
})
let initItems: FilledItem<T>[] | undefined = undefined
const updateMatrix = ({ detail }) => {
let isPointerUp = detail.isPointerUp
let citems: FilledItem<T>[]
if (isPointerUp) {
citems = JSON.parse(JSON.stringify(initItems))
initItems = undefined
} else {
if (initItems == undefined) {
initItems = JSON.parse(JSON.stringify(items))
}
citems = JSON.parse(JSON.stringify(initItems))
}
let activeItem = getItemById(detail.id, citems)
if (activeItem) {
activeItem = {
...activeItem,
[getComputedCols]: {
...activeItem[getComputedCols],
...detail.shadow
}
}
if (fillSpace) {
items = moveItemsAroundItem(
activeItem,
citems,
getComputedCols,
getItemById(detail.id, citems)
)
} else {
items = moveItem(activeItem, citems, getComputedCols, getItemById(detail.id, citems))
}
if (detail.onUpdate) detail.onUpdate()
dispatch('change', {
unsafeItem: activeItem,
id: activeItem.id,
cols: getComputedCols
})
}
}
const throttleMatrix = throttle(updateMatrix, throttleResize)
const handleRepaint = ({ detail }) => {
if (!detail.isPointerUp) {
throttleMatrix({ detail })
} else {
updateMatrix({ detail })
}
}
</script>
<div class="svlt-grid-container" style="height: {containerHeight}px" bind:this={container}>
{#if xPerPx || !fastStart}
{#each items as item, i (item.id)}
<MoveResize
on:repaint={handleRepaint}
on:pointerup={pointerup}
onTop={item.id == onTopId}
id={item.id}
resizable={item[getComputedCols] && item[getComputedCols].resizable}
draggable={item[getComputedCols] && item[getComputedCols].draggable}
{xPerPx}
{yPerPx}
width={Math.min(getComputedCols, item[getComputedCols] && item[getComputedCols].w) *
xPerPx -
gapX * 2}
height={(item[getComputedCols] && item[getComputedCols].h) * yPerPx - gapY * 2}
top={(item[getComputedCols] && item[getComputedCols].y) * yPerPx + gapY}
left={(item[getComputedCols] && item[getComputedCols].x) * xPerPx + gapX}
item={item[getComputedCols]}
min={item[getComputedCols] && item[getComputedCols].min}
max={item[getComputedCols] && item[getComputedCols].max}
cols={getComputedCols}
{gapX}
{gapY}
{sensor}
container={scroller}
nativeContainer={container}
let:resizePointerDown
let:movePointerDown
>
{#if item[getComputedCols]}
<slot
{movePointerDown}
{resizePointerDown}
dataItem={item}
item={item[getComputedCols]}
index={i}
/>
{/if}
</MoveResize>
{/each}
{/if}
</div>
<style>
.svlt-grid-container {
position: relative;
width: 100%;
}
</style>

View File

@@ -0,0 +1,23 @@
Anything under this repo was initially forked from svelte-grid whose LICENSE is below
MIT License
Copyright (c) 2019 Vahe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,404 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
export let sensor
export let width
export let height
export let left
export let top
export let resizable
export let draggable
export let id
export let container
export let xPerPx
export let yPerPx
export let gapX
export let gapY
export let item
export let max
export let min
export let cols
export let nativeContainer
export let onTop
let shadowElement
let shadow: { x: number; y: number; w: number; h: number } | undefined = undefined
let active = false
let initX, initY
let capturePos = {
x: 0,
y: 0
}
let cordDiff = { x: 0, y: 0 }
let newSize = { width, height }
let trans = false
let anima
const inActivate = () => {
if (shadowElement && shadow) {
let subgrid = shadowElement.closest('.subgrid')
let irect = shadowElement.getBoundingClientRect()
let shadowBound
if (subgrid) {
let subGridParent = subgrid.parentElement
let subGridParentRect = subGridParent.getBoundingClientRect()
let prect = subgrid.getBoundingClientRect()
const subGridOffset = {
x: subGridParentRect.x - prect.x,
y: subGridParentRect.y - prect.y
}
shadowBound = {
x: irect.x - prect.left - subGridOffset.x,
y: irect.y - prect.top - subGridOffset.y
}
} else {
shadowBound = irect
}
const xdragBound = rect.left + cordDiff.x
const ydragBound = rect.top + cordDiff.y
cordDiff.x = shadow.x * xPerPx + gapX - (shadowBound.x - xdragBound)
cordDiff.y = shadow.y * yPerPx + gapY - (shadowBound.y - ydragBound)
active = false
trans = true
clearTimeout(anima)
anima = setTimeout(() => {
trans = false
}, 100)
dispatch('pointerup', {
id
})
}
}
let repaint = (cb: (() => void) | undefined, isPointerUp: boolean) => {
dispatch('repaint', {
id,
shadow,
isPointerUp,
onUpdate: cb
})
}
// Autoscroll
let _scrollTop = 0
let containerFrame
let rect
let scrollElement
const getContainerFrame = (element) => {
if (element === document.documentElement || !element) {
const { height, top, right, bottom, left } = nativeContainer.getBoundingClientRect()
return {
top: Math.max(0, top),
bottom: Math.min(window.innerHeight, bottom)
}
}
return element.getBoundingClientRect()
}
const getScroller = (element) => (!element ? document.documentElement : element)
function computeRect(target) {
let gridItem = target.closest('.svlt-grid-item')
let subgrid = gridItem.closest('.subgrid')
let irect = gridItem.getBoundingClientRect()
if (subgrid) {
let subGridParent = subgrid.parentElement
let subGridParentRect = subGridParent.getBoundingClientRect()
let prect = subgrid.getBoundingClientRect()
const subGridOffset = {
x: subGridParentRect.x - prect.x,
y: subGridParentRect.y - prect.y
}
rect = {
top: irect.top - prect.top - subGridOffset.y,
left: irect.left - prect.left - subGridOffset.x
}
} else {
rect = irect
}
}
const pointerdown = ({ clientX, clientY, target }) => {
initX = clientX
initY = clientY
capturePos = { x: left, y: top }
shadow = { x: item.x, y: item.y, w: item.w, h: item.h }
newSize = { width, height }
containerFrame = getContainerFrame(container)
scrollElement = getScroller(container)
cordDiff = { x: 0, y: 0 }
computeRect(target)
active = true
trans = false
_scrollTop = scrollElement.scrollTop
window.addEventListener('pointermove', pointermove)
window.addEventListener('pointerup', pointerup)
}
let sign = { x: 0, y: 0 }
let vel = { x: 0, y: 0 }
let intervalId: NodeJS.Timer | undefined = undefined
const stopAutoscroll = () => {
intervalId && clearInterval(intervalId)
intervalId = undefined
sign = { x: 0, y: 0 }
vel = { x: 0, y: 0 }
}
const update = () => {
const _newScrollTop = scrollElement.scrollTop - _scrollTop
const boundX = capturePos.x + cordDiff.x
const boundY = capturePos.y + (cordDiff.y + _newScrollTop)
let gridX = Math.round(boundX / xPerPx)
let gridY = Math.round(boundY / yPerPx)
if (shadow) {
shadow.x = Math.max(Math.min(gridX, cols - shadow.w), 0)
shadow.y = Math.max(gridY, 0)
if (max.y) {
shadow.y = Math.min(shadow.y, max.y)
}
}
repaint(undefined, false)
}
const pointermove = (event) => {
event.preventDefault()
event.stopPropagation()
event.stopImmediatePropagation()
const { clientX, clientY } = event
cordDiff = { x: clientX - initX, y: clientY - initY }
const Y_SENSOR = sensor
let velocityTop = Math.max(0, (containerFrame.top + Y_SENSOR - clientY) / Y_SENSOR)
let velocityBottom = Math.max(0, (clientY - (containerFrame.bottom - Y_SENSOR)) / Y_SENSOR)
const topSensor = velocityTop > 0 && velocityBottom === 0
const bottomSensor = velocityBottom > 0 && velocityTop === 0
sign.y = topSensor ? -1 : bottomSensor ? 1 : 0
vel.y = sign.y === -1 ? velocityTop : velocityBottom
if (vel.y > 0) {
if (!intervalId) {
// Start scrolling
// TODO Use requestAnimationFrame
intervalId = setInterval(() => {
scrollElement.scrollTop += 2 * (vel.y + Math.sign(vel.y)) * sign.y
update()
}, 10)
}
} else if (intervalId) {
stopAutoscroll()
} else {
update()
}
}
const pointerup = (e) => {
stopAutoscroll()
window.removeEventListener('pointerdown', pointerdown)
window.removeEventListener('pointermove', pointermove)
window.removeEventListener('pointerup', pointerup)
repaint(inActivate, true)
}
// Resize
let resizeInitPos = { x: 0, y: 0 }
let initSize = { width: 0, height: 0 }
const resizePointerDown = (e) => {
e.stopPropagation()
const { pageX, pageY, target } = e
resizeInitPos = { x: pageX, y: pageY }
initSize = { width, height }
cordDiff = { x: 0, y: 0 }
computeRect(target)
newSize = { width, height }
active = true
trans = false
shadow = { x: item.x, y: item.y, w: item.w, h: item.h }
containerFrame = getContainerFrame(container)
scrollElement = getScroller(container)
window.addEventListener('pointermove', resizePointerMove)
window.addEventListener('pointerup', resizePointerUp)
}
const resizePointerMove = ({ pageX, pageY }) => {
if (shadow) {
newSize.width = initSize.width + pageX - resizeInitPos.x
newSize.height = initSize.height + pageY - resizeInitPos.y
// Get max col number
let maxWidth = cols - shadow.x
maxWidth = Math.min(max.w, maxWidth) || maxWidth
// Limit bound
newSize.width = Math.max(
Math.min(newSize.width, maxWidth * xPerPx - gapX * 2),
min.w * xPerPx - gapX * 2
)
newSize.height = Math.max(newSize.height, min.h * yPerPx - gapY * 2)
if (max.h) {
newSize.height = Math.min(newSize.height, max.h * yPerPx - gapY * 2)
}
// Limit col & row
shadow.w = Math.round((newSize.width + gapX * 2) / xPerPx)
shadow.h = Math.round((newSize.height + gapY * 2) / yPerPx)
repaint(undefined, false)
}
}
const resizePointerUp = (e) => {
e.stopPropagation()
repaint(inActivate, true)
window.removeEventListener('pointermove', resizePointerMove)
window.removeEventListener('pointerup', resizePointerUp)
}
</script>
<div
draggable={false}
on:pointerdown={item && item.customDragger ? null : draggable && pointerdown}
class="svlt-grid-item"
class:svlt-grid-active={active || (trans && rect)}
style="width: {active ? newSize.width : width}px; height:{active
? newSize.height
: height}px; {onTop ? 'z-index: 100;' : ''}
{active
? `transform: translate(${cordDiff.x}px, ${cordDiff.y}px);top:${rect.top}px;left:${rect.left}px;`
: trans
? `transform: translate(${cordDiff.x}px, ${cordDiff.y}px); position:absolute; transition: width 0.2s, height 0.2s;`
: `transition: transform 0.2s, opacity 0.2s; transform: translate(${left}px, ${top}px); `} "
>
<slot movePointerDown={pointerdown} {resizePointerDown} />
{#if resizable && !item.customResizer}
<div class="svlt-grid-resizer" on:pointerdown={resizePointerDown} />
{/if}
</div>
{#if (active || trans) && shadow}
<div
class="svlt-grid-shadow shadow-active"
style="width: {shadow.w * xPerPx - gapX * 2}px; height: {shadow.h * yPerPx -
gapY * 2}px; transform: translate({shadow.x * xPerPx + gapX}px, {shadow.y * yPerPx +
gapY}px); "
bind:this={shadowElement}
/>
{/if}
<style>
.svlt-grid-item {
touch-action: none;
position: absolute;
will-change: auto;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
.svlt-grid-resizer {
user-select: none;
width: 20px;
height: 20px;
position: absolute;
right: 0;
bottom: 0;
cursor: se-resize;
}
.svlt-grid-resizer::after {
content: '';
position: absolute;
right: 3px;
bottom: 3px;
width: 5px;
height: 5px;
border-right: 2px solid rgba(0, 0, 0, 0.4);
border-bottom: 2px solid rgba(0, 0, 0, 0.4);
}
.svlt-grid-active {
z-index: 3;
cursor: grabbing;
position: fixed;
opacity: 0.5;
/*No user*/
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
-o-backface-visibility: hidden;
-ms-backface-visibility: hidden;
user-select: none;
}
.shadow-active {
z-index: 2;
transition: all 0.2s;
}
.svlt-grid-shadow {
position: absolute;
background: red;
will-change: transform;
background: pink;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
</style>

View File

@@ -0,0 +1,25 @@
export interface Size {
w: number
h: number
}
export interface Positon {
x: number
y: number
}
interface ItemLayout extends Size, Positon {
fixed?: boolean
resizable?: boolean
draggable?: boolean
customDragger?: boolean
customResizer?: boolean
min?: Size
max?: Size
}
export type FilledItem<T> = T & { [width: number]: Required<ItemLayout>; data: any, id: string }

View File

@@ -0,0 +1,5 @@
import { getRowsCount } from "./other";
export function getContainerHeight(items, yPerPx, cols) {
return getRowsCount(items, cols) * yPerPx;
}

View File

@@ -0,0 +1,43 @@
import { makeMatrixFromItems } from "./matrix";
import { findFreeSpaceForItem, normalize, adjust } from "./item";
import { getRowsCount } from "./other";
function makeItem(item) {
const { min = { w: 1, h: 1 }, max } = item;
return {
fixed: false,
resizable: !item.fixed,
draggable: !item.fixed,
customDragger: false,
customResizer: false,
min: {
w: Math.max(1, min.w),
h: Math.max(1, min.h),
},
max: { ...max },
...item,
};
}
const gridHelp = {
normalize(items, col) {
return normalize(items, col);
},
adjust(items, col) {
return adjust(items, col);
},
item(obj) {
return makeItem(obj);
},
findSpace(item, items, cols) {
let matrix = makeMatrixFromItems(items, getRowsCount(items, cols), cols);
let position = findFreeSpaceForItem(matrix, item[cols]);
return position;
},
};
export default gridHelp;

View File

@@ -0,0 +1,260 @@
import type { FilledItem, ItemLayout } from '../types'
import {
makeMatrix,
makeMatrixFromItemsIgnore,
findCloseBlocks,
findItemsById,
makeMatrixFromItems
} from './matrix'
import { getRowsCount } from './other'
export function getItemById(id, items) {
return items.find((value) => value.id === id)
}
export function isEmpty(matrix: any[][], x: number, y: number, w: number, h: number) {
for (var i = 0; i < h; i++) {
if (matrix[y + i]) {
for (var j = 0; j < w; j++) {
if (matrix[y + i][x + j] != undefined) {
return false
}
}
}
}
return true
}
function distance(a, b): number {
return Math.abs(a.x - b.x + 0.25) + Math.abs(a.y - b.y + 0.25)
}
export function findFreeSpaceForItem<T>(matrix: FilledItem<T>[][], item: ItemLayout) {
const cols = matrix[0].length
const w = Math.min(cols, item.w)
const h = item.h
let xNtime = cols - w
let getMatrixRows = matrix.length
const range = Array.from({ length: getMatrixRows }, (_, y) =>
Array.from({ length: xNtime }, (_, x) => ({ x, y }))
)
.flat(1)
.sort((a, b) => {
let dst1 = distance(a, item)
let dst2 = distance(b, item)
if (dst1 > dst2) {
return 1
} else if (dst1 < dst2) {
return -1
} else {
return 0
}
})
for (const { x, y } of range.values()) {
if (isEmpty(matrix, x, y, w, h)) {
return { x, y }
}
}
return {
y: getMatrixRows,
x: 0
}
}
const getItem = (item, col) => {
return { ...item[col], id: item.id }
}
const updateItem = (elements, active, position, col) => {
return elements.map((value) => {
if (value.id === active.id) {
return { ...value, [col]: { ...value[col], ...position } }
}
return value
})
}
export function moveItemsAroundItem(active, items, cols, original) {
// Get current item from the breakpoint
const activeItem = getItem(active, cols)
const ids = items.map((value) => value.id).filter((value) => value !== activeItem.id)
const els = items.filter((value) => value.id !== activeItem.id)
// Update items
let newItems = updateItem(items, active, activeItem, cols)
let matrix = makeMatrixFromItemsIgnore(newItems, ids, getRowsCount(newItems, cols), cols)
let tempItems = newItems
// Exclude resolved elements ids in array
let exclude: string[] = []
els.forEach((item) => {
// Find position for element
let position = findFreeSpaceForItem(matrix, item[cols])
// Exclude item
exclude.push(item.id)
tempItems = updateItem(tempItems, item, position, cols)
// Recreate ids of elements
let getIgnoreItems = ids.filter((value) => exclude.indexOf(value) === -1)
// Update matrix for next iteration
matrix = makeMatrixFromItemsIgnore(
tempItems,
getIgnoreItems,
getRowsCount(tempItems, cols),
cols
)
})
// Return result
return tempItems
}
export function moveItem(active, items, cols, original) {
// Get current item from the breakpoint
const item = getItem(active, cols)
// Create matrix from the items expect the active
let matrix = makeMatrixFromItemsIgnore(items, [item.id], getRowsCount(items, cols), cols)
// Getting the ids of items under active Array<String>
const closeBlocks = findCloseBlocks(matrix, item)
// Getting the objects of items under active Array<Object>
let closeObj = findItemsById(closeBlocks, items)
// Getting whenever of these items is fixed
const fixed = closeObj.find((value) => value[cols].fixed)
// If found fixed, reset the active to its original position
if (fixed) return items
// Update items
items = updateItem(items, active, item, cols)
// Create matrix of items expect close elements
matrix = makeMatrixFromItemsIgnore(items, closeBlocks, getRowsCount(items, cols), cols)
// Create temp vars
let tempItems = items
let tempCloseBlocks = closeBlocks
// Exclude resolved elements ids in array
let exclude: string[] = []
// Iterate over close elements under active item
closeObj.forEach((item) => {
// Find position for element
let position = findFreeSpaceForItem(matrix, item[cols])
// Exclude item
exclude.push(item.id)
// Assign the position to the element in the column
tempItems = updateItem(tempItems, item, position, cols)
// Recreate ids of elements
let getIgnoreItems = tempCloseBlocks.filter((value) => exclude.indexOf(value) === -1)
// Update matrix for next iteration
matrix = makeMatrixFromItemsIgnore(
tempItems,
getIgnoreItems,
getRowsCount(tempItems, cols),
cols
)
})
// Return result
return tempItems
}
// Helper function
export function normalize(items, col) {
let result = items.slice()
result.forEach((value) => {
const getItem = value[col]
if (!getItem.static) {
result = moveItem(getItem, result, col, { ...getItem })
}
})
return result
}
// Helper function
export function adjust<T>(items: FilledItem<T>[], col) {
let matrix = makeMatrix(getRowsCount(items, col), col)
let res: FilledItem<T>[] = []
items.forEach((item) => {
let position = findFreeSpaceForItem(matrix, item[col])
res.push({
...item,
[col]: {
...item[col],
...position
}
})
matrix = makeMatrixFromItems(res, getRowsCount(res, col), col)
})
return res
}
export function getUndefinedItems(items, col, breakpoints) {
return items
.map((value) => {
if (!value[col]) {
return value.id
}
})
.filter(Boolean)
}
export function getClosestColumn(items, item, col, breakpoints) {
return breakpoints
.map(([_, column]) => item[column] && column)
.filter(Boolean)
.reduce(function (acc, value) {
const isLower = Math.abs(value - col) < Math.abs(acc - col)
return isLower ? value : acc
})
}
export function specifyUndefinedColumns(items, col, breakpoints) {
let matrix = makeMatrixFromItems(items, getRowsCount(items, col), col)
const getUndefinedElements = getUndefinedItems(items, col, breakpoints)
let newItems = [...items]
getUndefinedElements.forEach((elementId) => {
const getElement = items.find((item) => item.id === elementId)
const closestColumn = getClosestColumn(items, getElement, col, breakpoints)
const position = findFreeSpaceForItem(matrix, getElement[closestColumn])
const newItem = {
...getElement,
[col]: {
...getElement[closestColumn],
...position
}
}
newItems = newItems.map((value) => (value.id === elementId ? newItem : value))
matrix = makeMatrixFromItems(newItems, getRowsCount(newItems, col), col)
})
return newItems
}

View File

@@ -0,0 +1,65 @@
import type { FilledItem } from "../types";
export const makeMatrix: (w: number, h: number) => any[][] = (rows, cols) => Array.from(Array(rows), () => new Array(cols)); // make 2d array
export function makeMatrixFromItems<T>(items: FilledItem<T>[], row: number, col: number): FilledItem<T>[][] {
let matrix = makeMatrix(row, col);
for (var i = 0; i < items.length; i++) {
const value = items[i][col];
if (value) {
const { x, y, h } = value;
const id = items[i].id;
const w = Math.min(col, value.w);
for (var j = y; j < y + h; j++) {
const row = matrix[j];
for (var k = x; k < x + w; k++) {
row[k] = { ...value, id };
}
}
}
}
return matrix;
}
export function findCloseBlocks<T>(matrix: FilledItem<T>[][], curObject) {
const { h, x, y } = curObject;
const w = Math.min(matrix[0].length, curObject.w);
const tempR = matrix.slice(y, y + h);
let result: string[] = [];
for (var i = 0; i < tempR.length; i++) {
let tempA = tempR[i].slice(x, x + w);
result = [...result, ...tempA.map((val) => val?.id).filter((id) => id !== undefined && id !== curObject.id)];
}
return [...new Set(result)];
}
export function makeMatrixFromItemsIgnore(items, ignoreList, _row, _col) {
let matrix = makeMatrix(_row, _col);
for (var i = 0; i < items.length; i++) {
const value = items[i][_col];
const id = items[i].id;
const { x, y, h } = value;
const w = Math.min(_col, value.w);
if (ignoreList.indexOf(id) === -1) {
for (var j = y; j < y + h; j++) {
const row = matrix[j];
if (row) {
for (var k = x; k < x + w; k++) {
row[k] = { ...value, id };
}
}
}
}
}
return matrix;
}
export function findItemsById<T>(closeBlocks: string[], items: FilledItem<T>[]) {
return items.filter((value) => closeBlocks.indexOf(value.id) !== -1);
}

View File

@@ -0,0 +1,35 @@
export function throttle(func, timeFrame) {
let lastTime = new Date().getTime();
return function (...args) {
let now = new Date().getTime();
if (now - lastTime >= timeFrame) {
func(...args);
lastTime = now;
}
};
}
export function getRowsCount(items, cols) {
const getItemsMaxHeight = items.map((val) => {
const item = val[cols];
return (item && item.y) + (item && item.h) || 0;
});
return Math.max(...getItemsMaxHeight, 1);
}
export const getColumn = (containerWidth, columns) => {
const sortColumns = columns.slice().sort((a, b) => a[0] - b[0]);
const breakpoint = sortColumns.find((value) => {
const [width] = value;
return containerWidth <= width;
});
if (breakpoint) {
return breakpoint[1];
} else {
return sortColumns[sortColumns.length - 1][1];
}
};

View File

@@ -1,9 +1,8 @@
import type { Schema } from '$lib/common'
import type { Preview } from '$lib/gen'
import type { History } from '$lib/history'
import type { FilledItem } from '@windmill-labs/svelte-grid'
import type { Writable } from 'svelte/store'
import type VariableEditor from '../VariableEditor.svelte'
import type { AppComponent } from './editor/component/components'
import type {
AppInput,
@@ -16,6 +15,7 @@ import type {
UserAppInput
} from './inputType'
import type { World } from './rx'
import type { FilledItem } from './svelte-grid/types'
export type HorizontalAlignment = 'left' | 'center' | 'right'
export type VerticalAlignment = 'top' | 'center' | 'bottom'
@@ -137,7 +137,7 @@ export type AppViewerContext = {
}
export type AppEditorContext = {
history: History<App>
history: History<App> | undefined
componentControl: Writable<Record<string, { left?: () => boolean; right?: () => boolean }>>
pickVariableCallback: Writable<((path: string) => void) | undefined>
}

View File

@@ -0,0 +1,18 @@
<script>
import { faClose } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import { Icon } from 'svelte-awesome'
export let noBg = false
const dispatch = createEventDispatcher()
</script>
<button
on:click={() => dispatch('close')}
class="hover:bg-gray-200 {noBg
? ''
: 'bg-gray-100'} rounded-full w-8 h-8 flex items-center justify-center transition-all"
>
<Icon data={faClose} class="text-gray-500" />
</button>

View File

@@ -9,6 +9,7 @@
export let href = ''
export let rounded = false
export let dismissable = false
export let wrapperClass = ''
export let baseClass = 'text-center'
export let capitalize = false
export let icon: BadgeIconProps | undefined = undefined
@@ -56,7 +57,11 @@
const handleHide = () => (hidden = !hidden)
</script>
<span on:click on:keydown class="inline-flex justify-center items-center whitespace-nowrap">
<span
on:click
on:keydown
class="inline-flex justify-center items-center whitespace-nowrap {wrapperClass}"
>
<svelte:element
this={href ? 'a' : 'span'}
{href}

View File

@@ -3,6 +3,7 @@
import { faClose } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import { Icon } from 'svelte-awesome'
import CloseButton from '../CloseButton.svelte'
export let title: string | undefined = undefined
export let overflow_y = true
@@ -15,12 +16,7 @@
<div class="flex flex-col divide-y h-screen max-h-screen">
<div class="flex justify-between w-wull items-center px-2 py-2 ">
<div class="flex items-center gap-2">
<button
on:click={() => dispatch('close')}
class="hover:bg-gray-200 bg-gray-100 rounded-full w-8 h-8 flex items-center justify-center transition-all"
>
<Icon data={faClose} class="text-gray-500" />
</button>
<CloseButton on:close />
<span class="font-semibold truncate text-gray-800">{title}</span>
</div>

View File

@@ -63,7 +63,7 @@
}
show = !show
}}
class="relative cursor-pointer hover:bg-gray-100/30"
class="relative"
>
<slot class="triggerable" name="trigger" />
</div>

View File

@@ -16,6 +16,7 @@
export let innerClasses = ''
export let outerClasses = ''
export let transition: (node: Element, params?: Record<string, any>) => TransitionConfig = slide
export { openFocusIn as open, closed as close }
const states = ['closed', 'open-focus-in', 'open-focus-out'] as const
const stateMachine = createStateMachine(states, {
@@ -158,7 +159,7 @@
{#if $stateMachine.currentState !== 'closed'}
<div transition:transition|local={{ duration: 200 }} class={outerClasses}>
<div class={innerClasses}>
<slot close={closed} />
<slot open={openFocusIn} close={closed} />
</div>
</div>
{/if}

View File

@@ -86,7 +86,6 @@
</span>
<Dropdown
placement="bottom-end"
btnClasses="!text-gray-700 !bg-transparent hover:!bg-gray-400/20 !p-[6px]"
dropdownItems={[
{
displayName: 'View',
@@ -96,7 +95,8 @@
{
displayName: 'Edit',
icon: faPen,
href: `/apps/edit/${path}?nodraft=true`
href: `/apps/edit/${path}?nodraft=true`,
disabled: !canWrite
},
{
displayName: 'Use as template',
@@ -136,8 +136,6 @@
disabled: !canWrite
}
]}
>
<MoreVertical size={20} />
</Dropdown>
/>
</svelte:fragment>
</Row>

View File

@@ -130,7 +130,6 @@
<Dropdown
placement="bottom-end"
btnClasses="!text-gray-700 !bg-transparent hover:!bg-gray-400/20 !p-[6px]"
dropdownItems={[
{
displayName: 'View flow',
@@ -201,8 +200,6 @@
disabled: !canWrite
}
]}
>
<MoreVertical size={20} />
</Dropdown>
/>
</svelte:fragment>
</Row>

View File

@@ -21,7 +21,7 @@
}[kind]
</script>
<li
<div
class="hover:bg-gray-50 w-full inline-flex items-center p-4 gap-4 first-of-type:!border-t-0
first-of-type:rounded-t-md last-of-type:rounded-b-md {color}"
>
@@ -61,4 +61,4 @@ first-of-type:rounded-t-md last-of-type:rounded-b-md {color}"
/>
</div>
{/if}
</li>
</div>

View File

@@ -146,7 +146,6 @@
</span>
<Dropdown
placement="bottom-end"
btnClasses="!text-gray-700 !bg-transparent hover:!bg-gray-400/20 !p-[6px]"
dropdownItems={[
{
displayName: 'View script',
@@ -229,8 +228,6 @@
}
])
]}
>
<MoreVertical size={20} />
</Dropdown>
/>
</svelte:fragment>
</Row>

View File

@@ -35,6 +35,7 @@
)}
style={`${style} ${$selected?.startsWith(value) ? selectedStyle : ''}`}
on:click={() => update(value)}
on:pointerdown|stopPropagation
>
<slot />
</div>

View File

@@ -18,7 +18,6 @@
export { c as class }
export let wrapperClass = ''
export let style = ''
export let id = ''
$: selected && updateSelected()
@@ -43,7 +42,6 @@
<div
class={twMerge('border-b border-gray-200 flex flex-row whitespace-nowrap scrollbar-hidden', c)}
{style}
{id}
>
<slot {selected} />
</div>

View File

@@ -29,7 +29,7 @@
<div class="h-full overflow-hidden">
<FlowCard title="Settings">
<div class="h-full flex-1">
<Tabs bind:selected={$selectedId} id="flow-tutorial-3">
<Tabs bind:selected={$selectedId}>
<Tab value="settings-metadata">Metadata</Tab>
<Tab value="settings-schedule">Schedule</Tab>
<Tab value="settings-same-worker">Shared Directory</Tab>

View File

@@ -35,7 +35,7 @@
let is_owner = false
</script>
<div id="flow-tutorial-4" class="flex flex-row-reverse justify-between items-center gap-x-2">
<div class="flex flex-row-reverse justify-between items-center gap-x-2">
<Button
on:click={() => {
previewMode = 'whole'

View File

@@ -133,7 +133,7 @@
<FlowConstantsItem />
</div>
<div id="flow-tutorial-1" class="flex-auto grow" bind:clientHeight={minHeight}>
<div class="flex-auto grow" bind:clientHeight={minHeight}>
<FlowGraph
insertable
scroll

View File

@@ -84,7 +84,6 @@
{#if insertable && modules && (label != 'Input' || modules.length == 0)}
<div
id="flow-tutorial-2"
class="{openMenu ? 'z-10' : ''} w-7 absolute {whereInsert == 'after'
? 'top-12'
: '-top-10'} left-[50%] right-[50%] -translate-x-1/2"

View File

@@ -341,7 +341,7 @@
undefined,
1000,
loopDepth,
0,
modules.findIndex((m) => m.id == module.id) + 1,
true,
undefined,
module.id

View File

@@ -1,3 +0,0 @@
d3 is responsible for zooming and panning.
The code works but is obviously in a bad state. However, at least it is modularized so it doesn't clutter graphview anymore.

View File

@@ -1,58 +0,0 @@
import { writable, derived, get, readable } from 'svelte/store';
export function d3ZoomCreator(
nodeSelected,
movementStore,
backgroundStore,
canvasId,
gridSize,
dotSize,
d3Scale,
d3
) {
// TODO: Update d3Zoom type (refer to d3Zoom docs)
let d3Zoom: any = d3
.zoom()
.filter(() => !get(nodeSelected))
.scaleExtent([0.4, 2])
.on('zoom', handleZoom);
return d3Zoom;
// function to handle zoom events - arguments: d3ZoomEvent
function handleZoom(this: any, e: any): void {
if (!get(movementStore)) return;
//add a store that contains the current value of the d3-zoom's scale to be used in onMouseMove function
d3Scale.set(e.transform.k);
// should not run d3.select below if backgroundStore is false
if (get(backgroundStore)) {
d3.select(`#background-${canvasId}`)
.attr('x', e.transform.x)
.attr('y', e.transform.y)
.attr('width', gridSize * e.transform.k)
.attr('height', gridSize * e.transform.k)
.selectAll('#dot')
.attr('x', (gridSize * e.transform.k) / 2 - dotSize / 2)
.attr('y', (gridSize * e.transform.k) / 2 - dotSize / 2)
.attr('opacity', Math.min(e.transform.k, 1));
}
// transform 'g' SVG elements (edge, edge text, edge anchor)
d3.select(`.Edges-${canvasId} g`).attr('transform', e.transform);
// transform div elements (nodes)
let transform = d3.zoomTransform(this);
// selects and transforms all node divs from class 'Node' and performs transformation
d3.select(`.Node-${canvasId}`)
.style(
'transform',
'translate(' +
transform.x +
'px,' +
transform.y +
'px) scale(' +
transform.k +
')'
)
.style('transform-origin', '0 0');
}
}

View File

@@ -1,145 +0,0 @@
# CHANGELOG
This article documents the changes from Svelvet5 to Svelvet6. It is for internal use.
## custom-nodes
https://www.svelvet.io/docs/custom-nodes/
Anchor points no longer display on nodes in isolation. This is because anchor points are no longer tied to the node object.
<img src="./images/custom-nodes-before.png" width="150" height="150">
<img src="./images/custom-nodes-after.png" width="150" height="150">
## custom-edges
https://www.svelvet.io/docs/custom-edges/
Edges now use adaptive anchors by default. Previously, source/target anchors were placed on the top/bottom of the node by default. It looks nice in the specific causes when target nodes are located below source nodes, but in the general case there will be intersecting edges.
<img src="./images/custom-edges-before.png" width="150" height="150">
<img src="./images/custom-edges-after.png" width="150" height="150">
## panning and zooming
https://www.svelvet.io/docs/pan-and-zoom/
Panning and zooming is functional. Removed option to stop panning, lock nodes due to store pollution. This is simple to add back if it is a community requested feature.
TODO: need to update this text in docs:
```
play around with the flow diagram below! If you wish to stop panning, set the movement prop to false. If you wish to stop node dragging, pass in the locked prop.
```
## usage with typescript
https://www.svelvet.io/docs/typescript/
Type names changed from Node, Edge ,to UserNodeType, UserEdgeType. The main reason for this is:
- The internal state of Svelvet is stored in objects Node, Edge which are different from the specifications passed in by users (hereafter referred to as UserNode, UserEdge). For example, UserNodes have "sourcePosition" and "targetPosition" parameters. However, the internal node project does not have "sourcePosition" and "targetPosition"; instead that functionality has been abstracted to a separate Anchor class. This promotes greater code modularity.
## CSS-Background
https://www.svelvet.io/docs/CSS-Background/
<img src="./images/css-background-before.png" width="150" height="150">
<img src="./images/css-background-after.png" width="150" height="150">
No visual changes. Moved background color out of store in order to minimize store pollution. This means that background color will not be serialized using the import-export-store feature. We talk more about this later in import-export-store section.
## node-grouping
https://www.svelvet.io/docs/node-grouping/
No change in functionality.
## snap-to-grid
https://www.svelvet.io/docs/snap-to-grid/
http://localhost:3000/compatability-8-snap-to-grid/
No change in functionality
## html-docs
https://www.svelvet.io/docs/snap-to-grid/
http://localhost:3000/compatability-8-snap-to-grid/
<img src="./images/html-docs-before.png" width="150" height="150">
<img src="./images/html-docs-after.png" width="150" height="150">
no change in functionality
## node-create
https://www.svelvet.io/docs/Interactive-Nodes/
http://localhost:3000/compatibility-10-node-create/
<img src="./images/node-create-before.png" width="150" height="150">
<img src="./images/node-create-after.png" width="150" height="150">
- Previously, the new node inherits from the old node. Now, the new node is created with default parameters. Note that we have functionality to edit nodes.
- edge creation works the same as before (not shown)
- there are now 4 potential anchor points, rather than one source / on target anchor point.
## custom-svelte-components
https://www.svelvet.io/docs/Custom-Svelte/
http://localhost:3000/compatibility-11-custom-svelte-components/
<img src="./images/custom-svelte-components-before.png" width="150" height="150">
<img src="./images/custom-svelte-components-after.png" width="150" height="150">
Same functionality as before. The reason why it looks different is because I created my own dummy Svelte component to test.
## Minimap
http://localhost:3000/compatibility-12-minimap/
https://www.svelvet.io/docs/Minimap/
Same functionality as before. It looks like there is a big with HTML feature (unrelated to minimap)
<img src="./images/minimap-before.png" width="150" height="150">
<img src="./images/minimap-after.png" width="150" height="150">
## Initial zoom and location
Same functionality as Svelvet5. It appears the center of the canvas is 0,0
## node classes
<img src="./images/node-classes-before.png" width="150" height="150">
<img src="./images/node-classes-after.png" width="150" height="150">
You need to put !important in the CSS
## ImportDiagrams
https://www.svelvet.io/docs/importDiagrams/
http://localhost:3000/featureImportExport/
Feature works as expected. Note that previous version of Svelvet did not serialize callbacks (there is no general way to do this since callbacks can involve closures). We do not do serialize callbacks in Svelvet6; this includes anchors which are reset to adaptive upon serialization.
As before, any canvas-wide parameters (such as nodeCreate, backgroundColor, etc.) will not be serialized.
This feature is fragile since there is no guarantee that it will work with new features involving nodes and edges. In order to future-proof this feature, it would be best to specify that import/export of diagrams only serializes certain parameters.
## Diagram Boundary
http://localhost:3000/compatibility-15-diagramBoundary/
https://www.svelvet.io/docs/boundary/
Feature works as expected
## Iteractive Editable nodes
http://localhost:3000/compatibility-16-nodeEdit/
https://www.svelvet.io/docs/editNodes/
Feature works as expected. NodeEdit is now active by default. This is to reduce store pollution. In addition, node edit chaning width/height of nodes has been removed because this functionality is duplicated by resizableNodes
## Deletable nodes
This feature has been removed to reduce feature bloat. Its functionality is duplicated by Interactive Editable nodes feature.

Some files were not shown because too many files have changed in this diff Show More