Compare commits

..

6 Commits

Author SHA1 Message Date
Ádám Kovács
b76426ba83 faet(frontend): Add tour manager store 2023-03-10 20:11:25 +01:00
Ádám Kovács
1cb2a177c6 Merge branch 'main' into tutorials 2023-03-10 19:13:04 +01:00
Ádám Kovács
db87577a1b feat(frontend): Add app editor tour 2023-03-10 19:11:34 +01:00
Ádám Kovács
c219eb0ee9 feat(frontend): Add flow editor tour 2023-03-10 18:36:53 +01:00
Ádám Kovács
bfe5b56c99 feat(frontend): Add script editor tour 2023-03-10 09:38:34 +01:00
Ádám Kovács
4453690521 feat(frontend): Add onboarding tour to home page 2023-03-09 20:11:27 +01:00
175 changed files with 2915 additions and 3561 deletions

View File

@@ -1,39 +1,6 @@
# Changelog
## [1.76.0](https://github.com/windmill-labs/windmill/compare/v1.75.0...v1.76.0) (2023-03-13)
### Features
* **frontend:** add frontend (JS) scripts to apps ([f0b1b1f](https://github.com/windmill-labs/windmill/commit/f0b1b1f752731ba434b960a75624118152f53c00))
* **frontend:** Copy, Cut and Paste ([#1279](https://github.com/windmill-labs/windmill/issues/1279)) ([82c139e](https://github.com/windmill-labs/windmill/commit/82c139ed0992be401e250cfb7ecc0fca61b76772))
* **frontend:** disabled for action buttons can now depend on row ([75f87e7](https://github.com/windmill-labs/windmill/commit/75f87e7e1117a9c12afcf626379e94b134a9a493))
* **frontend:** improve drag-n-drop behavior ([cfd489a](https://github.com/windmill-labs/windmill/commit/cfd489a55059e7b6843f99bab261c70b3852e6a2))
### Bug Fixes
* **backend:** improve worker ping api ([c958480](https://github.com/windmill-labs/windmill/commit/c958480ce83844a989f58dd5a70eb288582e2194))
* **frontend:** General fixes and updates ([#1281](https://github.com/windmill-labs/windmill/issues/1281)) ([3e5a179](https://github.com/windmill-labs/windmill/commit/3e5a179eb8cd8001f49c92305141dade1571e20f))
## [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)

223
backend/Cargo.lock generated
View File

@@ -41,17 +41,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "ahash"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
version = "0.7.20"
@@ -265,9 +254,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axum"
version = "0.6.11"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13d8068b6ccb8b34db9de397c7043f91db8b4c66414952c6db944f238c4d3db3"
checksum = "2fb79c228270dcf2426e74864cabc94babb5dbab01a4314e702d2f16540e1591"
dependencies = [
"async-trait",
"axum-core",
@@ -292,6 +281,7 @@ dependencies = [
"sync_wrapper",
"tokio",
"tower",
"tower-http 0.3.5",
"tower-layer",
"tower-service",
]
@@ -477,7 +467,7 @@ version = "8.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "031718ddb8f78aa5def78a09e90defe30151d1f6c672f937af4dd916429ed996"
dependencies = [
"semver 1.0.17",
"semver 1.0.16",
"serde",
"toml",
"url",
@@ -500,9 +490,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.24"
version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
dependencies = [
"iana-time-zone",
"js-sys",
@@ -1133,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",
@@ -1148,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",
@@ -1158,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",
@@ -1186,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"
@@ -1207,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",
@@ -1218,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",
@@ -1347,16 +1337,7 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash 0.7.6",
]
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
dependencies = [
"ahash 0.8.3",
"ahash",
]
[[package]]
@@ -1365,7 +1346,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
dependencies = [
"hashbrown 0.12.3",
"hashbrown",
]
[[package]]
@@ -1522,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",
@@ -1610,7 +1591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"hashbrown",
"serde",
]
@@ -2534,9 +2515,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.52"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224"
checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
dependencies = [
"unicode-ident",
]
@@ -2544,7 +2525,7 @@ dependencies = [
[[package]]
name = "progenitor"
version = "0.2.1-dev"
source = "git+https://github.com/oxidecomputer/progenitor#ebc18cc6cd32d656ff787eb03777c1bbbbd8a0f2"
source = "git+https://github.com/oxidecomputer/progenitor#e9b6970d4485ad5813f93e95570480195528b0a9"
dependencies = [
"anyhow",
"built",
@@ -2562,7 +2543,7 @@ dependencies = [
[[package]]
name = "progenitor-client"
version = "0.2.1-dev"
source = "git+https://github.com/oxidecomputer/progenitor#ebc18cc6cd32d656ff787eb03777c1bbbbd8a0f2"
source = "git+https://github.com/oxidecomputer/progenitor#e9b6970d4485ad5813f93e95570480195528b0a9"
dependencies = [
"bytes",
"futures-core",
@@ -2576,7 +2557,7 @@ dependencies = [
[[package]]
name = "progenitor-impl"
version = "0.2.1-dev"
source = "git+https://github.com/oxidecomputer/progenitor#ebc18cc6cd32d656ff787eb03777c1bbbbd8a0f2"
source = "git+https://github.com/oxidecomputer/progenitor#e9b6970d4485ad5813f93e95570480195528b0a9"
dependencies = [
"getopts",
"heck",
@@ -2598,7 +2579,7 @@ dependencies = [
[[package]]
name = "progenitor-macro"
version = "0.2.1-dev"
source = "git+https://github.com/oxidecomputer/progenitor#ebc18cc6cd32d656ff787eb03777c1bbbbd8a0f2"
source = "git+https://github.com/oxidecomputer/progenitor#e9b6970d4485ad5813f93e95570480195528b0a9"
dependencies = [
"openapiv3",
"proc-macro2",
@@ -2643,9 +2624,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.26"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
dependencies = [
"proc-macro2",
]
@@ -2769,11 +2750,10 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "regress"
version = "0.5.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d995d590bd8ec096d1893f414bf3f5e8b0ee4c9eed9a5642b9766ef2c8e2e8e9"
checksum = "0a92ff21fe8026ce3f2627faaf43606f0b67b014dbc9ccf027181a804f75d92e"
dependencies = [
"hashbrown 0.13.2",
"memchr",
]
@@ -2919,7 +2899,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver 1.0.17",
"semver 1.0.16",
]
[[package]]
@@ -2973,7 +2953,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#8ff947e83a65b74c920edc63aed8a0a5854b8612"
source = "git+https://github.com/RustPython/RustPython#87728c44527272ece739cddbb4942ad7e176dd79"
dependencies = [
"num-bigint",
"rustpython-compiler-core",
@@ -2982,7 +2962,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#8ff947e83a65b74c920edc63aed8a0a5854b8612"
source = "git+https://github.com/RustPython/RustPython#87728c44527272ece739cddbb4942ad7e176dd79"
dependencies = [
"bitflags",
"bstr",
@@ -2995,9 +2975,9 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#8ff947e83a65b74c920edc63aed8a0a5854b8612"
source = "git+https://github.com/RustPython/RustPython#87728c44527272ece739cddbb4942ad7e176dd79"
dependencies = [
"ahash 0.7.6",
"ahash",
"anyhow",
"itertools",
"lalrpop",
@@ -3143,9 +3123,9 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.17"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a"
dependencies = [
"serde",
]
@@ -3167,9 +3147,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.155"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71f2b4817415c6d4210bfe1c7bfcf4801b2d904cb4d0e1a8fdb651013c9e86b8"
checksum = "8cdd151213925e7f1ab45a9bbfb129316bd00799784b174b7cc7bcd16961c49e"
dependencies = [
"serde_derive",
]
@@ -3196,9 +3176,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.155"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d071a94a3fac4aff69d023a7f411e33f40f3483f8c5190b1953822b6b76d7630"
checksum = "4fc80d722935453bcafdc2c9a73cd6fac4dc1938f0346035d84bf99fa9e33217"
dependencies = [
"proc-macro2",
"quote",
@@ -3503,7 +3483,7 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105"
dependencies = [
"ahash 0.7.6",
"ahash",
"atoi",
"base64 0.13.1",
"bitflags",
@@ -3638,9 +3618,9 @@ dependencies = [
[[package]]
name = "string_enum"
version = "0.3.3"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41491e23e7db79343236a6ced96325ff132eb09e29ac4c5b8132b9c55aaaae89"
checksum = "994453cd270ad0265796eb24abf5540091ed03e681c5f3c12bc33e4db33253e1"
dependencies = [
"pmutil",
"proc-macro2",
@@ -3687,11 +3667,11 @@ 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 0.7.6",
"ahash",
"ast_node",
"better_scoped_tls",
"cfg-if",
@@ -4072,9 +4052,9 @@ checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
[[package]]
name = "toml_edit"
version = "0.19.6"
version = "0.19.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08de71aa0d6e348f070457f85af8bd566e2bc452156a423ddf22861b3a953fae"
checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825"
dependencies = [
"indexmap",
"toml_datetime",
@@ -4127,6 +4107,25 @@ dependencies = [
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858"
dependencies = [
"bitflags",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-range-header",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.4.0"
@@ -4275,7 +4274,7 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "typify"
version = "0.0.11-dev"
source = "git+https://github.com/oxidecomputer/typify#bf60070e09d6a3753d5b173d9c605a8bf2ada748"
source = "git+https://github.com/oxidecomputer/typify#18e7faf1957812626e3bf459e20fbe16785de2d4"
dependencies = [
"typify-impl",
"typify-macro",
@@ -4284,7 +4283,7 @@ dependencies = [
[[package]]
name = "typify-impl"
version = "0.0.11-dev"
source = "git+https://github.com/oxidecomputer/typify#bf60070e09d6a3753d5b173d9c605a8bf2ada748"
source = "git+https://github.com/oxidecomputer/typify#18e7faf1957812626e3bf459e20fbe16785de2d4"
dependencies = [
"heck",
"log",
@@ -4302,7 +4301,7 @@ dependencies = [
[[package]]
name = "typify-macro"
version = "0.0.11-dev"
source = "git+https://github.com/oxidecomputer/typify#bf60070e09d6a3753d5b173d9c605a8bf2ada748"
source = "git+https://github.com/oxidecomputer/typify#18e7faf1957812626e3bf459e20fbe16785de2d4"
dependencies = [
"proc-macro2",
"quote",
@@ -4702,9 +4701,9 @@ dependencies = [
[[package]]
name = "whoami"
version = "1.4.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c70234412ca409cc04e864e89523cb0fc37f5e1344ebed5a3ebf4192b6b9f68"
checksum = "45dbc71f0cdca27dc261a9bd37ddec174e4a0af2b900b890f378460f745426e3"
dependencies = [
"wasm-bindgen",
"web-sys",
@@ -4743,7 +4742,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.76.0"
version = "1.74.2"
dependencies = [
"anyhow",
"axum",
@@ -4770,7 +4769,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.76.0"
version = "1.74.2"
dependencies = [
"anyhow",
"argon2",
@@ -4809,7 +4808,7 @@ dependencies = [
"tokio-util",
"tower",
"tower-cookies",
"tower-http",
"tower-http 0.4.0",
"tracing",
"tracing-subscriber",
"urlencoding",
@@ -4825,7 +4824,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.76.0"
version = "1.74.2"
dependencies = [
"base64 0.21.0",
"chrono",
@@ -4840,7 +4839,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.76.0"
version = "1.74.2"
dependencies = [
"chrono",
"serde",
@@ -4853,7 +4852,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.76.0"
version = "1.74.2"
dependencies = [
"anyhow",
"axum",
@@ -4878,7 +4877,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.76.0"
version = "1.74.2"
dependencies = [
"serde",
"serde_json",
@@ -4886,7 +4885,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.76.0"
version = "1.74.2"
dependencies = [
"anyhow",
"itertools",
@@ -4901,7 +4900,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.76.0"
version = "1.74.2"
dependencies = [
"anyhow",
"itertools",
@@ -4913,7 +4912,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.76.0"
version = "1.74.2"
dependencies = [
"anyhow",
"itertools",
@@ -4928,7 +4927,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.76.0"
version = "1.74.2"
dependencies = [
"anyhow",
"deno_core",
@@ -4942,7 +4941,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.76.0"
version = "1.74.2"
dependencies = [
"anyhow",
"chrono",
@@ -4965,7 +4964,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.76.0"
version = "1.74.2"
dependencies = [
"anyhow",
"async-recursion",
@@ -5024,9 +5023,9 @@ dependencies = [
[[package]]
name = "windows-targets"
version = "0.42.2"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
@@ -5039,45 +5038,45 @@ dependencies = [
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "winnow"

View File

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

View File

@@ -2501,57 +2501,6 @@
},
"query": "SELECT set_config('session.user', $1, true)"
},
"6d50a8dc9cfc040b6f37b58053daa0709671e008a10ab4189114ceff66efe603": {
"describe": {
"columns": [
{
"name": "worker",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "worker_instance",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "last_ping",
"ordinal": 2,
"type_info": "Int4"
},
{
"name": "started_at",
"ordinal": 3,
"type_info": "Timestamptz"
},
{
"name": "ip",
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "jobs_executed",
"ordinal": 5,
"type_info": "Int4"
}
],
"nullable": [
false,
false,
null,
false,
false,
false
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
}
},
"query": "SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2"
},
"6dfd1ac7168f098a9e7c92d530a470c0a156041d5081053d43d9efb00d81c464": {
"describe": {
"columns": [
@@ -2573,6 +2522,57 @@
},
"query": "SELECT policy from app WHERE path = $1 AND workspace_id = $2"
},
"6fc2cfae9df83eb24ea33e4c9567740100f4dd2285afc3ef474fc70041b0567b": {
"describe": {
"columns": [
{
"name": "worker",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "worker_instance",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "ping_at",
"ordinal": 2,
"type_info": "Timestamptz"
},
{
"name": "started_at",
"ordinal": 3,
"type_info": "Timestamptz"
},
{
"name": "ip",
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "jobs_executed",
"ordinal": 5,
"type_info": "Int4"
}
],
"nullable": [
false,
false,
false,
false,
false,
false
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
}
},
"query": "SELECT * FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2"
},
"701f215eb14ba67a79afea15d7effc0dd394ba6c4a72c95d4560c5a377015d4e": {
"describe": {
"columns": [],
@@ -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": [],

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.76.0
version: 1.74.2
title: Windmill API
contact:
@@ -204,6 +204,7 @@ paths:
schema:
type: string
/w/{workspace}/users/is_owner/{path}:
get:
summary: is owner of path
@@ -1548,7 +1549,7 @@ paths:
required: true
content:
application/json:
schema:
schema:
type: object
properties:
value: {}
@@ -2102,6 +2103,7 @@ paths:
items:
type: string
/w/{workspace}/scripts/create:
post:
summary: create script
@@ -2833,6 +2835,7 @@ paths:
schema:
$ref: "#/components/schemas/AppWithLastVersion"
/w/{workspace}/apps/secret_of/{path}:
get:
summary: get public secret of app
@@ -3165,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
@@ -3197,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
@@ -3236,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
@@ -3441,7 +3438,7 @@ paths:
/w/{workspace}/jobs/resume_urls/{id}/{resume_id}:
get:
summary: get resume urls given a job_id, resume_id and a nonce to resume a flow
summary: get resume urls given a job_id, resume_id and a nonce to resume a flow
operationId: getResumeUrls
tags:
- job
@@ -3467,7 +3464,7 @@ paths:
properties:
approvalPage:
type: string
resume:
resume:
type: string
cancel:
type: string
@@ -4112,7 +4109,7 @@ paths:
type: string
owners:
type: array
items:
items:
type: string
extra_perms:
additionalProperties:
@@ -4146,7 +4143,7 @@ paths:
properties:
owners:
type: array
items:
items:
type: string
extra_perms:
additionalProperties:
@@ -4317,8 +4314,7 @@ paths:
required: true
schema:
type: string
enum:
[script, group_, resource, schedule, variable, flow, folder, app]
enum: [script, group_, resource, schedule, variable, flow, folder, app]
responses:
"200":
description: acls
@@ -4343,8 +4339,7 @@ paths:
required: true
schema:
type: string
enum:
[script, group_, resource, schedule, variable, flow, folder, app]
enum: [script, group_, resource, schedule, variable, flow, folder, app]
requestBody:
description: acl to add
required: true
@@ -4380,8 +4375,7 @@ paths:
required: true
schema:
type: string
enum:
[script, group_, resource, schedule, variable, flow, folder, app]
enum: [script, group_, resource, schedule, variable, flow, folder, app]
requestBody:
description: acl to add
required: true
@@ -4401,18 +4395,19 @@ paths:
schema:
type: string
/w/{workspace}/capture_u/{path}:
post:
summary: update flow preview capture
operationId: updateCapture
tags:
- capture
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"204":
description: flow preview captured
post:
summary: update flow preview capture
operationId: updateCapture
tags:
- capture
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"204":
description: flow preview captured
/w/{workspace}/capture/{path}:
put:
@@ -4619,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:
@@ -4651,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
@@ -5568,8 +5550,9 @@ components:
type: string
worker_instance:
type: string
last_ping:
type: number
ping_at:
type: string
format: date-time
started_at:
type: string
format: date-time

View File

@@ -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("*");

View File

@@ -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,14 +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("'", "''")));
}
sqlb
}
#[derive(Deserialize, Clone)]
@@ -1730,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>,
@@ -1739,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(

View File

@@ -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,

View File

@@ -28,7 +28,7 @@ pub fn global_service() -> Router {
struct WorkerPing {
worker: String,
worker_instance: String,
last_ping: Option<i32>,
ping_at: chrono::DateTime<chrono::Utc>,
started_at: chrono::DateTime<chrono::Utc>,
ip: String,
jobs_executed: i32,
@@ -45,7 +45,7 @@ async fn list_worker_pings(
let rows = sqlx::query_as!(
WorkerPing,
"SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2",
"SELECT * FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2",
per_page as i64,
offset as i64
)

View File

@@ -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();

View File

@@ -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 {

View File

@@ -264,7 +264,7 @@ const ROOT_CACHE_DIR: &str = "/tmp/windmill/cache/";
const PIP_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "pip");
const DENO_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "deno");
const GO_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "go");
const NUM_SECS_PING: u64 = 5;
const NUM_SECS_ENV_CHECK: u64 = 15;
const NUM_SECS_SYNC: u64 = 60 * 10;
const INCLUDE_DEPS_PY_SH_CONTENT: &str = include_str!("../nsjail/download_deps.py.sh");
@@ -553,7 +553,7 @@ pub async fn run_worker(
let do_break = async {
if last_ping.elapsed().as_secs() > NUM_SECS_PING {
if last_ping.elapsed().as_secs() > NUM_SECS_ENV_CHECK {
sqlx::query!(
"UPDATE worker_ping SET ping_at = now(), jobs_executed = $1 WHERE worker = $2",
jobs_executed,

View File

@@ -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 }),
);
}
}

View File

@@ -18,7 +18,7 @@ export type Context = {
};
async function tryResolveWorkspace(
opts: GlobalOptions
opts: GlobalOptions,
): Promise<
{ isError: false; value: Workspace } | { isError: true; error: string }
> {
@@ -49,20 +49,17 @@ async function tryResolveWorkspace(
}
export async function resolveWorkspace(
opts: GlobalOptions
opts: GlobalOptions,
): Promise<Workspace> {
const res = await tryResolveWorkspace(opts);
if (res.isError) {
console.log(colors.red.bold(res.error));
return Deno.exit(-1);
} else {
return res.value;
}
}
export async function requireLogin(
opts: GlobalOptions
): Promise<GlobalUserInfo> {
export async function requireLogin(opts: GlobalOptions): Promise<GlobalUserInfo> {
const workspace = await resolveWorkspace(opts);
let token = await tryGetLoginInfo(opts);
@@ -76,7 +73,7 @@ export async function requireLogin(
return await UserService.globalWhoami();
} catch {
console.log(
"! Could not reach API given existing credentials. Attempting to reauth..."
"! Could not reach API given existing credentials. Attempting to reauth...",
);
const newToken = await loginInteractive(workspace.remote);
if (!newToken) {
@@ -88,14 +85,14 @@ export async function requireLogin(
setClient(
token,
workspace.remote.substring(0, workspace.remote.length - 1)
workspace.remote.substring(0, workspace.remote.length - 1),
);
return await UserService.globalWhoami();
}
}
export async function tryResolveVersion(
opts: GlobalOptions
opts: GlobalOptions,
): Promise<number | undefined> {
if ((opts as any).__cache_version) {
return (opts as any).__cache_version;
@@ -105,12 +102,12 @@ export async function tryResolveVersion(
if (workspaceRes.isError) return undefined;
const response = await fetch(
new URL(new URL(workspaceRes.value.remote).origin + "/api/version")
new URL(new URL(workspaceRes.value.remote).origin + "/api/version"),
);
const version = await response.text();
try {
return Number.parseInt(
version.split("-", 1)[0].replaceAll(".", "").replace("v", "")
version.split("-", 1)[0].replaceAll(".", "").replace("v", ""),
);
} catch {
return undefined;
@@ -119,7 +116,7 @@ export async function tryResolveVersion(
export async function validatePath(
opts: GlobalOptions,
path: string
path: string,
): Promise<boolean> {
const backendVersion = await tryResolveVersion(opts);
if (path.startsWith("f")) {
@@ -127,16 +124,19 @@ export async function validatePath(
return true;
}
console.log(
`Attempting to use folders, but the current remote does not have support. Remote version is ${backendVersion} but folders are supported from 1560.`
`Attempting to use folders, but the current remote does not have support. Remote version is ${backendVersion} but folders are supported from 1560.`,
);
return false;
}
if (!(path.startsWith("g") || path.startsWith("u"))) {
if (
!(path.startsWith("g") ||
path.startsWith("u"))
) {
console.log(
colors.red(
"Given remote path looks invalid. Remote paths are typically of the form <u|g|f>/<username|group|folder>/..."
)
"Given remote path looks invalid. Remote paths are typically of the form <u|g|f>/<username|group|folder>/...",
),
);
return false;
}

View File

@@ -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 }),
);
}
}

View File

@@ -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));

View File

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

View File

@@ -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 = {};

View File

@@ -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 }),
);
}
}

View File

@@ -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
}

View File

@@ -13,7 +13,7 @@ import {
ResourceService,
VariableService,
AppService,
FlowService,
FlowService
} from "./deps.ts";
import {
Difference,
@@ -25,13 +25,16 @@ import {
import { downloadZip } from "./pull.ts";
import { FolderFile } from "./folder.ts";
import { ResourceTypeFile } from "./resource-type.ts";
import { handleScriptMetadata, ScriptFile } from "./script.ts";
import {
handleScriptMetadata,
ScriptFile,
} from "./script.ts";
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;
@@ -41,7 +44,10 @@ type DynFSElement = {
};
async function FSFSElement(p: string): Promise<DynFSElement> {
function _internal_element(localP: string, isDir: boolean): DynFSElement {
function _internal_element(
localP: string,
isDir: boolean,
): DynFSElement {
return {
isDirectory: isDir,
path: localP.substring(p.length + 1),
@@ -107,14 +113,16 @@ function ZipFSElement(zip: JSZip): DynFSElement {
async function* readDirRecursiveWithIgnore(
ignore: (path: string, isDirectory: boolean) => boolean,
root: DynFSElement
): AsyncGenerator<{
path: string;
ignored: boolean;
isDirectory: boolean;
getContentBytes(): Promise<Uint8Array>;
getContentText(): Promise<string>;
}> {
root: DynFSElement,
): AsyncGenerator<
{
path: string;
ignored: boolean;
isDirectory: boolean;
getContentBytes(): Promise<Uint8Array>;
getContentText(): Promise<string>;
}
> {
const stack: {
path: string;
isDirectory: boolean;
@@ -122,20 +130,18 @@ async function* readDirRecursiveWithIgnore(
c(): AsyncIterable<DynFSElement>;
getContentBytes(): Promise<Uint8Array>;
getContentText(): Promise<string>;
}[] = [
{
path: root.path,
ignored: ignore(root.path, root.isDirectory),
isDirectory: root.isDirectory,
c: root.getChildren,
getContentBytes(): Promise<Uint8Array> {
throw undefined;
},
getContentText(): Promise<string> {
throw undefined;
},
}[] = [{
path: root.path,
ignored: ignore(root.path, root.isDirectory),
isDirectory: root.isDirectory,
c: root.getChildren,
getContentBytes(): Promise<Uint8Array> {
throw undefined;
},
];
getContentText(): Promise<string> {
throw undefined;
},
}];
while (stack.length > 0) {
const e = stack.pop()!;
@@ -155,17 +161,17 @@ async function* readDirRecursiveWithIgnore(
}
type Added = { name: "added"; path: string; content: string };
type Deleted = { name: "deleted"; path: string };
type Edit = { name: "edited"; path: string; before: string; after: string };
type Deleted = { name: "deleted"; path: string; };
type Edit = { name: "edited"; path: string; before: string; after: string; };
type Change = Added | Deleted | Edit;
async function elementsToMap(
els: DynFSElement,
ignore: (path: string, isDirectory: boolean) => boolean
): Promise<{ [key: string]: string }> {
async function elementsToMap(els: DynFSElement, ignore: (path: string, isDirectory: boolean) => boolean): Promise<{ [key: string]: string }> {
const map: { [key: string]: string } = {};
for await (const entry of readDirRecursiveWithIgnore(ignore, els)) {
for await (const entry of readDirRecursiveWithIgnore(
ignore,
els,
)) {
if (entry.isDirectory || entry.ignored) continue;
const content = await entry.getContentText();
map[entry.path] = content;
@@ -173,26 +179,20 @@ async function elementsToMap(
return map;
}
async function compareDynFSElement(
els1: DynFSElement,
els2: DynFSElement | undefined,
ignore: (path: string, isDirectory: boolean) => boolean
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[] = [];
for (const [k, v] of Object.entries(m1)) {
if (m2[k] === undefined) {
changes.push({ name: "added", path: k, content: v });
} else if (
m2[k] != v &&
(!k.endsWith(".json") || !equal(JSON.parse(v), JSON.parse(m2[k])))
) {
} else if (m2[k] != v && (!k.endsWith(".json") || !equal(JSON.parse(v), JSON.parse(m2[k])))) {
// await Deno.writeTextFile("/tmp/k", m2[k])
// await Deno.writeTextFile("/tmp/v", v)
// console.log(k)
@@ -208,54 +208,54 @@ async function compareDynFSElement(
}
}
return changes;
return changes
}
const isNotWmillFile = (p: string, isDirectory: boolean) => {
if (p.endsWith("/")) {
return false;
return false
}
if (isDirectory) {
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/");
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/")
}
try {
const typ = getTypeStrFromPath(p);
if (typ == "resource-type") {
return p.includes("/");
const typ = getTypeStrFromPath(p)
if (typ == 'resource-type') {
return p.includes('/')
} else {
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/");
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/")
}
} catch {
return true;
return true
}
};
}
const isWhitelisted = (p: string) => {
return p == "./" || p == "" || p == "u" || p == "f" || p == "g";
};
return p == "./" || p == "" || p == "u" || p == "f" || p == "g"
}
async function ignoreF() {
try {
const ignore: {
accepts(file: string): boolean;
denies(file: string): boolean;
} = gitignore_parser.compile(await Deno.readTextFile(".wmillignore"));
} = gitignore_parser.compile(
await Deno.readTextFile(".wmillignore"),
);
return (p: string, isDirectory: boolean) => {
return (
!isWhitelisted(p) &&
(isNotWmillFile(p, isDirectory) || ignore.denies(p))
);
};
return !isWhitelisted(p) && (isNotWmillFile(p, isDirectory) || ignore.denies(p));
}
} catch (e) {
return (p: string, isDirectory: boolean) =>
!isWhitelisted(p) && isNotWmillFile(p, isDirectory);
return (p: string, isDirectory: boolean) => !isWhitelisted(p) && isNotWmillFile(p, isDirectory)
}
}
async function pull(
opts: GlobalOptions & { raw: boolean; yes: boolean; failConflicts: boolean }
opts: GlobalOptions & { raw: boolean; yes: boolean, failConflicts: boolean },
) {
if (!opts.raw) {
await ensureDir(path.join(Deno.cwd(), ".wmill"));
}
@@ -263,96 +263,74 @@ async function pull(
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
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());
console.log(colors.gray("Computing the files to update locally to match remote (taking .wmillignore into account)"));
const remote = ZipFSElement((await downloadZip(workspace))!)
const local = await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill"))
const changes = await compareDynFSElement(remote, local, await ignoreF(), opts.raw)
console.log(`remote -> local: ${changes.length} changes to apply`);
if (changes.length > 0) {
prettyChanges(changes);
prettyChanges(changes)
if (
!opts.yes &&
!opts.raw &&
!(await Confirm.prompt({
message: `Do you want to apply these ${changes.length} changes?`,
default: true,
}))
!opts.yes && !opts.raw && !(await Confirm.prompt({ message: `Do you want to apply these ${changes.length} changes?`, default: true }))
) {
return;
return
}
const conflicts = [];
const conflicts = []
console.log(colors.gray(`Applying changes to files ...`));
for await (const change of changes) {
const target = path.join(Deno.cwd(), change.path);
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path)
if (change.name === "edited") {
try {
const currentLocal = await Deno.readTextFile(target);
if (currentLocal !== change.before && currentLocal !== change.after) {
console.log(
colors.red(
`Conflict detected on ${change.path}\nBoth local and remote have been modified.`
)
);
const currentLocal = await Deno.readTextFile(target)
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,
});
conflicts.push({ local: currentLocal, change, path: change.path })
continue;
} else if (opts.yes) {
console.log(
colors.red(
`Override local version with remote since --yes was passed and no --fail-conflicts.`
)
);
} else {
showConflict(change.path, currentLocal, change.after);
if (
await Confirm.prompt(
"Preserve local (push to change remote and avoid seeing this again)?"
)
) {
console.log(colors.red(`Override local version with remote since --yes was passed and no --fail-conflicts.`))
}
else {
showConflict(change.path, currentLocal, change.after)
if (await Confirm.prompt("Preserve local (push to change remote and avoid seeing this again)?")) {
continue;
}
}
}
} catch {}
} catch { }
if (change.path.endsWith(".json")) {
const diffs = microdiff(
JSON.parse(change.before),
JSON.parse(change.after),
{ cyclesFix: false }
);
const diffs =
microdiff(
JSON.parse(change.before),
JSON.parse(change.after),
{ cyclesFix: false },
)
console.log(
`Editing ${getTypeStrFromPath(change.path)} json ${change.path}`
console.log(`Editing ${getTypeStrFromPath(change.path)} json ${change.path}`)
await applyDiff(
diffs,
target,
);
await applyDiff(diffs, target);
} else {
console.log(`Editing script ${change.path}`);
console.log(`Editing script ${change.path}`)
await Deno.writeTextFile(target, change.after);
}
if (!opts.raw) {
await ensureDir(path.dirname(stateTarget));
await ensureDir(path.dirname(stateTarget))
await Deno.copyFile(target, stateTarget);
}
} else if (change.name === "added") {
await ensureDir(path.dirname(target));
await ensureDir(path.dirname(target))
if (!opts.raw) {
await ensureDir(path.dirname(stateTarget));
console.log(
`Adding ${getTypeStrFromPath(change.path)} ${change.path}`
);
await ensureDir(path.dirname(stateTarget))
console.log(`Adding ${getTypeStrFromPath(change.path)} ${change.path}`)
}
await Deno.writeTextFile(target, change.content);
if (!opts.raw) {
@@ -360,10 +338,8 @@ async function pull(
}
} else if (change.name === "deleted") {
try {
console.log(
`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`
);
await Deno.remove(target);
console.log(`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`)
await Deno.remove(target)
if (!opts.raw) {
await Deno.remove(stateTarget);
}
@@ -376,29 +352,24 @@ async function pull(
}
if (opts.failConflicts) {
if (conflicts.length > 0) {
console.error(colors.red(`Conflicts were found`));
console.log("Conflicts:");
console.error(colors.red(`Conflicts were found`))
console.log("Conflicts:")
for (const conflict of conflicts) {
showConflict(conflict.path, conflict.local, conflict.change.after);
showConflict(conflict.path, conflict.local, conflict.change.after)
}
console.log(
colors.red(`Please resolve theses conflicts manually by either:
console.log(colors.red(`Please resolve theses conflicts manually by either:
- reverting the content back to its remote (\`wmill pull\` and refuse to preserve local when prompted)
- pushing the changes with \`wmill push --skip-pull\` to override wmill with all your local changes
`)
);
Deno.exit(1);
`))
Deno.exit(1)
}
}
console.log(
colors.green.underline(
`Done! All ${changes.length} changes applied locally.`
)
);
console.log(colors.green.underline(`Done! All ${changes.length} changes applied locally.`));
}
function showConflict(path: string, local: string, remote: string) {
console.log(colors.yellow(`- ${path}`));
console.log(colors.yellow(`- ${path}`))
let finalString = "";
for (const character of diffCharacters(local, remote)) {
@@ -414,8 +385,8 @@ async function pull(
}
}
console.log(finalString);
console.log("\x1b[31mlocal\x1b[31m - \x1b[32mremote\x1b[32m");
console.log();
console.log("\x1b[31mlocal\x1b[31m - \x1b[32mremote\x1b[32m")
console.log()
}
async function applyDiff(diffs: Difference[], file: string) {
ensureDir(path.dirname(file));
@@ -444,20 +415,17 @@ async function pull(
}
}
function prettyChanges(changes: Change[]) {
for (const change of changes) {
if (change.name === "added") {
console.log(
colors.green(`+ ${getTypeStrFromPath(change.path)} ` + change.path)
);
console.log(colors.green(`+ ${getTypeStrFromPath(change.path)} ` + change.path));
} else if (change.name === "deleted") {
console.log(
colors.red(`- ${getTypeStrFromPath(change.path)} ` + change.path)
);
console.log(colors.red(`- ${getTypeStrFromPath(change.path)} ` + change.path));
} else if (change.name === "edited") {
console.log(
colors.yellow(`~ ${getTypeStrFromPath(change.path)} ` + change.path)
);
console.log(colors.yellow(`~ ${getTypeStrFromPath(change.path)} ` + change.path));
}
}
}
@@ -484,208 +452,138 @@ function prettyDiff(diffs: Difference[]) {
}
}
function removeSuffix(str: string, suffix: string) {
return str.slice(0, str.length - suffix.length);
}
async function push(
opts: GlobalOptions & {
raw: boolean;
yes: boolean;
skipPull: boolean;
failConflicts: boolean;
}
) {
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.")
);
await pull(opts);
console.log(colors.green("Pull done, now pushing."));
console.log();
console.log(colors.gray("You need to be up-to-date before pushing, pulling first."))
await pull(opts)
console.log(colors.green("Pull done, now pushing."))
console.log()
}
}
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
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 local = await FSFSElement(path.join(Deno.cwd(), ""));
const changes = await compareDynFSElement(local, remote, await ignoreF());
console.log(colors.gray("Computing the files to update on the remote to match local (taking .wmillignore into account)"));
const remote = ZipFSElement((await downloadZip(workspace))!)
const local = await FSFSElement(path.join(Deno.cwd(), ""))
const changes = await compareDynFSElement(local, remote, await ignoreF(), opts.raw)
console.log(`remote <- local: ${changes.length} changes to apply`);
if (changes.length > 0) {
prettyChanges(changes);
prettyChanges(changes)
if (
!opts.yes &&
!(await Confirm.prompt({
message: `Do you want to apply these ${changes.length} changes?`,
default: true,
}))
!opts.yes && !(await Confirm.prompt({ message: `Do you want to apply these ${changes.length} changes?`, default: true }))
) {
return;
return
}
console.log(colors.gray(`Applying changes to files ...`));
const alreadySynced: string[] = [];
const alreadySynced: string[] = []
for await (const change of changes) {
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path)
if (change.name === "edited") {
if (
await handleScriptMetadata(
change.path,
workspace.workspaceId,
alreadySynced
)
) {
if (await handleScriptMetadata(change.path, workspace.workspaceId, alreadySynced)) {
if (!opts.raw) {
await Deno.writeTextFile(stateTarget, change.after);
}
continue;
} else if (
await handleFile(
change.path,
change.after,
workspace.workspaceId,
alreadySynced
)
) {
continue
} else if (await handleFile(change.path, change.after, workspace.workspaceId, alreadySynced)) {
if (!opts.raw) {
await Deno.writeTextFile(stateTarget, change.after);
}
continue;
continue
}
if (!opts.raw) {
await ensureDir(path.dirname(stateTarget));
console.log(
`Editing ${getTypeStrFromPath(change.path)} ${change.path}`
);
await ensureDir(path.dirname(stateTarget))
console.log(`Editing ${getTypeStrFromPath(change.path)} ${change.path}`)
}
const obj = inferTypeFromPath(change.path, JSON.parse(change.after));
const obj = inferTypeFromPath(change.path, JSON.parse(change.after))
const diff = microdiff(
inferTypeFromPath(change.path, JSON.parse(change.before)),
obj,
{ cyclesFix: false }
);
const diff = microdiff(inferTypeFromPath(change.path, JSON.parse(change.before)), obj, { cyclesFix: false });
await applyDiff(
workspace.workspaceId,
change.path.split(".")[0],
obj,
diff
diff,
);
if (!opts.raw) {
await Deno.writeTextFile(stateTarget, change.after);
}
} else if (change.name === "added") {
if (change.path.endsWith(".script.json")) {
continue;
} else if (
await handleFile(
change.path,
change.content,
workspace.workspaceId,
alreadySynced
)
) {
continue;
continue
} else if (await handleFile(change.path, change.content, workspace.workspaceId, alreadySynced)) {
continue
}
if (!opts.raw) {
await ensureDir(path.dirname(stateTarget));
console.log(
`Adding ${getTypeStrFromPath(change.path)} ${change.path}`
);
await ensureDir(path.dirname(stateTarget))
console.log(`Adding ${getTypeStrFromPath(change.path)} ${change.path}`)
}
const obj = inferTypeFromPath(change.path, JSON.parse(change.content));
const obj = inferTypeFromPath(change.path, JSON.parse(change.content))
const diff = microdiff({}, obj, { cyclesFix: false });
await applyDiff(
workspace.workspaceId,
change.path.split(".")[0],
obj,
diff
diff,
);
if (!opts.raw) {
await Deno.writeTextFile(stateTarget, change.content);
}
} else if (change.name === "deleted") {
if (!change.path.includes(".json")) {
continue;
continue
}
console.log(
`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`
);
const typ = getTypeStrFromPath(change.path);
console.log(`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`)
const typ = getTypeStrFromPath(change.path)
const workspaceId = workspace.workspaceId;
switch (typ) {
case "script": {
const script = await ScriptService.getScriptByPath({
workspace: workspaceId,
path: removeSuffix(change.path, ".script.json"),
});
await ScriptService.deleteScriptByHash({
workspace: workspaceId,
hash: script.hash,
});
const script = await ScriptService.getScriptByPath({ workspace: workspaceId, path: removeSuffix(change.path, ".script.json") })
await ScriptService.deleteScriptByHash({ workspace: workspaceId, hash: script.hash })
break;
}
case "folder":
await FolderService.deleteFolder({
workspace: workspaceId,
name: change.path.split("/")[1],
});
await FolderService.deleteFolder({ workspace: workspaceId, name: change.path.split('/')[1] })
break;
case "resource":
await ResourceService.deleteResource({
workspace: workspaceId,
path: removeSuffix(change.path, ".resource.json"),
});
await ResourceService.deleteResource({ workspace: workspaceId, path: removeSuffix(change.path, ".resource.json") })
break;
case "resource-type":
await ResourceService.deleteResourceType({
workspace: workspaceId,
path: removeSuffix(change.path, ".resource-type.json"),
});
break;
await ResourceService.deleteResourceType({ workspace: workspaceId, path: removeSuffix(change.path, ".resource-type.json") })
break
case "flow":
await FlowService.deleteFlowByPath({
workspace: workspaceId,
path: removeSuffix(change.path, ".flow.json"),
});
break;
await FlowService.deleteFlowByPath({ workspace: workspaceId, path: removeSuffix(change.path, ".flow.json") })
break
case "app":
await AppService.deleteApp({
workspace: workspaceId,
path: removeSuffix(change.path, ".app.json"),
});
break;
await AppService.deleteApp({ workspace: workspaceId, path: removeSuffix(change.path, ".app.json") })
break
case "variable":
await VariableService.deleteVariable({
workspace: workspaceId,
path: removeSuffix(change.path, ".variable.json"),
});
break;
await VariableService.deleteVariable({ workspace: workspaceId, path: removeSuffix(change.path, ".variable.json") })
break
default:
break;
}
try {
await Deno.remove(stateTarget);
} catch {}
Deno.remove(stateTarget)
} catch { }
}
}
console.log(
colors.green.underline(
`Done! All ${changes.length} changes pushed to the remote workspace.`
)
);
console.log(colors.green.underline(`Done! All ${changes.length} changes pushed to the remote workspace.`));
}
async function applyDiff(
workspace: string,
remotePath: string,
@@ -696,11 +594,11 @@ async function push(
| ResourceFile
| ResourceTypeFile
| FolderFile,
diffs: Difference[]
diffs: Difference[],
) {
if (file instanceof ScriptFile) {
throw new Error(
"This code path should be unreachable - we should never generate diffs for scripts"
"This code path should be unreachable - we should never generate diffs for scripts",
);
} else if (file instanceof FolderFile) {
const parts = remotePath.split("/");
@@ -711,14 +609,14 @@ async function push(
}
}
if (diffs.length === 0) {
console.log("No diffs to apply to " + remotePath);
console.log("No diffs to apply to " + remotePath)
return;
}
try {
await file.pushDiffs(workspace, remotePath, diffs);
} catch (e) {
console.error("Failing to apply diffs to " + remotePath);
console.error(JSON.stringify(e));
console.error("Failing to apply diffs to " + remotePath)
console.error(e.body)
}
}
}
@@ -726,24 +624,18 @@ async function push(
const command = new Command()
.command("pull")
.description(
"Pull any remote changes and apply them locally. Use --raw for usage without local state tracking."
)
.option(
"--fail-conflicts",
"Error on conflicts (both remote and local have changes on the same item)"
"Pull any remote changes and apply them locally. 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("--yes", "Pull without needing confirmation")
.option("--raw", "Pull without using state, just overwrite.")
.action(pull as any)
.command("push")
.description(
"Push any local changes and apply them remotely. Use --raw for usage without local state tracking."
"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("--fail-conflicts", "Error on conflicts (both remote and local have changes on the same item)")
.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);

View File

@@ -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 }),
);
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "windmill",
"version": "1.76.0",
"version": "1.74.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "windmill",
"version": "1.76.0",
"version": "1.74.2",
"dependencies": {
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
@@ -48,6 +48,7 @@
"@types/vscode": "~1.74.0",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.48.0",
"@windmill-labs/svelte-grid": "^5.1.6",
"@zerodevx/svelte-toast": "^0.8.1",
"autoprefixer": "^10.4.13",
"cssnano": "^5.1.14",
@@ -63,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",
@@ -1600,6 +1602,12 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@windmill-labs/svelte-grid": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/@windmill-labs/svelte-grid/-/svelte-grid-5.1.6.tgz",
"integrity": "sha512-wXHIG7XeMxwxnfaHajhjvz9c0u5uGgiQtTk/QxAVyViGdshsclfIfIWZF113QQBKCKp2jY/zP1cj64BrqioRoQ==",
"dev": true
},
"node_modules/@zeit/schemas": {
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.21.0.tgz",
@@ -6646,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",
@@ -6732,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",
@@ -9277,6 +9309,12 @@
"eslint-visitor-keys": "^3.3.0"
}
},
"@windmill-labs/svelte-grid": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/@windmill-labs/svelte-grid/-/svelte-grid-5.1.6.tgz",
"integrity": "sha512-wXHIG7XeMxwxnfaHajhjvz9c0u5uGgiQtTk/QxAVyViGdshsclfIfIWZF113QQBKCKp2jY/zP1cj64BrqioRoQ==",
"dev": true
},
"@zeit/schemas": {
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.21.0.tgz",
@@ -12976,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",
@@ -13036,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",

View File

@@ -1,6 +1,6 @@
{
"name": "windmill",
"version": "1.76.0",
"version": "1.74.2",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -27,6 +27,7 @@
"@types/vscode": "~1.74.0",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.48.0",
"@windmill-labs/svelte-grid": "^5.1.6",
"@zerodevx/svelte-toast": "^0.8.1",
"autoprefixer": "^10.4.13",
"cssnano": "^5.1.14",
@@ -42,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",

View File

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

View File

@@ -81,7 +81,7 @@
$: key =
apiTokenApps[resource_type]?.key ??
(args != undefined
? Object.keys(args).filter((x) => ['token', 'password', 'api_key', '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"

View File

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

View File

@@ -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>

View File

@@ -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} />

View File

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

View File

@@ -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
>

View File

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

View File

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

View File

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

View File

@@ -45,7 +45,7 @@
label: 'Failed',
data:
failed?.map((job) => ({
x: job.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: {

View File

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

View File

@@ -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>

View File

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

View File

@@ -2,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}

View File

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

View File

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

View File

@@ -9,7 +9,9 @@
...new Set(
workers
.filter((worker) => {
return worker.ip != 'unretrievable IP' && worker.last_ping && worker.last_ping < 300
const date = new Date().getTime() - 300 * 60
const ping_at = new Date(worker.ping_at).getTime()
return worker.ip != 'unretrievable IP' && ping_at > date
})
.map((worker) => worker.ip)
)

View File

@@ -26,7 +26,7 @@
export const staticOutputs: string[] = ['loading', 'result']
const { worldStore, app } = getContext<AppViewerContext>('AppViewerContext')
const { runnableComponents, worldStore, app } = getContext<AppViewerContext>('AppViewerContext')
let labelValue: string
let color: ButtonType.Color
@@ -109,6 +109,10 @@
} else {
await runnableComponent?.runComponent()
}
if (recomputeIds) {
await Promise.all(recomputeIds.map((id) => $runnableComponents?.[id]?.()))
}
}
</script>
@@ -123,7 +127,6 @@
<InputValue
{id}
input={configuration.disabled}
extraContext={extraQueryParams}
bind:value={disabled}
bind:error={errors.disabled}
/>
@@ -132,7 +135,6 @@
<RunnableWrapper
flexWrap
{recomputeIds}
bind:runnableComponent
{componentInput}
{id}

View File

@@ -23,7 +23,8 @@
export const staticOutputs: string[] = ['loading', 'result']
const { app, worldStore, stateId } = getContext<AppViewerContext>('AppViewerContext')
const { app, runnableComponents, worldStore, stateId } =
getContext<AppViewerContext>('AppViewerContext')
let labelValue: string = 'Default label'
let color: ButtonType.Color
@@ -61,7 +62,6 @@
<InputValue {id} input={configuration.size} bind:value={size} />
<RunnableWrapper
{recomputeIds}
{render}
bind:runnableComponent
{componentInput}
@@ -103,6 +103,12 @@
}}
on:click={() => {
runnableComponent?.runComponent()
if (recomputeIds) {
recomputeIds.forEach((id) => {
$runnableComponents[id]?.()
})
}
}}
{size}
{color}

View File

@@ -94,7 +94,6 @@
}}
>
<RunnableWrapper
{recomputeIds}
{render}
bind:runnableComponent
{componentInput}
@@ -129,6 +128,12 @@
on:click={async () => {
await runnableComponent?.runComponent()
if (recomputeIds) {
recomputeIds.forEach((id) => {
$runnableComponents[id]?.()
})
}
open = false
}}
size="xs"

View File

@@ -12,7 +12,6 @@
export let name: string
export let inlineScript: InlineScript | undefined
export let fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
export let autoRefresh: boolean = false
let result: any = undefined
@@ -23,7 +22,6 @@
render={false}
{id}
{fields}
{autoRefresh}
bind:result
runnable={{
name,
@@ -31,7 +29,6 @@
type: 'runnableByName'
}}
wrapperClass="hidden"
recomputable
>
<slot />
</RunnableComponent>

View File

@@ -5,7 +5,6 @@
import type { AppInput, EvalAppInput, UploadAppInput } from '../../inputType'
import type { AppViewerContext } from '../../types'
import { accessPropertyByPath } from '../../utils'
import { computeGlobalContext, eval_like } from './eval'
type T = string | number | boolean | Record<string | number, any> | undefined
@@ -13,7 +12,6 @@
export let value: T
export let id: string | undefined = undefined
export let error: string = ''
export let extraContext: Record<string, any> = {}
let lastInput = input ? JSON.parse(JSON.stringify(input)) : undefined
@@ -25,13 +23,13 @@
}
}
const { worldStore, state, mode } = getContext<AppViewerContext>('AppViewerContext')
const { worldStore } = getContext<AppViewerContext>('AppViewerContext')
$: stateId = $worldStore?.state
$: state = $worldStore?.state
let timeout: NodeJS.Timeout | undefined = undefined
const debounce_ms = 50
function debounce(cb: () => Promise<void>) {
function debounce(cb: () => void) {
if (timeout) {
clearTimeout(timeout)
}
@@ -41,22 +39,20 @@
$: lastInput && $worldStore && debounce(handleConnection)
$: lastInput &&
lastInput.type == 'template' &&
$stateId &&
$state &&
debounce(async () => (value = await getValue(lastInput)))
debounce(() => (value = getValue(lastInput)))
$: lastInput &&
lastInput.type == 'eval' &&
$stateId &&
state &&
debounce(async () => (value = await evalExpr(lastInput)))
$state &&
debounce(() => (value = evalExpr(lastInput)))
async function handleConnection() {
function handleConnection() {
if (lastInput.type === 'connected') {
$worldStore?.connect<any>(lastInput, onValueChange)
} else if (lastInput.type === 'static' || lastInput.type == 'template') {
value = await getValue(lastInput)
value = getValue(lastInput)
} else if (lastInput.type == 'eval') {
value = await evalExpr(lastInput as EvalAppInput)
value = evalExpr(lastInput as EvalAppInput)
} else if (lastInput.type == 'upload') {
value = (lastInput as UploadAppInput).value
} else {
@@ -64,15 +60,9 @@
}
}
async function evalExpr(input: EvalAppInput) {
function evalExpr(input: EvalAppInput) {
try {
const r = await eval_like(
input.expr,
computeGlobalContext($worldStore, id, extraContext),
true,
$state,
$mode == 'dnd'
)
const r = eval_like(input.expr, computeGlobalContext())
error = ''
return r
} catch (e) {
@@ -81,16 +71,23 @@
}
}
async function getValue(input: AppInput) {
function computeGlobalContext() {
return Object.fromEntries(
Object.entries($worldStore?.outputsById ?? {})
.filter(([k, _]) => k != id)
.map(([key, value]) => {
return [
key,
Object.fromEntries(Object.entries(value ?? {}).map((x) => [x[0], x[1].peak()]))
]
})
)
}
export function getValue(input: AppInput) {
if (input.type === 'template' && isCodeInjection(input.eval)) {
try {
const r = await eval_like(
'`' + input.eval + '`',
computeGlobalContext($worldStore, id, extraContext),
true,
$state,
$mode == 'dnd'
)
const r = eval_like('`' + input.eval + '`', computeGlobalContext())
error = ''
return r
} catch (e) {
@@ -103,6 +100,31 @@
}
}
function create_context_function_template(eval_string, context) {
return `
return function (context) {
"use strict";
${
Object.keys(context).length > 0
? `let ${Object.keys(context).map((key) => ` ${key} = context['${key}']`)};`
: ``
}
return ${eval_string};
}
`
}
function make_context_evaluator(eval_string, context) {
let template = create_context_function_template(eval_string, context)
let functor = Function(template)
return functor()
}
function eval_like(text, context = {}) {
let evaluator = make_context_evaluator(text, context)
return evaluator(context)
}
function onValueChange(newValue: any): void {
if (lastInput.type === 'connected' && newValue !== undefined && newValue !== null) {
const { connection } = lastInput

View File

@@ -1,5 +1,7 @@
<script lang="ts">
import { RefreshCw } from 'lucide-svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { faRefresh } from '@fortawesome/free-solid-svg-icons'
import { RefreshCcw, RefreshCw } from 'lucide-svelte'
import { getContext } from 'svelte'
import type { AppViewerContext } from '../../types'

View File

@@ -7,12 +7,12 @@
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
import { AppService, type CompletedJob } from '$lib/gen'
import { classNames, defaultIfEmptyString, emptySchema, sendUserToast } from '$lib/utils'
import { Bug } from 'lucide-svelte'
import { getContext } from 'svelte'
import { initOutput } from '../../editor/appUtils'
import { Bug, Loader2 } from 'lucide-svelte'
import { getContext, onMount } from 'svelte'
import { fade } from 'svelte/transition'
import type { AppInputs, Runnable } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext } from '../../types'
import { computeGlobalContext, eval_like } from './eval'
import InputValue from './InputValue.svelte'
import RefreshButton from './RefreshButton.svelte'
@@ -31,8 +31,6 @@
export let gotoUrl: string | undefined = undefined
export let gotoNewTab: boolean | undefined = undefined
export let render: boolean
export let recomputable: boolean = false
export let recomputeIds: string[] = []
const {
worldStore,
@@ -44,27 +42,17 @@
noBackend,
errorByComponent,
mode,
stateId,
state
stateId
} = getContext<AppViewerContext>('AppViewerContext')
$: autoRefresh && handleAutorefresh()
if (recomputable) {
$runnableComponents[id] = async () => {
await executeComponent(true)
}
$runnableComponents = $runnableComponents
}
function handleAutorefresh() {
if (autoRefresh && $worldStore) {
onMount(() => {
if (autoRefresh) {
$runnableComponents[id] = async () => {
await executeComponent(true)
}
executeComponent(true)
}
}
})
let args: Record<string, any> | undefined = undefined
let testIsLoading = false
@@ -92,23 +80,30 @@
$: fields && (currentStaticValues = computeStaticValues())
$: if (JSON.stringify(currentStaticValues) != JSON.stringify(lazyStaticValues)) {
lazyStaticValues = currentStaticValues
refreshIfAutoRefresh()
}
function refreshIfAutoRefresh() {
if (autoRefresh) {
setDebouncedExecute()
}
}
$: fields && (lazyStaticValues = computeStaticValues())
$: (runnableInputValues || extraQueryParams || args) && testJobLoader && refreshIfAutoRefresh()
$: (runnableInputValues || extraQueryParams || args) &&
autoRefresh &&
testJobLoader &&
setDebouncedExecute()
// Test job internal state
let testJob: CompletedJob | undefined = undefined
let testJobLoader: TestJobLoader | undefined = undefined
$: outputs = initOutput($worldStore, id, { result: undefined, loading: false })
$: outputs = $worldStore?.outputsById[id] as {
result: Output<Array<any>>
loading: Output<boolean>
}
$: if (outputs?.loading != undefined) {
outputs.loading.set(false, true)
}
$: outputs?.loading?.set(testIsLoading)
$: schemaStripped = stripSchema(fields, $stateId)
@@ -149,24 +144,6 @@
)
async function executeComponent(noToast = false) {
if (runnable?.type === 'runnableByName' && runnable.inlineScript?.language === 'frontend') {
outputs?.loading?.set(true)
try {
const r = await eval_like(
runnable.inlineScript?.content,
computeGlobalContext($worldStore, id, {}),
false,
$state,
$mode == 'dnd'
)
setResult(r)
$state = $state
} catch (e) {
sendUserToast('Error running frontend script: ' + e.message, true)
}
outputs?.loading?.set(false)
return
}
if (noBackend) {
if (!noToast) {
sendUserToast('This app is not connected to a windmill backend, it is a static preview')
@@ -239,31 +216,6 @@
}
}
function setResult(res: any) {
outputs.result?.set(res)
result = res
const previousJobId = Object.keys($errorByComponent).find(
(key) => $errorByComponent[key].componentId === id
)
if (previousJobId && !result?.error) {
delete $errorByComponent[previousJobId]
$errorByComponent = $errorByComponent
}
if (gotoUrl && gotoUrl != '' && result?.error == undefined) {
if (gotoNewTab) {
window.open(gotoUrl, '_blank')
} else {
goto(gotoUrl)
}
}
if (recomputeIds) {
recomputeIds.map((id) => $runnableComponents?.[id]?.())
}
}
$: result?.error && recordError(result.error)
</script>
@@ -280,7 +232,24 @@
const startedAt = new Date(testJob.started_at).getTime()
if (startedAt > lastStartedAt) {
lastStartedAt = startedAt
setResult(e.detail.result)
outputs.result?.set(testJob?.result)
result = testJob.result
const previousJobId = Object.keys($errorByComponent).find(
(key) => $errorByComponent[key].componentId === id
)
if (previousJobId && !result?.error) {
delete $errorByComponent[previousJobId]
$errorByComponent = $errorByComponent
}
if (gotoUrl && gotoUrl != '' && result?.error == undefined) {
if (gotoNewTab) {
window.open(gotoUrl, '_blank')
} else {
goto(gotoUrl)
}
}
}
}
}}

View File

@@ -21,7 +21,6 @@
export let goto: string | undefined = undefined
export let gotoNewTab: boolean | undefined = undefined
export let render: boolean
export let recomputeIds: string[] = []
const { staticExporter, noBackend } = getContext<AppViewerContext>('AppViewerContext')
@@ -45,7 +44,6 @@
<slot />
{:else if componentInput.type === 'runnable' && isRunnableDefined()}
<RunnableComponent
{recomputeIds}
gotoUrl={goto}
{gotoNewTab}
{flexWrap}

View File

@@ -1,69 +0,0 @@
import { goto } from '$app/navigation'
import { sendUserToast } from '$lib/utils'
import type { World } from '../../rx'
export function computeGlobalContext(
world: World | undefined,
id: string | undefined,
extraContext: any = {}
) {
return {
...Object.fromEntries(
Object.entries(world?.outputsById ?? {})
.filter(([k, _]) => k != id)
.map(([key, value]) => {
return [
key,
Object.fromEntries(Object.entries(value ?? {}).map((x) => [x[0], x[1].peak()]))
]
})
),
...extraContext
}
}
function create_context_function_template(eval_string, context, noReturn: boolean) {
return `
return async function (context, state, goto) {
"use strict";
${
Object.keys(context).length > 0
? `let ${Object.keys(context).map((key) => ` ${key} = context['${key}']`)};`
: ``
}
${noReturn ? `return ${eval_string}` : eval_string}
}
`
}
function make_context_evaluator(
eval_string,
context,
noReturn: boolean
): (context, state, goto) => Promise<any> {
let template = create_context_function_template(eval_string, context, noReturn)
let functor = Function(template)
return functor()
}
export async function eval_like(
text,
context = {},
noReturn: boolean,
state: any,
editor: boolean
) {
let evaluator = make_context_evaluator(text, context, noReturn)
return await evaluator(context, state, async (x, newTab) => {
if (newTab || editor) {
if (!newTab) {
sendUserToast(
'In editor mode, `goto` opens a new tab to prevent losing your work. To test the redirection , use the preview mode.'
)
}
window.open(x, '_blank')
} else {
await goto(x)
}
})
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@
import { buildWorld, type World } from '../rx'
import type {
App,
AppEditorContext,
AppViewerContext,
ConnectingInput,
EditorBreakpoint,
@@ -62,14 +61,7 @@
openDebugRun: writable(undefined),
focusedGrid: writable(undefined),
stateId: writable(0),
parentWidth: writable(0),
state: writable({})
})
setContext<AppEditorContext>('AppEditorContext', {
history: undefined,
componentControl: writable({}),
pickVariableCallback: writable(undefined)
parentWidth: writable(0)
})
let mounted = false

View File

@@ -41,14 +41,13 @@
{#if pointerdown || selected || hover}
<button
title="Lock Position"
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-[2.5rem] z-50 cursor-pointer',
' hover:bg-gray-300',
selected ? 'bg-gray-200/80' : 'bg-gray-200/80'
)}
on:click={() => dispatch('lock')}
on:pointerdown|stopPropagation
>
{#if locked}
<Anchor aria-label="Unlock position" size={14} class="text-orange-500" />
@@ -57,16 +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')}
on:pointerdown|stopPropagation
>
<Expand aria-label="Expand position" size={14} />
<Expand aria-label="Lock position" size={14} />
</button>
{/if}
@@ -75,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'
)}
>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { getContext, afterUpdate } from 'svelte'
import type { App, AppEditorContext, AppViewerContext, GridItem } from '../types'
import Grid from '@windmill-labs/svelte-grid'
import { classNames } from '$lib/utils'
import { columnConfiguration, disableDrag, enableDrag, isFixed, toggleFixed } from '../gridUtils'
import { twMerge } from 'tailwind-merge'
@@ -12,7 +13,6 @@
import { deepEqual } from 'fast-equals'
import { push } from '$lib/history'
import { expandGriditem, findGridItem } from './appUtils'
import Grid from '../svelte-grid/Grid.svelte'
export let policy: Policy
@@ -108,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)
}
}
@@ -215,13 +211,11 @@
if (gridItem) {
toggleFixed(gridItem)
}
$app = $app
}}
on:expand={() => {
push(history, $app)
$selectedComponent = gridComponent.data.id
expandGriditem($app.grid, gridComponent, $breakpoint)
$app = $app
$app = { ...$app }
}}
/>
</div>
@@ -240,7 +234,6 @@
inlineScript={script.inlineScript}
name={script.name}
fields={script.fields}
autoRefresh={script.autoRefresh ?? false}
bind:staticOutputs={$staticOutputs[`bg_${index}`]}
/>
{/if}

View File

@@ -5,7 +5,7 @@
import Button from '../../common/button/Button.svelte'
import type { AppViewerContext } from '../types'
const { runnableComponents, app } = getContext<AppViewerContext>('AppViewerContext')
const { runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
let loading: boolean = false
let timeout: NodeJS.Timer | undefined = undefined
let interval: number | undefined = undefined
@@ -36,12 +36,6 @@
loading = true
Promise.all(
Object.keys($runnableComponents).map((id) => {
if (id.startsWith('bg_')) {
let index = parseInt(id.split('_')[1])
if (!$app.hiddenInlineScripts[index]?.autoRefresh) {
return
}
}
return $runnableComponents?.[id]?.()
})
).finally(() => {

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import Toggle from '$lib/components/Toggle.svelte'
import { getContext } from 'svelte'
import type { AppViewerContext } from '../types'
import GridPanel from './GridPanel.svelte'
@@ -21,27 +20,15 @@
{#each $app?.hiddenInlineScripts ?? [] as script, index (script.name)}
{#if $selectedComponent === `bg_${index}`}
<div class="min-h-full flex flex-col divide-y">
<PanelSection title={`Configuration`}>
<Toggle
bind:checked={script.autoRefresh}
options={{ right: 'Run on start and app refresh' }}
<PanelSection title={`Background script inputs`}>
{#key $stateId}
<InputsSpecsEditor
id={`bg_${index}`}
shouldCapitalize={false}
bind:inputSpecs={script.fields}
userInputEnabled={false}
/>
</PanelSection>
{#if Object.keys(script.fields).length > 0}
<PanelSection title={`Inputs`}>
{#key $stateId}
<InputsSpecsEditor
id={`bg_${index}`}
shouldCapitalize={false}
bind:inputSpecs={script.fields}
userInputEnabled={false}
/>
{/key}
</PanelSection>
{/if}
<div class="grow shrink" />
</div>
{/key}
</PanelSection>
{/if}
{/each}

View File

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

View File

@@ -1,10 +1,9 @@
import { getNextId } from '$lib/components/flows/flowStateUtils'
import type { App, EditorBreakpoint, FocusedGrid, GridItem } from '../types'
import { getRecommendedDimensionsByComponent, type AppComponent } from './component'
import gridHelp from '@windmill-labs/svelte-grid/src/utils/helper'
import { gridColumns } from '../gridUtils'
import { allItems } from '../utils'
import type { Output, World } from '../rx'
import gridHelp from '../svelte-grid/utils/helper'
function findGridItemById(
root: GridItem[],
@@ -19,7 +18,7 @@ function findGridItemById(
return undefined
}
export function findGridItemParentGrid(app: App, id: string): string | undefined {
export function findGridItemParentId(app: App, id: string): string | undefined {
const gridItem = app.grid.find((x) => x.id === id)
if (gridItem) {
return undefined
@@ -28,7 +27,7 @@ export function findGridItemParentGrid(app: App, id: string): string | undefined
const subGrid = app.subgrids[key]
const gridItem = subGrid.find((x) => x.id === id)
if (gridItem) {
return key
return key.split('-')[0]
}
}
}
@@ -47,6 +46,10 @@ export function getNextGridItemId(app: App): string {
}
export function createNewGridItem(grid: GridItem[], id: string, data: AppComponent): GridItem {
const appComponent = data
appComponent.id = id
const newComponent = {
resizable: true,
draggable: true,
@@ -54,8 +57,7 @@ export function createNewGridItem(grid: GridItem[], id: string, data: AppCompone
y: 0
}
let newData: AppComponent = JSON.parse(JSON.stringify(data))
newData.id = id
let newData: AppComponent = JSON.parse(JSON.stringify(appComponent))
const newItem: GridItem = {
data: newData,
@@ -63,7 +65,7 @@ export function createNewGridItem(grid: GridItem[], id: string, data: AppCompone
}
gridColumns.forEach((column) => {
const rec = getRecommendedDimensionsByComponent(newData.type, column)
const rec = getRecommendedDimensionsByComponent(appComponent.type, column)
newItem[column] = {
...newComponent,
@@ -82,16 +84,6 @@ export function createNewGridItem(grid: GridItem[], id: string, data: AppCompone
return newItem
}
export function getGridItems(app: App, focusedGrid: FocusedGrid | undefined): GridItem[] {
if (!focusedGrid) {
return app.grid
} else {
const { parentComponentId, subGridIndex } = focusedGrid
const key = `${parentComponentId}-${subGridIndex ?? 0}`
return app?.subgrids?.[key] ?? []
}
}
export function insertNewGridItem(
app: App,
data: AppComponent,
@@ -104,6 +96,18 @@ export function insertNewGridItem(
app.subgrids = {}
}
if (!focusedGrid) {
const newItem = createNewGridItem(app.grid, id, data)
app.grid.push(newItem)
} else {
const { parentComponentId, subGridIndex } = focusedGrid
const key = `${parentComponentId}-${subGridIndex ?? 0}`
const subGrid = app.subgrids[key] ?? []
subGrid.push(createNewGridItem(subGrid, id, data))
app.subgrids[key] = subGrid
}
// We only want to set subgrids when we are not moving
if (!keepId) {
for (let i = 0; i < (data.numberOfSubgrids ?? 0); i++) {
@@ -111,17 +115,6 @@ export function insertNewGridItem(
}
}
const key = focusedGrid
? `${focusedGrid?.parentComponentId}-${focusedGrid?.subGridIndex ?? 0}`
: undefined
let grid = focusedGrid ? app.subgrids[key!] : app.grid
const newItem = createNewGridItem(grid, id, data)
grid.push(newItem)
if (focusedGrid) {
app.subgrids[key!] = grid
}
return id
}
@@ -176,6 +169,28 @@ export function deleteGridItem(
return components
}
export function duplicateGridItem(
app: App,
parent: string | undefined,
id: string
): string | undefined {
const gridItem = findGridItem(app, id)
if (gridItem) {
const newId = getNextGridItemId(app)
const newItem = JSON.parse(JSON.stringify(gridItem))
newItem.id = newId
newItem.data.id = newId
let focusedGrid = parent
? { parentComponentId: parent.split('-')[0], subGridIndex: Number(parent.split('-')[1]) }
: undefined
return insertNewGridItem(app, newItem.data, focusedGrid)
}
return undefined
}
type AvailableSpace = {
left: number
right: number
@@ -191,7 +206,7 @@ export function findAvailableSpace(
): AvailableSpace | undefined {
if (gridItem) {
const breakpoint = editorBreakpoint === 'sm' ? 3 : 12
const maxHeight = parentGridItem ? parentGridItem[breakpoint].h - 1 : 16
const maxHeight = parentGridItem ? parentGridItem[breakpoint].h - 1 : 12
const maxWidth = 12
const availableSpace = {
@@ -282,28 +297,6 @@ function isOverlapping(item1: any, item2: any) {
)
}
type Outputtable<Type> = {
-readonly [Property in keyof Type]: Output<Type[Property]>
}
export function initOutput<I extends Record<string, any>>(
world: World | undefined,
id: string,
init: I
): Outputtable<I> {
if (!world) {
return {} as any
}
const output = world.outputsById[id] as Outputtable<I>
if (init) {
for (const key in init) {
if (output && output[key] && output[key].peak() == undefined) {
output[key].set(init[key] as any)
}
}
}
return output
}
export function expandGriditem(
grid: GridItem[],
gridComponent: GridItem,
@@ -318,10 +311,10 @@ export function expandGriditem(
const { left, right, top, bottom } = availableSpace
const width = $breakpoint === 'sm' ? 3 : 12
const item = gridComponent[width]
const previousGridItem = JSON.parse(JSON.stringify(gridComponent[width]))
item.x = item.x - left
item.y = item.y - top
item.w = item.w + left + right
item.h = item.h + top + bottom
gridComponent[width].x = previousGridItem.x - left
gridComponent[width].y = previousGridItem.y - top
gridComponent[width].w = previousGridItem.w + left + right
gridComponent[width].h = previousGridItem.h + top + bottom
}

View File

@@ -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}

View File

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

View File

@@ -1,3 +1,4 @@
import type { Size } from '@windmill-labs/svelte-grid'
import type { IntRange } from '../../../../common'
import type { NARROW_GRID_COLUMNS, WIDE_GRID_COLUMNS } from '../../gridUtils'
import { defaultAlignement } from '../componentsPanel/componentDefaultProps'
@@ -35,7 +36,6 @@ import {
AtSignIcon
} from 'lucide-svelte'
import type { BaseAppComponent } from '../../types'
import type { Size } from '../../svelte-grid/types'
type BaseComponent<T extends string> = {
type: T
@@ -74,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,

View File

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

View File

@@ -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 }}>

View File

@@ -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 }}

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
import { classNames } from '$lib/utils'
import { X } from 'lucide-svelte'
import { getContext } from 'svelte'
@@ -11,7 +10,7 @@
import PanelSection from '../settingsPanel/common/PanelSection.svelte'
import ComponentOutputViewer from './ComponentOutputViewer.svelte'
const { connectingInput, staticOutputs, worldStore, selectedComponent, app, state } =
const { connectingInput, staticOutputs, worldStore, selectedComponent, app } =
getContext<AppViewerContext>('AppViewerContext')
function connectInput(componentId: string, path: string) {
@@ -79,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 }}
@@ -156,12 +155,5 @@
</div>
{/each}
</div>
<PanelSection noPadding titlePadding="px-4 pt-2 pb-0.5" title="State">
<div class="mx-2 px-1 border w-full mb-8">
{#key $state}
<ObjectViewer json={$state} />
{/key}
</div>
</PanelSection>
</div>
</PanelSection>

View File

@@ -3,7 +3,6 @@
import { Button, Drawer, DrawerContent, Tab, Tabs } from '$lib/components/common'
import FlowScriptPicker from '$lib/components/flows/pickers/FlowScriptPicker.svelte'
import PickHubScript from '$lib/components/flows/pickers/PickHubScript.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { Script, type Preview } from '$lib/gen'
import { inferArgs } from '$lib/infer'
import { initialCode } from '$lib/script_helpers'
@@ -146,29 +145,7 @@
</Drawer>
<div class="flex flex-col px-4 py-2 gap-2 text-sm" in:fly={{ duration: 50 }}>
<div class="mt-4 flex justify-between gap-8 mb-2">
<div class="font-bold items-baseline">Choose a language:</div>
<div class="flex gap-2">
<Button
on:click={() => picker?.openDrawer()}
size="xs"
color="blue"
startIcon={{ icon: faCodeBranch }}
btnClasses="truncate"
>
fork a detached script
</Button>
<Button
on:click={() => dispatch('delete')}
size="xs"
color="red"
variant="border"
btnClasses="truncate"
>
Cancel
</Button>
</div>
</div>
<div>Choose a language:</div>
<div class="flex gap-2 flex-row flex-wrap">
{#each langs as lang}
<FlowScriptPicker
@@ -187,7 +164,6 @@
createInlineScriptByLanguage(Script.language.DENO, name, 'pgsql')
}}
/>
<!-- <FlowScriptPicker
label={`MySQL`}
lang="mysql"
@@ -196,34 +172,26 @@
}}
/> -->
</div>
<div>
<div class="text-xs mb-1 mt-2"
>Frontend only script:&nbsp;<Tooltip
>Frontend scripts are executed in the browser and can manipulate the app context directly</Tooltip
></div
<div class="mt-4">
<Button
on:click={() => picker?.openDrawer()}
size="xs"
color="blue"
startIcon={{ icon: faCodeBranch }}
btnClasses="truncate"
>
<FlowScriptPicker
label={`JavaScript`}
lang="javascript"
on:click={() => {
const newInlineScript = {
content: `// read outputs and ctx
console.log(ctx.email)
// access a global state store
if (!state.foo) { state.foo = 0 }
state.foo += 1
// you can also navigate to another page
//await goto("?foo=bar")
return state.foo`,
language: 'frontend',
path: 'frontend script',
schema: undefined
}
dispatch('new', newInlineScript)
}}
/>
or fork a detached/Workspace/Hub script
</Button>
<Button
on:click={() => dispatch('delete')}
size="xs"
color="red"
variant="border"
startIcon={{ icon: faTrash }}
btnClasses="truncate"
>
Delete
</Button>
</div>
</div>

View File

@@ -8,6 +8,7 @@
import { inferArgs } from '$lib/infer'
import type { Schema } from '$lib/common'
import Badge from '$lib/components/common/badge/Badge.svelte'
import { fly } from 'svelte/transition'
import Editor from '$lib/components/Editor.svelte'
import { emptySchema, scriptLangToEditorLang } from '$lib/utils'
import Popover from '../../../Popover.svelte'
@@ -15,8 +16,6 @@
import { deepEqual } from 'fast-equals'
import type { AppInput } from '../../inputType'
import Kbd from '$lib/components/common/kbd/Kbd.svelte'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import { buildExtraLib } from '../../utils'
let inlineScriptEditorDrawer: InlineScriptEditorDrawer
@@ -27,8 +26,7 @@
export let fields: Record<string, AppInput> = {}
export let syncFields: boolean = false
const { runnableComponents, stateId, worldStore, state } =
getContext<AppViewerContext>('AppViewerContext')
const { runnableComponents, stateId } = getContext<AppViewerContext>('AppViewerContext')
let editor: Editor
let validCode = true
@@ -51,13 +49,11 @@
onMount(async () => {
if (inlineScript && !inlineScript.schema) {
if (inlineScript.language != 'frontend') {
inlineScript.schema = await inferInlineScriptSchema(
inlineScript?.language,
inlineScript?.content,
emptySchema()
)
}
inlineScript.schema = await inferInlineScriptSchema(
inlineScript?.language,
inlineScript?.content,
emptySchema()
)
}
if (inlineScript.schema) {
loadSchemaAndInputsByName()
@@ -68,7 +64,7 @@
async function loadSchemaAndInputsByName() {
if (syncFields) {
const newSchema = inlineScript.schema ?? emptySchema()
const newSchema = inlineScript.schema
const newFields = computeFields(newSchema, defaultUserInput, fields)
if (!deepEqual(newFields, fields)) {
@@ -79,26 +75,14 @@
}
let isMac = navigator.userAgent.indexOf('Mac OS X') !== -1
$: extraLib =
inlineScript.language == 'frontend' && worldStore
? buildExtraLib($worldStore?.outputsById ?? {}, id, false, $state, true)
: undefined
</script>
{#if inlineScript.language != 'frontend'}
<InlineScriptEditorDrawer {editor} bind:this={inlineScriptEditorDrawer} bind:inlineScript />
{/if}
<InlineScriptEditorDrawer {editor} bind:this={inlineScriptEditorDrawer} bind:inlineScript />
<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}
@@ -121,23 +105,20 @@
<svelte:fragment slot="text">Delete</svelte:fragment>
</Popover>
{/if}
{#if inlineScript.language != 'frontend'}
<Popover notClickable placement="bottom">
<Button
size="xs"
color="light"
btnClasses="!px-2 !bg-gray-100 hover:!bg-gray-200"
aria-label="Open full editor"
on:click={() => {
inlineScriptEditorDrawer?.openDrawer()
}}
>
<Maximize2 size={14} />
</Button>
<svelte:fragment slot="text">Open full editor</svelte:fragment>
</Popover>
{/if}
<Popover notClickable placement="bottom">
<Button
size="xs"
color="light"
btnClasses="!px-2 !bg-gray-100 hover:!bg-gray-200"
aria-label="Open full editor"
on:click={() => {
inlineScriptEditorDrawer?.openDrawer()
}}
>
<Maximize2 size={14} />
</Button>
<svelte:fragment slot="text">Open full editor</svelte:fragment>
</Popover>
<Button
variant="border"
size="xs"
@@ -187,44 +168,30 @@
</div>
<div class="border h-full">
{#if inlineScript.language != 'frontend'}
<Editor
bind:this={editor}
class="flex flex-1 grow h-full"
lang={scriptLangToEditorLang(inlineScript?.language)}
bind:code={inlineScript.content}
fixedOverflowWidgets={true}
cmdEnterAction={async () => {
runLoading = true
await $runnableComponents[id]?.()
runLoading = false
}}
on:change={async (e) => {
if (inlineScript && inlineScript.language != 'frontend') {
const oldSchema = JSON.stringify(inlineScript.schema)
if (inlineScript.schema == undefined) {
inlineScript.schema = emptySchema()
}
await inferInlineScriptSchema(inlineScript?.language, e.detail, inlineScript.schema)
if (JSON.stringify(inlineScript.schema) != oldSchema) {
inlineScript = inlineScript
loadSchemaAndInputsByName()
}
<Editor
bind:this={editor}
class="flex flex-1 grow h-full"
lang={scriptLangToEditorLang(inlineScript?.language)}
bind:code={inlineScript.content}
fixedOverflowWidgets={true}
cmdEnterAction={async () => {
runLoading = true
await $runnableComponents[id]?.()
runLoading = false
}}
on:change={async (e) => {
if (inlineScript) {
const oldSchema = JSON.stringify(inlineScript.schema)
if (inlineScript.schema == undefined) {
inlineScript.schema = emptySchema()
}
}}
/>
{:else}
<SimpleEditor
cmdEnterAction={async () => {
runLoading = true
await $runnableComponents[id]?.()
runLoading = false
}}
class="h-full"
{extraLib}
bind:code={inlineScript.content}
lang="javascript"
/>
{/if}
await inferInlineScriptSchema(inlineScript?.language, e.detail, inlineScript.schema)
if (JSON.stringify(inlineScript.schema) != oldSchema) {
inlineScript = inlineScript
loadSchemaAndInputsByName()
}
}
}}
/>
</div>
</div>

View File

@@ -2,7 +2,6 @@
import { Button, Drawer, DrawerContent } from '$lib/components/common'
import type Editor from '$lib/components/Editor.svelte'
import ScriptEditor from '$lib/components/ScriptEditor.svelte'
import type { Preview } from '$lib/gen'
import { faSave } from '@fortawesome/free-solid-svg-icons'
import type { InlineScript } from '../../types'
@@ -25,7 +24,7 @@
editor?.setCode(inlineScript.content)
}}
>
{#if inlineScript && inlineScript.language != 'frontend'}
{#if inlineScript}
<ScriptEditor
noSyncFromGithub
lang={inlineScript.language}

View File

@@ -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' ||

View File

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

View File

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

View File

@@ -1,8 +1,6 @@
<script lang="ts">
import Button from '$lib/components/common/button/Button.svelte'
import CloseButton from '$lib/components/common/CloseButton.svelte'
import { faPlus, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { CrossIcon } from 'lucide-svelte'
import { getContext } from 'svelte'
import type { AppViewerContext } from '../../types'
import { deleteGridItem } from '../appUtils'
@@ -56,10 +54,20 @@
<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 right-1">
<CloseButton noBg on:close={() => deleteSubgrid(index)} />
<div class="absolute top-1 right-1">
<Button
size="xs"
color="light"
variant="border"
on:click={() => {
deleteSubgrid(index)
}}
iconOnly
btnClasses="!text-red-500"
startIcon={{ icon: faTrashAlt }}
/>
</div>
</div>
{/each}

View File

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

View File

@@ -47,7 +47,6 @@
</td>
<td class="relative whitespace-nowrap px-4 py-2 ">
<input
class="windmillapp"
type="checkbox"
on:change={(event) => onChange(event, id)}
checked={recomputeIds?.includes(id)}

View File

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

View File

@@ -10,13 +10,13 @@
export let id: string
export let hasRows: boolean = false
const { onchange, worldStore, state } = getContext<AppViewerContext>('AppViewerContext')
const { onchange, worldStore } = getContext<AppViewerContext>('AppViewerContext')
$: componentInput && onchange?.()
$: extraLib =
componentInput?.expr && $worldStore
? buildExtraLib($worldStore?.outputsById ?? {}, id, hasRows, $state, false)
? buildExtraLib($worldStore?.outputsById ?? {}, id, hasRows)
: undefined
</script>

View File

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

View File

@@ -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,12 +11,13 @@
error = e.message
}
}
let error = ''
$: code && parseJson()
</script>
<div class="flex flex-col w-full">
<div class="border border-gray-300 w-full">
<SimpleEditor on:change autoHeight lang="json" bind:code />
<SimpleEditor autoHeight lang="json" bind:code />
</div>
{#if error != ''}
<span class="text-red-600 text-xs">{error}</span>

View File

@@ -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'}

View File

@@ -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}

View File

@@ -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' }>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
import type { Schema } from '$lib/common'
import type { Preview } from '$lib/gen'
import type { History } from '$lib/history'
import type { FilledItem } from '@windmill-labs/svelte-grid'
import type { Writable } from 'svelte/store'
import type VariableEditor from '../VariableEditor.svelte'
import type { AppComponent } from './editor/component/components'
import type {
AppInput,
@@ -15,7 +16,6 @@ import type {
UserAppInput
} from './inputType'
import type { World } from './rx'
import type { FilledItem } from './svelte-grid/types'
export type HorizontalAlignment = 'left' | 'center' | 'right'
export type VerticalAlignment = 'top' | 'center' | 'bottom'
@@ -44,14 +44,14 @@ export interface BaseAppComponent extends Partial<Aligned> {
configuration: Record<
string,
GeneralAppInput &
(
| StaticAppInput
| ConnectedAppInput
| UserAppInput
| RowAppInput
| EvalAppInput
| UploadAppInput
)
(
| StaticAppInput
| ConnectedAppInput
| UserAppInput
| RowAppInput
| EvalAppInput
| UploadAppInput
)
>
card: boolean | undefined
customCss?: ComponentCustomCSS
@@ -84,9 +84,9 @@ export type GridItem = FilledItem<{
export type InlineScript = {
content: string
language: Preview.language | 'frontend'
path?: string
schema?: Schema
language: Preview.language
path: string
schema: Schema
}
export type App = {
@@ -100,7 +100,6 @@ export type App = {
name: string
inlineScript: InlineScript | undefined
fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
autoRefresh?: boolean
}>
css?: Record<'viewer' | 'grid' | AppComponent['type'], ComponentCustomCSS>
subgrids?: Record<string, GridItem[]>
@@ -135,11 +134,10 @@ export type AppViewerContext = {
focusedGrid: Writable<FocusedGrid | undefined>
stateId: Writable<number>
parentWidth: Writable<number>
state: Writable<Record<string, any>>
}
export type AppEditorContext = {
history: History<App> | undefined
history: History<App>
componentControl: Writable<Record<string, { left?: () => boolean; right?: () => boolean }>>
pickVariableCallback: Writable<((path: string) => void) | undefined>
}

View File

@@ -162,24 +162,21 @@ export function toStatic(
export function buildExtraLib(
components: Record<string, Record<string, Output<any>>>,
idToExclude: string,
hasRows: boolean,
state: Record<string, any>,
goto: boolean
hasRows: boolean
): string {
const cs = Object.entries(components)
.filter(([k, v]) => k != idToExclude)
.map(([k, v]) => [k, Object.fromEntries(Object.entries(v).map(([k, v]) => [k, v.peak()]))])
.map(
([k, v]) => `declare const ${k}: ${JSON.stringify(v)};
`
)
.join('\n')
return (
Object.entries(components)
.filter(([k, v]) => k != idToExclude)
.map(([k, v]) => [k, Object.fromEntries(Object.entries(v).map(([k, v]) => [k, v.peak()]))])
.map(
([k, v]) => `
declare const ${k} = ${JSON.stringify(v)};
return `${cs}
${hasRows ? 'declare const row: Record<string, any>;' : ''}
${goto ? 'declare const goto: async (path: string, newTab?: boolean) => void)' : ''}
declare const state: ${JSON.stringify(state)};
`
)
.join('\n') + (hasRows ? 'declare const row: Record<string, any>;' : '')
)
}
export function getAllScriptNames(app: App): string[] {

View File

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

View File

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

View File

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

View File

@@ -1,11 +0,0 @@
<script>
export let width = 16
export let height = 16
</script>
<svg xmlns="http://www.w3.org/2000/svg" {width} {height} viewBox="0 0 630 630">
<rect width="630" height="630" fill="#f7df1e" />
<path
d="m423.2 492.19c12.69 20.72 29.2 35.95 58.4 35.95 24.53 0 40.2-12.26 40.2-29.2 0-20.3-16.1-27.49-43.1-39.3l-14.8-6.35c-42.72-18.2-71.1-41-71.1-89.2 0-44.4 33.83-78.2 86.7-78.2 37.64 0 64.7 13.1 84.2 47.4l-46.1 29.6c-10.15-18.2-21.1-25.37-38.1-25.37-17.34 0-28.33 11-28.33 25.37 0 17.76 11 24.95 36.4 35.95l14.8 6.34c50.3 21.57 78.7 43.56 78.7 93 0 53.3-41.87 82.5-98.1 82.5-54.98 0-90.5-26.2-107.88-60.54zm-209.13 5.13c9.3 16.5 17.76 30.45 38.1 30.45 19.45 0 31.72-7.61 31.72-37.2v-201.3h59.2v202.1c0 61.3-35.94 89.2-88.4 89.2-47.4 0-74.85-24.53-88.81-54.075z"
/>
</svg>

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