Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b76426ba83 | ||
|
|
1cb2a177c6 | ||
|
|
db87577a1b | ||
|
|
c219eb0ee9 | ||
|
|
bfe5b56c99 | ||
|
|
4453690521 |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,23 +1,6 @@
|
||||
# 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.27"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549"
|
||||
checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -1138,9 +1138,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.27"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac"
|
||||
checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -1148,15 +1148,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.27"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd"
|
||||
checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.27"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83"
|
||||
checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
@@ -1176,9 +1176,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.27"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91"
|
||||
checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
@@ -1197,9 +1197,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.27"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6"
|
||||
checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1208,21 +1208,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.27"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2"
|
||||
checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.27"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879"
|
||||
checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.27"
|
||||
version = "0.3.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab"
|
||||
checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -1503,9 +1503,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.25"
|
||||
version = "0.14.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899"
|
||||
checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
@@ -2953,7 +2953,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-ast"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/RustPython/RustPython#415cdb1ef9b832fbca858a05f6894087e133dd31"
|
||||
source = "git+https://github.com/RustPython/RustPython#87728c44527272ece739cddbb4942ad7e176dd79"
|
||||
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#415cdb1ef9b832fbca858a05f6894087e133dd31"
|
||||
source = "git+https://github.com/RustPython/RustPython#87728c44527272ece739cddbb4942ad7e176dd79"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bstr",
|
||||
@@ -2975,7 +2975,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-parser"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/RustPython/RustPython#415cdb1ef9b832fbca858a05f6894087e133dd31"
|
||||
source = "git+https://github.com/RustPython/RustPython#87728c44527272ece739cddbb4942ad7e176dd79"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
@@ -3667,9 +3667,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "swc_common"
|
||||
version = "0.29.37"
|
||||
version = "0.29.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5005cd73617e18592faa31298225b26f1c407b84a681d67efb735c3d3458e101"
|
||||
checksum = "2d515be281f603cb97afaa89896aca5e5c748fde11c7f926e35cdaa8ff8da705"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"ast_node",
|
||||
@@ -4052,9 +4052,9 @@ checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.5"
|
||||
version = "0.19.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7082a95d48029677a28f181e5f6422d0c8339ad8396a39d3f33d62a90c1f6c30"
|
||||
checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825"
|
||||
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#d1cae0f7d5115782c9557a46396dae786ad5f592"
|
||||
source = "git+https://github.com/oxidecomputer/typify#18e7faf1957812626e3bf459e20fbe16785de2d4"
|
||||
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#d1cae0f7d5115782c9557a46396dae786ad5f592"
|
||||
source = "git+https://github.com/oxidecomputer/typify#18e7faf1957812626e3bf459e20fbe16785de2d4"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"log",
|
||||
@@ -4301,7 +4301,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "typify-macro"
|
||||
version = "0.0.11-dev"
|
||||
source = "git+https://github.com/oxidecomputer/typify#d1cae0f7d5115782c9557a46396dae786ad5f592"
|
||||
source = "git+https://github.com/oxidecomputer/typify#18e7faf1957812626e3bf459e20fbe16785de2d4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4742,7 +4742,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.75.0"
|
||||
version = "1.74.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -4769,7 +4769,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.75.0"
|
||||
version = "1.74.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -4824,7 +4824,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.75.0"
|
||||
version = "1.74.2"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"chrono",
|
||||
@@ -4839,7 +4839,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.75.0"
|
||||
version = "1.74.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
@@ -4852,7 +4852,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.75.0"
|
||||
version = "1.74.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -4877,7 +4877,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.75.0"
|
||||
version = "1.74.2"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4885,7 +4885,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.75.0"
|
||||
version = "1.74.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
@@ -4900,7 +4900,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.75.0"
|
||||
version = "1.74.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
@@ -4912,7 +4912,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.75.0"
|
||||
version = "1.74.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
@@ -4927,7 +4927,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.75.0"
|
||||
version = "1.74.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deno_core",
|
||||
@@ -4941,7 +4941,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.75.0"
|
||||
version = "1.74.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -4964,7 +4964,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.75.0"
|
||||
version = "1.74.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.75.0"
|
||||
version = "1.74.2"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -19,7 +19,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.75.0"
|
||||
version = "1.74.2"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -2984,6 +2984,20 @@
|
||||
},
|
||||
"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": [
|
||||
@@ -3154,21 +3168,6 @@
|
||||
},
|
||||
"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": [
|
||||
@@ -4698,27 +4697,6 @@
|
||||
},
|
||||
"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.75.0
|
||||
version: 1.74.2
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -3168,14 +3168,12 @@ paths:
|
||||
- $ref: "#/components/parameters/ScriptExactPath"
|
||||
- $ref: "#/components/parameters/ScriptStartPath"
|
||||
- $ref: "#/components/parameters/ScriptExactHash"
|
||||
- $ref: "#/components/parameters/StartedBefore"
|
||||
- $ref: "#/components/parameters/StartedAfter"
|
||||
- $ref: "#/components/parameters/CreatedBefore"
|
||||
- $ref: "#/components/parameters/CreatedAfter"
|
||||
- $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
|
||||
@@ -3200,12 +3198,10 @@ paths:
|
||||
- $ref: "#/components/parameters/ScriptExactPath"
|
||||
- $ref: "#/components/parameters/ScriptStartPath"
|
||||
- $ref: "#/components/parameters/ScriptExactHash"
|
||||
- $ref: "#/components/parameters/StartedBefore"
|
||||
- $ref: "#/components/parameters/StartedAfter"
|
||||
- $ref: "#/components/parameters/CreatedBefore"
|
||||
- $ref: "#/components/parameters/CreatedAfter"
|
||||
- $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
|
||||
@@ -3239,11 +3235,9 @@ paths:
|
||||
- $ref: "#/components/parameters/ScriptExactPath"
|
||||
- $ref: "#/components/parameters/ScriptStartPath"
|
||||
- $ref: "#/components/parameters/ScriptExactHash"
|
||||
- $ref: "#/components/parameters/StartedBefore"
|
||||
- $ref: "#/components/parameters/StartedAfter"
|
||||
- $ref: "#/components/parameters/CreatedBefore"
|
||||
- $ref: "#/components/parameters/CreatedAfter"
|
||||
- $ref: "#/components/parameters/JobKinds"
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- $ref: "#/components/parameters/ResultFilter"
|
||||
- name: is_skipped
|
||||
description: is the job skipped
|
||||
in: query
|
||||
@@ -4620,15 +4614,15 @@ components:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
StartedBefore:
|
||||
name: started_before
|
||||
CreatedBefore:
|
||||
name: created_before
|
||||
description: filter on created before (inclusive) timestamp
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
StartedAfter:
|
||||
name: started_after
|
||||
CreatedAfter:
|
||||
name: created_after
|
||||
description: filter on created after (exclusive) timestamp
|
||||
in: query
|
||||
schema:
|
||||
@@ -4652,19 +4646,6 @@ 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,9 +16,10 @@ use axum::{
|
||||
routing::{delete, get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use windmill_audit::{audit_log, ActionKind};
|
||||
use windmill_common::{
|
||||
error::{self, to_anyhow, Error, JsonResult, Result},
|
||||
error::{self, Error, JsonResult, Result},
|
||||
users::username_to_permissioned_as,
|
||||
utils::{not_found_if_none, paginate, Pagination},
|
||||
};
|
||||
@@ -261,26 +262,13 @@ 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", "?".bind(&display_name));
|
||||
sqlb.set("display_name", display_name);
|
||||
}
|
||||
if let Some(owners) = ng.owners {
|
||||
sqlb.set(
|
||||
"owners",
|
||||
"?".bind(&format!(
|
||||
"{{{}}}",
|
||||
owners
|
||||
.iter()
|
||||
.map(|x| format!("\"{x}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
)),
|
||||
);
|
||||
sqlb.set_str("owners", format!("{{{}}}", owners.into_iter().join(",")));
|
||||
}
|
||||
if let Some(extra_perms) = ng.extra_perms {
|
||||
sqlb.set(
|
||||
"extra_perms",
|
||||
"?".bind(&serde_json::to_string(&extra_perms).map_err(to_anyhow)?),
|
||||
);
|
||||
sqlb.set_str("extra_perms", extra_perms.to_string());
|
||||
}
|
||||
|
||||
sqlb.returning("*");
|
||||
|
||||
@@ -327,15 +327,13 @@ pub struct ListQueueQuery {
|
||||
pub script_path_exact: Option<String>,
|
||||
pub script_hash: Option<String>,
|
||||
pub created_by: Option<String>,
|
||||
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_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 {
|
||||
@@ -364,11 +362,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.started_before {
|
||||
sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
if let Some(dt) = &lq.created_before {
|
||||
sqlb.and_where_lt("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(dt) = &lq.created_after {
|
||||
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
}
|
||||
|
||||
if let Some(s) = &lq.suspended {
|
||||
@@ -378,7 +376,6 @@ 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",
|
||||
@@ -386,10 +383,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -462,14 +455,13 @@ async fn list_jobs(
|
||||
script_path_exact: lq.script_path_exact,
|
||||
script_hash: lq.script_hash,
|
||||
created_by: lq.created_by,
|
||||
started_before: lq.started_before,
|
||||
started_after: lq.started_after,
|
||||
created_before: lq.created_before,
|
||||
created_after: lq.created_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",
|
||||
@@ -1144,14 +1136,14 @@ 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,
|
||||
@@ -1695,11 +1687,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.started_before {
|
||||
sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
if let Some(dt) = &lq.created_before {
|
||||
sqlb.and_where_lt("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(dt) = &lq.created_after {
|
||||
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
}
|
||||
if let Some(sk) = &lq.is_skipped {
|
||||
sqlb.and_where_eq("is_skipped", sk);
|
||||
@@ -1714,15 +1706,6 @@ 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)]
|
||||
@@ -1731,8 +1714,8 @@ pub struct ListCompletedQuery {
|
||||
pub script_path_exact: Option<String>,
|
||||
pub script_hash: Option<String>,
|
||||
pub created_by: Option<String>,
|
||||
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub success: Option<bool>,
|
||||
pub parent_job: Option<String>,
|
||||
pub order_desc: Option<bool>,
|
||||
@@ -1740,10 +1723,6 @@ 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,7 +26,6 @@ use axum::{
|
||||
Json, Router,
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
use serde_json::Value;
|
||||
use windmill_audit::{audit_log, ActionKind};
|
||||
use windmill_common::{
|
||||
error::{Error, JsonResult, Result},
|
||||
@@ -379,26 +378,9 @@ 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, value = $2 WHERE path = $3 AND workspace_id = $4",
|
||||
"UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3",
|
||||
npath,
|
||||
v,
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
@@ -437,23 +419,6 @@ 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,7 +1073,6 @@ struct ScriptMetadata {
|
||||
schema: Option<Schema>,
|
||||
is_template: bool,
|
||||
lock: Vec<String>,
|
||||
kind: String,
|
||||
}
|
||||
|
||||
enum ArchiveImpl {
|
||||
@@ -1236,7 +1235,6 @@ 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::{self, Display},
|
||||
fmt::Display,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
@@ -103,18 +103,6 @@ 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,18 +28,14 @@ export class AppFile implements Resource, PushDiffs {
|
||||
remotePath: string,
|
||||
diffs: Difference[],
|
||||
): Promise<void> {
|
||||
let app: AppWithLastVersion | undefined = undefined;
|
||||
try {
|
||||
app = await AppService.getAppByPath({ workspace, path: remotePath });
|
||||
} catch (e) {}
|
||||
|
||||
if (app) {
|
||||
if (await AppService.existsApp({ workspace, path: remotePath })) {
|
||||
console.log(
|
||||
colors.bold.yellow(
|
||||
`Applying ${diffs.length} diffs to existing app... ${remotePath}`,
|
||||
`Applying ${diffs.length} diffs to existing app...`,
|
||||
),
|
||||
);
|
||||
const changeset: {
|
||||
path?: string | undefined;
|
||||
summary?: string | undefined;
|
||||
value?: any;
|
||||
policy?: Policy | undefined;
|
||||
@@ -50,7 +46,7 @@ export class AppFile implements Resource, PushDiffs {
|
||||
(
|
||||
diff.path[0] !== "value" && diff.path[0] !== "policy" && (
|
||||
diff.path.length !== 1 ||
|
||||
!["summary"].includes(
|
||||
!["path", "summary"].includes(
|
||||
diff.path[0] as string,
|
||||
)
|
||||
)
|
||||
@@ -65,13 +61,6 @@ 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"
|
||||
);
|
||||
@@ -98,10 +87,19 @@ 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({}, this, { cyclesFix: false }),
|
||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
12
cli/flow.ts
12
cli/flow.ts
@@ -94,7 +94,6 @@ export class FlowFile implements Resource, PushDiffs {
|
||||
...changeset,
|
||||
...base_changeset,
|
||||
}
|
||||
|
||||
await FlowService.updateFlow({
|
||||
workspace: workspace,
|
||||
path: remotePath,
|
||||
@@ -115,11 +114,20 @@ 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({}, this, { cyclesFix: false }),
|
||||
microdiff(remote ?? {}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { colors, Command, FolderService, microdiff } from "./deps.ts";
|
||||
import { colors, Command, Folder, FolderService, microdiff } from "./deps.ts";
|
||||
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
||||
import {
|
||||
Difference,
|
||||
@@ -22,9 +22,7 @@ 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);
|
||||
@@ -33,10 +31,16 @@ 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({}, this, { cyclesFix: false }),
|
||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,24 +66,22 @@ export class FolderFile implements Resource, PushDiffs {
|
||||
if (exists) {
|
||||
console.log(
|
||||
colors.bold.yellow(
|
||||
`Applying ${diffs.length} diffs to existing folder... ${remotePath}`,
|
||||
`Applying ${diffs.length} diffs to existing folder...`,
|
||||
),
|
||||
);
|
||||
|
||||
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", "display_name"].includes(diff.path[0] as string)
|
||||
!["owners", "extra_perms"].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") {
|
||||
@@ -95,10 +97,11 @@ export class FolderFile implements Resource, PushDiffs {
|
||||
if (!hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
await FolderService.updateFolder({
|
||||
workspace: workspace,
|
||||
name: remotePath,
|
||||
requestBody: {...changeset, extra_perms: changeset.extra_perms ? Object.fromEntries(this.extra_perms?.entries() ?? []) : undefined}
|
||||
requestBody: changeset,
|
||||
});
|
||||
} 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.75.0";
|
||||
const VERSION = "v1.74.2";
|
||||
|
||||
let command: any = new Command()
|
||||
.name("wmill")
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
EditResourceType,
|
||||
microdiff,
|
||||
ResourceService,
|
||||
ResourceType,
|
||||
Table,
|
||||
} from "./deps.ts";
|
||||
import { Any, decoverto, model, property } from "./decoverto.ts";
|
||||
@@ -25,10 +26,19 @@ export class ResourceTypeFile implements ResourceI, PushDiffs {
|
||||
description?: string;
|
||||
|
||||
async push(workspace: string, remotePath: string): Promise<void> {
|
||||
await this.pushDiffs(
|
||||
let existing: ResourceType | undefined;
|
||||
try {
|
||||
existing = await ResourceService.getResourceType({
|
||||
workspace,
|
||||
path: remotePath,
|
||||
});
|
||||
} catch {
|
||||
existing = undefined;
|
||||
}
|
||||
this.pushDiffs(
|
||||
workspace,
|
||||
remotePath,
|
||||
microdiff({}, this, { cyclesFix: false }),
|
||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,8 +65,8 @@ export class ResourceTypeFile implements ResourceI, PushDiffs {
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
colors.yellow.bold(
|
||||
`Applying ${diffs.length} diffs to existing resource type... ${remotePath}`,
|
||||
colors.yellow(
|
||||
`Applying ${diffs.length} diffs to existing resource type...`,
|
||||
),
|
||||
);
|
||||
const changeset: EditResourceType = {};
|
||||
|
||||
@@ -43,7 +43,7 @@ export class ResourceFile implements Resource2, PushDiffs {
|
||||
})
|
||||
) {
|
||||
console.log(
|
||||
colors.yellow.bold(`Applying ${diffs.length} diffs to existing resource... ${remotePath}`),
|
||||
colors.yellow(`Applying ${diffs.length} diffs to existing resource...`),
|
||||
);
|
||||
|
||||
const changeset: EditResource = {
|
||||
@@ -51,7 +51,11 @@ export class ResourceFile implements Resource2, PushDiffs {
|
||||
};
|
||||
for (const diff of diffs) {
|
||||
if (diff.path[0] === "is_oauth") {
|
||||
//is_oauth is not updatable
|
||||
console.log(
|
||||
colors.yellow(
|
||||
"! is_oauth has been removed in newer versions. Ignoring.",
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
@@ -60,10 +64,9 @@ 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") {
|
||||
@@ -107,10 +110,19 @@ 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({}, this, { cyclesFix: false }),
|
||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ export class ScriptFile {
|
||||
},
|
||||
toPlain: (data) => data,
|
||||
})
|
||||
@property(() => String)
|
||||
kind?: "script" | "failure" | "trigger" | "command" | "approval";
|
||||
|
||||
constructor(summary: string, description: string) {
|
||||
@@ -106,21 +105,11 @@ export async function handleFile(path: string, content: string, workspace: strin
|
||||
} catch { }
|
||||
const language = inferContentTypeFromFilePath(path);
|
||||
|
||||
let remote = undefined
|
||||
try {
|
||||
remote = await ScriptService.getScriptByPath({
|
||||
const 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: {
|
||||
@@ -134,11 +123,10 @@ 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}`))
|
||||
} else {
|
||||
} catch {
|
||||
// no parent hash
|
||||
await ScriptService.createScript({
|
||||
workspace: workspace,
|
||||
@@ -156,6 +144,7 @@ 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@v1.5.0/mod.ts";
|
||||
import { diffCharacters } from "https://deno.land/x/diff@v0.3.5/mod.ts";
|
||||
import { equal } from "https://deno.land/x/equal/mod.ts";
|
||||
import { diffCharacters } from "https://deno.land/x/diff/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 | undefined,
|
||||
els1: DynFSElement, els2: DynFSElement,
|
||||
ignore: (path: string, isDirectory: boolean) => boolean,
|
||||
raw: boolean
|
||||
): Promise<Change[]> {
|
||||
|
||||
const [m1, m2] = els2
|
||||
? await Promise.all([elementsToMap(els1, ignore), elementsToMap(els2, ignore)])
|
||||
: [await elementsToMap(els1, ignore), {}];
|
||||
const [m1, m2] = raw ? [await elementsToMap(els1, ignore), {}] :
|
||||
await Promise.all([elementsToMap(els1, ignore), elementsToMap(els2, 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 = opts.raw ? undefined : await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill"))
|
||||
const changes = await compareDynFSElement(remote, local, await ignoreF())
|
||||
const local = await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill"))
|
||||
const changes = await compareDynFSElement(remote, local, await ignoreF(), opts.raw)
|
||||
|
||||
|
||||
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 && currentLocal !== change.after) {
|
||||
if (currentLocal !== change.before) {
|
||||
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,6 +459,7 @@ 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."))
|
||||
@@ -473,9 +474,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 = opts.raw ? undefined : ZipFSElement((await downloadZip(workspace))!)
|
||||
const remote = ZipFSElement((await downloadZip(workspace))!)
|
||||
const local = await FSFSElement(path.join(Deno.cwd(), ""))
|
||||
const changes = await compareDynFSElement(local, remote, await ignoreF())
|
||||
const changes = await compareDynFSElement(local, remote, await ignoreF(), opts.raw)
|
||||
|
||||
console.log(`remote <- local: ${changes.length} changes to apply`);
|
||||
if (changes.length > 0) {
|
||||
@@ -574,7 +575,7 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull
|
||||
break;
|
||||
}
|
||||
try {
|
||||
await Deno.remove(stateTarget)
|
||||
Deno.remove(stateTarget)
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
@@ -615,7 +616,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(JSON.stringify(e))
|
||||
console.error(e.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -634,7 +635,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 (you have pulled prior)")
|
||||
.option("--skip-pull", "Push without pulling first")
|
||||
.option("--yes", "Push without needing confirmation")
|
||||
.option("--raw", "Push without using state, just overwrite.")
|
||||
.action(push as any);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
colors,
|
||||
Command,
|
||||
EditVariable,
|
||||
ListableVariable,
|
||||
microdiff,
|
||||
Table,
|
||||
VariableService,
|
||||
@@ -66,7 +67,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... ${remotePath}`,
|
||||
`Applying ${diffs.length} diffs to existing variable...`,
|
||||
),
|
||||
);
|
||||
const changeset: EditVariable = {};
|
||||
@@ -75,12 +76,11 @@ export class VariableFile implements Resource, PushDiffs {
|
||||
diff.type !== "REMOVE" &&
|
||||
(
|
||||
diff.path.length !== 1 ||
|
||||
!["path", "value", "is_secret", "description", "account", "is_oauth"].includes(
|
||||
!["path", "value", "is_secret", "description"].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,7 +96,6 @@ export class VariableFile implements Resource, PushDiffs {
|
||||
if (!hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
await VariableService.updateVariable({
|
||||
workspace,
|
||||
path: remotePath,
|
||||
@@ -104,6 +103,7 @@ export class VariableFile implements Resource, PushDiffs {
|
||||
requestBody: changeset,
|
||||
});
|
||||
|
||||
console.log(changeset);
|
||||
} else {
|
||||
console.log(colors.yellow.bold("Creating new variable..."));
|
||||
await VariableService.createVariable({
|
||||
@@ -121,10 +121,19 @@ 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({}, this, { cyclesFix: false }),
|
||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
46
frontend/package-lock.json
generated
46
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill",
|
||||
"version": "1.75.0",
|
||||
"version": "1.74.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill",
|
||||
"version": "1.75.0",
|
||||
"version": "1.74.2",
|
||||
"dependencies": {
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
@@ -64,6 +64,7 @@
|
||||
"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",
|
||||
@@ -6653,6 +6654,24 @@
|
||||
"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",
|
||||
@@ -6739,6 +6758,12 @@
|
||||
"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",
|
||||
@@ -12989,6 +13014,17 @@
|
||||
"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",
|
||||
@@ -13049,6 +13085,12 @@
|
||||
"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.75.0",
|
||||
"version": "1.74.2",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
@@ -43,6 +43,7 @@
|
||||
"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",
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
$: key =
|
||||
apiTokenApps[resource_type]?.key ??
|
||||
(args != undefined
|
||||
? Object.keys(args).filter((x) => ['token', 'password', 'api_key', 'key'].includes(x))[0]
|
||||
? Object.keys(args).filter((x) => ['token', 'password', 'api_key'].includes(x))[0]
|
||||
: undefined)
|
||||
|
||||
let scopes: string[] = []
|
||||
@@ -259,7 +259,6 @@
|
||||
args['token'] == '' &&
|
||||
args['password'] == '' &&
|
||||
args['api_key'] == '' &&
|
||||
args['key'] == '' &&
|
||||
key != undefined) ||
|
||||
(step == 3 && pathError != '') ||
|
||||
!isValid
|
||||
@@ -397,7 +396,7 @@
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
initialPath=""
|
||||
namePlaceholder={resource_type}
|
||||
namePlaceholder="{resource_type}"
|
||||
kind="resource"
|
||||
/>
|
||||
|
||||
@@ -434,7 +433,7 @@
|
||||
{:else}
|
||||
<Path
|
||||
initialPath=""
|
||||
namePlaceholder={resource_type}
|
||||
namePlaceholder="{resource_type}"
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
kind="resource"
|
||||
|
||||
@@ -232,8 +232,14 @@
|
||||
<Badge color={validCode ? 'green' : 'red'} class="min-w-[60px] mr-3">
|
||||
{validCode ? 'Valid' : 'Invalid'}
|
||||
</Badge>
|
||||
<div class="flex items-center divide-x">
|
||||
<Popover notClickable placement="bottom" disapperTimoout={0} class="pr-1" disablePopup={!iconOnly}>
|
||||
<div id="script-tutorial-3" 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"
|
||||
@@ -245,11 +251,15 @@
|
||||
>
|
||||
+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"
|
||||
@@ -261,11 +271,15 @@
|
||||
>
|
||||
+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"
|
||||
@@ -277,11 +291,15 @@
|
||||
>
|
||||
+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"
|
||||
@@ -293,11 +311,15 @@
|
||||
>
|
||||
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"
|
||||
@@ -320,11 +342,15 @@
|
||||
{/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"
|
||||
@@ -342,7 +368,13 @@
|
||||
</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"
|
||||
@@ -354,9 +386,7 @@
|
||||
>
|
||||
Script
|
||||
</Button>
|
||||
<svelte:fragment slot="text">
|
||||
Script
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="text">Script</svelte:fragment>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
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
|
||||
@@ -27,6 +28,7 @@
|
||||
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
|
||||
@@ -246,6 +248,10 @@
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
{#if tour}
|
||||
<Tour tutorial="flow" />
|
||||
{/if}
|
||||
|
||||
{#if !$userStore?.operator}
|
||||
<ScriptEditorDrawer bind:this={$scriptEditorDrawer} />
|
||||
|
||||
|
||||
@@ -234,14 +234,6 @@
|
||||
name,
|
||||
requestBody: { username: member_name }
|
||||
})
|
||||
await GranularAclService.removeGranularAcls({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: name,
|
||||
kind: 'group_',
|
||||
requestBody: {
|
||||
owner: 'u/' + member_name
|
||||
}
|
||||
})
|
||||
loadGroup()
|
||||
}}>remove</button
|
||||
>
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
}}
|
||||
items={collection}
|
||||
class="text-clip grow min-w-0"
|
||||
placeholder="{resourceType ?? 'any'} resource"
|
||||
placeholder="{resourceType} 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.started_at as any,
|
||||
x: job.created_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.started_at as any,
|
||||
x: job.created_at as any,
|
||||
y: job.duration_ms,
|
||||
id: job.id,
|
||||
path: job.script_path
|
||||
@@ -71,10 +71,7 @@
|
||||
enabled: true,
|
||||
modifierKey: 'ctrl' as 'ctrl',
|
||||
onPanComplete: ({ chart }) => {
|
||||
dispatch('zoom', {
|
||||
min: addSeconds(new Date(chart.scales.x.min), -1),
|
||||
max: addSeconds(new Date(chart.scales.x.max), 1)
|
||||
})
|
||||
dispatch('zoom', { min: new Date(chart.scales.x.min), max: new Date(chart.scales.x.max) })
|
||||
}
|
||||
},
|
||||
zoom: {
|
||||
@@ -83,10 +80,7 @@
|
||||
},
|
||||
mode: 'x' as 'x',
|
||||
onZoom: ({ chart }) => {
|
||||
dispatch('zoom', {
|
||||
min: addSeconds(new Date(chart.scales.x.min), -1),
|
||||
max: addSeconds(new Date(chart.scales.x.max), 1)
|
||||
})
|
||||
dispatch('zoom', { min: new Date(chart.scales.x.min), max: new Date(chart.scales.x.max) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,22 +88,6 @@
|
||||
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
|
||||
@@ -137,8 +115,7 @@
|
||||
display: false
|
||||
},
|
||||
type: 'time',
|
||||
min: minTime,
|
||||
max: maxTime
|
||||
min: jobs?.[jobs?.length - 1]?.created_at ?? new Date().toString()
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
let summaryC: HTMLInputElement | undefined = undefined
|
||||
let pathC: Path | undefined = undefined
|
||||
|
||||
$: setQueryWithoutLoad($page.url, [{ key: 'state', value: encodeState(script) }])
|
||||
$: setQueryWithoutLoad($page.url, 'state', encodeState(script))
|
||||
$: step = Number($page.url.searchParams.get('step')) || 1
|
||||
|
||||
if (script.content == '') {
|
||||
@@ -346,6 +346,7 @@
|
||||
lang={script.language}
|
||||
{initialArgs}
|
||||
{kind}
|
||||
tour
|
||||
/>
|
||||
{:else if step === 3}
|
||||
<CenteredPage>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
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()
|
||||
@@ -27,6 +28,7 @@
|
||||
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 }
|
||||
|
||||
@@ -114,6 +116,10 @@
|
||||
|
||||
<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
|
||||
@@ -146,6 +152,7 @@
|
||||
<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)
|
||||
@@ -180,7 +187,7 @@
|
||||
</Pane>
|
||||
<Pane size={40} minSize={10}>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="px-2 w-full border-b py-1">
|
||||
<div id="script-tutorial-4" class="px-2 w-full border-b py-1">
|
||||
{#if testIsLoading}
|
||||
<Button on:click={testJobLoader?.cancelJob} btnClasses="w-full" color="red" size="xs">
|
||||
<WindmillIcon
|
||||
@@ -211,7 +218,7 @@
|
||||
</div>
|
||||
<Splitpanes horizontal class="!max-h-[calc(100%-43px)]">
|
||||
<Pane size={33}>
|
||||
<div class="px-2">
|
||||
<div id="script-tutorial-2" class="px-2">
|
||||
<div class="break-words relative font-sans">
|
||||
<SchemaForm compact {schema} bind:args bind:isValid />
|
||||
</div>
|
||||
|
||||
@@ -2,18 +2,13 @@
|
||||
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
|
||||
|
||||
@@ -43,8 +43,6 @@
|
||||
: []
|
||||
if (defaultValue) {
|
||||
value = JSON.stringify(defaultValue)
|
||||
} else if (listItems.length > 0) {
|
||||
value = listItems[0]?.value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
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'
|
||||
@@ -47,6 +48,7 @@
|
||||
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)
|
||||
@@ -152,6 +154,10 @@
|
||||
|
||||
<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"
|
||||
@@ -196,6 +202,7 @@
|
||||
$selectedComponent = undefined
|
||||
$focusedGrid = undefined
|
||||
}}
|
||||
id="app-tutorial-3"
|
||||
class={twMerge(
|
||||
'bg-gray-100 h-full w-full',
|
||||
$appStore.css?.['app']?.['viewer']?.class
|
||||
@@ -221,7 +228,7 @@
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={$connectingInput?.opened ? 0 : 30}>
|
||||
<div class="relative h-full w-full">
|
||||
<div id="app-tutorial-4" class="relative h-full w-full">
|
||||
<InlineScriptsPanel />
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
@@ -220,23 +220,18 @@
|
||||
return
|
||||
}
|
||||
loading.save = true
|
||||
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
|
||||
}
|
||||
await computeTriggerables()
|
||||
await AppService.updateApp({
|
||||
workspace: $workspaceStore!,
|
||||
path: $page.params.path,
|
||||
requestBody: {
|
||||
value: $app!,
|
||||
summary: $summary,
|
||||
policy
|
||||
}
|
||||
})
|
||||
loading.save = false
|
||||
sendUserToast('App saved')
|
||||
}
|
||||
|
||||
let selectedJobId: string | undefined = undefined
|
||||
@@ -476,8 +471,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 && $history?.index === $history.history.length - 1 }}
|
||||
undoProps={{ disabled: $history.index === 0 }}
|
||||
redoProps={{ disabled: $history.index === $history.history.length - 1 }}
|
||||
on:undo={() => {
|
||||
$app = undo(history, $app)
|
||||
}}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import { buildWorld, type World } from '../rx'
|
||||
import type {
|
||||
App,
|
||||
AppEditorContext,
|
||||
AppViewerContext,
|
||||
ConnectingInput,
|
||||
EditorBreakpoint,
|
||||
@@ -65,12 +64,6 @@
|
||||
parentWidth: writable(0)
|
||||
})
|
||||
|
||||
setContext<AppEditorContext>('AppEditorContext', {
|
||||
history: undefined,
|
||||
componentControl: writable({}),
|
||||
pickVariableCallback: writable(undefined)
|
||||
})
|
||||
|
||||
let mounted = false
|
||||
|
||||
onMount(() => {
|
||||
|
||||
@@ -56,15 +56,15 @@
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
title="Expand"
|
||||
title="Position locking"
|
||||
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-[4.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-[0.5rem] z-50 cursor-pointer',
|
||||
' hover:bg-gray-300',
|
||||
selected ? 'bg-gray-200/80' : 'bg-gray-200/80'
|
||||
)}
|
||||
on:click={() => dispatch('expand')}
|
||||
>
|
||||
<Expand aria-label="Expand position" size={14} />
|
||||
<Expand aria-label="Lock position" size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -73,7 +73,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-[0.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-[4.5rem] z-50 cursor-move',
|
||||
'bg-gray-200/80'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getContext, afterUpdate, setContext } from 'svelte'
|
||||
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'
|
||||
@@ -11,7 +11,7 @@
|
||||
import HiddenComponent from '../components/helpers/HiddenComponent.svelte'
|
||||
import Component from './component/Component.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { push, type History } from '$lib/history'
|
||||
import { push } from '$lib/history'
|
||||
import { expandGriditem, findGridItem } from './appUtils'
|
||||
|
||||
export let policy: Policy
|
||||
@@ -108,17 +108,13 @@
|
||||
let pointerdown = false
|
||||
let lastapp: App | undefined = undefined
|
||||
const onpointerdown = () => {
|
||||
if (history) {
|
||||
lastapp = JSON.parse(JSON.stringify($app))
|
||||
pointerdown = true
|
||||
}
|
||||
lastapp = JSON.parse(JSON.stringify($app))
|
||||
pointerdown = true
|
||||
}
|
||||
const onpointerup = () => {
|
||||
if (history) {
|
||||
pointerdown = false
|
||||
if (!deepEqual(lastapp, $app)) {
|
||||
push(history, lastapp, false, true)
|
||||
}
|
||||
pointerdown = false
|
||||
if (!deepEqual(lastapp, $app)) {
|
||||
push(history, lastapp, false, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@
|
||||
recomputeIds={component.recomputeIds}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'selectcomponent' || component.type === 'resourceselectcomponent'}
|
||||
{:else if component.type === 'selectcomponent'}
|
||||
<AppSelect
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
|
||||
@@ -74,8 +74,7 @@ export type AggridComponent = BaseComponent<'aggridcomponent'>
|
||||
export type DisplayComponent = BaseComponent<'displaycomponent'>
|
||||
export type ImageComponent = BaseComponent<'imagecomponent'>
|
||||
export type InputComponent = BaseComponent<'inputcomponent'>
|
||||
export type SelectComponent = BaseComponent<'resourceselectcomponent'>
|
||||
export type ResourceSelectComponent = BaseComponent<'selectcomponent'>
|
||||
export type SelectComponent = BaseComponent<'selectcomponent'>
|
||||
export type MultiSelectComponent = BaseComponent<'multiselectcomponent'>
|
||||
export type CheckboxComponent = BaseComponent<'checkboxcomponent'>
|
||||
export type RadioComponent = BaseComponent<'radiocomponent'>
|
||||
@@ -117,7 +116,6 @@ export type AppComponent = BaseAppComponent &
|
||||
| PieChartComponent
|
||||
| ScatterChartComponent
|
||||
| SelectComponent
|
||||
| ResourceSelectComponent
|
||||
| MultiSelectComponent
|
||||
| CheckboxComponent
|
||||
| FormComponent
|
||||
@@ -913,7 +911,7 @@ Hello \${ctx.username}
|
||||
}
|
||||
},
|
||||
multiselectcomponent: {
|
||||
name: 'Multi Select',
|
||||
name: 'MultiSelect',
|
||||
icon: List,
|
||||
dims: '2:1-3:1',
|
||||
data: {
|
||||
@@ -942,36 +940,6 @@ 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,7 +32,6 @@ const inputs: ComponentSet = {
|
||||
'fileinputcomponent',
|
||||
'checkboxcomponent',
|
||||
'selectcomponent',
|
||||
'resourceselectcomponent',
|
||||
'multiselectcomponent',
|
||||
]
|
||||
} as const
|
||||
|
||||
@@ -65,7 +65,11 @@
|
||||
No components found
|
||||
</div>
|
||||
{:else}
|
||||
<div in:fade|local={{ duration: 50, delay: 50 }} out:fade|local={{ duration: 50 }}>
|
||||
<div
|
||||
id="app-tutorial-1"
|
||||
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,13 +23,7 @@
|
||||
<label class="block pb-2">
|
||||
<div class="text-xs font-medium pb-0.5"> Style </div>
|
||||
<div class="relative">
|
||||
<input
|
||||
on:keydown|stopPropagation
|
||||
type="text"
|
||||
class="!pr-7"
|
||||
bind:value={value.style}
|
||||
on:focus
|
||||
/>
|
||||
<input type="text" class="!pr-7" bind:value={value.style} on:focus />
|
||||
{#if value?.style}
|
||||
<button
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
@@ -47,13 +41,7 @@
|
||||
<label class="block">
|
||||
<div class="text-xs font-medium pb-0.5"> Tailwind classes </div>
|
||||
<div class="relative">
|
||||
<input
|
||||
on:keydown|stopPropagation
|
||||
type="text"
|
||||
class="!pr-7"
|
||||
bind:value={value.class}
|
||||
on:focus
|
||||
/>
|
||||
<input type="text" class="!pr-7" bind:value={value.class} on:focus />
|
||||
{#if value?.class}
|
||||
<button
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative p-2">
|
||||
<div id="app-tutorial-2" class="relative p-2">
|
||||
{#each filteredPanels as [componentId, outputs] (componentId)}
|
||||
<div
|
||||
animate:flip={{ duration: 300 }}
|
||||
|
||||
@@ -82,12 +82,7 @@
|
||||
<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
|
||||
on:keydown|stopPropagation
|
||||
bind:value={name}
|
||||
placeholder="Inline script name"
|
||||
class="!text-xs !rounded-xs"
|
||||
/>
|
||||
<input 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,8 +22,6 @@
|
||||
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' ||
|
||||
|
||||
@@ -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 on:keydown|stopPropagation type="number" bind:value />
|
||||
<input type="number" bind:value />
|
||||
|
||||
<div class="absolute top-1 right-1">
|
||||
<Button
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<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 on:keydown|stopPropagation type="text" bind:value />
|
||||
<input type="text" bind:value />
|
||||
|
||||
<div class="absolute top-1 right-1">
|
||||
<Button
|
||||
|
||||
@@ -73,13 +73,7 @@
|
||||
<Loader2 class="animate-spin" size={18} />
|
||||
</div>
|
||||
{:else if filteredItems}
|
||||
<input
|
||||
on:keydown|stopPropagation
|
||||
bind:value={search}
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
class="col-span-4 mb-2"
|
||||
/>
|
||||
<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)}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
export let code: string
|
||||
export let value: any = undefined
|
||||
export let error = ''
|
||||
export let value: any
|
||||
|
||||
function parseJson() {
|
||||
try {
|
||||
@@ -12,6 +11,7 @@
|
||||
error = e.message
|
||||
}
|
||||
}
|
||||
let error = ''
|
||||
$: code && parseJson()
|
||||
</script>
|
||||
|
||||
|
||||
@@ -39,29 +39,6 @@
|
||||
</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,7 +19,6 @@
|
||||
<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,7 +17,6 @@ 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
|
||||
@@ -140,11 +139,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>
|
||||
@@ -157,12 +156,9 @@ 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' }>
|
||||
|
||||
@@ -137,7 +137,7 @@ export type AppViewerContext = {
|
||||
}
|
||||
|
||||
export type AppEditorContext = {
|
||||
history: History<App> | undefined
|
||||
history: History<App>
|
||||
componentControl: Writable<Record<string, { left?: () => boolean; right?: () => boolean }>>
|
||||
pickVariableCallback: Writable<((path: string) => void) | undefined>
|
||||
}
|
||||
|
||||
@@ -96,8 +96,7 @@
|
||||
{
|
||||
displayName: 'Edit',
|
||||
icon: faPen,
|
||||
href: `/apps/edit/${path}?nodraft=true`,
|
||||
disabled: !canWrite
|
||||
href: `/apps/edit/${path}?nodraft=true`
|
||||
},
|
||||
{
|
||||
displayName: 'Use as template',
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
}[kind]
|
||||
</script>
|
||||
|
||||
<div
|
||||
<li
|
||||
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}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
export { c as class }
|
||||
export let wrapperClass = ''
|
||||
export let style = ''
|
||||
export let id = ''
|
||||
|
||||
$: selected && updateSelected()
|
||||
|
||||
@@ -42,6 +43,7 @@
|
||||
<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}>
|
||||
<Tabs bind:selected={$selectedId} id="flow-tutorial-3">
|
||||
<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 class="flex flex-row-reverse justify-between items-center gap-x-2">
|
||||
<div id="flow-tutorial-4" class="flex flex-row-reverse justify-between items-center gap-x-2">
|
||||
<Button
|
||||
on:click={() => {
|
||||
previewMode = 'whole'
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
<FlowConstantsItem />
|
||||
</div>
|
||||
|
||||
<div class="flex-auto grow" bind:clientHeight={minHeight}>
|
||||
<div id="flow-tutorial-1" class="flex-auto grow" bind:clientHeight={minHeight}>
|
||||
<FlowGraph
|
||||
insertable
|
||||
scroll
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
|
||||
{#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"
|
||||
|
||||
@@ -245,69 +245,74 @@
|
||||
/>
|
||||
|
||||
<CenteredPage>
|
||||
<div class="flex flex-wrap gap-2 items-center justify-between w-full">
|
||||
<div class="flex justify-start">
|
||||
<ToggleButtonGroup bind:selected={itemKind}>
|
||||
<ToggleButton light position="left" value="all" size="sm">All</ToggleButton>
|
||||
<ToggleButton light position="center" value="script" size="sm">
|
||||
<div class="flex gap-1 items-center">
|
||||
<Code2 size={16} />
|
||||
Scripts
|
||||
</div>
|
||||
</ToggleButton>
|
||||
<ToggleButton light position="center" value="flow" size="sm">
|
||||
<div class="flex gap-1 items-center">
|
||||
<Icon data={faBarsStaggered} scale={0.8} class="mr-1" />
|
||||
Flows
|
||||
</div>
|
||||
</ToggleButton>
|
||||
<ToggleButton light position="right" value="app" size="sm">
|
||||
<div class="flex gap-1 items-center">
|
||||
<LayoutDashboard size={16} />
|
||||
Apps
|
||||
</div>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<div id="welcome-tutorial-1">
|
||||
<div class="flex flex-wrap gap-2 items-center justify-between w-full">
|
||||
<div class="flex justify-start">
|
||||
<ToggleButtonGroup bind:selected={itemKind}>
|
||||
<ToggleButton light position="left" value="all" size="sm">All</ToggleButton>
|
||||
<ToggleButton light position="center" value="script" size="sm">
|
||||
<div class="flex gap-1 items-center">
|
||||
<Code2 size={16} />
|
||||
Scripts
|
||||
</div>
|
||||
</ToggleButton>
|
||||
<ToggleButton light position="center" value="flow" size="sm">
|
||||
<div class="flex gap-1 items-center">
|
||||
<Icon data={faBarsStaggered} scale={0.8} class="mr-1" />
|
||||
Flows
|
||||
</div>
|
||||
</ToggleButton>
|
||||
<ToggleButton light position="right" value="app" size="sm">
|
||||
<div class="flex gap-1 items-center">
|
||||
<LayoutDashboard size={16} />
|
||||
Apps
|
||||
</div>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="relative text-gray-600 grow min-w-[100px]">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
placeholder="Search Scripts, Flows & Apps"
|
||||
bind:value={filter}
|
||||
class="bg-white !h-10 !px-4 !pr-10 !rounded-lg text-sm focus:outline-none"
|
||||
/>
|
||||
<button type="submit" class="absolute right-0 top-0 mt-3 mr-4">
|
||||
<svg
|
||||
class="h-4 w-4 fill-current"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 56.966 56.966"
|
||||
style="enable-background:new 0 0 56.966 56.966;"
|
||||
xml:space="preserve"
|
||||
width="512px"
|
||||
height="512px"
|
||||
>
|
||||
<path
|
||||
d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23 s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92 c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17 s-17-7.626-17-17S14.61,6,23.984,6z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative text-gray-600 grow min-w-[100px]">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
placeholder="Search Scripts, Flows & Apps"
|
||||
bind:value={filter}
|
||||
class="bg-white !h-10 !px-4 !pr-10 !rounded-lg text-sm focus:outline-none"
|
||||
/>
|
||||
<button type="submit" class="absolute right-0 top-0 mt-3 mr-4">
|
||||
<svg
|
||||
class="h-4 w-4 fill-current"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 56.966 56.966"
|
||||
style="enable-background:new 0 0 56.966 56.966;"
|
||||
xml:space="preserve"
|
||||
width="512px"
|
||||
height="512px"
|
||||
>
|
||||
<path
|
||||
d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23 s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92 c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17 s-17-7.626-17-17S14.61,6,23.984,6z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="relative flex justify-between items-start">
|
||||
<ListFilters bind:selectedFilter={ownerFilter} filters={owners} />
|
||||
{#if !loading}
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={archived}
|
||||
options={{ right: 'Show archived' }}
|
||||
class="pt-2"
|
||||
textClass="whitespace-nowrap"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<ListFilters bind:selectedFilter={ownerFilter} filters={owners} />
|
||||
{#if !loading}
|
||||
<div class="absolute -bottom-2 right-0 bg-white/90">
|
||||
<Toggle size="xs" bind:checked={archived} options={{ right: 'Show archived' }} /></div
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
{#if filteredItems == undefined}
|
||||
<div class="mt-4" />
|
||||
@@ -318,7 +323,7 @@
|
||||
{:else if filteredItems.length === 0}
|
||||
<NoItemFound />
|
||||
{:else}
|
||||
<div class="border rounded-md divide-y divide-gray-200">
|
||||
<ul class="border rounded-md divide-y divide-gray-200">
|
||||
<!-- <VirtualList {items} let:item bind:start bind:end> -->
|
||||
{#each (items ?? []).slice(0, nbDisplayed) as item, i (item.type + '/' + item.path + (item.summary ?? ''))}
|
||||
{#if item.type == 'script'}
|
||||
@@ -354,7 +359,7 @@
|
||||
{/if}
|
||||
{/each}
|
||||
<!-- </VirtualList> -->
|
||||
</div>
|
||||
</ul>
|
||||
{#if items && items?.length > 30}
|
||||
<span class="text-xs"
|
||||
>{nbDisplayed} items out of {items.length}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
export let icon: IconDefinition
|
||||
export let isCollapsed: boolean
|
||||
export let disabled: boolean = false
|
||||
export let id: string = ''
|
||||
|
||||
let isSelected = false
|
||||
|
||||
@@ -33,6 +34,7 @@
|
||||
$$props.class
|
||||
)}
|
||||
target={href.includes('http') ? '_blank' : null}
|
||||
{id}
|
||||
>
|
||||
<Icon
|
||||
data={icon}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
const mainMenuLinks = [
|
||||
{ label: 'Home', href: '/', icon: faHomeAlt },
|
||||
{ label: 'Runs', href: '/runs', icon: faPlay },
|
||||
{ label: 'Runs', href: '/runs', icon: faPlay, id: 'welcome-tutorial-4' },
|
||||
{ label: 'Variables', href: '/variables', icon: faDollarSign, disabled: $userStore?.operator },
|
||||
{ label: 'Resources', href: '/resources', icon: faCubes, disabled: $userStore?.operator }
|
||||
]
|
||||
|
||||
143
frontend/src/lib/components/tutorial/Tour.svelte
Normal file
143
frontend/src/lib/components/tutorial/Tour.svelte
Normal file
@@ -0,0 +1,143 @@
|
||||
<script lang="ts">
|
||||
import Shepherd from 'shepherd.js'
|
||||
import { steps, tourStore, type TutorialName } from './'
|
||||
|
||||
export let tutorial: TutorialName
|
||||
/**
|
||||
* If the index of a step is added to a tour, the custom footer will not be added
|
||||
* (ie. the steps won't be shown and the buttons won't be positioned)
|
||||
*/
|
||||
const noPageNumber: Partial<Record<TutorialName, number[]>> = {
|
||||
welcome: [0]
|
||||
}
|
||||
let tour: Shepherd.Tour | undefined
|
||||
|
||||
$: $tourStore.includes(tutorial) && initiate()
|
||||
|
||||
function initiate() {
|
||||
if (!tourStore.isTourActive(tutorial)) {
|
||||
return
|
||||
}
|
||||
tour?.cancel()
|
||||
tour = new Shepherd.Tour({
|
||||
tourName: tutorial,
|
||||
useModalOverlay: true,
|
||||
defaultStepOptions: {
|
||||
classes: 'max-w-[460px] shadow-xl bg-white border rounded px-6 py-4 z-[1100]',
|
||||
scrollTo: true
|
||||
}
|
||||
})
|
||||
tour.addSteps(steps[tutorial](tour))
|
||||
tour.steps.forEach((step, i) => {
|
||||
if (noPageNumber[tutorial]?.includes(i)) {
|
||||
return
|
||||
}
|
||||
step.on('show', () => {
|
||||
const currentStep = tour?.getCurrentStep()
|
||||
const currentStepElement = currentStep?.getElement()
|
||||
const header = currentStepElement?.querySelector('.shepherd-header')
|
||||
if (currentStep && currentStepElement && header) {
|
||||
const progress = document.createElement('span')
|
||||
progress.className = 'shepherd-progress text-sm text-gray-500 pr-2'
|
||||
progress.innerText = `${(tour?.steps.indexOf(currentStep) ?? 0) + 1}/${
|
||||
tour?.steps.length
|
||||
}`
|
||||
header.prepend(progress)
|
||||
}
|
||||
})
|
||||
})
|
||||
;['complete', 'cancel'].forEach((event) => {
|
||||
tour?.on(event, () => {
|
||||
tourStore.markTourAsDone(tutorial)
|
||||
})
|
||||
})
|
||||
tour.start()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style global>
|
||||
.shepherd-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.shepherd-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.shepherd-button {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
transition-duration: 200ms;
|
||||
}
|
||||
|
||||
/* Simple primary button */
|
||||
.shepherd-button:not(.shepherd-button-secondary.special) {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.shepherd-button:not(.shepherd-button-secondary.special):hover,
|
||||
.shepherd-button:not(.shepherd-button-secondary.special):focus {
|
||||
background-color: #eff6ff;
|
||||
}
|
||||
|
||||
/* Simple secondary button */
|
||||
.shepherd-button.shepherd-button-secondary:not(.special) {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.shepherd-button.shepherd-button-secondary:not(.special):hover,
|
||||
.shepherd-button.shepherd-button-secondary:not(.special):focus {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
/* Special primary button */
|
||||
.shepherd-button.special:not(.shepherd-button-secondary) {
|
||||
background-color: #3b82f6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.shepherd-button.special:not(.shepherd-button-secondary):hover,
|
||||
.shepherd-button.special:not(.shepherd-button-secondary):focus {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
/* Special secondary button */
|
||||
.shepherd-button.special.shepherd-button-secondary {
|
||||
background-color: #eff6ff;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.shepherd-button.special.shepherd-button-secondary:hover,
|
||||
.shepherd-button.special.shepherd-button-secondary:focus {
|
||||
background-color: #dbeafe;
|
||||
}
|
||||
|
||||
.shepherd-cancel-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
background-color: #fff;
|
||||
transition-duration: 200ms;
|
||||
}
|
||||
|
||||
.shepherd-cancel-icon:hover,
|
||||
.shepherd-cancel-icon:focus {
|
||||
background-color: #ededed;
|
||||
}
|
||||
|
||||
.shepherd-highlighted-element {
|
||||
box-shadow: 0 0 0 6px #93c4fdbb;
|
||||
background-color: #93c4fd3a;
|
||||
position: relative;
|
||||
z-index: 1090;
|
||||
}
|
||||
</style>
|
||||
113
frontend/src/lib/components/tutorial/app-steps.ts
Normal file
113
frontend/src/lib/components/tutorial/app-steps.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type Shepherd from 'shepherd.js'
|
||||
|
||||
export default (tour: Shepherd.Tour) => {
|
||||
const steps: object[] | Shepherd.Step[] = [
|
||||
{
|
||||
id: 'welcome',
|
||||
text: `<div class="text-xl font-bold mb-6">Windmill App Editor</div>
|
||||
<p class="mb-6">Build your own UI quick and easy with drag-and-drop components. Connect your scripts and data to them and deploy a fully functioning application.</p>`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'See by myself',
|
||||
action: tour.cancel,
|
||||
secondary: true,
|
||||
classes: 'special'
|
||||
},
|
||||
{
|
||||
text: 'Give me an overview',
|
||||
action: tour.next,
|
||||
classes: 'special'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'components',
|
||||
text: `<div class="text-xl font-bold mb-2">Components</div>
|
||||
<p class="mb-2">Click to add and move them around by dragging. Configure the inputs, settings and styling, even with Tailwind classes.</p>`,
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
attachTo: { element: '#app-tutorial-1', on: 'left-start' },
|
||||
highlightClass: 'shepherd-highlighted-element',
|
||||
scrollTo: false,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Back',
|
||||
action: tour.back,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Next',
|
||||
action: tour.next
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'outputs',
|
||||
text: `<div class="text-xl font-bold mb-2">Outputs</div>
|
||||
<p class="mb-2">Each component has their outputs, which can be used to easily hook into the app state.</p>`,
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
attachTo: { element: '#app-tutorial-2', on: 'right-start' },
|
||||
highlightClass: 'shepherd-highlighted-element',
|
||||
scrollTo: false,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Back',
|
||||
action: tour.back,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Next',
|
||||
action: tour.next
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'parameters',
|
||||
text: `<div class="text-xl font-bold mb-2">Canvas</div>
|
||||
<p class="mb-2">It allows you to position, scale and group the components.</p>`,
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
attachTo: { element: '#app-tutorial-3', on: 'bottom' },
|
||||
highlightClass: 'shepherd-highlighted-element',
|
||||
scrollTo: false,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Back',
|
||||
action: tour.back,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Next',
|
||||
action: tour.next
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'test',
|
||||
text: `<div class="text-xl font-bold mb-2">Runnable editor</div>
|
||||
<p class="mb-2">Here you can create, edit and manage the scripts of your app.</p>`,
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
attachTo: { element: '#app-tutorial-4', on: 'top-start' },
|
||||
highlightClass: 'shepherd-highlighted-element',
|
||||
scrollTo: false,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Back',
|
||||
action: tour.back,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Finish',
|
||||
action: tour.complete
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
return steps
|
||||
}
|
||||
113
frontend/src/lib/components/tutorial/flow-steps.ts
Normal file
113
frontend/src/lib/components/tutorial/flow-steps.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type Shepherd from 'shepherd.js'
|
||||
|
||||
export default (tour: Shepherd.Tour) => {
|
||||
const steps: object[] | Shepherd.Step[] = [
|
||||
{
|
||||
id: 'welcome',
|
||||
text: `<div class="text-xl font-bold mb-6">Windmill Flow Editor</div>
|
||||
<p class="mb-6">Build complex workflows from your scripts, configure them and get a clear visualization.</p>`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'See by myself',
|
||||
action: tour.cancel,
|
||||
secondary: true,
|
||||
classes: 'special'
|
||||
},
|
||||
{
|
||||
text: 'Give me an overview',
|
||||
action: tour.next,
|
||||
classes: 'special'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'graph',
|
||||
text: `<div class="text-xl font-bold mb-2">Flow graph</div>
|
||||
<p class="mb-2">Know how your flow is structured at a glimpse.</p>`,
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
attachTo: { element: '#flow-tutorial-1', on: 'right-start' },
|
||||
highlightClass: 'shepherd-highlighted-element',
|
||||
scrollTo: false,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Back',
|
||||
action: tour.back,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Next',
|
||||
action: tour.next
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'modules',
|
||||
text: `<div class="text-xl font-bold mb-2">Insert modules</div>
|
||||
<p class="mb-2">Start building by adding modules. A module can be a simple script, a loop or a branching for example.</p>`,
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
attachTo: { element: '#flow-tutorial-2 button', on: 'right-start' },
|
||||
highlightClass: 'shepherd-highlighted-element',
|
||||
scrollTo: false,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Back',
|
||||
action: tour.back,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Next',
|
||||
action: tour.next
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
text: `<div class="text-xl font-bold mb-2">Settings</div>
|
||||
<p class="mb-2">Configure schedules, authorizations, sharing parameters and many more.</p>`,
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
attachTo: { element: '#flow-tutorial-3', on: 'bottom-start' },
|
||||
highlightClass: 'shepherd-highlighted-element',
|
||||
scrollTo: false,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Back',
|
||||
action: tour.back,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Next',
|
||||
action: tour.next
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'test',
|
||||
text: `<div class="text-xl font-bold mb-2">Test flow</div>
|
||||
<p class="mb-2">Time to try your work! Test the flow and iterate as much as you want.</p>`,
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
attachTo: { element: '#flow-tutorial-4', on: 'left-start' },
|
||||
highlightClass: 'shepherd-highlighted-element',
|
||||
scrollTo: false,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Back',
|
||||
action: tour.back,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Finish',
|
||||
action: tour.complete
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
return steps
|
||||
}
|
||||
51
frontend/src/lib/components/tutorial/index.ts
Normal file
51
frontend/src/lib/components/tutorial/index.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { browser } from '$app/environment'
|
||||
import type Shepherd from 'shepherd.js'
|
||||
import welcome from './welcome-steps'
|
||||
import script from './script-steps'
|
||||
import flow from './flow-steps'
|
||||
import app from './app-steps'
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
export { default as Tour } from './Tour.svelte'
|
||||
|
||||
export const TUTORIAL_NAMES = ['welcome', 'script', 'flow', 'app'] as const
|
||||
export type TutorialName = (typeof TUTORIAL_NAMES)[number]
|
||||
|
||||
export const steps: Record<TutorialName, (tour: Shepherd.Tour) => object[] | Shepherd.Step[]> = {
|
||||
welcome,
|
||||
script,
|
||||
flow,
|
||||
app
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'tutorials-to-show' as const
|
||||
const store = writable<Readonly<TutorialName[]>>([])
|
||||
export const tourStore = {
|
||||
subscribe: store.subscribe,
|
||||
initiateTours: () => {
|
||||
if (!browser) {
|
||||
return
|
||||
}
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(TUTORIAL_NAMES))
|
||||
store.set(TUTORIAL_NAMES)
|
||||
},
|
||||
isTourActive: (name: TutorialName) => {
|
||||
if (!browser) {
|
||||
return false
|
||||
}
|
||||
const toursToShow = JSON.parse(
|
||||
window.localStorage.getItem(STORAGE_KEY) || '[]'
|
||||
) as TutorialName[]
|
||||
return toursToShow.includes(name)
|
||||
},
|
||||
markTourAsDone: (name: TutorialName) => {
|
||||
if (!browser) {
|
||||
return
|
||||
}
|
||||
const toursToShow = (
|
||||
JSON.parse(window.localStorage.getItem(STORAGE_KEY) || '[]') as TutorialName[]
|
||||
).filter((t) => t !== name)
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(toursToShow))
|
||||
store.set(toursToShow)
|
||||
}
|
||||
}
|
||||
113
frontend/src/lib/components/tutorial/script-steps.ts
Normal file
113
frontend/src/lib/components/tutorial/script-steps.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type Shepherd from 'shepherd.js'
|
||||
|
||||
export default (tour: Shepherd.Tour) => {
|
||||
const steps: object[] | Shepherd.Step[] = [
|
||||
{
|
||||
id: 'welcome',
|
||||
text: `<div class="text-xl font-bold mb-6">Windmill Script Builder</div>
|
||||
<p class="mb-6">Build scripts with our own web editor, then use them anywhere in your flows, apps or as standalone computations with their auto-generated UI.</p>`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'See by myself',
|
||||
action: tour.cancel,
|
||||
secondary: true,
|
||||
classes: 'special'
|
||||
},
|
||||
{
|
||||
text: 'Give me an overview',
|
||||
action: tour.next,
|
||||
classes: 'special'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'editor',
|
||||
text: `<div class="text-xl font-bold mb-2">Web editor</div>
|
||||
<p class="mb-2">Code directly from the web editor.</p>`,
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
attachTo: { element: '#script-tutorial-1', on: 'right-start' },
|
||||
highlightClass: 'shepherd-highlighted-element',
|
||||
scrollTo: false,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Back',
|
||||
action: tour.back,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Next',
|
||||
action: tour.next
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'preview',
|
||||
text: `<div class="text-xl font-bold mb-2">UI preview</div>
|
||||
<p class="mb-2">Preview the user interface generated from the signature of the script.</p>`,
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
attachTo: { element: '#script-tutorial-2', on: 'left-start' },
|
||||
highlightClass: 'shepherd-highlighted-element',
|
||||
scrollTo: false,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Back',
|
||||
action: tour.back,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Next',
|
||||
action: tour.next
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'parameters',
|
||||
text: `<div class="text-xl font-bold mb-2">Parameters</div>
|
||||
<p class="mb-2">Add variables, resources and configure your script.</p>`,
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
attachTo: { element: '#script-tutorial-3', on: 'bottom-start' },
|
||||
highlightClass: 'shepherd-highlighted-element',
|
||||
scrollTo: false,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Back',
|
||||
action: tour.back,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Next',
|
||||
action: tour.next
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'test',
|
||||
text: `<div class="text-xl font-bold mb-2">Test script</div>
|
||||
<p class="mb-2">Time to try your work! Test the script and iterate as much as you want.</p>`,
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
attachTo: { element: '#script-tutorial-4', on: 'left-start' },
|
||||
highlightClass: 'shepherd-highlighted-element',
|
||||
scrollTo: false,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Back',
|
||||
action: tour.back,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Finish',
|
||||
action: tour.complete
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
return steps
|
||||
}
|
||||
114
frontend/src/lib/components/tutorial/welcome-steps.ts
Normal file
114
frontend/src/lib/components/tutorial/welcome-steps.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import type Shepherd from 'shepherd.js'
|
||||
|
||||
export default (tour: Shepherd.Tour) => {
|
||||
const steps: object[] | Shepherd.Step[] = [
|
||||
{
|
||||
id: 'welcome',
|
||||
text: `<div class="text-xl font-bold mb-6">Welcome to Windmill Cloud App!</div>
|
||||
<p class="mb-6">Thank your for your interest in Windmill. Let us guide you through the home screen in strictly less than a minute.</p>`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'See by myself',
|
||||
action: tour.cancel,
|
||||
secondary: true,
|
||||
classes: 'special'
|
||||
},
|
||||
{
|
||||
text: 'Give me an overview',
|
||||
action: tour.next,
|
||||
classes: 'special'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'workspace',
|
||||
text: `<div class="text-xl font-bold mb-2">Workspace</div>
|
||||
<p>You can see all the scripts, flows and apps here.</p>
|
||||
<p class="mb-2">Use the search and filters to find just what you are looking for.</p>`,
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
attachTo: { element: '#welcome-tutorial-1', on: 'bottom-start' },
|
||||
highlightClass: 'shepherd-highlighted-element',
|
||||
scrollTo: false,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Back',
|
||||
action: tour.back,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Next',
|
||||
action: tour.next
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'create',
|
||||
text: `<div class="text-xl font-bold mb-2">Create</div>
|
||||
<p class="mb-2">Build your own tools in minutes.</p>`,
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
attachTo: { element: '#welcome-tutorial-2', on: 'bottom-end' },
|
||||
highlightClass: 'shepherd-highlighted-element',
|
||||
scrollTo: false,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Back',
|
||||
action: tour.back,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Next',
|
||||
action: tour.next
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'inspiration',
|
||||
text: `<div class="text-xl font-bold mb-2">Take inspiration</div>
|
||||
<p class="mb-2">Explore templates built by the community and import them directly from <a href="https://hub.windmill.dev" target="_blank">Windmill Hub</a>.</p>`,
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
attachTo: { element: '#welcome-tutorial-3', on: 'bottom-start' },
|
||||
highlightClass: 'shepherd-highlighted-element',
|
||||
scrollTo: false,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Back',
|
||||
action: tour.back,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Next',
|
||||
action: tour.next
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'monitor',
|
||||
text: `<div class="text-xl font-bold mb-2">Monitor</div>
|
||||
<p class="mb-2">Keep an eye an all past and scheduled executions of scripts and flows.</p>`,
|
||||
cancelIcon: {
|
||||
enabled: true
|
||||
},
|
||||
attachTo: { element: '#welcome-tutorial-4-wrapper #welcome-tutorial-4', on: 'right-start' },
|
||||
highlightClass: 'shepherd-highlighted-element',
|
||||
scrollTo: false,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Back',
|
||||
action: tour.back,
|
||||
secondary: true
|
||||
},
|
||||
{
|
||||
text: 'Finish',
|
||||
action: tour.complete
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
return steps
|
||||
}
|
||||
@@ -7,43 +7,34 @@ export function initHistory<T>(initial: T): History<T> {
|
||||
return writable({ history: [initial], index: 0 })
|
||||
}
|
||||
|
||||
export function undo<T>(history: History<T> | undefined, now: T): T {
|
||||
if (history) {
|
||||
|
||||
let chistory = get(history)
|
||||
if (chistory.index == 0) return now
|
||||
if (!deepEqual(chistory.history[chistory.index], now)) {
|
||||
push(history, now, true)
|
||||
} else {
|
||||
history.update((history) => {
|
||||
history.index--
|
||||
return history
|
||||
})
|
||||
}
|
||||
let nhistory = get(history)
|
||||
return nhistory.history[nhistory.index]
|
||||
export function undo<T>(history: History<T>, now: T): T {
|
||||
let chistory = get(history)
|
||||
if (chistory.index == 0) return now
|
||||
if (!deepEqual(chistory.history[chistory.index], now)) {
|
||||
push(history, now, true)
|
||||
} else {
|
||||
return now
|
||||
}
|
||||
}
|
||||
|
||||
export function redo<T>(history: History<T> | undefined): T {
|
||||
if (history) {
|
||||
history?.update((history) => {
|
||||
if (history.index < history.history.length - 1) {
|
||||
history.index++
|
||||
}
|
||||
history.update((history) => {
|
||||
history.index--
|
||||
return history
|
||||
})
|
||||
let nhistory = get(history)
|
||||
return nhistory.history[nhistory.index]
|
||||
} else {
|
||||
return undefined as T
|
||||
}
|
||||
let nhistory = get(history)
|
||||
return nhistory.history[nhistory.index]
|
||||
}
|
||||
|
||||
export function push<T>(history: History<T> | undefined, value: T, noSetIndex: boolean = false, skipCopy: boolean = false) {
|
||||
history?.update((history) => {
|
||||
export function redo<T>(history: History<T>): T {
|
||||
history.update((history) => {
|
||||
if (history.index < history.history.length - 1) {
|
||||
history.index++
|
||||
}
|
||||
return history
|
||||
})
|
||||
let nhistory = get(history)
|
||||
return nhistory.history[nhistory.index]
|
||||
}
|
||||
|
||||
export function push<T>(history: History<T>, value: T, noSetIndex: boolean = false, skipCopy: boolean = false) {
|
||||
history.update((history) => {
|
||||
history.history = JSON.parse(JSON.stringify(history.history.slice(0, history.index + 1)))
|
||||
const toPush = skipCopy ? value : JSON.parse(JSON.stringify(value))
|
||||
history.history.push(toPush)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { goto } from '$app/navigation'
|
||||
import {
|
||||
AppService,
|
||||
FlowService,
|
||||
FolderService,
|
||||
Script,
|
||||
ScriptService,
|
||||
UserService,
|
||||
@@ -233,7 +234,6 @@ export function canWrite(
|
||||
if (user.folders.findIndex((x) => path.startsWith('f/' + x)) != -1) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -297,25 +297,17 @@ export async function setQuery(url: URL, key: string, value: string): Promise<vo
|
||||
}
|
||||
|
||||
let debounced: NodeJS.Timeout | undefined = undefined
|
||||
export function setQueryWithoutLoad(url: URL, args: { key: string, value: string | null | undefined }[], bounceTime?: number): void {
|
||||
export function setQueryWithoutLoad(url: URL, key: string, value: string): void {
|
||||
debounced && clearTimeout(debounced)
|
||||
debounced = setTimeout(() => {
|
||||
const nurl = new URL(url.toString())
|
||||
console.log(url.toString())
|
||||
for (const { key, value } of args) {
|
||||
if (value) {
|
||||
nurl.searchParams.set(key, value)
|
||||
} else {
|
||||
nurl.searchParams.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
nurl.searchParams.set(key, value)
|
||||
try {
|
||||
history.replaceState(history.state, '', nurl.toString())
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}, bounceTime ?? 200)
|
||||
}, 200)
|
||||
}
|
||||
|
||||
export function groupBy<T>(
|
||||
|
||||
@@ -202,7 +202,10 @@
|
||||
isCollapsed ? 'md:w-12' : 'md:w-40'
|
||||
)}
|
||||
>
|
||||
<div class="flex-1 flex flex-col min-h-0 h-screen shadow-lg bg-[#2e3440]">
|
||||
<div
|
||||
id="welcome-tutorial-4-wrapper"
|
||||
class="flex-1 flex flex-col min-h-0 h-screen shadow-lg bg-[#2e3440]"
|
||||
>
|
||||
<button
|
||||
on:click={() => {
|
||||
goto('/')
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
import AppPreview from '$lib/components/apps/editor/AppPreview.svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import type { EditorBreakpoint } from '$lib/components/apps/types'
|
||||
import { Tour, tourStore } from '../../../lib/components/tutorial'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
type Tab = 'hubscripts' | 'hubflows' | 'hubapps' | 'workspace'
|
||||
|
||||
@@ -60,8 +62,12 @@
|
||||
appViewerApp = hub
|
||||
appViewer.openDrawer?.()
|
||||
}
|
||||
|
||||
onMount(tourStore.initiateTours)
|
||||
</script>
|
||||
|
||||
<Tour tutorial="welcome" />
|
||||
|
||||
<Drawer bind:this={codeViewer} size="900px">
|
||||
<DrawerContent title={codeViewerObj?.summary ?? ''} on:close={codeViewer.closeDrawer}>
|
||||
<svelte:fragment slot="actions">
|
||||
@@ -181,7 +187,7 @@
|
||||
</Alert>
|
||||
{/if}
|
||||
<PageHeader title="Home">
|
||||
<div class="flex flex-row gap-4 flex-wrap justify-end items-center">
|
||||
<div id="welcome-tutorial-2" class="flex flex-row gap-4 flex-wrap justify-end items-center">
|
||||
{#if !$userStore?.operator}
|
||||
<span class="text-sm text-gray-500">Create a new:</span>
|
||||
<CreateActionsScript />
|
||||
@@ -192,7 +198,7 @@
|
||||
</PageHeader>
|
||||
|
||||
{#if !$userStore?.operator}
|
||||
<div class="w-full overflow-auto scrollbar-hidden">
|
||||
<div id="welcome-tutorial-3" class="w-full">
|
||||
<Tabs bind:selected={tab}>
|
||||
<Tab size="md" value="workspace">
|
||||
<div class="flex gap-2 items-center my-1">
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
{#if value}
|
||||
<div class="h-screen">
|
||||
{#key value}
|
||||
<AppEditor {summary} app={value} path={''} {policy} fromHub={hubId != null} />
|
||||
<AppEditor {summary} app={value} path={''} {policy} fromHub={hubId != null} tour />
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -3,20 +3,17 @@
|
||||
import AppPreview from '$lib/components/apps/editor/AppPreview.svelte'
|
||||
import type { EditorBreakpoint } from '$lib/components/apps/types'
|
||||
|
||||
import { Button, Skeleton } from '$lib/components/common'
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
import { AppService, AppWithLastVersion } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { canWrite } from '$lib/utils'
|
||||
import { faPen } from '@fortawesome/free-solid-svg-icons'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { writable } from 'svelte/store'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
let app: AppWithLastVersion | undefined = undefined
|
||||
let can_write = false
|
||||
|
||||
async function loadApp() {
|
||||
app = await AppService.getAppByPath({ workspace: $workspaceStore!, path: $page.params.path })
|
||||
can_write = canWrite(app?.path, app?.extra_perms!, $userStore)
|
||||
}
|
||||
|
||||
let queryId = $page.url.searchParams.get('workspace_id')
|
||||
@@ -52,13 +49,6 @@
|
||||
isEditor={false}
|
||||
noBackend={false}
|
||||
/>
|
||||
{#if can_write}
|
||||
<div class="absolute bottom-4 z-20 right-4">
|
||||
<Button size="sm" startIcon={{ icon: faPen }} variant="border" href="/apps/edit/{app.path}"
|
||||
>Edit</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<Skeleton layout={[10]} />
|
||||
|
||||
@@ -89,4 +89,4 @@
|
||||
$dirtyStore = true
|
||||
</script>
|
||||
|
||||
<FlowBuilder {flowStore} {flowStateStore} {selectedId} {loading} />
|
||||
<FlowBuilder {flowStore} {flowStateStore} {selectedId} {loading} tour />
|
||||
|
||||
@@ -171,9 +171,10 @@
|
||||
<CenteredPage>
|
||||
{#if flow}
|
||||
<div class="prose-sm mx-auto mt-6">
|
||||
<div
|
||||
class="flex flex-row-reverse w-full flex-wrap md:flex-nowrap justify-between gap-x-2 gap-y-4"
|
||||
>
|
||||
<div class="flex flex-row w-full justify-between item-center">
|
||||
<h1 class="mb-1 truncate">
|
||||
{defaultIfEmptyString(flow.summary, flow.path)}
|
||||
</h1>
|
||||
<div class="flex flex-row-reverse gap-2 h-full">
|
||||
<Button
|
||||
href="/flows/run/{path}"
|
||||
@@ -197,7 +198,7 @@
|
||||
</Button>
|
||||
<Button
|
||||
href="/flows/add?template={flow.path}"
|
||||
variant="border"
|
||||
variant="contained"
|
||||
color="blue"
|
||||
size="md"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
@@ -205,19 +206,10 @@
|
||||
Fork
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
href="/runs/{flow.path}"
|
||||
variant="border"
|
||||
color="blue"
|
||||
size="md"
|
||||
startIcon={{ icon: faList }}
|
||||
>
|
||||
<Button href="/runs/{flow.path}" color="blue" size="md" startIcon={{ icon: faList }}>
|
||||
View runs
|
||||
</Button>
|
||||
</div>
|
||||
<h1 class="mb-1 truncate grow">
|
||||
{defaultIfEmptyString(flow.summary, flow.path)}
|
||||
</h1>
|
||||
</div>
|
||||
{#if !emptyString(flow.summary)}
|
||||
<span class="text-lg font-semibold">{flow.path}</span>
|
||||
@@ -226,7 +218,7 @@
|
||||
{/if}
|
||||
<ShareModal bind:this={shareModal} />
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 max-w-7xl pb-6">
|
||||
<div class="grid grid-cols-1 gap-6 max-w-7xl pb-6 mt-2">
|
||||
<Skeleton
|
||||
loading={!flow}
|
||||
layout={[[{ h: 1.5, w: 40 }], 1, [4], 2.25, [{ h: 1.5, w: 30 }], 1, [10]]}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import RunForm from '$lib/components/RunForm.svelte'
|
||||
import { Button, Kbd, Skeleton } from '$lib/components/common'
|
||||
import { faEye, faPen, faPlay } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faEye, faPlay, faScroll } from '@fortawesome/free-solid-svg-icons'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
|
||||
const path = $page.params.path
|
||||
@@ -87,21 +87,9 @@
|
||||
<div
|
||||
class="flex flex-row-reverse w-full flex-wrap md:flex-nowrap justify-between gap-x-1"
|
||||
>
|
||||
<div class="flex flex-row gap-4">
|
||||
{#if !$userStore?.operator && can_write}
|
||||
<div>
|
||||
<Button
|
||||
size="sm"
|
||||
startIcon={{ icon: faPen }}
|
||||
disabled={flow == undefined}
|
||||
variant="border"
|
||||
href="/flows/edit/{flow?.path}">Edit</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="md:pr-4">
|
||||
<div class="flex flex-row">
|
||||
<div>
|
||||
<Button
|
||||
size="sm"
|
||||
startIcon={{ icon: faEye }}
|
||||
disabled={flow == undefined}
|
||||
btnClasses="mr-4"
|
||||
|
||||
@@ -281,7 +281,7 @@
|
||||
><Badge color="gray">{truncateHash(job.script_hash)}</Badge></a
|
||||
>
|
||||
{/if}
|
||||
{#if job && 'job_kind' in job}<Badge baseClass="ml-2" color="blue">{job.job_kind}</Badge>
|
||||
{#if job && 'job_kind' in job}<Badge color="blue">{job.job_kind}</Badge>
|
||||
{/if}
|
||||
{#if !job.visible_to_owner}<Badge color="red"
|
||||
>only visible to you <Tooltip
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { JobService, Job, CompletedJob, ScriptService, FlowService } from '$lib/gen'
|
||||
import { setQuery, setQueryWithoutLoad } from '$lib/utils'
|
||||
import { setQuery } from '$lib/utils'
|
||||
|
||||
import { page } from '$app/stores'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
@@ -18,12 +18,11 @@
|
||||
import Icon from 'svelte-awesome'
|
||||
import AutoComplete from 'simple-svelte-autocomplete'
|
||||
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
|
||||
import Slider from '$lib/components/Slider.svelte'
|
||||
import JsonEditor from '$lib/components/apps/editor/settingsPanel/inputEditor/JsonEditor.svelte'
|
||||
|
||||
let jobs: Job[] | undefined
|
||||
let error: Error | undefined
|
||||
let intervalId: NodeJS.Timer | undefined
|
||||
let createdBefore: string | undefined = $page.url.searchParams.get('createdBefore') ?? undefined
|
||||
|
||||
let success: boolean | undefined =
|
||||
$page.url.searchParams.get('success') != undefined
|
||||
@@ -34,11 +33,6 @@
|
||||
? $page.url.searchParams.get('is_skipped') == 'true'
|
||||
: false
|
||||
|
||||
let argFilter: any = $page.url.searchParams.get('arg') ?? undefined
|
||||
let resultFilter: any = $page.url.searchParams.get('result') ?? undefined
|
||||
let minTs = $page.url.searchParams.get('min_ts') ?? undefined
|
||||
let maxTs = $page.url.searchParams.get('max_ts') ?? undefined
|
||||
|
||||
let nbOfJobs = 30
|
||||
|
||||
$: path = $page.params.path
|
||||
@@ -59,44 +53,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: ($workspaceStore && loadJobs()) || (path && success && isSkipped && jobKinds)
|
||||
|
||||
let filterTimeout: NodeJS.Timeout | undefined = undefined
|
||||
function debounceSyncer() {
|
||||
filterTimeout && clearTimeout(filterTimeout)
|
||||
filterTimeout = setTimeout(() => {
|
||||
loadJobs()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
$: (true || argFilter || resultFilter) && debounceSyncer()
|
||||
$: ($workspaceStore && loadJobs(createdBefore)) || (path && success && isSkipped && jobKinds)
|
||||
|
||||
async function fetchJobs(
|
||||
startedBefore: string | undefined,
|
||||
startedAfter: string | undefined
|
||||
createdBefore: string | undefined,
|
||||
createdAfter: string | undefined
|
||||
): Promise<Job[]> {
|
||||
return JobService.listJobs({
|
||||
workspace: $workspaceStore!,
|
||||
startedBefore,
|
||||
startedAfter,
|
||||
createdBefore,
|
||||
createdAfter,
|
||||
scriptPathExact: path === '' ? undefined : path,
|
||||
jobKinds,
|
||||
success,
|
||||
isSkipped,
|
||||
isFlowStep: jobKindsCat != 'all' ? false : undefined,
|
||||
args:
|
||||
argFilter && argFilter != '{}' && argFilter != '' && argError == '' ? argFilter : undefined,
|
||||
result:
|
||||
resultFilter && resultFilter != '{}' && resultFilter != '' && resultError == ''
|
||||
? resultFilter
|
||||
: undefined
|
||||
isFlowStep: jobKindsCat != 'all' ? false : undefined
|
||||
})
|
||||
}
|
||||
|
||||
async function loadJobs(): Promise<void> {
|
||||
jobs = undefined
|
||||
async function loadJobs(createdBefore: string | undefined): Promise<void> {
|
||||
try {
|
||||
const newJobs = await fetchJobs(maxTs, minTs)
|
||||
const newJobs = await fetchJobs(createdBefore, undefined)
|
||||
jobs = newJobs
|
||||
} catch (err) {
|
||||
sendUserToast(`There was a problem fetching jobs: ${err}`, true)
|
||||
@@ -112,7 +89,7 @@
|
||||
}
|
||||
|
||||
async function syncer() {
|
||||
if (sync && jobs && maxTs == undefined) {
|
||||
if (jobs && createdBefore === undefined) {
|
||||
const reversedJobs = [...jobs].reverse()
|
||||
const lastIndex = reversedJobs.findIndex((x) => x.type == Job.type.QUEUED_JOB) - 1
|
||||
let ts = lastIndex >= 0 ? reversedJobs[lastIndex].created_at : undefined
|
||||
@@ -133,17 +110,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
let sync = true
|
||||
onMount(() => {
|
||||
loadPaths()
|
||||
intervalId = setInterval(syncer, 5000)
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
sync = false
|
||||
} else {
|
||||
sync = true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
let paths: string[] = []
|
||||
@@ -153,23 +122,11 @@
|
||||
const npaths_flows = await FlowService.listFlowPaths({ workspace: $workspaceStore ?? '' })
|
||||
paths = npaths_scripts.concat(npaths_flows).sort()
|
||||
}
|
||||
|
||||
function syncWithUrl(arg: string, value: string) {
|
||||
setQueryWithoutLoad($page.url, [{ key: arg, value }])
|
||||
async function syncCatWithURL() {
|
||||
await setQuery($page.url, 'job_kinds', jobKindsCat)
|
||||
}
|
||||
|
||||
async function syncTsWithURL(minTs?: string, maxTs?: string) {
|
||||
console.log(minTs, maxTs)
|
||||
setQueryWithoutLoad($page.url, [
|
||||
{ key: 'min_ts', value: minTs },
|
||||
{ key: 'max_ts', value: maxTs }
|
||||
])
|
||||
}
|
||||
|
||||
$: syncWithUrl('job_kinds', jobKindsCat)
|
||||
$: syncWithUrl('arg', argFilter)
|
||||
$: syncWithUrl('result', resultFilter)
|
||||
$: syncTsWithURL(minTs, maxTs)
|
||||
$: jobKindsCat && syncCatWithURL()
|
||||
|
||||
$: completedJobs =
|
||||
jobs?.filter((x) => x.type == 'CompletedJob').map((x) => x as CompletedJob) ?? []
|
||||
@@ -181,15 +138,14 @@
|
||||
})
|
||||
let searchPath = ''
|
||||
$: searchPath = path
|
||||
let minTs = undefined
|
||||
let maxTs = undefined
|
||||
|
||||
$: searchPath && onSearchPathChange()
|
||||
|
||||
function onSearchPathChange() {
|
||||
goto(`/runs/${searchPath}?${$page.url.searchParams.toString()}`)
|
||||
}
|
||||
|
||||
let argError = ''
|
||||
let resultError = ''
|
||||
</script>
|
||||
|
||||
<CenteredPage>
|
||||
@@ -254,49 +210,28 @@
|
||||
<input type="text" value={maxTs ?? 'zoom x axis to set max'} disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-2 mb-2 w-full flex-wrap">
|
||||
<div>
|
||||
<div class="flex flex-row gap-x-2">
|
||||
{#key path}
|
||||
<AutoComplete
|
||||
items={paths}
|
||||
value={path}
|
||||
bind:selectedItem={searchPath}
|
||||
placeholder="Search by path"
|
||||
/>
|
||||
{/key}
|
||||
<Button
|
||||
title="Clear path and time filters"
|
||||
variant="border"
|
||||
on:click={async () => {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
jobs = undefined
|
||||
await goto('/runs?' + $page.url.searchParams.toString())
|
||||
loadJobs()
|
||||
}}
|
||||
size="xs"
|
||||
>
|
||||
<Icon data={faSearchMinus} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
><Slider
|
||||
text="Filter by args {argFilter ? '(set)' : ''}"
|
||||
tooltip={'Filter by a json being a subset of the args. Try \'{"foo": "bar"}\''}
|
||||
><JsonEditor bind:error={argError} bind:code={argFilter} /></Slider
|
||||
></div
|
||||
>
|
||||
<div
|
||||
><Slider
|
||||
text="Filter by result {resultFilter ? '(set)' : ''}"
|
||||
tooltip={'Filter by a json being a subset of the result. Try \'{"foo": "bar"}\''}
|
||||
><JsonEditor bind:error={resultError} bind:code={resultFilter} /></Slider
|
||||
></div
|
||||
<div class="flex flex-row gap-x-2 mb-2 w-full">
|
||||
{#key path}
|
||||
<AutoComplete
|
||||
items={paths}
|
||||
value={path}
|
||||
bind:selectedItem={searchPath}
|
||||
placeholder="Search by path of script/flow"
|
||||
/>
|
||||
{/key}
|
||||
<Button
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
goto('/runs?' + $page.url.searchParams.toString())
|
||||
fetchJobs(createdBefore, undefined)
|
||||
}}
|
||||
size="xs"
|
||||
>
|
||||
<Icon data={faSearchMinus} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Skeleton loading={!jobs} layout={[[6], 1, [6], 1, [6], 1, [6], 1, [6]]} />
|
||||
|
||||
{#if jobs}
|
||||
|
||||
@@ -200,9 +200,9 @@
|
||||
<Skeleton {loading} layout={[[{ h: 1.5, w: 40 }], 1, [{ h: 1, w: 30 }]]} />
|
||||
|
||||
<div class="prose-sm mx-auto mt-6">
|
||||
<div
|
||||
class="flex flex-row-reverse w-full flex-wrap md:flex-nowrap justify-between gap-x-2 gap-y-4"
|
||||
>
|
||||
<div class="flex flex-row w-full justify-between item-center">
|
||||
<h1 class="mb-1 truncate">{defaultIfEmptyString(script.summary, script.path)}</h1>
|
||||
|
||||
<div class="flex flex-row-reverse gap-2 h-full">
|
||||
<Button
|
||||
href={`/scripts/run/${script.hash}`}
|
||||
@@ -243,16 +243,13 @@
|
||||
View Runs
|
||||
</Button>
|
||||
</div>
|
||||
<div class="grow truncate">
|
||||
<h1 class="mb-1 truncate">{defaultIfEmptyString(script.summary, script.path)}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !emptyString(script.summary)}
|
||||
<span class="text-lg font-semibold">{script.path}</span>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-row gap-x-2 flex-wrap items-center mt-2">
|
||||
<div class="flex flex-row gap-x-2 flex-wrap items-center mt-4">
|
||||
<span class="text-sm text-gray-600">
|
||||
Edited {displayDaysAgo(script.created_at || '')} by {script.created_by || 'unknown'}
|
||||
</span>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import RunForm from '$lib/components/RunForm.svelte'
|
||||
import { Alert, Badge, Button, Kbd, Skeleton } from '$lib/components/common'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import { faEye, faPen, faPlay, faScroll } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faPlay, faScroll } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
$: hash = $page.params.hash
|
||||
let script: Script | undefined
|
||||
@@ -112,25 +112,15 @@
|
||||
<div
|
||||
class="flex flex-row-reverse w-full flex-wrap md:flex-nowrap justify-between gap-x-1 gap-y-2"
|
||||
>
|
||||
<div class="flex flex-row gap-4">
|
||||
{#if !$userStore?.operator && can_write}
|
||||
<div>
|
||||
<Button
|
||||
size="sm"
|
||||
startIcon={{ icon: faPen }}
|
||||
disabled={script == undefined}
|
||||
variant="border"
|
||||
href="/scripts/edit/{script?.hash}?step=2">Edit</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="md:pr-4">
|
||||
<div class="flex flex-row">
|
||||
<div>
|
||||
<Button
|
||||
size="sm"
|
||||
startIcon={{ icon: faEye }}
|
||||
startIcon={{ icon: faScroll }}
|
||||
disabled={script == undefined}
|
||||
btnClasses="mr-4"
|
||||
variant="border"
|
||||
href="/scripts/get/{script?.hash}?workspace_id={$workspaceStore}">View</Button
|
||||
href="/scripts/get/{script?.hash}?workspace_id={$workspaceStore}"
|
||||
>View script</Button
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -4,8 +4,8 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
wmill = ">=1.75.0"
|
||||
wmill_pg = ">=1.75.0"
|
||||
wmill = ">=1.74.2"
|
||||
wmill_pg = ">=1.74.2"
|
||||
sendgrid = "*"
|
||||
mysql-connector-python = "*"
|
||||
pymongo = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.75.0
|
||||
version: 1.74.2
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill"
|
||||
version = "1.75.0"
|
||||
version = "1.74.2"
|
||||
description = "A client library for accessing Windmill server wrapping the Windmill client API"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
@@ -16,7 +16,7 @@ include = ["wmill/py.typed"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7"
|
||||
windmill-api = "^1.75.0"
|
||||
windmill-api = "^1.74.2"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=1.0.2", "poetry-dynamic-versioning"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill-pg"
|
||||
version = "1.75.0"
|
||||
version = "1.74.2"
|
||||
description = "An extension client for the wmill client library focused on pg"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.75.0
|
||||
1.74.2
|
||||
|
||||
Reference in New Issue
Block a user