Compare commits
26 Commits
tutorials
...
app-compon
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3373edddf | ||
|
|
71da2f67e6 | ||
|
|
75f87e7e11 | ||
|
|
3e5a179eb8 | ||
|
|
c082c6350e | ||
|
|
cfd489a550 | ||
|
|
1f4ae53fb4 | ||
|
|
0f4da49886 | ||
|
|
ae81d7ee2c | ||
|
|
0dcbf270da | ||
|
|
82c139ed09 | ||
|
|
98f8b845a6 | ||
|
|
f521fb63fa | ||
|
|
fce464434b | ||
|
|
ee18804c87 | ||
|
|
0789bef120 | ||
|
|
1a7dc0a3bd | ||
|
|
ce323709a9 | ||
|
|
61a5e1f1ac | ||
|
|
3b44f9a72c | ||
|
|
b349308ff7 | ||
|
|
6b5963f9d2 | ||
|
|
f87b722a21 | ||
|
|
d00a80a8f6 | ||
|
|
0e9be7f300 | ||
|
|
8681e83b57 |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -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
84
backend/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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("*");
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
30
cli/apps.ts
30
cli/apps.ts
@@ -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 }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
12
cli/flow.ts
12
cli/flow.ts
@@ -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 }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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 }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
29
cli/sync.ts
29
cli/sync.ts
@@ -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);
|
||||
|
||||
@@ -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 }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
59
frontend/package-lock.json
generated
59
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
77
frontend/src/global.d.ts
vendored
77
frontend/src/global.d.ts
vendored
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"> </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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
43
frontend/src/lib/components/SettingSection.svelte
Normal file
43
frontend/src/lib/components/SettingSection.svelte
Normal 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}
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,5 +18,7 @@
|
||||
data={faInfoCircle}
|
||||
{scale}
|
||||
/>
|
||||
<span slot="text"> <slot /> </span>
|
||||
<svelte:fragment slot="text">
|
||||
<slot />
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
<InputValue
|
||||
{id}
|
||||
input={configuration.disabled}
|
||||
extraContext={extraQueryParams}
|
||||
bind:value={disabled}
|
||||
bind:error={errors.disabled}
|
||||
/>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
: []
|
||||
if (defaultValue) {
|
||||
value = JSON.stringify(defaultValue)
|
||||
} else if (listItems.length > 0) {
|
||||
value = listItems[0]?.value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 for 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 for 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -32,6 +32,7 @@ const inputs: ComponentSet = {
|
||||
'fileinputcomponent',
|
||||
'checkboxcomponent',
|
||||
'selectcomponent',
|
||||
'resourceselectcomponent',
|
||||
'multiselectcomponent',
|
||||
]
|
||||
} as const
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
|
||||
@@ -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' ||
|
||||
|
||||
@@ -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
|
||||
<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>←</Kbd><Kbd
|
||||
>↑</Kbd
|
||||
><Kbd>→</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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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' }>
|
||||
|
||||
196
frontend/src/lib/components/apps/svelte-grid/Grid.svelte
Normal file
196
frontend/src/lib/components/apps/svelte-grid/Grid.svelte
Normal 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>
|
||||
23
frontend/src/lib/components/apps/svelte-grid/LICENSE
Normal file
23
frontend/src/lib/components/apps/svelte-grid/LICENSE
Normal 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.
|
||||
404
frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte
Normal file
404
frontend/src/lib/components/apps/svelte-grid/MoveResize.svelte
Normal 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>
|
||||
25
frontend/src/lib/components/apps/svelte-grid/types.d.ts
vendored
Normal file
25
frontend/src/lib/components/apps/svelte-grid/types.d.ts
vendored
Normal 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 }
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { getRowsCount } from "./other";
|
||||
|
||||
export function getContainerHeight(items, yPerPx, cols) {
|
||||
return getRowsCount(items, cols) * yPerPx;
|
||||
}
|
||||
43
frontend/src/lib/components/apps/svelte-grid/utils/helper.ts
Normal file
43
frontend/src/lib/components/apps/svelte-grid/utils/helper.ts
Normal 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;
|
||||
260
frontend/src/lib/components/apps/svelte-grid/utils/item.ts
Normal file
260
frontend/src/lib/components/apps/svelte-grid/utils/item.ts
Normal 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
|
||||
}
|
||||
65
frontend/src/lib/components/apps/svelte-grid/utils/matrix.ts
Normal file
65
frontend/src/lib/components/apps/svelte-grid/utils/matrix.ts
Normal 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);
|
||||
}
|
||||
35
frontend/src/lib/components/apps/svelte-grid/utils/other.ts
Normal file
35
frontend/src/lib/components/apps/svelte-grid/utils/other.ts
Normal 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];
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
18
frontend/src/lib/components/common/CloseButton.svelte
Normal file
18
frontend/src/lib/components/common/CloseButton.svelte
Normal 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>
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
}
|
||||
show = !show
|
||||
}}
|
||||
class="relative cursor-pointer hover:bg-gray-100/30"
|
||||
class="relative"
|
||||
>
|
||||
<slot class="triggerable" name="trigger" />
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
)}
|
||||
style={`${style} ${$selected?.startsWith(value) ? selectedStyle : ''}`}
|
||||
on:click={() => update(value)}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -341,7 +341,7 @@
|
||||
undefined,
|
||||
1000,
|
||||
loopDepth,
|
||||
0,
|
||||
modules.findIndex((m) => m.id == module.id) + 1,
|
||||
true,
|
||||
undefined,
|
||||
module.id
|
||||
|
||||
@@ -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.
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user