mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-20 16:02:28 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6a9fc2b87 | ||
|
|
2e7910c0d3 | ||
|
|
844edd1117 | ||
|
|
fba9e7ef03 | ||
|
|
72109b01d7 | ||
|
|
e7428bf84d | ||
|
|
56b417c669 | ||
|
|
8e1e37bf9e | ||
|
|
5e965aec15 | ||
|
|
04ffbf8c26 | ||
|
|
b601883bc0 | ||
|
|
81ed965812 | ||
|
|
e1ff00117c | ||
|
|
55d599d4b6 | ||
|
|
46c784574a | ||
|
|
3063001491 | ||
|
|
d30979d04e | ||
|
|
7bf9e25ede | ||
|
|
f406da9976 | ||
|
|
07f91af535 | ||
|
|
cb559d6083 | ||
|
|
63af3ce0b1 | ||
|
|
3bc891a0dd | ||
|
|
5569e4d495 | ||
|
|
7f290bbf6a | ||
|
|
69fc8a98ae | ||
|
|
eff2a4c496 | ||
|
|
5219062cc1 | ||
|
|
318b654106 | ||
|
|
731299ee14 | ||
|
|
dde3c5bef3 | ||
|
|
3ada264c4a | ||
|
|
e2cd8dbdf0 | ||
|
|
487d84bd7f | ||
|
|
82d37d6ace | ||
|
|
a85ebfbbf4 | ||
|
|
33b0e71523 | ||
|
|
630e54f65c |
@@ -16,7 +16,7 @@ jobs:
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.83.0
|
||||
toolchain: 1.85.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.83.0
|
||||
toolchain: 1.85.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.83.0
|
||||
toolchain: 1.85.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.83.0
|
||||
toolchain: 1.85.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.83.0
|
||||
toolchain: 1.85.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
|
||||
@@ -30,6 +30,12 @@ jobs:
|
||||
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.85.0
|
||||
override: true
|
||||
|
||||
- name: Substitute EE code
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -32,6 +32,12 @@ jobs:
|
||||
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.85.0
|
||||
override: true
|
||||
|
||||
- name: Substitute EE code
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -1,5 +1,71 @@
|
||||
# Changelog
|
||||
|
||||
## [1.473.1](https://github.com/windmill-labs/windmill/compare/v1.473.0...v1.473.1) (2025-03-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** copilot info exists_ai_resource ([#5415](https://github.com/windmill-labs/windmill/issues/5415)) ([844edd1](https://github.com/windmill-labs/windmill/commit/844edd1117bc5fdcc108adf53aec3974a7c66384))
|
||||
* improve cancel performance ([fba9e7e](https://github.com/windmill-labs/windmill/commit/fba9e7ef03b91d3e2c78ff833ddcc5207b7436d2))
|
||||
|
||||
## [1.473.0](https://github.com/windmill-labs/windmill/compare/v1.472.1...v1.473.0) (2025-03-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* app s3 input anonymous delete ([#5401](https://github.com/windmill-labs/windmill/issues/5401)) ([46c7845](https://github.com/windmill-labs/windmill/commit/46c784574add176cb75d3627c9d3f55b6fb945f8))
|
||||
* track workspace runnables used in flows ([#5369](https://github.com/windmill-labs/windmill/issues/5369)) ([7bf9e25](https://github.com/windmill-labs/windmill/commit/7bf9e25ede82486115eae71865202f85aa931a8d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve db loads by adding index on audit ([e1ff001](https://github.com/windmill-labs/windmill/commit/e1ff00117ca5b66dd0b9365e63b9ff7dc277bc2c))
|
||||
* migrations do not refer to public schema anymore ([#5400](https://github.com/windmill-labs/windmill/issues/5400)) ([3063001](https://github.com/windmill-labs/windmill/commit/3063001491b49a4b6d0cd5985818b32aa4d3f16f))
|
||||
* remove typings_extensions from python sdk ([04ffbf8](https://github.com/windmill-labs/windmill/commit/04ffbf8c266a06c3efcebcbbaee767f0ab0771e2))
|
||||
|
||||
## [1.472.1](https://github.com/windmill-labs/windmill/compare/v1.472.0...v1.472.1) (2025-02-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* disable bundling using env var ([#5396](https://github.com/windmill-labs/windmill/issues/5396)) ([cb559d6](https://github.com/windmill-labs/windmill/commit/cb559d6083553c400e18e6077002c4891289a8a2))
|
||||
|
||||
## [1.472.0](https://github.com/windmill-labs/windmill/compare/v1.471.1...v1.472.0) (2025-02-26)
|
||||
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* downgrade v8 to fix some rare panics ([5569e4d](https://github.com/windmill-labs/windmill/commit/5569e4d4953a01f2ad03ea8b71e695e833964bea))
|
||||
* **frontend:** markdown shows single backtick in single line code block ([#5391](https://github.com/windmill-labs/windmill/issues/5391)) ([7f290bb](https://github.com/windmill-labs/windmill/commit/7f290bbf6a33e2811dbe2bd8ee905c0fa8e8db3b))
|
||||
* migrate toggle to melt (4/4) ([#5329](https://github.com/windmill-labs/windmill/issues/5329)) ([69fc8a9](https://github.com/windmill-labs/windmill/commit/69fc8a98ae78bc01dc3d97f9732ee28864b323dd))
|
||||
|
||||
## [1.471.1](https://github.com/windmill-labs/windmill/compare/v1.471.0...v1.471.1) (2025-02-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* update to rust 1.86.0 ([3ada264](https://github.com/windmill-labs/windmill/commit/3ada264c4ad49f666c3a053eb48c7df294bf085b))
|
||||
|
||||
## [1.471.0](https://github.com/windmill-labs/windmill/compare/v1.470.1...v1.471.0) (2025-02-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for claude sonnet 3.7 thinking ([#5387](https://github.com/windmill-labs/windmill/issues/5387)) ([487d84b](https://github.com/windmill-labs/windmill/commit/487d84bd7fdc39a2401df4108fca6183189cf38a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** improve pagination handling and filter refreshes ([#5378](https://github.com/windmill-labs/windmill/issues/5378)) ([a85ebfb](https://github.com/windmill-labs/windmill/commit/a85ebfbbf48590812c0931ad93179c322f819849))
|
||||
|
||||
## [1.470.1](https://github.com/windmill-labs/windmill/compare/v1.470.0...v1.470.1) (2025-02-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* multiple app initializations fixes ([630e54f](https://github.com/windmill-labs/windmill/commit/630e54f65c950ec0073b3cdac9974cb666c1ab3f))
|
||||
|
||||
## [1.470.0](https://github.com/windmill-labs/windmill/compare/v1.469.0...v1.470.0) (2025-02-26)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
ARG DEBIAN_IMAGE=debian:bookworm-slim
|
||||
ARG RUST_IMAGE=rust:1.83-slim-bookworm
|
||||
ARG RUST_IMAGE=rust:1.85-slim-bookworm
|
||||
|
||||
FROM ${RUST_IMAGE} AS rust_base
|
||||
|
||||
|
||||
+15
-15
@@ -130,28 +130,28 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"name": "ai_models",
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"name": "code_completion_model",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"name": "teams_command_script",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"ordinal": 26,
|
||||
"name": "teams_team_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"ordinal": 27,
|
||||
"name": "teams_team_name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"name": "ai_models",
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"name": "code_completion_model",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -185,10 +185,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE flow_workspace_runnables SET workspace_id = $1 WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2bb2cf6accb18d3e37a63388cca52a6591e7593b1a7c3d7a6848587679a48187"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow_workspace_runnables (flow_path, runnable_path, runnable_is_flow, workspace_id) VALUES ($1, $2, TRUE, $3) ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "35795d27c4ca69d2f145b4dba08a6ed16c25aea4584103c1b9a3651eb31bfe53"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "CREATE INDEX CONCURRENTLY ix_audit_timestamps ON audit (timestamp DESC)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "47baef9304533898ae52328d946edc053bd3332f886a8bc6cb173cdea7e8aa16"
|
||||
}
|
||||
+15
-15
@@ -130,28 +130,28 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"name": "ai_models",
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"name": "code_completion_model",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"name": "teams_command_script",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"ordinal": 26,
|
||||
"name": "teams_team_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"ordinal": 27,
|
||||
"name": "teams_team_name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"name": "ai_models",
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"name": "code_completion_model",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -185,10 +185,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT f.path\n FROM flow_workspace_runnables fwr \n JOIN flow f \n ON fwr.flow_path = f.path AND fwr.workspace_id = f.workspace_id\n WHERE fwr.runnable_path = $1 AND fwr.runnable_is_flow = $2 AND fwr.workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9aeee333b1dbe58ba819ba3b2713242b54d77b46b5f785ae66b8104f89f43219"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM flow_workspace_runnables WHERE flow_path = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "acbea8740b28c26942c50edcf5618cd141e68cf83a7dcae7d3c1b8a7ba94425b"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE flow_workspace_runnables SET flow_path = REGEXP_REPLACE(flow_path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1') WHERE flow_path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b656927cd70b6667f3c72186ec04f0bf040da3af9e2eac3229264ec95b4755d8"
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow_workspace_runnables (flow_path, runnable_path, script_hash, runnable_is_flow, workspace_id) VALUES ($1, $2, $3, FALSE, $4) ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c35f44f91b08fa57e29a2b4a685706f62e700695810f23108e975dfcd1fee7a3"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE flow_workspace_runnables SET runnable_path = REGEXP_REPLACE(runnable_path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1') WHERE runnable_path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "dc58e5b4715601a93b3c01a2564a4420f232867a23cacb9a62b386f129a86a4b"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id AS \"id!\" FROM v2_job WHERE parent_job = $1 AND workspace_id = $2",
|
||||
"query": "SELECT id AS \"id!\" FROM v2_job_queue INNER JOIN v2_job USING (id) WHERE parent_job = $1 AND v2_job.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -19,5 +19,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c5259e37703c3e48104438bad6e1f3615f4439c090a75e6fde03702a21589b25"
|
||||
"hash": "eb68469026be39048c5f42a80a2c538fbb54ad269ec81aea89e431a511245a1e"
|
||||
}
|
||||
Generated
+544
-751
File diff suppressed because it is too large
Load Diff
+16
-16
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.470.0"
|
||||
version = "1.473.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -30,7 +30,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.470.0"
|
||||
version = "1.473.1"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -173,7 +173,7 @@ serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
|
||||
uuid = { version = "^1", features = ["serde", "v4"] }
|
||||
thiserror = "^2"
|
||||
anyhow = "^1"
|
||||
chrono = { version = "0.4.35", features = ["serde"] }
|
||||
chrono = { version = "=0.4.39", features = ["serde"] }
|
||||
chrono-tz = "^0.10.1"
|
||||
tracing = "^0"
|
||||
tracing-subscriber = { version = "^0", features = ["env-filter", "json"] }
|
||||
@@ -211,20 +211,20 @@ itertools = "^0"
|
||||
regex = "^1"
|
||||
semver = "^1"
|
||||
|
||||
v8 = "=134.4.0" # Exact version
|
||||
deno_fetch = "0.216.0"
|
||||
deno_tls = "0.179.0"
|
||||
deno_console = "0.192.0"
|
||||
deno_url = "0.192.0"
|
||||
deno_webidl = "0.192.0"
|
||||
deno_web = "0.223.0"
|
||||
deno_io = "0.102.0"
|
||||
deno_net = "0.184.0"
|
||||
deno_core = "0.338.0"
|
||||
v8 = "=130.0.7" # Exact version
|
||||
deno_fetch = "0.214.0"
|
||||
deno_tls = "0.177.0"
|
||||
deno_console = "0.190.0"
|
||||
deno_url = "0.190.0"
|
||||
deno_webidl = "0.190.0"
|
||||
deno_web = "0.221.0"
|
||||
deno_io = "0.100.0"
|
||||
deno_net = "0.182.0"
|
||||
deno_core = "0.336.0"
|
||||
deno_ast = { version = "=0.44.0", features = ["transpiling"] }
|
||||
deno_permissions = "0.51.0"
|
||||
deno_runtime = { version = "0.200.0", features = ["transpile"] }
|
||||
deno_telemetry = "0.14.0"
|
||||
deno_permissions = "0.49.0"
|
||||
deno_runtime = { version = "0.198.0", features = ["transpile"] }
|
||||
deno_telemetry = "0.12.0"
|
||||
deno_error = "=0.5.5"
|
||||
|
||||
swc_common = "=0.37.5"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
DO
|
||||
$$
|
||||
DECLARE
|
||||
tbl_name text;
|
||||
policy_exists boolean;
|
||||
current_sch text;
|
||||
tbl_names text[] := ARRAY['account', 'app', 'audit', 'capture', 'completed_job', 'flow', 'folder', 'http_trigger', 'queue', 'raw_app', 'resource', 'schedule', 'script', 'usr_to_group', 'variable'];
|
||||
BEGIN
|
||||
-- Get the current schema
|
||||
SELECT current_schema() INTO current_sch;
|
||||
|
||||
FOR tbl_name IN SELECT unnest(tbl_names)
|
||||
LOOP
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_policies
|
||||
WHERE schemaname = current_sch
|
||||
AND tablename = tbl_name
|
||||
AND policyname = 'admin_policy'
|
||||
) INTO policy_exists;
|
||||
|
||||
IF NOT policy_exists THEN
|
||||
EXECUTE format('CREATE POLICY admin_policy ON %I.%I TO windmill_admin USING (true);', current_sch, tbl_name);
|
||||
END IF;
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
@@ -1 +1 @@
|
||||
d1dd29ec1aba66dd105e6193db9f430476bf5dfc
|
||||
a165bb74c6c33bdcf618d564498fd4cbb5742451
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,37 @@
|
||||
DO
|
||||
$do$
|
||||
DECLARE
|
||||
current_schema_name TEXT;
|
||||
BEGIN
|
||||
-- Get the current schema for the session
|
||||
SELECT current_schema() INTO current_schema_name;
|
||||
|
||||
-- Lock the roles table to prevent race conditions
|
||||
LOCK TABLE pg_catalog.pg_roles;
|
||||
|
||||
|
||||
|
||||
EXECUTE format('GRANT USAGE ON SCHEMA %I TO windmill_user', current_schema_name);
|
||||
EXECUTE format('GRANT USAGE ON SCHEMA %I TO windmill_admin', current_schema_name);
|
||||
|
||||
|
||||
-- Grant privileges dynamically to the current schema
|
||||
EXECUTE format('GRANT ALL ON ALL TABLES IN SCHEMA %I TO windmill_user', current_schema_name);
|
||||
EXECUTE format('GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA %I TO windmill_user', current_schema_name);
|
||||
|
||||
-- Alter default privileges dynamically
|
||||
EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA %I GRANT ALL ON TABLES TO windmill_user', current_schema_name);
|
||||
EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA %I GRANT ALL ON SEQUENCES TO windmill_user', current_schema_name);
|
||||
|
||||
-- Grant privileges dynamically to the current schema
|
||||
EXECUTE format('GRANT ALL ON ALL TABLES IN SCHEMA %I TO windmill_admin', current_schema_name);
|
||||
EXECUTE format('GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA %I TO windmill_admin', current_schema_name);
|
||||
|
||||
-- Alter default privileges dynamically
|
||||
EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA %I GRANT ALL ON TABLES TO windmill_admin', current_schema_name);
|
||||
EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA %I GRANT ALL ON SEQUENCES TO windmill_admin', current_schema_name);
|
||||
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE NOTICE 'Error granting proper permissions to windmill users: %', SQLERRM;
|
||||
END
|
||||
$do$;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE flow_workspace_runnables;
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE flow_workspace_runnables (
|
||||
flow_path VARCHAR(255) NOT NULL,
|
||||
runnable_path VARCHAR(255) NOT NULL,
|
||||
script_hash BIGINT NULL,
|
||||
runnable_is_flow BOOLEAN NOT NULL,
|
||||
workspace_id VARCHAR(50) NOT NULL,
|
||||
FOREIGN KEY (workspace_id, flow_path) REFERENCES flow (workspace_id, path) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX flow_workspace_without_hash_unique_idx ON flow_workspace_runnables (flow_path, runnable_path, runnable_is_flow, workspace_id) WHERE script_hash IS NULL;
|
||||
CREATE UNIQUE INDEX flow_workspace_with_hash_unique_idx ON flow_workspace_runnables (flow_path, runnable_path, script_hash, runnable_is_flow, workspace_id) WHERE script_hash IS NOT NULL;
|
||||
CREATE INDEX flow_workspace_runnable_path_is_flow_idx ON flow_workspace_runnables (runnable_path, runnable_is_flow, workspace_id);
|
||||
@@ -4,6 +4,7 @@ use windmill_parser::{Arg, MainArgSignature, ObjectProperty, Typ};
|
||||
use windmill_parser_bash::parse_powershell_sig;
|
||||
use windmill_parser_ts::{parse_deno_signature, parse_expr_for_ids, parse_expr_for_imports};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen_test]
|
||||
fn test_parse_deno_sig() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
@@ -146,6 +147,8 @@ export function main(test1?: string, test2: string = \"burkina\",
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen_test]
|
||||
fn test_parse_deno_sig_implicit_types() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
@@ -224,6 +227,7 @@ export function main(test2 = \"burkina\",
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen_test]
|
||||
fn test_parse_deno_types() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
@@ -274,6 +278,7 @@ export function main(foo: FooBar, {a, b}: FooBar, {c, d}: FooBar = {a: \"foo\",
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen_test]
|
||||
fn test_parse_enum_list() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
@@ -305,6 +310,7 @@ export function main(foo: (\"foo\" | \"bar\")[]) {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen_test]
|
||||
fn test_parse_extract_ident() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
@@ -324,6 +330,7 @@ fn test_parse_extract_ident() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen_test]
|
||||
fn test_parse_imports() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
@@ -347,6 +354,7 @@ fn test_parse_imports() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen_test]
|
||||
fn test_parse_imports_dts() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
@@ -359,6 +367,7 @@ export type foo = number
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen_test]
|
||||
fn test_parse_powershell_sig() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
|
||||
@@ -115,6 +115,25 @@ where
|
||||
}
|
||||
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
// https://github.com/denoland/deno/blob/main/cli/main.rs#L477
|
||||
#[cfg(feature = "deno_core")]
|
||||
let unrecognized_v8_flags = deno_core::v8_set_flags(vec![
|
||||
"--stack-size=1024".to_string(),
|
||||
// TODO(bartlomieju): I think this can be removed as it's handled by `deno_core`
|
||||
// and its settings.
|
||||
// deno_ast removes TypeScript `assert` keywords, so this flag only affects JavaScript
|
||||
// TODO(petamoriken): Need to check TypeScript `assert` keywords in deno_ast
|
||||
"--no-harmony-import-assertions".to_string(),
|
||||
])
|
||||
.into_iter()
|
||||
.skip(1)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
if !unrecognized_v8_flags.is_empty() {
|
||||
println!("Unrecognized V8 flags: {:?}", unrecognized_v8_flags);
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
deno_core::JsRuntime::init_platform(None, false);
|
||||
create_and_run_current_thread_inner(windmill_main())
|
||||
|
||||
@@ -34,7 +34,6 @@ use windmill_common::ee::{jobs_waiting_alerts, worker_groups_alerts};
|
||||
#[cfg(feature = "oauth2")]
|
||||
use windmill_common::global_settings::OAUTH_SETTING;
|
||||
use windmill_common::{
|
||||
auth::JWT_SECRET,
|
||||
ee::CriticalErrorChannel,
|
||||
error,
|
||||
flow_status::{FlowStatus, FlowStatusModule},
|
||||
@@ -51,6 +50,7 @@ use windmill_common::{
|
||||
},
|
||||
indexer::load_indexer_config,
|
||||
jobs::QueuedJob,
|
||||
jwt::JWT_SECRET,
|
||||
oauth2::REQUIRE_PREEXISTING_USER_FOR_OAUTH,
|
||||
server::load_smtp_config,
|
||||
tracing_init::JSON_FMT,
|
||||
|
||||
@@ -22,12 +22,12 @@ use windmill_api_client::types::{CreateFlowBody, RawScript};
|
||||
use windmill_api_client::types::{EditSchedule, NewSchedule, ScriptArgs};
|
||||
|
||||
use serde::Serialize;
|
||||
use windmill_common::auth::JWT_SECRET;
|
||||
use windmill_common::worker::WORKER_CONFIG;
|
||||
use windmill_common::{
|
||||
flow_status::{FlowStatus, FlowStatusModule, RestartedFrom},
|
||||
flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform},
|
||||
jobs::{JobKind, JobPayload, RawCode},
|
||||
jwt::JWT_SECRET,
|
||||
scripts::{ScriptHash, ScriptLang},
|
||||
worker::{
|
||||
MIN_VERSION_IS_AT_LEAST_1_427, MIN_VERSION_IS_AT_LEAST_1_432, MIN_VERSION_IS_AT_LEAST_1_440,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.470.0
|
||||
version: 1.473.1
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -5292,6 +5292,26 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/FlowVersion"
|
||||
|
||||
/w/{workspace}/flows/list_paths_from_workspace_runnable/{runnable_kind}/{path}:
|
||||
get:
|
||||
summary: list flow paths from workspace runnable
|
||||
operationId: listFlowPathsFromWorkspaceRunnable
|
||||
tags:
|
||||
- flow
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/RunnableKind"
|
||||
- $ref: "#/components/parameters/ScriptPath"
|
||||
responses:
|
||||
"200":
|
||||
description: list of flow paths
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/flows/get/v/{version}/p/{path}:
|
||||
get:
|
||||
summary: get flow version
|
||||
@@ -6212,6 +6232,92 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/apps_u/upload_s3_file/{path}:
|
||||
post:
|
||||
summary: upload s3 file from app
|
||||
operationId: uploadS3FileFromApp
|
||||
tags:
|
||||
- app
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
- name: file_key
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: file_extension
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: s3_resource_path
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: resource_type
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: storage
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: content_type
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: content_disposition
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: File content
|
||||
required: true
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
"200":
|
||||
description: file uploaded
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
file_key:
|
||||
type: string
|
||||
delete_token:
|
||||
type: string
|
||||
required:
|
||||
- file_key
|
||||
- delete_token
|
||||
|
||||
/w/{workspace}/apps_u/delete_s3_file:
|
||||
delete:
|
||||
summary: delete s3 file from app
|
||||
operationId: deleteS3FileFromApp
|
||||
tags:
|
||||
- app
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: delete_token
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: file deleted
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/jobs/run/f/{path}:
|
||||
post:
|
||||
summary: run flow by path
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
job_helpers_ee::{
|
||||
download_s3_file_internal, get_random_file_name, get_s3_resource,
|
||||
get_workspace_s3_resource, load_image_preview_internal, upload_file_from_req,
|
||||
DownloadFileQuery, LoadImagePreviewQuery, UploadFileResponse,
|
||||
DownloadFileQuery, LoadImagePreviewQuery,
|
||||
},
|
||||
users::fetch_api_authed_from_permissioned_as,
|
||||
};
|
||||
@@ -51,8 +51,6 @@ use sqlx::{types::Uuid, FromRow};
|
||||
use std::str;
|
||||
use windmill_audit::audit_ee::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
#[cfg(feature = "parquet")]
|
||||
use windmill_common::s3_helpers::build_object_store_client;
|
||||
use windmill_common::variables::encrypt;
|
||||
use windmill_common::{
|
||||
apps::{AppScriptId, ListAppQuery},
|
||||
@@ -69,6 +67,8 @@ use windmill_common::{
|
||||
worker::{to_raw_value, CLOUD_HOSTED},
|
||||
HUB_BASE_URL,
|
||||
};
|
||||
#[cfg(feature = "parquet")]
|
||||
use windmill_common::{jwt, s3_helpers::build_object_store_client};
|
||||
|
||||
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
use windmill_queue::{push, PushArgs, PushArgsOwned, PushIsolationLevel};
|
||||
@@ -96,6 +96,7 @@ pub fn unauthed_service() -> Router {
|
||||
Router::new()
|
||||
.route("/execute_component/*path", post(execute_component))
|
||||
.route("/upload_s3_file/*path", post(upload_s3_file_from_app))
|
||||
.route("/delete_s3_file", delete(delete_s3_file_from_app))
|
||||
.route("/download_s3_file/*path", get(download_s3_file_from_app))
|
||||
.route(
|
||||
"/load_image_preview/*path",
|
||||
@@ -1497,6 +1498,13 @@ async fn upload_s3_file_from_app() -> Result<()> {
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "parquet"))]
|
||||
async fn delete_s3_file_from_app() -> Result<()> {
|
||||
return Err(Error::BadRequest(
|
||||
"This endpoint requires the parquet feature to be enabled".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct UploadFileToS3Query {
|
||||
@@ -1511,6 +1519,24 @@ struct UploadFileToS3Query {
|
||||
force_viewer_allowed_resources: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DeleteTokenClaims {
|
||||
file_key: String,
|
||||
on_behalf_of_email: String,
|
||||
permissioned_as: String,
|
||||
username: String,
|
||||
s3_resource_path: Option<String>,
|
||||
pub exp: usize,
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
#[derive(Serialize)]
|
||||
struct AppUploadFileResponse {
|
||||
file_key: String,
|
||||
delete_token: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
async fn upload_s3_file_from_app(
|
||||
OptAuthed(opt_authed): OptAuthed,
|
||||
@@ -1518,7 +1544,7 @@ async fn upload_s3_file_from_app(
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Query(query): Query<UploadFileToS3Query>,
|
||||
request: axum::extract::Request,
|
||||
) -> JsonResult<UploadFileResponse> {
|
||||
) -> JsonResult<AppUploadFileResponse> {
|
||||
let policy = if let Some(file_key_regex) = query.force_viewer_file_key_regex {
|
||||
Some(Policy {
|
||||
execution_mode: ExecutionMode::Viewer,
|
||||
@@ -1554,7 +1580,10 @@ async fn upload_s3_file_from_app(
|
||||
|
||||
let user_db = UserDB::new(db.clone());
|
||||
|
||||
let (s3_resource_opt, file_key) = if policy.as_ref().is_some_and(|p| p.s3_inputs.is_some()) {
|
||||
let (s3_resource_opt, file_key, on_behalf_of_email, permissioned_as, username) = if policy
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.s3_inputs.is_some())
|
||||
{
|
||||
let policy = policy.unwrap();
|
||||
let s3_inputs = policy.s3_inputs.as_ref().unwrap();
|
||||
|
||||
@@ -1562,11 +1591,11 @@ async fn upload_s3_file_from_app(
|
||||
get_on_behalf_details_from_policy_and_authed(&policy, &opt_authed).await?;
|
||||
|
||||
let on_behalf_authed = fetch_api_authed_from_permissioned_as(
|
||||
permissioned_as,
|
||||
email,
|
||||
permissioned_as.clone(),
|
||||
email.clone(),
|
||||
&w_id,
|
||||
&db,
|
||||
Some(username),
|
||||
Some(username.clone()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1617,6 +1646,9 @@ async fn upload_s3_file_from_app(
|
||||
.await?,
|
||||
),
|
||||
file_key,
|
||||
email,
|
||||
permissioned_as,
|
||||
username,
|
||||
)
|
||||
} else {
|
||||
return Err(Error::BadRequest(
|
||||
@@ -1640,13 +1672,16 @@ async fn upload_s3_file_from_app(
|
||||
.await?,
|
||||
),
|
||||
file_key,
|
||||
email,
|
||||
permissioned_as,
|
||||
username,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
let (_, s3_resource_opt) =
|
||||
get_workspace_s3_resource(&on_behalf_authed, &db, None, "", &w_id, None)
|
||||
.await?;
|
||||
(s3_resource_opt, file_key)
|
||||
(s3_resource_opt, file_key, email, permissioned_as, username)
|
||||
}
|
||||
} else {
|
||||
return Err(Error::BadRequest(
|
||||
@@ -1672,7 +1707,7 @@ async fn upload_s3_file_from_app(
|
||||
let (_, s3_resource_opt) =
|
||||
get_workspace_s3_resource(&on_behalf_authed, &db, None, "", &w_id, None).await?;
|
||||
|
||||
(s3_resource_opt, file_key)
|
||||
(s3_resource_opt, file_key, email, permissioned_as, username)
|
||||
}
|
||||
} else {
|
||||
// backward compatibility (no policy)
|
||||
@@ -1682,6 +1717,12 @@ async fn upload_s3_file_from_app(
|
||||
.file_key
|
||||
.unwrap_or_else(|| get_random_file_name(query.file_extension));
|
||||
|
||||
let (on_behalf_of_email, permissioned_as, username) = (
|
||||
authed.email.clone(),
|
||||
username_to_permissioned_as(&authed.username),
|
||||
authed.display_username().to_string(),
|
||||
);
|
||||
|
||||
if let Some(ref s3_resource_path) = query.s3_resource_path {
|
||||
(
|
||||
Some(
|
||||
@@ -1698,12 +1739,21 @@ async fn upload_s3_file_from_app(
|
||||
.await?,
|
||||
),
|
||||
file_key,
|
||||
on_behalf_of_email,
|
||||
permissioned_as,
|
||||
username,
|
||||
)
|
||||
} else {
|
||||
let (_, s3_resource) =
|
||||
get_workspace_s3_resource(&authed, &db, None, "", &w_id, None).await?;
|
||||
|
||||
(s3_resource, file_key)
|
||||
(
|
||||
s3_resource,
|
||||
file_key,
|
||||
on_behalf_of_email,
|
||||
permissioned_as,
|
||||
username,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return Err(Error::BadRequest("Missing s3 policy".to_string()));
|
||||
@@ -1733,7 +1783,81 @@ async fn upload_s3_file_from_app(
|
||||
|
||||
upload_file_from_req(s3_client, &file_key, request, options).await?;
|
||||
|
||||
return Ok(Json(UploadFileResponse { file_key }));
|
||||
let delete_token = jwt::encode_with_internal_secret(DeleteTokenClaims {
|
||||
file_key: file_key.clone(),
|
||||
on_behalf_of_email,
|
||||
permissioned_as,
|
||||
username,
|
||||
s3_resource_path: query.s3_resource_path,
|
||||
exp: (chrono::Utc::now() + chrono::Duration::seconds(3600 * 24)).timestamp() as usize,
|
||||
})
|
||||
.await?;
|
||||
|
||||
return Ok(Json(AppUploadFileResponse { file_key, delete_token }));
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
#[derive(Deserialize)]
|
||||
struct DeleteS3FileQuery {
|
||||
delete_token: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
async fn delete_s3_file_from_app(
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Query(query): Query<DeleteS3FileQuery>,
|
||||
) -> Result<()> {
|
||||
let DeleteTokenClaims {
|
||||
file_key,
|
||||
on_behalf_of_email,
|
||||
permissioned_as,
|
||||
username,
|
||||
s3_resource_path,
|
||||
..
|
||||
} = jwt::decode_with_internal_secret::<DeleteTokenClaims>(&query.delete_token).await?;
|
||||
|
||||
let on_behalf_authed = fetch_api_authed_from_permissioned_as(
|
||||
permissioned_as,
|
||||
on_behalf_of_email,
|
||||
&w_id,
|
||||
&db,
|
||||
Some(username),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let s3_resource = if let Some(s3_resource_path) = s3_resource_path {
|
||||
get_s3_resource(
|
||||
&on_behalf_authed,
|
||||
&db,
|
||||
Some(user_db),
|
||||
"",
|
||||
&w_id,
|
||||
s3_resource_path.as_str(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
let (_, s3_resource) =
|
||||
get_workspace_s3_resource(&on_behalf_authed, &db, None, "", &w_id, None).await?;
|
||||
|
||||
s3_resource.ok_or(Error::internal_err(
|
||||
"No files storage resource defined at the workspace level".to_string(),
|
||||
))?
|
||||
};
|
||||
|
||||
let s3_client = build_object_store_client(&s3_resource).await?;
|
||||
|
||||
let path = object_store::path::Path::from(file_key.as_str());
|
||||
|
||||
s3_client.delete(&path).await.map_err(|err| {
|
||||
tracing::error!("Error deleting file: {:?}", err);
|
||||
Error::internal_err(format!("Error deleting file: {}", err.to_string()))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "parquet"))]
|
||||
|
||||
@@ -21,7 +21,8 @@ use std::sync::{
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use windmill_common::{
|
||||
auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims, JWT_SECRET},
|
||||
auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims},
|
||||
jwt,
|
||||
users::{COOKIE_NAME, SUPERADMIN_SECRET_EMAIL},
|
||||
};
|
||||
|
||||
@@ -99,55 +100,44 @@ impl AuthCache {
|
||||
}
|
||||
}
|
||||
_ if token.starts_with("jwt_") => {
|
||||
let jwt_secret = JWT_SECRET.read().await;
|
||||
if !jwt_secret.is_empty() {
|
||||
let jwt_token = token.trim_start_matches("jwt_");
|
||||
let jwt_token = token.trim_start_matches("jwt_");
|
||||
|
||||
let jwt_result = jsonwebtoken::decode::<JWTAuthClaims>(
|
||||
jwt_token,
|
||||
&jsonwebtoken::DecodingKey::from_secret(jwt_secret.as_bytes()),
|
||||
&jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256),
|
||||
);
|
||||
let jwt_result = jwt::decode_with_internal_secret::<JWTAuthClaims>(jwt_token).await;
|
||||
|
||||
match jwt_result {
|
||||
Ok(payload) => {
|
||||
if w_id.is_some_and(|w_id| w_id != payload.claims.workspace_id) {
|
||||
tracing::error!("JWT auth error: workspace_id mismatch");
|
||||
return None;
|
||||
}
|
||||
|
||||
let username_override =
|
||||
username_override_from_label(payload.claims.label);
|
||||
let authed = crate::db::ApiAuthed {
|
||||
email: payload.claims.email,
|
||||
username: payload.claims.username,
|
||||
is_admin: payload.claims.is_admin,
|
||||
is_operator: payload.claims.is_operator,
|
||||
groups: payload.claims.groups,
|
||||
folders: payload.claims.folders,
|
||||
scopes: None,
|
||||
username_override,
|
||||
};
|
||||
|
||||
self.cache.insert(
|
||||
key,
|
||||
ExpiringAuthCache {
|
||||
authed: authed.clone(),
|
||||
expiry: chrono::Utc
|
||||
.timestamp_nanos(payload.claims.exp as i64 * 1_000_000_000),
|
||||
},
|
||||
);
|
||||
|
||||
Some(authed)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("JWT auth error: {:?}", err);
|
||||
None
|
||||
match jwt_result {
|
||||
Ok(claims) => {
|
||||
if w_id.is_some_and(|w_id| w_id != claims.workspace_id) {
|
||||
tracing::error!("JWT auth error: workspace_id mismatch");
|
||||
return None;
|
||||
}
|
||||
|
||||
let username_override = username_override_from_label(claims.label);
|
||||
let authed = crate::db::ApiAuthed {
|
||||
email: claims.email,
|
||||
username: claims.username,
|
||||
is_admin: claims.is_admin,
|
||||
is_operator: claims.is_operator,
|
||||
groups: claims.groups,
|
||||
folders: claims.folders,
|
||||
scopes: None,
|
||||
username_override,
|
||||
};
|
||||
|
||||
self.cache.insert(
|
||||
key,
|
||||
ExpiringAuthCache {
|
||||
authed: authed.clone(),
|
||||
expiry: chrono::Utc
|
||||
.timestamp_nanos(claims.exp as i64 * 1_000_000_000),
|
||||
},
|
||||
);
|
||||
|
||||
Some(authed)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("JWT auth error: {:?}", err);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
tracing::error!("JWT auth error: no jwt secret set");
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -52,6 +52,7 @@ use crate::{
|
||||
args::WebhookArgs,
|
||||
db::{ApiAuthed, DB},
|
||||
users::fetch_api_authed,
|
||||
utils::RunnableKind,
|
||||
};
|
||||
|
||||
const KEEP_LAST: i64 = 20;
|
||||
@@ -139,7 +140,7 @@ pub struct KafkaTriggerConfig {
|
||||
pub struct SqsTriggerConfig {
|
||||
pub queue_url: String,
|
||||
pub aws_resource_path: String,
|
||||
pub message_attributes: Option<Vec<String>>
|
||||
pub message_attributes: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "nats"))]
|
||||
@@ -300,8 +301,7 @@ async fn set_config(
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
let nc = if let TriggerKind::Postgres = nc.trigger_kind {
|
||||
set_postgres_trigger_config(&w_id, authed.clone(), &db, user_db.clone(), nc).await?
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
nc
|
||||
};
|
||||
|
||||
@@ -362,13 +362,6 @@ struct Capture {
|
||||
trigger_extra: Option<SqlxJson<Box<serde_json::value::RawValue>>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum RunnableKind {
|
||||
Script,
|
||||
Flow,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ListCapturesQuery {
|
||||
trigger_kind: Option<TriggerKind>,
|
||||
|
||||
@@ -32,6 +32,28 @@ async fn current_database(conn: &mut PgConnection) -> Result<String, MigrateErro
|
||||
.await?)
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref OVERRIDDEN_MIGRATIONS: std::collections::HashMap<i64, String> = vec![(20221207103910, include_str!(
|
||||
"../../custom_migrations/create_workspace_without_md5.sql"
|
||||
).to_string()),
|
||||
(20240216100535, include_str!(
|
||||
"../../migrations/20240216100535_improve_policies.up.sql"
|
||||
).replace("public.", "")),
|
||||
(20240403083110, include_str!(
|
||||
"../../migrations/20240403083110_remove_team_id_constraint.up.sql"
|
||||
).replace("public.", "")),
|
||||
(20240613150524, include_str!(
|
||||
"../../migrations/20240613150524_add_job_perms.up.sql"
|
||||
).replace("public.", "")),
|
||||
(20250102145420, include_str!(
|
||||
"../../migrations/20250102145420_more_captures.up.sql"
|
||||
).replace("public.", "")),
|
||||
(20241006144414, include_str!(
|
||||
"../../custom_migrations/grant_all_current_schema.sql"
|
||||
).to_string()),
|
||||
].into_iter().collect();
|
||||
}
|
||||
|
||||
struct CustomMigrator {
|
||||
inner: PoolConnection<Postgres>,
|
||||
}
|
||||
@@ -132,12 +154,13 @@ impl Migrate for CustomMigrator {
|
||||
migration.version,
|
||||
migration.description
|
||||
);
|
||||
if migration.version == 20221207103910 {
|
||||
tracing::info!("Skipping migration 20221207103910 to avoid using md5");
|
||||
|
||||
|
||||
if let Some(migration_sql) = OVERRIDDEN_MIGRATIONS.get(&migration.version) {
|
||||
tracing::info!("Using custom migration for version {}", migration.version);
|
||||
|
||||
self.inner
|
||||
.execute(include_str!(
|
||||
"../../custom_migrations/create_workspace_without_md5.sql"
|
||||
))
|
||||
.execute(&**migration_sql)
|
||||
.await?;
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
@@ -528,6 +551,7 @@ async fn v2_finalize(db: &DB) -> Result<(), Error> {
|
||||
)
|
||||
.await?;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -745,6 +769,12 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> {
|
||||
.execute(db)
|
||||
.await?;
|
||||
});
|
||||
|
||||
run_windmill_migration!("audit_timestamps", db, |tx| {
|
||||
sqlx::query!("CREATE INDEX CONCURRENTLY ix_audit_timestamps ON audit (timestamp DESC)")
|
||||
.execute(db)
|
||||
.await?;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::db::ApiAuthed;
|
||||
use crate::triggers::{
|
||||
get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail,
|
||||
};
|
||||
use crate::utils::WithStarredInfoQuery;
|
||||
use crate::utils::{RunnableKind, WithStarredInfoQuery};
|
||||
use crate::{
|
||||
db::DB,
|
||||
schedule::clear_schedule,
|
||||
@@ -65,6 +65,10 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/list_paths", get(list_paths))
|
||||
.route("/history/p/*path", get(get_flow_history))
|
||||
.route("/get_latest_version/*path", get(get_latest_version))
|
||||
.route(
|
||||
"/list_paths_from_workspace_runnable/:runnable_kind/*path",
|
||||
get(list_paths_from_workspace_runnable),
|
||||
)
|
||||
.route(
|
||||
"/history_update/v/:version/p/*path",
|
||||
post(update_flow_history),
|
||||
@@ -324,6 +328,28 @@ async fn check_path_conflict<'c>(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
async fn list_paths_from_workspace_runnable(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, runnable_kind, path)): Path<(String, RunnableKind, StripPath)>,
|
||||
) -> JsonResult<Vec<String>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let runnables = sqlx::query_scalar!(
|
||||
r#"SELECT f.path
|
||||
FROM flow_workspace_runnables fwr
|
||||
JOIN flow f
|
||||
ON fwr.flow_path = f.path AND fwr.workspace_id = f.workspace_id
|
||||
WHERE fwr.runnable_path = $1 AND fwr.runnable_is_flow = $2 AND fwr.workspace_id = $3"#,
|
||||
path.to_path(),
|
||||
matches!(runnable_kind, RunnableKind::Flow),
|
||||
w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(runnables))
|
||||
}
|
||||
|
||||
async fn create_flow(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
|
||||
@@ -2478,6 +2478,22 @@ async fn update_username_in_workpsace<'c>(
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"UPDATE flow_workspace_runnables SET flow_path = REGEXP_REPLACE(flow_path,'u/' || $2 || '/(.*)','u/' || $1 || '/\1') WHERE flow_path LIKE ('u/' || $2 || '/%') AND workspace_id = $3"#,
|
||||
new_username,
|
||||
old_username,
|
||||
w_id
|
||||
).execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"UPDATE flow_workspace_runnables SET runnable_path = REGEXP_REPLACE(runnable_path,'u/' || $2 || '/(.*)','u/' || $1 || '/\1') WHERE runnable_path LIKE ('u/' || $2 || '/%') AND workspace_id = $3"#,
|
||||
new_username,
|
||||
old_username,
|
||||
w_id
|
||||
).execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"UPDATE flow_node SET path = REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\1') WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3"#,
|
||||
new_username,
|
||||
|
||||
@@ -29,6 +29,13 @@ pub struct WithStarredInfoQuery {
|
||||
pub with_starred_info: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RunnableKind {
|
||||
Script,
|
||||
Flow,
|
||||
}
|
||||
|
||||
pub async fn require_super_admin(db: &DB, email: &str) -> error::Result<()> {
|
||||
let is_admin = is_super_admin_email(db, email).await?;
|
||||
|
||||
|
||||
@@ -796,7 +796,7 @@ async fn get_copilot_info(
|
||||
|
||||
let (ai_provider, exists_ai_resource) = if let Some(ai_resource) = record.ai_resource {
|
||||
let ai_resource = serde_json::from_value::<AIResource>(ai_resource)?;
|
||||
(Some(ai_resource.provider), true)
|
||||
(Some(ai_resource.provider), ai_resource.path.is_some())
|
||||
} else {
|
||||
(None, false)
|
||||
};
|
||||
|
||||
@@ -189,6 +189,14 @@ pub(crate) async fn change_workspace_id(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE flow_workspace_runnables SET workspace_id = $1 WHERE workspace_id = $2",
|
||||
&rw.new_id,
|
||||
&old_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE flow_node SET workspace_id = $1 WHERE workspace_id = $2",
|
||||
&rw.new_id,
|
||||
|
||||
@@ -61,6 +61,8 @@ async-stream.workspace = true
|
||||
const_format.workspace = true
|
||||
crc.workspace = true
|
||||
windmill-macros.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
backon.workspace = true
|
||||
|
||||
semver.workspace = true
|
||||
croner = "2.0.6"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
db::Authed,
|
||||
@@ -9,10 +7,6 @@ use crate::{
|
||||
DB,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref JWT_SECRET : Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct JWTAuthClaims {
|
||||
pub email: String,
|
||||
|
||||
@@ -59,6 +59,10 @@ impl Authable for Authed {
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref PG_SCHEMA: Option<String> = std::env::var("PG_SCHEMA").ok();
|
||||
}
|
||||
|
||||
impl UserDB {
|
||||
pub fn new(db: DB) -> Self {
|
||||
Self { db }
|
||||
@@ -95,6 +99,12 @@ impl UserDB {
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if let Some(schema) = PG_SCHEMA.as_ref() {
|
||||
sqlx::query(&format!("SET LOCAL search_path TO {}", schema))
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"SELECT set_config('session.user', $1, true)",
|
||||
authed.username()
|
||||
|
||||
@@ -40,7 +40,7 @@ pub const JWT_SECRET_SETTING: &str = "jwt_secret";
|
||||
pub const EMAIL_DOMAIN_SETTING: &str = "email_domain";
|
||||
pub const OTEL_SETTING: &str = "otel";
|
||||
|
||||
pub const ENV_SETTINGS: [&str; 56] = [
|
||||
pub const ENV_SETTINGS: [&str; 57] = [
|
||||
"DISABLE_NSJAIL",
|
||||
"MODE",
|
||||
"NUM_WORKERS",
|
||||
@@ -97,6 +97,7 @@ pub const ENV_SETTINGS: [&str; 56] = [
|
||||
"OTEL_TRACING",
|
||||
"OTEL_LOGS",
|
||||
"DISABLE_S3_STORE",
|
||||
"PG_SCHEMA",
|
||||
];
|
||||
|
||||
use crate::error;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
use crate::error::{self, to_anyhow, Error};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref JWT_SECRET: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
|
||||
}
|
||||
|
||||
pub async fn encode_with_internal_secret<T: Serialize>(claims: T) -> error::Result<String> {
|
||||
let jwt_secret = JWT_SECRET.read().await;
|
||||
|
||||
if jwt_secret.is_empty() {
|
||||
return Err(Error::internal_err("JWT secret is not set".to_string()));
|
||||
}
|
||||
|
||||
let token = jsonwebtoken::encode(
|
||||
&jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256),
|
||||
&claims,
|
||||
&jsonwebtoken::EncodingKey::from_secret(jwt_secret.as_bytes()),
|
||||
)
|
||||
.map_err(to_anyhow)?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
pub async fn decode_with_internal_secret<T: DeserializeOwned>(token: &str) -> error::Result<T> {
|
||||
let jwt_secret = JWT_SECRET.read().await;
|
||||
|
||||
if jwt_secret.is_empty() {
|
||||
return Err(Error::internal_err("JWT secret is not set".to_string()));
|
||||
}
|
||||
|
||||
let result = jsonwebtoken::decode::<T>(
|
||||
token,
|
||||
&jsonwebtoken::DecodingKey::from_secret(jwt_secret.as_bytes()),
|
||||
&jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256),
|
||||
)
|
||||
.map_err(to_anyhow)?;
|
||||
|
||||
Ok(result.claims)
|
||||
}
|
||||
@@ -33,6 +33,7 @@ pub mod job_metrics;
|
||||
#[cfg(feature = "parquet")]
|
||||
pub mod job_s3_helpers_ee;
|
||||
pub mod jobs;
|
||||
pub mod jwt;
|
||||
pub mod more_serde;
|
||||
pub mod oauth2;
|
||||
pub mod otel_ee;
|
||||
|
||||
@@ -19,6 +19,8 @@ use crate::{
|
||||
|
||||
use crate::worker::HUB_CACHE_DIR;
|
||||
use anyhow::Context;
|
||||
use backon::ConstantBuilder;
|
||||
use backon::{BackoffBuilder, Retryable};
|
||||
use serde::de::Error as _;
|
||||
use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serialize};
|
||||
|
||||
@@ -415,6 +417,8 @@ pub async fn get_hub_script_by_path(
|
||||
Some(db),
|
||||
)
|
||||
.await?
|
||||
.error_for_status()
|
||||
.map_err(to_anyhow)?
|
||||
.text()
|
||||
.await
|
||||
.map_err(to_anyhow);
|
||||
@@ -440,6 +444,8 @@ pub async fn get_hub_script_by_path(
|
||||
Some(db),
|
||||
)
|
||||
.await?
|
||||
.error_for_status()
|
||||
.map_err(to_anyhow)?
|
||||
.text()
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
@@ -494,49 +500,67 @@ async fn get_full_hub_script_by_path_inner(
|
||||
) -> crate::error::Result<HubScript> {
|
||||
let hub_base_url = HUB_BASE_URL.read().await.clone();
|
||||
|
||||
let result = http_get_from_hub(
|
||||
http_client,
|
||||
&format!("{}/raw2/{}", hub_base_url, path),
|
||||
true,
|
||||
None,
|
||||
db,
|
||||
)
|
||||
.await?
|
||||
.json::<HubScript>()
|
||||
.await
|
||||
.context("Decoding hub response to script");
|
||||
let response = (|| async {
|
||||
let response = http_get_from_hub(
|
||||
http_client,
|
||||
&format!("{}/raw2/{}", hub_base_url, path),
|
||||
true,
|
||||
None,
|
||||
db,
|
||||
)
|
||||
.await
|
||||
.and_then(|r| r.error_for_status().map_err(|e| to_anyhow(e).into()));
|
||||
|
||||
match result {
|
||||
Ok(result) => Ok(result),
|
||||
Err(e) => {
|
||||
if hub_base_url != DEFAULT_HUB_BASE_URL
|
||||
&& path
|
||||
.split("/")
|
||||
.next()
|
||||
.is_some_and(|x| x.parse::<i32>().is_ok_and(|x| x < 10_000_000))
|
||||
{
|
||||
tracing::info!(
|
||||
"Not found on private hub, fallback to default hub for {}",
|
||||
path
|
||||
);
|
||||
let value = http_get_from_hub(
|
||||
http_client,
|
||||
&format!("{}/raw2/{}", DEFAULT_HUB_BASE_URL, path),
|
||||
true,
|
||||
None,
|
||||
db,
|
||||
)
|
||||
.await?
|
||||
.json::<HubScript>()
|
||||
.await
|
||||
.context("Decoding hub response to script")?;
|
||||
|
||||
Ok(value)
|
||||
} else {
|
||||
Err(e)?
|
||||
match response {
|
||||
Ok(response) => Ok(response),
|
||||
Err(e) => {
|
||||
if hub_base_url != DEFAULT_HUB_BASE_URL
|
||||
&& path
|
||||
.split("/")
|
||||
.next()
|
||||
.is_some_and(|x| x.parse::<i32>().is_ok_and(|x| x < 10_000_000))
|
||||
{
|
||||
// TODO: should only fallback to default hub if status is 404 (hub returns 500 currently)
|
||||
tracing::info!(
|
||||
"Not found on private hub, fallback to default hub for {}",
|
||||
path
|
||||
);
|
||||
http_get_from_hub(
|
||||
http_client,
|
||||
&format!("{}/raw2/{}", DEFAULT_HUB_BASE_URL, path),
|
||||
true,
|
||||
None,
|
||||
db,
|
||||
)
|
||||
.await?
|
||||
.error_for_status()
|
||||
.map_err(|e| to_anyhow(e).into())
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.retry(
|
||||
ConstantBuilder::default()
|
||||
.with_delay(std::time::Duration::from_secs(5))
|
||||
.with_max_times(2)
|
||||
.build(),
|
||||
)
|
||||
.notify(|err, dur| {
|
||||
tracing::warn!(
|
||||
"Could not get hub script at path {path}, retrying in {dur:#?}, err: {err:#?}"
|
||||
);
|
||||
})
|
||||
.sleep(tokio::time::sleep)
|
||||
.await?;
|
||||
|
||||
let script = response
|
||||
.json::<HubScript>()
|
||||
.await
|
||||
.context(format!("Decoding hub response for script at path {path}"))?;
|
||||
|
||||
Ok(script)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
|
||||
@@ -247,7 +247,7 @@ pub async fn cancel_job<'c>(
|
||||
while !jobs.is_empty() {
|
||||
let p_job = jobs.pop();
|
||||
let new_jobs = sqlx::query_scalar!(
|
||||
"SELECT id AS \"id!\" FROM v2_job WHERE parent_job = $1 AND workspace_id = $2",
|
||||
"SELECT id AS \"id!\" FROM v2_job_queue INNER JOIN v2_job USING (id) WHERE parent_job = $1 AND v2_job.workspace_id = $2",
|
||||
p_job,
|
||||
w_id
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ use windmill_common::{
|
||||
get_latest_hash_for_path,
|
||||
jobs::QueuedJob,
|
||||
scripts::ScriptLang,
|
||||
worker::{exists_in_cache, save_cache, write_file},
|
||||
worker::{exists_in_cache, save_cache, write_file, DISABLE_BUNDLING},
|
||||
DB,
|
||||
};
|
||||
|
||||
@@ -660,7 +660,7 @@ pub async fn prebundle_bun_script(
|
||||
return Ok(());
|
||||
}
|
||||
let annotation = windmill_common::worker::TypeScriptAnnotations::parse(inner_content);
|
||||
if annotation.nobundling {
|
||||
if annotation.nobundling || *DISABLE_BUNDLING {
|
||||
return Ok(());
|
||||
}
|
||||
let origin = format!("{job_dir}/main.js");
|
||||
@@ -802,23 +802,25 @@ pub async fn handle_bun_job(
|
||||
) -> error::Result<Box<RawValue>> {
|
||||
let mut annotation = windmill_common::worker::TypeScriptAnnotations::parse(inner_content);
|
||||
|
||||
let (mut has_bundle_cache, cache_logs, local_path, remote_path) =
|
||||
if requirements_o.is_some() && !annotation.nobundling && codebase.is_none() {
|
||||
let (local_path, remote_path) = compute_bundle_local_and_remote_path(
|
||||
inner_content,
|
||||
requirements_o,
|
||||
job.script_path(),
|
||||
Some(db.clone()),
|
||||
&job.workspace_id,
|
||||
)
|
||||
.await;
|
||||
let (mut has_bundle_cache, cache_logs, local_path, remote_path) = if requirements_o.is_some()
|
||||
&& !annotation.nobundling
|
||||
&& !*DISABLE_BUNDLING
|
||||
&& codebase.is_none()
|
||||
{
|
||||
let (local_path, remote_path) = compute_bundle_local_and_remote_path(
|
||||
inner_content,
|
||||
requirements_o,
|
||||
job.script_path(),
|
||||
Some(db.clone()),
|
||||
&job.workspace_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (cache, logs) =
|
||||
windmill_common::worker::load_cache(&local_path, &remote_path).await;
|
||||
(cache, logs, local_path, remote_path)
|
||||
} else {
|
||||
(false, "".to_string(), "".to_string(), "".to_string())
|
||||
};
|
||||
let (cache, logs) = windmill_common::worker::load_cache(&local_path, &remote_path).await;
|
||||
(cache, logs, local_path, remote_path)
|
||||
} else {
|
||||
(false, "".to_string(), "".to_string(), "".to_string())
|
||||
};
|
||||
|
||||
if !codebase.is_some() && !has_bundle_cache {
|
||||
let _ = write_file(job_dir, "main.ts", inner_content)?;
|
||||
@@ -1088,6 +1090,7 @@ try {{
|
||||
|
||||
let build_cache = !has_bundle_cache
|
||||
&& !annotation.nobundling
|
||||
&& !*DISABLE_BUNDLING
|
||||
&& !codebase.is_some()
|
||||
&& (requirements_o.is_some() || annotation.native);
|
||||
|
||||
|
||||
@@ -96,19 +96,23 @@ pub enum PyVersion {
|
||||
}
|
||||
|
||||
impl PyVersion {
|
||||
pub async fn from_instance_version() -> Self {
|
||||
match INSTANCE_PYTHON_VERSION.read().await.clone() {
|
||||
pub async fn from_instance_version(job_id: &Uuid, w_id: &str, db: &Pool<Postgres>) -> Self {
|
||||
let mut err = None;
|
||||
let pyv = match INSTANCE_PYTHON_VERSION.read().await.clone() {
|
||||
Some(v) => PyVersion::from_string_with_dots(&v).unwrap_or_else(|| {
|
||||
let v = PyVersion::default();
|
||||
tracing::error!(
|
||||
"Cannot parse INSTANCE_PYTHON_VERSION ({:?}), fallback to latest_stable ({v:?})",
|
||||
*INSTANCE_PYTHON_VERSION
|
||||
);
|
||||
err = Some(format!("\nCannot parse INSTANCE_PYTHON_VERSION ({:?}), fallback to latest_stable ({v:?})", *INSTANCE_PYTHON_VERSION));
|
||||
v
|
||||
}),
|
||||
// Use latest stable
|
||||
None => PyVersion::default(),
|
||||
};
|
||||
|
||||
if let Some(msg) = err {
|
||||
append_logs(job_id, w_id, &msg, db).await;
|
||||
tracing::error!(msg);
|
||||
}
|
||||
pyv
|
||||
}
|
||||
/// e.g.: `/tmp/windmill/cache/python_3xy`
|
||||
pub fn to_cache_dir(&self) -> String {
|
||||
@@ -223,9 +227,18 @@ impl PyVersion {
|
||||
|
||||
if let Err(ref e) = res {
|
||||
tracing::error!(
|
||||
"worker_name: {worker_name}, w_id: {w_id}, job_id: {job_id}\n
|
||||
"worker_name: {worker_name}, w_id: {w_id}, job_id: {job_id}\n
|
||||
Error while getting python from uv, falling back to system python: {e:?}"
|
||||
);
|
||||
append_logs(
|
||||
job_id,
|
||||
w_id,
|
||||
format!(
|
||||
"\nError while getting python from uv, falling back to system python: {e:?}"
|
||||
),
|
||||
db,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
res
|
||||
}
|
||||
@@ -1430,7 +1443,7 @@ async fn handle_python_deps(
|
||||
let mut annotated_pyv = None;
|
||||
let mut annotated_pyv_numeric = None;
|
||||
let is_deployed = requirements_o.is_some();
|
||||
let instance_pyv = PyVersion::from_instance_version().await;
|
||||
let instance_pyv = PyVersion::from_instance_version(job_id, w_id, db).await;
|
||||
let annotations = windmill_common::worker::PythonAnnotations::parse(inner_content);
|
||||
let requirements = match requirements_o {
|
||||
Some(r) => r,
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
|
||||
use windmill_common::{
|
||||
apps::AppScriptId,
|
||||
auth::{fetch_authed_from_permissioned_as, JWTAuthClaims, JobPerms, JWT_SECRET},
|
||||
auth::{fetch_authed_from_permissioned_as, JWTAuthClaims, JobPerms},
|
||||
cache::{ScriptData, ScriptMetadata},
|
||||
jwt,
|
||||
scripts::PREVIEW_IS_TAR_CODEBASE_HASH,
|
||||
utils::WarnAfterExt,
|
||||
worker::{
|
||||
@@ -205,12 +206,6 @@ pub async fn create_token_for_owner(
|
||||
return Ok(token.clone());
|
||||
}
|
||||
|
||||
let jwt_secret = JWT_SECRET.read().await;
|
||||
|
||||
if jwt_secret.is_empty() {
|
||||
return Err(Error::internal_err("No JWT secret found".to_string()));
|
||||
}
|
||||
|
||||
let job_authed = match sqlx::query_as!(
|
||||
JobPerms,
|
||||
"SELECT * FROM job_perms WHERE job_id = $1 AND workspace_id = $2",
|
||||
@@ -248,17 +243,10 @@ pub async fn create_token_for_owner(
|
||||
scopes: None,
|
||||
};
|
||||
|
||||
let token = jsonwebtoken::encode(
|
||||
&jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256),
|
||||
&payload,
|
||||
&jsonwebtoken::EncodingKey::from_secret(jwt_secret.as_bytes()),
|
||||
)
|
||||
.map_err(|err| {
|
||||
Error::internal_err(format!(
|
||||
"Could not encode JWT token for job {job_id}: {:?}",
|
||||
err
|
||||
))
|
||||
})?;
|
||||
let token = jwt::encode_with_internal_secret(&payload)
|
||||
.await
|
||||
.with_context(|| format!("Could not encode JWT token for job {job_id}"))?;
|
||||
|
||||
Ok(format!("jwt_{}", token))
|
||||
}
|
||||
|
||||
@@ -785,7 +773,7 @@ pub async fn run_worker(
|
||||
worker_dir.clone(),
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = PyVersion::from_instance_version()
|
||||
if let Err(e) = PyVersion::from_instance_version(&Uuid::nil(), "", &db)
|
||||
.await
|
||||
.get_python(&Uuid::nil(), &mut 0, &db, &worker_name, "", &mut None)
|
||||
.await
|
||||
|
||||
@@ -620,6 +620,13 @@ pub async fn handle_flow_dependency_job(
|
||||
|
||||
tx = clear_dependency_parent_path(&parent_path, &job_path, &job.workspace_id, "flow", tx)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"DELETE FROM flow_workspace_runnables WHERE flow_path = $1 AND workspace_id = $2",
|
||||
job_path,
|
||||
job.workspace_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
let modified_ids;
|
||||
let errors;
|
||||
(flow.modules, tx, modified_ids, errors) = lock_modules(
|
||||
@@ -981,6 +988,27 @@ async fn lock_modules<'c>(
|
||||
}
|
||||
.into();
|
||||
}
|
||||
FlowModuleValue::Script { path, hash, .. } if !path.starts_with("hub/") => {
|
||||
sqlx::query!(
|
||||
"INSERT INTO flow_workspace_runnables (flow_path, runnable_path, script_hash, runnable_is_flow, workspace_id) VALUES ($1, $2, $3, FALSE, $4) ON CONFLICT DO NOTHING",
|
||||
job_path,
|
||||
path,
|
||||
hash.map(|h| h.0),
|
||||
job.workspace_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
FlowModuleValue::Flow { path, .. } => {
|
||||
sqlx::query!(
|
||||
"INSERT INTO flow_workspace_runnables (flow_path, runnable_path, runnable_is_flow, workspace_id) VALUES ($1, $2, TRUE, $3) ON CONFLICT DO NOTHING",
|
||||
job_path,
|
||||
path,
|
||||
job.workspace_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
modified_ids.extend(nmodified_ids);
|
||||
@@ -1673,7 +1701,7 @@ async fn python_dep(
|
||||
|
||||
let final_version = annotated_pyv_numeric
|
||||
.and_then(|pyv| PyVersion::from_numeric(pyv))
|
||||
.unwrap_or(PyVersion::from_instance_version().await);
|
||||
.unwrap_or(PyVersion::from_instance_version(job_id, w_id, db).await);
|
||||
|
||||
let req: std::result::Result<String, Error> = uv_pip_compile(
|
||||
job_id,
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.470.0";
|
||||
export const VERSION = "v1.473.1";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.470.0";
|
||||
export const VERSION = "1.473.1";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
Generated
+6
-6
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.470.0",
|
||||
"version": "1.473.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.470.0",
|
||||
"version": "1.473.1",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@aws-crypto/sha256-js": "^4.0.0",
|
||||
"@codingame/monaco-vscode-configuration-service-override": "~11.1.2",
|
||||
"@codingame/monaco-vscode-standalone-css-language-features": "~11.1.2",
|
||||
@@ -185,9 +185,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.32.1.tgz",
|
||||
"integrity": "sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==",
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.37.0.tgz",
|
||||
"integrity": "sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.470.0",
|
||||
"version": "1.473.1",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
@@ -9,7 +9,7 @@
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
||||
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. .",
|
||||
"package": "./preprocess_pkg_toggle.sh && svelte-package -o package && ./preprocess_pkg_toggle.sh",
|
||||
"package": "svelte-package -o package",
|
||||
"generate-backend-client": "openapi-ts --input ../backend/windmill-api/openapi.yaml --output ./src/lib/gen --useOptions --enums javascript --format false",
|
||||
"generate-backend-client-mac": "openapi-ts --input ../backend/windmill-api/openapi.yaml --output ./src/lib/gen --useOptions --enums javascript",
|
||||
"pretest": "tsc --incremental -p tests/tsconfig.json",
|
||||
@@ -84,7 +84,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@aws-crypto/sha256-js": "^4.0.0",
|
||||
"@codingame/monaco-vscode-configuration-service-override": "~11.1.2",
|
||||
"@codingame/monaco-vscode-standalone-css-language-features": "~11.1.2",
|
||||
@@ -120,8 +120,8 @@
|
||||
"minimatch": "^10.0.1",
|
||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~11.1.2",
|
||||
"monaco-editor-wrapper": "6.1.1",
|
||||
"monaco-languageclient": "9.1.1",
|
||||
"monaco-graphql": "^1.6.0",
|
||||
"monaco-languageclient": "9.1.1",
|
||||
"monaco-vim": "^0.4.1",
|
||||
"ol": "^7.4.0",
|
||||
"openai": "^4.57.2",
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Define the file to modify
|
||||
FILE="src/lib/components/apps/editor/AppEditorHeader.svelte"
|
||||
|
||||
# Function to toggle commenting of a line
|
||||
toggle_line_comment() {
|
||||
local line="$1"
|
||||
if grep -q "^//$line" "$FILE"; then
|
||||
# Uncomment the line
|
||||
sed -i "s|^//$line|$line|" "$FILE"
|
||||
else
|
||||
# Comment the line
|
||||
sed -i "s|^$line|//$line|" "$FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to toggle commenting of the first matching block
|
||||
toggle_first_block_comment() {
|
||||
local start_marker="$1"
|
||||
local end_marker="$2"
|
||||
|
||||
# Escape the end marker for use in sed
|
||||
local escaped_end_marker=$(echo "$end_marker" | sed 's|/|\\/|g')
|
||||
|
||||
# Check if the block is already commented
|
||||
if sed -n "/$start_marker/,/$escaped_end_marker/p" "$FILE" | grep -q "^<!--"; then
|
||||
# Uncomment the block
|
||||
sed -i "/$start_marker/,/$escaped_end_marker/{
|
||||
s|^<!-- ||
|
||||
s| -->$||
|
||||
}" "$FILE"
|
||||
else
|
||||
# Comment the block
|
||||
sed -i "/$start_marker/,/$escaped_end_marker/{
|
||||
s|^|<!-- |
|
||||
s|$| -->|
|
||||
}" "$FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Define the line and block
|
||||
LINE=" import UnsavedConfirmationModal "
|
||||
BLOCK_START="<UnsavedConfirmationModal"
|
||||
BLOCK_END="/>"
|
||||
|
||||
# Toggle the line comment
|
||||
toggle_line_comment "$LINE"
|
||||
|
||||
# Toggle the first block comment
|
||||
toggle_first_block_comment "$BLOCK_START" "$BLOCK_END"
|
||||
|
||||
echo "Toggled line and first block comments in $FILE."
|
||||
Vendored
+5
-5
@@ -1,7 +1,7 @@
|
||||
declare namespace svelte.JSX {
|
||||
interface DOMAttributes<T> {
|
||||
onclick_outside?: CompositionEventHandler<T>
|
||||
onpointerdown_outside?: (event: CustomEvent) => void
|
||||
onpointerdown_connecting?: (event: CustomEvent) => void
|
||||
declare namespace svelteHTML {
|
||||
interface HTMLAttributes<T> {
|
||||
'on:click_outside'?: (event: CustomEvent) => void
|
||||
'on:pointerdown_outside'?: (event: CustomEvent) => void
|
||||
'on:pointerdown_connecting'?: (event: CustomEvent) => void
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,26 +92,24 @@
|
||||
{/if}
|
||||
|
||||
<span class="text-xs mb-1 pt-2 leading-6">Role</span>
|
||||
<ToggleButtonGroup bind:selected class="mb-4">
|
||||
<ToggleButtonGroup bind:selected class="mb-4" let:item>
|
||||
<ToggleButton
|
||||
value="operator"
|
||||
size="sm"
|
||||
label="Operator"
|
||||
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
position="center"
|
||||
value="developer"
|
||||
size="sm"
|
||||
label="Developer"
|
||||
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
position="right"
|
||||
value="admin"
|
||||
size="sm"
|
||||
label="Admin"
|
||||
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
|
||||
{item}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
<Button
|
||||
|
||||
@@ -756,14 +756,16 @@
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
{#if oneOf && oneOf.length >= 2}
|
||||
<ToggleButtonGroup
|
||||
bind:selected={oneOfSelected}
|
||||
on:selected={() => {
|
||||
value = { label: oneOfSelected }
|
||||
selected={oneOfSelected}
|
||||
on:selected={({ detail }) => {
|
||||
oneOfSelected = detail
|
||||
value = { label: detail }
|
||||
redraw += 1
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
{#each oneOf as obj}
|
||||
<ToggleButton value={obj.title} label={obj.title} />
|
||||
<ToggleButton value={obj.title ?? ''} label={obj.title} {item} />
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
{#if oneOfSelected}
|
||||
|
||||
@@ -52,9 +52,15 @@
|
||||
<label class="block pb-2">
|
||||
<div class="flex gap-2 items-end">
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={value['custom']}>
|
||||
<ToggleButton value={false} label={'Org'} />
|
||||
<ToggleButton value={true} label="Custom" />
|
||||
<ToggleButtonGroup
|
||||
selected={value['custom'] ? 'custom' : 'org'}
|
||||
on:selected={({ detail }) => {
|
||||
value['custom'] = detail === 'custom'
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton value="org" label={'Org'} {item} />
|
||||
<ToggleButton value="custom" label="Custom" {item} />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="grow">
|
||||
|
||||
@@ -264,28 +264,23 @@
|
||||
on:selected={(e) => dispatch('dirty')}
|
||||
bind:selected={config.integration.type}
|
||||
class="mb-4 mt-2"
|
||||
let:item
|
||||
>
|
||||
<ToggleButton
|
||||
value="dryrun"
|
||||
size="sm"
|
||||
label="Dry run"
|
||||
tooltip="See autoscaling events but not actual scaling actions will be performed"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="script"
|
||||
size="sm"
|
||||
label="Custom script"
|
||||
tooltip="Run a custom script to scale your worker group"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton position="center" disabled value="ecs" size="sm" label="ECS (soon)" />
|
||||
<ToggleButton position="right" disabled value="nomad" size="sm" label="Nomad (soon)" />
|
||||
<ToggleButton
|
||||
position="right"
|
||||
disabled
|
||||
value="kubernetes"
|
||||
size="sm"
|
||||
label="Kubernetes (soon)"
|
||||
/>
|
||||
<ToggleButton disabled value="ecs" label="ECS (soon)" {item} />
|
||||
<ToggleButton disabled value="nomad" label="Nomad (soon)" {item} />
|
||||
<ToggleButton disabled value="kubernetes" label="Kubernetes (soon)" {item} />
|
||||
</ToggleButtonGroup>
|
||||
|
||||
{#if config.integration.type === 'script'}
|
||||
@@ -347,12 +342,12 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<ToggleButtonGroup selected={'script'} disabled class="mb-4 mt-2">
|
||||
<ToggleButton value="dryrun" size="sm" label="Dry run" />
|
||||
<ToggleButton value="script" size="sm" label="Custom script" />
|
||||
<ToggleButton position="center" value="ecs" size="sm" label="ECS (soon)" />
|
||||
<ToggleButton position="right" value="nomad" size="sm" label="Nomad (soon)" />
|
||||
<ToggleButton position="right" value="kubernetes" size="sm" label="Kubernetes (soon)" />
|
||||
<ToggleButtonGroup selected={'script'} disabled class="mb-4 mt-2" let:item>
|
||||
<ToggleButton value="dryrun" label="Dry run" {item} />
|
||||
<ToggleButton value="script" label="Custom script" {item} />
|
||||
<ToggleButton value="ecs" label="ECS (soon)" {item} />
|
||||
<ToggleButton value="nomad" label="Nomad (soon)" {item} />
|
||||
<ToggleButton value="kubernetes" label="Kubernetes (soon)" {item} />
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<label>
|
||||
|
||||
@@ -157,15 +157,23 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2 flex-wrap sticky top-0 left-0 right-0 bg-surface">
|
||||
<div class="p-2">
|
||||
<ToggleButtonGroup bind:selected={searchKind} class="h-10 ">
|
||||
<ToggleButton small light value="all" label={'All' + counts.all} />
|
||||
<ToggleButton small light value="scripts" icon={Code2} label={'Scripts' + counts.scripts} />
|
||||
<ToggleButtonGroup bind:selected={searchKind} class="h-10 " let:item>
|
||||
<ToggleButton small light value="all" label={'All' + counts.all} {item} />
|
||||
<ToggleButton
|
||||
small
|
||||
light
|
||||
value="scripts"
|
||||
icon={Code2}
|
||||
label={'Scripts' + counts.scripts}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
small
|
||||
light
|
||||
value="resources"
|
||||
icon={Boxes}
|
||||
label={'Resources' + counts.resources}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
small
|
||||
@@ -174,6 +182,7 @@
|
||||
label={'Flows' + counts.flows}
|
||||
icon={FlowIcon}
|
||||
selectedColor="#14b8a6"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
small
|
||||
@@ -182,6 +191,7 @@
|
||||
label={'Apps' + counts.apps}
|
||||
icon={LayoutDashboard}
|
||||
selectedColor="#fb923c"
|
||||
{item}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
@@ -53,6 +53,12 @@
|
||||
$: dbSchema = resourcePath && resourcePath in $dbSchemas ? $dbSchemas[resourcePath] : undefined
|
||||
|
||||
$: shouldDisplayError = resourcePath && resourcePath in $dbSchemas && !$dbSchemas[resourcePath]
|
||||
|
||||
function handleSelected({ detail }: CustomEvent<string>) {
|
||||
if (dbSchema && dbSchema.lang !== 'graphql') {
|
||||
dbSchema.publicOnly = detail === 'dbo'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
@@ -86,9 +92,14 @@
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
{#if dbSchema.lang !== 'graphql' && (dbSchema.schema?.public || dbSchema.schema?.PUBLIC || dbSchema.schema?.dbo)}
|
||||
<ToggleButtonGroup class="mb-4" bind:selected={dbSchema.publicOnly}>
|
||||
<ToggleButton value={true} label={dbSchema.schema.dbo ? 'Dbo' : 'Public'} />
|
||||
<ToggleButton value={false} label="All" />
|
||||
<ToggleButtonGroup
|
||||
class="mb-4"
|
||||
selected={dbSchema.publicOnly ? 'dbo' : 'all'}
|
||||
on:selected={handleSelected}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton value="dbo" label={dbSchema.schema.dbo ? 'Dbo' : 'Public'} {item} />
|
||||
<ToggleButton value="all" label="All" {item} />
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
{#if dbSchema.lang === 'graphql'}
|
||||
|
||||
@@ -146,9 +146,9 @@
|
||||
</Button>
|
||||
{/if}
|
||||
<!-- <div>
|
||||
<ToggleButtonGroup bind:selected={format}>
|
||||
<ToggleButton light small value={'local'} label="local" />
|
||||
<ToggleButton light small value={'utc'} label="utc" />
|
||||
<ToggleButtonGroup bind:selected={format} let:item>
|
||||
<ToggleButton light small value={'local'} label="local" {item} />
|
||||
<ToggleButton light small value={'utc'} label="utc" {item} />
|
||||
</ToggleButtonGroup>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import HighlightTheme from './HighlightTheme.svelte'
|
||||
import PdfViewer from './display/PdfViewer.svelte'
|
||||
import type { DisplayResultUi } from './custom_ui'
|
||||
import { getContext, hasContext } from 'svelte'
|
||||
|
||||
export let result: any
|
||||
export let requireHtmlApproval = false
|
||||
@@ -48,6 +50,7 @@
|
||||
export let nodeId: string | undefined = undefined
|
||||
export let language: string | undefined = undefined
|
||||
export let appPath: string | undefined = undefined
|
||||
export let customUi: DisplayResultUi | undefined = undefined
|
||||
|
||||
const IMG_MAX_SIZE = 10000000
|
||||
const TABLE_MAX_SIZE = 5000000
|
||||
@@ -389,6 +392,10 @@
|
||||
let globalForceJson: boolean = false
|
||||
|
||||
let seeS3PreviewFileFromList = ''
|
||||
|
||||
const disableTooltips = hasContext('disableTooltips')
|
||||
? getContext('disableTooltips') === true
|
||||
: false
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
@@ -402,10 +409,11 @@
|
||||
on:selected={(ev) => {
|
||||
globalForceJson = ev.detail === 'json'
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton class="px-1.5" value="pretty" label="Pretty" icon={Highlighter} />
|
||||
<ToggleButton class="px-1.5" value="pretty" label="Pretty" icon={Highlighter} {item} />
|
||||
|
||||
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} />
|
||||
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} {item} />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -438,44 +446,57 @@
|
||||
{#if !hideAsJson && !['json', 's3object'].includes(resultKind ?? '') && typeof result === 'object'}<ToggleButtonGroup
|
||||
class="h-6"
|
||||
selected={forceJson ? 'json' : resultKind?.startsWith('table-') ? 'table' : 'pretty'}
|
||||
let:item
|
||||
on:selected={(ev) => {
|
||||
forceJson = ev.detail === 'json'
|
||||
}}
|
||||
>
|
||||
{#if ['table-col', 'table-row', 'table-row-object'].includes(resultKind ?? '')}
|
||||
<ToggleButton class="px-1.5" value="table" label="Table" icon={Table2} />
|
||||
<ToggleButton class="px-1.5" value="table" label="Table" icon={Table2} {item} />
|
||||
{:else}
|
||||
<ToggleButton class="px-1.5" value="pretty" label="Pretty" icon={Highlighter} />
|
||||
<ToggleButton
|
||||
class="px-1.5"
|
||||
value="pretty"
|
||||
label="Pretty"
|
||||
icon={Highlighter}
|
||||
{item}
|
||||
/>
|
||||
{/if}
|
||||
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} />
|
||||
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} {item} />
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-secondary text-xs flex gap-2.5 z-10 items-center">
|
||||
<slot name="copilot-fix" />
|
||||
{#if customUi?.disableAiFix !== true}
|
||||
<slot name="copilot-fix" />
|
||||
{/if}
|
||||
{#if !disableExpand && !noControls}
|
||||
<a
|
||||
download="{filename ?? 'result'}.json"
|
||||
class="-mt-1 text-current"
|
||||
href={workspaceId && jobId
|
||||
? nodeId
|
||||
? `${base}/api/w/${workspaceId}/jobs/result_by_id/${jobId}/${nodeId}`
|
||||
: `${base}/api/w/${workspaceId}/jobs_u/completed/get_result/${jobId}`
|
||||
: `data:text/json;charset=utf-8,${encodeURIComponent(toJsonStr(result))}`}
|
||||
>
|
||||
<Download size={14} />
|
||||
</a>
|
||||
<Popover
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/rich_display_rendering"
|
||||
>
|
||||
<svelte:fragment slot="text">
|
||||
The result renderer in Windmill supports rich display rendering, allowing you to
|
||||
customize the display format of your results.
|
||||
</svelte:fragment>
|
||||
<div class="-mt-1">
|
||||
<InfoIcon size={14} />
|
||||
</div>
|
||||
</Popover>
|
||||
{#if customUi?.disableDownload !== true}
|
||||
<a
|
||||
download="{filename ?? 'result'}.json"
|
||||
class="-mt-1 text-current"
|
||||
href={workspaceId && jobId
|
||||
? nodeId
|
||||
? `${base}/api/w/${workspaceId}/jobs/result_by_id/${jobId}/${nodeId}`
|
||||
: `${base}/api/w/${workspaceId}/jobs_u/completed/get_result/${jobId}`
|
||||
: `data:text/json;charset=utf-8,${encodeURIComponent(toJsonStr(result))}`}
|
||||
>
|
||||
<Download size={14} />
|
||||
</a>
|
||||
{/if}
|
||||
{#if disableTooltips !== true}
|
||||
<Popover
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/rich_display_rendering"
|
||||
>
|
||||
<svelte:fragment slot="text">
|
||||
The result renderer in Windmill supports rich display rendering, allowing you to
|
||||
customize the display format of your results.
|
||||
</svelte:fragment>
|
||||
<div class="-mt-1">
|
||||
<InfoIcon size={14} />
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
<button on:click={() => copyToClipboard(toJsonStr(result))} class="-mt-1">
|
||||
<ClipboardCopy size={14} />
|
||||
</button>
|
||||
@@ -885,19 +906,21 @@
|
||||
<Drawer bind:this={jsonViewer} bind:open={drawerOpen} size="900px">
|
||||
<DrawerContent title="Expanded Result" on:close={jsonViewer.closeDrawer}>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
download="{filename ?? 'result'}.json"
|
||||
href={workspaceId && jobId
|
||||
? nodeId
|
||||
? `${base}/api/w/${workspaceId}/jobs/result_by_id/${jobId}/${nodeId}`
|
||||
: `${base}/api/w/${workspaceId}/jobs_u/completed/get_result/${jobId}`
|
||||
: `data:text/json;charset=utf-8,${encodeURIComponent(toJsonStr(result))}`}
|
||||
startIcon={{ icon: Download }}
|
||||
color="light"
|
||||
size="xs"
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
{#if customUi?.disableDownload !== true}
|
||||
<Button
|
||||
download="{filename ?? 'result'}.json"
|
||||
href={workspaceId && jobId
|
||||
? nodeId
|
||||
? `${base}/api/w/${workspaceId}/jobs/result_by_id/${jobId}/${nodeId}`
|
||||
: `${base}/api/w/${workspaceId}/jobs_u/completed/get_result/${jobId}`
|
||||
: `data:text/json;charset=utf-8,${encodeURIComponent(toJsonStr(result))}`}
|
||||
startIcon={{ icon: Download }}
|
||||
color="light"
|
||||
size="xs"
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
on:click={() => copyToClipboard(toJsonStr(result))}
|
||||
color="light"
|
||||
|
||||
@@ -93,7 +93,13 @@
|
||||
{#if $$slots.buttonReplacement}
|
||||
<slot name="buttonReplacement" />
|
||||
{:else}
|
||||
<Button nonCaptureEvent size="xs" color="light" startIcon={{ icon: MoreVertical }} />
|
||||
<Button
|
||||
nonCaptureEvent
|
||||
size="xs"
|
||||
color="light"
|
||||
startIcon={{ icon: MoreVertical }}
|
||||
btnClasses="bg-transparent"
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { tweened } from 'svelte/motion'
|
||||
import type { SchemaDiff } from '$lib/components/schema/schemaUtils'
|
||||
import type { EditableSchemaFormUi } from '$lib/components/custom_ui'
|
||||
|
||||
export let schema: Schema | any
|
||||
export let schemaSkippedValues: string[] = []
|
||||
@@ -53,6 +54,7 @@
|
||||
export let disableDnd: boolean = false
|
||||
export let shouldDispatchChanges: boolean = false
|
||||
export let isValid: boolean = true
|
||||
export let customUi: EditableSchemaFormUi | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -201,7 +203,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
let jsonView: boolean = false
|
||||
let jsonView: boolean = customUi?.jsonOnly == true
|
||||
let schemaString: string = JSON.stringify(schema, null, '\t')
|
||||
let error: string | undefined = undefined
|
||||
let editor: SimpleEditor | undefined = undefined
|
||||
@@ -307,6 +309,7 @@
|
||||
}}
|
||||
{shouldDispatchChanges}
|
||||
bind:isValid
|
||||
noVariablePicker={noVariablePicker || customUi?.disableVariablePicker === true}
|
||||
/>
|
||||
|
||||
<slot name="runButton" />
|
||||
@@ -325,7 +328,7 @@
|
||||
<slot name="extraTab" />
|
||||
{:else}
|
||||
<!-- WIP -->
|
||||
{#if jsonEnabled}
|
||||
{#if jsonEnabled && customUi?.jsonOnly != true}
|
||||
<div class="w-full p-3 flex justify-end">
|
||||
<Toggle
|
||||
bind:checked={jsonView}
|
||||
@@ -465,6 +468,7 @@
|
||||
<ToggleButtonGroup
|
||||
tabListClass="flex-wrap"
|
||||
class="h-auto"
|
||||
let:item
|
||||
bind:selected
|
||||
on:selected={(e) => {
|
||||
const isS3 = e.detail == 'S3'
|
||||
@@ -545,7 +549,7 @@
|
||||
}}
|
||||
>
|
||||
{#each [['String', 'string'], ['Number', 'number'], ['Integer', 'integer'], ['Object', 'object'], ['OneOf', 'oneOf'], ['Array', 'array'], ['Boolean', 'boolean'], ['S3 Object', 'S3']] as x}
|
||||
<ToggleButton value={x[1]} label={x[0]} />
|
||||
<ToggleButton value={x[1]} label={x[0]} {item} />
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
</Label>
|
||||
|
||||
@@ -593,7 +593,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#if showResourcePicker}
|
||||
{#if showResourcePicker && customUi?.resource != false}
|
||||
<Button
|
||||
title="Add resource"
|
||||
btnClasses="!font-medium text-tertiary"
|
||||
@@ -623,18 +623,20 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
title="Reset Content"
|
||||
btnClasses="!font-medium text-tertiary"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
color="light"
|
||||
on:click={clearContent}
|
||||
{iconOnly}
|
||||
startIcon={{ icon: RotateCw }}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
{#if customUi?.reset != false}
|
||||
<Button
|
||||
title="Reset Content"
|
||||
btnClasses="!font-medium text-tertiary"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
color="light"
|
||||
on:click={clearContent}
|
||||
{iconOnly}
|
||||
startIcon={{ icon: RotateCw }}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#if customUi?.assistants != false}
|
||||
{#if lang == 'deno' || lang == 'python3' || lang == 'go' || lang == 'bash'}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import Toggle from './Toggle.svelte'
|
||||
import JsonInputs from './JsonInputs.svelte'
|
||||
import FlowHistoryJobPicker from './FlowHistoryJobPicker.svelte'
|
||||
import { NEVER_TESTED_THIS_FAR } from './flows/models'
|
||||
|
||||
export let previewMode: 'upTo' | 'whole'
|
||||
export let open: boolean
|
||||
@@ -172,8 +173,11 @@
|
||||
$: selectedJobStep !== undefined && onSelectedJobStepChange()
|
||||
|
||||
async function loadIndividualStepsStates() {
|
||||
// console.log('loadIndividualStepsStates')
|
||||
dfs($flowStore.value.modules, async (module) => {
|
||||
if ($flowStateStore[module.id]?.previewResult) {
|
||||
// console.log('module', $flowStateStore[module.id], module.id)
|
||||
const prev = $flowStateStore[module.id]?.previewResult
|
||||
if (prev && prev != NEVER_TESTED_THIS_FAR) {
|
||||
return
|
||||
}
|
||||
const previousJobId = await JobService.listJobs({
|
||||
@@ -183,6 +187,7 @@
|
||||
page: 1,
|
||||
perPage: 1
|
||||
})
|
||||
// console.log('previousJobId', previousJobId, module.id)
|
||||
|
||||
if (previousJobId.length > 0) {
|
||||
const getJobResult = await JobService.getCompletedJobResultMaybe({
|
||||
|
||||
@@ -184,9 +184,13 @@
|
||||
</Alert>
|
||||
<div class="flex items-center gap-1">
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={ownerKind} on:selected={() => (ownerItem = '')}>
|
||||
<ToggleButton value="user" size="xs" label="User" />
|
||||
<ToggleButton value="group" size="xs" label="Group" />
|
||||
<ToggleButtonGroup
|
||||
bind:selected={ownerKind}
|
||||
on:selected={() => (ownerItem = '')}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton value="user" label="User" {item} />
|
||||
<ToggleButton value="group" label="Group" {item} />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
@@ -194,7 +198,9 @@
|
||||
<AutoComplete
|
||||
required
|
||||
noInputStyles
|
||||
items={ownerKind === 'user' ? usernames.filter((x) => !perms?.map((y) => y.owner_name).includes('u/'+x)) : groups.filter((x) => !perms?.map((y) => y.owner_name).includes('g/'+x))}
|
||||
items={ownerKind === 'user'
|
||||
? usernames.filter((x) => !perms?.map((y) => y.owner_name).includes('u/' + x))
|
||||
: groups.filter((x) => !perms?.map((y) => y.owner_name).includes('g/' + x))}
|
||||
bind:selectedItem={ownerItem}
|
||||
/>
|
||||
{#if ownerKind == 'group'}
|
||||
@@ -271,6 +277,7 @@
|
||||
<div>
|
||||
<ToggleButtonGroup
|
||||
selected={role}
|
||||
let:item
|
||||
on:selected={async (e) => {
|
||||
const role = e.detail
|
||||
// const wasInFolder = (folder?.owners ?? []).includes(folder)
|
||||
@@ -309,25 +316,25 @@
|
||||
>
|
||||
<ToggleButton
|
||||
value="viewer"
|
||||
size="xs"
|
||||
label="Viewer"
|
||||
tooltip="A viewer of a folder has read-only access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
|
||||
{item}
|
||||
/>
|
||||
|
||||
<ToggleButton
|
||||
position="center"
|
||||
value="writer"
|
||||
size="xs"
|
||||
label="Writer"
|
||||
tooltip="A writer of a folder has read AND write access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
|
||||
{item}
|
||||
/>
|
||||
|
||||
<ToggleButton
|
||||
position="right"
|
||||
value="admin"
|
||||
size="xs"
|
||||
label="Admin"
|
||||
tooltip="An admin of a folder has read AND write access to all the elements inside the folders and can manage the permissions as well as add new admins"
|
||||
{item}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
@@ -150,6 +150,7 @@
|
||||
<div>
|
||||
<ToggleButtonGroup
|
||||
selected={role}
|
||||
let:item
|
||||
on:selected={async (e) => {
|
||||
const role = e.detail
|
||||
// const wasInGroup = (group?.members ?? []).includes(group)
|
||||
@@ -215,6 +216,7 @@
|
||||
size="xs"
|
||||
label="Member"
|
||||
tooltip="A Member of a group can see everything the group can see, write to everything the group can write, and generally act on behalf of the group"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
position="right"
|
||||
@@ -222,6 +224,7 @@
|
||||
size="xs"
|
||||
label="Admin"
|
||||
tooltip="An admin of a group is a member of a group that can also add and remove members to the group, or make them admin."
|
||||
{item}
|
||||
/>
|
||||
{#if role === 'manager'}
|
||||
<ToggleButton
|
||||
@@ -230,6 +233,7 @@
|
||||
label="Manager"
|
||||
tooltip="A manager of a group can manage the group, adding and removing users and
|
||||
change their roles. Being a manager does not make you a member"
|
||||
{item}
|
||||
/>
|
||||
{/if}
|
||||
</ToggleButtonGroup>
|
||||
|
||||
@@ -358,6 +358,7 @@
|
||||
<div>
|
||||
<ToggleButtonGroup
|
||||
selected={propertyType}
|
||||
let:item
|
||||
on:selected={(e) => {
|
||||
if (e.detail == propertyType) return
|
||||
const staticTemplate = isStaticTemplate(inputCat)
|
||||
@@ -417,9 +418,10 @@
|
||||
value="static"
|
||||
size="xs2"
|
||||
label={'${}'}
|
||||
{item}
|
||||
/>
|
||||
{:else}
|
||||
<ToggleButton small label="Static" value="static" />
|
||||
<ToggleButton small label="Static" value="static" {item} />
|
||||
{/if}
|
||||
|
||||
{#if codeInjectionDetected && propertyType == 'static'}
|
||||
@@ -441,6 +443,7 @@
|
||||
tooltip="JavaScript expression ('flow_input' or 'results')."
|
||||
value="javascript"
|
||||
icon={FunctionSquare}
|
||||
{item}
|
||||
/>
|
||||
{/if}
|
||||
</ToggleButtonGroup>
|
||||
|
||||
@@ -185,12 +185,13 @@
|
||||
</span>
|
||||
{/if}
|
||||
</label>
|
||||
<ToggleButtonGroup bind:selected={$values[setting.key]}>
|
||||
<ToggleButtonGroup bind:selected={$values[setting.key]} let:item={toggleButton}>
|
||||
{#each setting.select_items ?? [] as item}
|
||||
<ToggleButton
|
||||
value={item.value ?? item.label}
|
||||
label={item.label}
|
||||
tooltip={item.tooltip}
|
||||
item={toggleButton}
|
||||
/>
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
|
||||
@@ -80,28 +80,26 @@
|
||||
bind:value={email}
|
||||
class="mr-4"
|
||||
/>
|
||||
<ToggleButtonGroup bind:selected>
|
||||
<ToggleButtonGroup bind:selected let:item>
|
||||
<ToggleButton
|
||||
value="operator"
|
||||
size="sm"
|
||||
label="Operator"
|
||||
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
|
||||
{item}
|
||||
/>
|
||||
|
||||
<ToggleButton
|
||||
position="center"
|
||||
value="developer"
|
||||
size="sm"
|
||||
label="Developer"
|
||||
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
|
||||
{item}
|
||||
/>
|
||||
|
||||
<ToggleButton
|
||||
position="right"
|
||||
value="admin"
|
||||
size="sm"
|
||||
label="Admin"
|
||||
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
|
||||
{item}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
<Button
|
||||
|
||||
@@ -52,9 +52,15 @@
|
||||
<label class="block pb-2">
|
||||
<div class="flex gap-2 items-end">
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={value['custom']}>
|
||||
<ToggleButton value={false} label={'Org'} />
|
||||
<ToggleButton value={true} label="Custom" />
|
||||
<ToggleButtonGroup
|
||||
selected={value['custom'] ? 'custom' : 'org'}
|
||||
on:selected={({ detail }) => {
|
||||
value['custom'] = detail === 'custom'
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton value="org" label={'Org'} {item} />
|
||||
<ToggleButton value="custom" label="Custom" {item} />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="grow">
|
||||
|
||||
@@ -374,6 +374,7 @@
|
||||
}
|
||||
}
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton
|
||||
icon={User}
|
||||
@@ -383,6 +384,7 @@
|
||||
value="user"
|
||||
position="left"
|
||||
label="User"
|
||||
{item}
|
||||
/>
|
||||
<!-- <ToggleButton light size="xs" value="group" position="center">Group</ToggleButton> -->
|
||||
<ToggleButton
|
||||
@@ -393,6 +395,7 @@
|
||||
value="folder"
|
||||
position="right"
|
||||
label="Folder"
|
||||
{item}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
import TimeAgo from './TimeAgo.svelte'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
import { autoPlacement } from '@floating-ui/core'
|
||||
import { Calendar, CornerDownLeft } from 'lucide-svelte'
|
||||
import RunFormAdvancedPopup from './RunFormAdvancedPopup.svelte'
|
||||
import { page } from '$app/stores'
|
||||
@@ -205,24 +204,7 @@
|
||||
{scheduledForStr ? 'Schedule to run later' : buttonText}
|
||||
</Button>
|
||||
<div>
|
||||
<Popover
|
||||
floatingConfig={{
|
||||
middleware: [
|
||||
autoPlacement({
|
||||
allowedPlacements: [
|
||||
'bottom-start',
|
||||
'bottom-end',
|
||||
'top-start',
|
||||
'top-end',
|
||||
'top',
|
||||
'bottom'
|
||||
]
|
||||
})
|
||||
]
|
||||
}}
|
||||
closeButton
|
||||
usePointerDownOutside
|
||||
>
|
||||
<Popover placement="bottom" closeButton usePointerDownOutside>
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button nonCaptureEvent startIcon={{ icon: Calendar }} size="xs" color="light">
|
||||
Advanced
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
let initialPrimarySchedule: Writable<ScheduleTrigger | false | undefined> = writable(undefined)
|
||||
|
||||
async function updateSchedules(forceRefresh: boolean) {
|
||||
const loadPrimarySchedule = true
|
||||
loadSchedules(
|
||||
forceRefresh,
|
||||
path,
|
||||
@@ -37,7 +38,8 @@
|
||||
primarySchedule,
|
||||
initialPrimarySchedule,
|
||||
$workspaceStore ?? '',
|
||||
triggersCount
|
||||
triggersCount,
|
||||
loadPrimarySchedule
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -335,27 +335,30 @@
|
||||
{#if linkedSecretCandidates?.includes(argName)}
|
||||
<div>
|
||||
<ToggleButtonGroup
|
||||
selected={linkedSecret == argName}
|
||||
selected={linkedSecret == argName ? 'secret' : 'inlined'}
|
||||
on:selected={(e) => {
|
||||
if (e.detail) {
|
||||
if (e.detail === 'secret') {
|
||||
linkedSecret = argName
|
||||
} else if (linkedSecret == argName) {
|
||||
linkedSecret = undefined
|
||||
}
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton
|
||||
value={false}
|
||||
value="inlined"
|
||||
size="sm"
|
||||
label="Inlined"
|
||||
tooltip="The value is inlined in the resource and thus has no special treatment."
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
position="right"
|
||||
value={true}
|
||||
value="secret"
|
||||
size="sm"
|
||||
label="Secret"
|
||||
tooltip="The value will be stored in a newly created linked secret variable at the same path. That variable can be permissioned differently, will be treated as a secret the UI, operators will not be able to load it and every access will generate a corresponding audit log."
|
||||
{item}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>{/if}</svelte:fragment
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
} from '$lib/gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { initialCode } from '$lib/script_helpers'
|
||||
import { page } from '$app/stores'
|
||||
import { defaultScripts, enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import {
|
||||
cleanValueProperties,
|
||||
@@ -64,7 +63,7 @@
|
||||
import { writable } from 'svelte/store'
|
||||
import { defaultScriptLanguages, processLangs } from '$lib/scripts'
|
||||
import DefaultScripts from './DefaultScripts.svelte'
|
||||
import { createEventDispatcher, setContext } from 'svelte'
|
||||
import { createEventDispatcher, onMount, setContext } from 'svelte'
|
||||
import CustomPopover from './CustomPopover.svelte'
|
||||
import Summary from './Summary.svelte'
|
||||
import type { ScriptBuilderWhitelabelCustomUi } from './custom_ui'
|
||||
@@ -77,6 +76,7 @@
|
||||
} from '$lib/script_helpers'
|
||||
import CaptureTable from './triggers/CaptureTable.svelte'
|
||||
import type { SavedAndModifiedValue } from './common/confirmationModal/unsavedTypes'
|
||||
import type { ScriptBuilderFunctionExports } from './scriptBuilder'
|
||||
|
||||
export let script: NewScript
|
||||
export let fullyLoaded: boolean = true
|
||||
@@ -94,6 +94,8 @@
|
||||
window.history.replaceState(null, '', url)
|
||||
export let customUi: ScriptBuilderWhitelabelCustomUi = {}
|
||||
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
|
||||
export let functionExports: ((exports: ScriptBuilderFunctionExports) => void) | undefined =
|
||||
undefined
|
||||
|
||||
export function getInitialAndModifiedValues(): SavedAndModifiedValue {
|
||||
return {
|
||||
@@ -139,6 +141,25 @@
|
||||
|
||||
$: initialPath != '' && loadTriggers()
|
||||
|
||||
onMount(() => {
|
||||
if (functionExports) {
|
||||
console.log('functionExports set')
|
||||
functionExports({
|
||||
setPreviewArgs: (args: Record<string, any>) => {
|
||||
scriptEditor?.setArgs(args)
|
||||
},
|
||||
runPreview: async () => await scriptEditor?.runTest(),
|
||||
setCode: (code: string, language?: Script['language']) => {
|
||||
if (language) {
|
||||
script.language = language
|
||||
}
|
||||
editor?.setCode(code)
|
||||
},
|
||||
getCode: () => editor?.getCode() ?? ''
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
async function loadTriggers() {
|
||||
$triggersCount = await ScriptService.getTriggersCountOfScript({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -180,9 +201,13 @@
|
||||
$defaultScripts?.order ?? Object.keys(defaultScriptLanguages)
|
||||
)
|
||||
.map((l) => [defaultScriptLanguages[l], l])
|
||||
.filter(
|
||||
(x) => $defaultScripts?.hidden == undefined || !$defaultScripts.hidden.includes(x[1])
|
||||
) as [string, SupportedLanguage | 'docker' | 'bunnative'][]
|
||||
.filter((x) => $defaultScripts?.hidden == undefined || !$defaultScripts.hidden.includes(x[1]))
|
||||
.filter((x) => {
|
||||
if (customUi?.settingsPanel?.metadata?.languages === undefined) {
|
||||
return true
|
||||
}
|
||||
return customUi.settingsPanel.metadata.languages.includes(x[1] as SupportedLanguage)
|
||||
}) as [string, SupportedLanguage | 'docker' | 'bunnative'][]
|
||||
|
||||
const scriptKindOptions: {
|
||||
value: Script['kind']
|
||||
@@ -245,16 +270,17 @@
|
||||
}
|
||||
|
||||
async function isTemplateScript() {
|
||||
let getInitBlockTemplate = $page.url.searchParams.get('id')
|
||||
if (getInitBlockTemplate === null) {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const templateId = params.get('id')
|
||||
if (templateId === null) {
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
getInitBlockTemplate = await PostgresTriggerService.getTemplateScript({
|
||||
const templateScript = await PostgresTriggerService.getTemplateScript({
|
||||
workspace: $workspaceStore!,
|
||||
id: getInitBlockTemplate as string
|
||||
id: templateId
|
||||
})
|
||||
return getInitBlockTemplate
|
||||
return templateScript
|
||||
} catch (error) {
|
||||
sendUserToast(
|
||||
'An error occured when trying to load your template script, please try again later',
|
||||
@@ -654,7 +680,24 @@
|
||||
let path: Path | undefined = undefined
|
||||
let dirtyPath = false
|
||||
|
||||
let selectedTab: 'metadata' | 'runtime' | 'ui' | 'triggers' = 'metadata'
|
||||
let selectedTab: 'metadata' | 'runtime' | 'ui' | 'triggers' = (() => {
|
||||
if (customUi?.settingsPanel?.disableMetadata !== true) {
|
||||
// first option: either no custom UI or metadata is enabled
|
||||
return 'metadata'
|
||||
}
|
||||
if (customUi?.settingsPanel?.disableRuntime !== true) {
|
||||
return 'runtime'
|
||||
}
|
||||
if (customUi?.settingsPanel?.disableGeneratedUi !== true) {
|
||||
return 'ui'
|
||||
}
|
||||
if (customUi?.settingsPanel?.disableTriggers !== true) {
|
||||
return 'triggers'
|
||||
}
|
||||
return 'metadata'
|
||||
})()
|
||||
|
||||
setContext('disableTooltips', customUi?.disableTooltips === true)
|
||||
|
||||
let deploymentMsg = ''
|
||||
let msgInput: HTMLInputElement | undefined = undefined
|
||||
@@ -728,23 +771,31 @@
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<div class="flex flex-col h-full">
|
||||
<Tabs bind:selected={selectedTab} wrapperClass="flex-none w-full">
|
||||
<Tab value="metadata">Metadata</Tab>
|
||||
<Tab value="runtime">Runtime</Tab>
|
||||
<Tab value="ui">
|
||||
Generated UI
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/json_schema_and_parsing"
|
||||
>
|
||||
The arguments are synced with the main signature but you may refine the parts that
|
||||
cannot be inferred from the type directly.
|
||||
</Tooltip>
|
||||
</Tab>
|
||||
<Tab value="triggers">
|
||||
Triggers
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/getting_started/triggers">
|
||||
Configure how this script will be triggered.
|
||||
</Tooltip>
|
||||
</Tab>
|
||||
{#if customUi?.settingsPanel?.disableMetadata !== true}
|
||||
<Tab value="metadata">Metadata</Tab>
|
||||
{/if}
|
||||
{#if customUi?.settingsPanel?.disableRuntime !== true}
|
||||
<Tab value="runtime">Runtime</Tab>
|
||||
{/if}
|
||||
{#if customUi?.settingsPanel?.disableGeneratedUi !== true}
|
||||
<Tab value="ui">
|
||||
Generated UI
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/json_schema_and_parsing"
|
||||
>
|
||||
The arguments are synced with the main signature but you may refine the parts that
|
||||
cannot be inferred from the type directly.
|
||||
</Tooltip>
|
||||
</Tab>
|
||||
{/if}
|
||||
{#if customUi?.settingsPanel?.disableTriggers !== true}
|
||||
<Tab value="triggers">
|
||||
Triggers
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/getting_started/triggers">
|
||||
Configure how this script will be triggered.
|
||||
</Tooltip>
|
||||
</Tab>
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
<div class="min-h-0 grow overflow-y-auto">
|
||||
@@ -752,14 +803,16 @@
|
||||
<div class="flex flex-col gap-8 px-4 py-2">
|
||||
<Section label="Metadata">
|
||||
<svelte:fragment slot="action">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<ErrorHandlerToggleButton
|
||||
kind="script"
|
||||
scriptOrFlowPath={script.path}
|
||||
bind:errorHandlerMuted={script.ws_error_handler_muted}
|
||||
iconOnly={false}
|
||||
/>
|
||||
</div>
|
||||
{#if customUi?.settingsPanel?.metadata?.disableMute !== true}
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<ErrorHandlerToggleButton
|
||||
kind="script"
|
||||
scriptOrFlowPath={script.path}
|
||||
bind:errorHandlerMuted={script.ws_error_handler_muted}
|
||||
iconOnly={false}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Label label="Summary">
|
||||
@@ -893,36 +946,40 @@
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section label="Script kind">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/script_editor/script_kinds"
|
||||
{#if customUi?.settingsPanel?.metadata?.disableScriptKind !== true}
|
||||
<Section label="Script kind">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/script_editor/script_kinds"
|
||||
>
|
||||
Tag this script's purpose within flows such that it is available as the
|
||||
corresponding action.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<ToggleButtonGroup
|
||||
class="h-10"
|
||||
selected={script.kind}
|
||||
on:selected={({ detail }) => {
|
||||
template = 'script'
|
||||
script.kind = detail
|
||||
initContent(script.language, detail, template)
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
Tag this script's purpose within flows such that it is available as the
|
||||
corresponding action.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<ToggleButtonGroup
|
||||
class="h-10"
|
||||
selected={script.kind}
|
||||
on:selected={({ detail }) => {
|
||||
template = 'script'
|
||||
script.kind = detail
|
||||
initContent(script.language, detail, template)
|
||||
}}
|
||||
>
|
||||
{#each scriptKindOptions as { value, title, desc, documentationLink, Icon }}
|
||||
<ToggleButton
|
||||
label={title}
|
||||
{value}
|
||||
tooltip={desc}
|
||||
{documentationLink}
|
||||
icon={Icon}
|
||||
showTooltipIcon={Boolean(desc)}
|
||||
/>
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
</Section>
|
||||
{#each scriptKindOptions as { value, title, desc, documentationLink, Icon }}
|
||||
<ToggleButton
|
||||
label={title}
|
||||
{value}
|
||||
tooltip={desc}
|
||||
{documentationLink}
|
||||
icon={Icon}
|
||||
showTooltipIcon={Boolean(desc)}
|
||||
{item}
|
||||
/>
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
</Section>
|
||||
{/if}
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="runtime">
|
||||
@@ -1321,7 +1378,10 @@
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="ui" class="h-full p-4">
|
||||
<ScriptSchema bind:schema={script.schema} />
|
||||
<ScriptSchema
|
||||
bind:schema={script.schema}
|
||||
customUi={customUi?.settingsPanel?.metadata?.editableSchemaForm}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value="triggers">
|
||||
<TriggersEditor
|
||||
@@ -1396,18 +1456,20 @@
|
||||
{#if customUi?.topBar?.path != false}
|
||||
<div class="flex justify-start w-full border rounded-md overflow-hidden">
|
||||
<div>
|
||||
<button
|
||||
on:click={async () => {
|
||||
metadataOpen = true
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
color="gray"
|
||||
class="center-center !bg-surface-secondary !text-tertiary !h-[28px] !w-[70px] rounded-none hover:!bg-surface-hover transition-all"
|
||||
{#if customUi?.topBar?.editablePath != false}
|
||||
<button
|
||||
on:click={async () => {
|
||||
metadataOpen = true
|
||||
}}
|
||||
>
|
||||
<Pen size={12} class="mr-2" /> Path
|
||||
</Badge>
|
||||
</button>
|
||||
<Badge
|
||||
color="gray"
|
||||
class="center-center !bg-surface-secondary !text-tertiary !h-[28px] !w-[70px] rounded-none hover:!bg-surface-hover transition-all"
|
||||
>
|
||||
<Pen size={12} class="mr-2" /> Path
|
||||
</Badge>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
import { slide } from 'svelte/transition'
|
||||
import CaptureTable from '$lib/components/triggers/CaptureTable.svelte'
|
||||
import CaptureButton from './triggers/CaptureButton.svelte'
|
||||
import { setContext } from 'svelte'
|
||||
|
||||
// Exported
|
||||
export let schema: Schema | any = emptySchema()
|
||||
@@ -50,7 +51,7 @@
|
||||
export let noHistory = false
|
||||
export let saveToWorkspace = false
|
||||
export let watchChanges = false
|
||||
export let customUi: ScriptEditorWhitelabelCustomUi = {}
|
||||
export let customUi: ScriptEditorWhitelabelCustomUi | undefined = undefined
|
||||
export let args: Record<string, any> = initialArgs
|
||||
export let selectedTab: 'main' | 'preprocessor' = 'main'
|
||||
export let hasPreprocessor = false
|
||||
@@ -107,11 +108,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
function runTest() {
|
||||
export function setArgs(nargs: Record<string, any>) {
|
||||
args = nargs
|
||||
}
|
||||
|
||||
export async function runTest() {
|
||||
// Not defined if JobProgressBar not loaded
|
||||
if (jobProgressReset) jobProgressReset()
|
||||
//@ts-ignore
|
||||
testJobLoader.runPreview(
|
||||
let job = await testJobLoader.runPreview(
|
||||
path,
|
||||
code,
|
||||
lang,
|
||||
@@ -119,6 +124,7 @@
|
||||
tag
|
||||
)
|
||||
setFocusToLogs()
|
||||
return job
|
||||
}
|
||||
|
||||
async function loadPastTests(): Promise<void> {
|
||||
@@ -258,6 +264,8 @@
|
||||
}
|
||||
|
||||
let setFocusToLogs = () => {}
|
||||
|
||||
setContext('disableTooltips', customUi?.disableTooltips === true)
|
||||
</script>
|
||||
|
||||
<TestJobLoader
|
||||
@@ -407,7 +415,7 @@
|
||||
/>
|
||||
Cancel
|
||||
</Button>
|
||||
{:else}
|
||||
{:else if customUi?.previewPanel?.disableTriggerButton !== true}
|
||||
<div class="flex flex-row divide-x divide-gray-800 dark:divide-gray-300 items-stretch">
|
||||
<Button
|
||||
color="dark"
|
||||
@@ -430,6 +438,28 @@
|
||||
</Button>
|
||||
<CaptureButton on:openTriggers />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row divide-x divide-gray-800 dark:divide-gray-300 items-stretch">
|
||||
<Button
|
||||
color="dark"
|
||||
on:click={() => {
|
||||
runTest()
|
||||
}}
|
||||
btnClasses="w-full"
|
||||
size="xs"
|
||||
startIcon={{
|
||||
icon: Play,
|
||||
classes: 'animate-none'
|
||||
}}
|
||||
shortCut={{ Icon: CornerDownLeft, hide: testIsLoading }}
|
||||
>
|
||||
{#if testIsLoading}
|
||||
Running
|
||||
{:else}
|
||||
Test
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Splitpanes horizontal class="!max-h-[calc(100%-43px)]">
|
||||
@@ -448,6 +478,7 @@
|
||||
{schema}
|
||||
bind:args
|
||||
bind:isValid
|
||||
noVariablePicker={customUi?.previewPanel?.disableVariablePicker === true}
|
||||
showSchemaExplorer
|
||||
/>
|
||||
{/key}
|
||||
@@ -465,6 +496,7 @@
|
||||
{diffEditor}
|
||||
{args}
|
||||
showCaptures={true}
|
||||
customUi={customUi?.previewPanel}
|
||||
>
|
||||
{#if scriptProgress}
|
||||
<!-- Put to the slot in logpanel -->
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
items = (
|
||||
await ScriptService.listScripts({
|
||||
workspace: $workspaceStore!,
|
||||
kinds: kinds.join(','),
|
||||
kinds: kinds.join(',')
|
||||
})
|
||||
).map((script) => ({
|
||||
value: script.path,
|
||||
@@ -85,9 +85,9 @@
|
||||
<div class="flex flex-row items-center gap-4 w-full mt-2">
|
||||
{#if options.length > 1}
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={itemKind}>
|
||||
<ToggleButtonGroup bind:selected={itemKind} let:item>
|
||||
{#each options as [label, value, icon, selectedColor]}
|
||||
<ToggleButton {icon} {disabled} {value} {label} {selectedColor} />
|
||||
<ToggleButton {icon} {disabled} {value} {label} {selectedColor} {item} />
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import type { EditableSchemaFormUi } from '$lib/components/custom_ui'
|
||||
|
||||
import EditableSchemaForm from './EditableSchemaForm.svelte'
|
||||
|
||||
export let schema: Schema | any
|
||||
export let customUi: EditableSchemaFormUi | undefined = undefined
|
||||
|
||||
export function setSchema(newSchema: Schema) {
|
||||
schema = newSchema
|
||||
}
|
||||
</script>
|
||||
|
||||
<EditableSchemaForm bind:schema uiOnly editTab="inputEditor" />
|
||||
<EditableSchemaForm bind:schema uiOnly {customUi} editTab="inputEditor" />
|
||||
|
||||
@@ -190,9 +190,15 @@
|
||||
{#if selectedVersionIndex !== undefined && versions?.slice(selectedVersionIndex + 1).length}
|
||||
<div class="p-2 flex flex-row items-center gap-2 h-8">
|
||||
<div class="w-min">
|
||||
<ToggleButtonGroup bind:selected={showDiff}>
|
||||
<ToggleButton light small value={false} label="Code" icon={Code} />
|
||||
<ToggleButton light small value={true} label="Diff" icon={Diff} />
|
||||
<ToggleButtonGroup
|
||||
selected={showDiff ? 'diff' : 'code'}
|
||||
let:item
|
||||
on:selected={({ detail }) => {
|
||||
showDiff = detail === 'diff'
|
||||
}}
|
||||
>
|
||||
<ToggleButton light small value="code" label="Code" icon={Code} {item} />
|
||||
<ToggleButton light small value="diff" label="Diff" icon={Diff} {item} />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -123,9 +123,13 @@
|
||||
{#if own}
|
||||
<div class="flex flex-row flex-wrap gap-2 items-center">
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={ownerKind} on:selected={() => (owner = '')}>
|
||||
<ToggleButton value="user" size="xs" label="User" />
|
||||
<ToggleButton value="group" size="xs" label="Group" />
|
||||
<ToggleButtonGroup
|
||||
bind:selected={ownerKind}
|
||||
on:selected={() => (owner = '')}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton value="user" size="xs" label="User" {item} />
|
||||
<ToggleButton value="group" size="xs" label="Group" {item} />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{#key ownerKind}
|
||||
@@ -163,9 +167,10 @@
|
||||
}
|
||||
loadAcls()
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton value="viewer" size="xs" label="Viewer" />
|
||||
<ToggleButton value="writer" size="xs" label="Writer" />
|
||||
<ToggleButton value="viewer" size="xs" label="Viewer" {item} />
|
||||
<ToggleButton value="writer" size="xs" label="Writer" {item} />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{:else}{write}{/if}</td
|
||||
|
||||
@@ -133,9 +133,16 @@
|
||||
disableVariablePicker = undefined
|
||||
}
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
{#each FIELD_SETTINGS as x}
|
||||
<ToggleButton value={x[1]} label={x[0]} tooltip={x[2]} showTooltipIcon={Boolean(x[2])} />
|
||||
<ToggleButton
|
||||
value={x[1]}
|
||||
label={x[0]}
|
||||
tooltip={x[2]}
|
||||
showTooltipIcon={Boolean(x[2])}
|
||||
{item}
|
||||
/>
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
|
||||
@@ -292,15 +292,22 @@
|
||||
sendUserToast('User updated')
|
||||
listUsers(activeOnly)
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton value={'user'} size="xs" label="User" />
|
||||
<ToggleButton value={'user'} size="xs" label="User" {item} />
|
||||
<ToggleButton
|
||||
value={'devops'}
|
||||
size="xs"
|
||||
label="Devops"
|
||||
tooltip="Devops is a role that grants visibilty similar to that of a super admin, but without giving all rights. For example devops users can see service logs and crtical alerts. You can think of it as a 'readonly' super admin"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value={'super_admin'}
|
||||
size="xs"
|
||||
label="Superadmin"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton value={'super_admin'} size="xs" label="Superadmin" />
|
||||
</ToggleButtonGroup>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
</script>
|
||||
|
||||
<div class="max-w-min">
|
||||
<ToggleButtonGroup bind:selected>
|
||||
<ToggleButton value="hub" icon={WindmillIcon} label="Hub" />
|
||||
<ToggleButton value="workspace" icon={Building} label="Workspace" />
|
||||
<ToggleButtonGroup bind:selected let:item>
|
||||
<ToggleButton value="hub" icon={WindmillIcon} label="Hub" {item} />
|
||||
<ToggleButton value="workspace" icon={Building} label="Workspace" {item} />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
</script>
|
||||
|
||||
<div class="max-w-min">
|
||||
<ToggleButtonGroup id="flow-editor-flow-providers" bind:selected>
|
||||
<ToggleButton value="all" label="All" light small />
|
||||
<ToggleButton value="hub" icon={WindmillIcon2} label="Hub" light small />
|
||||
<ToggleButton value="workspace" icon={Building} label="Workspace" light small />
|
||||
<ToggleButtonGroup id="flow-editor-flow-providers" bind:selected let:item>
|
||||
<ToggleButton value="all" label="All" light small {item} />
|
||||
<ToggleButton value="hub" icon={WindmillIcon2} label="Hub" light small {item} />
|
||||
<ToggleButton value="workspace" icon={Building} label="Workspace" light small {item} />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Popover from './Popover.svelte'
|
||||
import { ExternalLink, InfoIcon } from 'lucide-svelte'
|
||||
import { gfmPlugin } from 'svelte-exmarkdown/gfm'
|
||||
import { getContext, hasContext } from 'svelte'
|
||||
export let light = false
|
||||
export let wrapperClass = ''
|
||||
export let placement: PopoverPlacement | undefined = undefined
|
||||
@@ -12,37 +13,43 @@
|
||||
export let markdownTooltip: string | undefined = undefined
|
||||
export let customSize: string = '100%'
|
||||
const plugins = [gfmPlugin()]
|
||||
|
||||
const disableTooltips = hasContext('disableTooltips')
|
||||
? getContext('disableTooltips') === true
|
||||
: false
|
||||
</script>
|
||||
|
||||
<Popover
|
||||
notClickable
|
||||
{placement}
|
||||
class={wrapperClass}
|
||||
style="transform: scale({parseFloat(customSize) / 100});"
|
||||
>
|
||||
<div
|
||||
class="inline-flex w-3 mx-0.5 h-3 {light
|
||||
? 'text-tertiary-inverse'
|
||||
: 'text-tertiary'} {$$props.class} relative"
|
||||
{#if disableTooltips !== true}
|
||||
<Popover
|
||||
notClickable
|
||||
{placement}
|
||||
class={wrapperClass}
|
||||
style="transform: scale({parseFloat(customSize) / 100});"
|
||||
>
|
||||
<InfoIcon class="{small ? 'bottom-0' : '-bottom-0.5'} absolute" size={small ? 12 : 14} />
|
||||
</div>
|
||||
<svelte:fragment slot="text">
|
||||
{#if markdownTooltip}
|
||||
<div class="prose-sm">
|
||||
<Markdown md={markdownTooltip} {plugins} />
|
||||
</div>
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
{#if documentationLink}
|
||||
<a href={documentationLink} target="_blank" class="text-blue-300 text-xs">
|
||||
<div class="flex flex-row gap-2 mt-4">
|
||||
See documentation
|
||||
<ExternalLink size="16" />
|
||||
<div
|
||||
class="inline-flex w-3 mx-0.5 h-3 {light
|
||||
? 'text-tertiary-inverse'
|
||||
: 'text-tertiary'} {$$props.class} relative"
|
||||
>
|
||||
<InfoIcon class="{small ? 'bottom-0' : '-bottom-0.5'} absolute" size={small ? 12 : 14} />
|
||||
</div>
|
||||
<svelte:fragment slot="text">
|
||||
{#if markdownTooltip}
|
||||
<div class="prose-sm">
|
||||
<Markdown md={markdownTooltip} {plugins} />
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
{#if documentationLink}
|
||||
<a href={documentationLink} target="_blank" class="text-blue-300 text-xs">
|
||||
<div class="flex flex-row gap-2 mt-4">
|
||||
See documentation
|
||||
<ExternalLink size="16" />
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
@@ -183,10 +183,10 @@
|
||||
>{/if}{/if}
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<ToggleButtonGroup bind:selected={editorKind}>
|
||||
<ToggleButton value="plain" label="Plain" />
|
||||
<ToggleButton value="json" label="Json" />
|
||||
<ToggleButton value="yaml" label="YAML" />
|
||||
<ToggleButtonGroup bind:selected={editorKind} let:item>
|
||||
<ToggleButton value="plain" label="Plain" {item} />
|
||||
<ToggleButton value="json" label="Json" {item} />
|
||||
<ToggleButton value="yaml" label="YAML" {item} />
|
||||
</ToggleButtonGroup>
|
||||
{#if editorKind == 'plain'}
|
||||
<textarea
|
||||
|
||||
@@ -263,13 +263,21 @@
|
||||
}
|
||||
}}
|
||||
class="mb-4"
|
||||
let:item
|
||||
>
|
||||
<ToggleButton position="left" value="normal" size="sm" label="Any jobs within worker tags" />
|
||||
<ToggleButton
|
||||
position="left"
|
||||
value="normal"
|
||||
size="sm"
|
||||
label="Any jobs within worker tags"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
position="dedicated"
|
||||
value="dedicated"
|
||||
size="sm"
|
||||
label="Dedicated to a script/flow"
|
||||
{item}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
{#if selected == 'normal'}
|
||||
@@ -682,9 +690,10 @@
|
||||
envvar.value = undefined
|
||||
}
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton position="left" value="dynamic" label="Dynamic" />
|
||||
<ToggleButton position="right" value="static" label="Static" />
|
||||
<ToggleButton value="dynamic" label="Dynamic" {item} />
|
||||
<ToggleButton value="static" label="Static" {item} />
|
||||
</ToggleButtonGroup>
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
|
||||
import { initCss } from '../../utils'
|
||||
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
|
||||
import Markdown from 'svelte-exmarkdown'
|
||||
import { Markdown, type Plugin } from 'svelte-exmarkdown'
|
||||
import { gfmPlugin } from 'svelte-exmarkdown/gfm'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import { rehypeGithubAlerts } from 'rehype-github-alerts'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { components } from '../../editor/component'
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import { gfmPlugin } from 'svelte-exmarkdown/gfm'
|
||||
export let id: string
|
||||
export let componentInput: AppInput | undefined
|
||||
export let initializing: boolean | undefined = undefined
|
||||
@@ -18,6 +20,11 @@
|
||||
export let render: boolean
|
||||
export let configuration: RichConfigurations
|
||||
|
||||
const plugins: Plugin[] = [
|
||||
gfmPlugin(),
|
||||
{ rehypePlugin: [rehypeRaw] },
|
||||
{ rehypePlugin: [rehypeGithubAlerts] }
|
||||
]
|
||||
const { app, worldStore, mode } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const resolvedConfig = initConfig(
|
||||
@@ -86,7 +93,7 @@
|
||||
{id}
|
||||
bind:initializing
|
||||
bind:result
|
||||
>{#if result}{#key result}<Markdown md={result} plugins={[gfmPlugin()]} />{/key}{/if}
|
||||
>{#if result}{#key result}<Markdown md={result} {plugins} />{/key}{/if}
|
||||
</RunnableWrapper>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -143,4 +150,12 @@
|
||||
.wm-markdown h4:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.wm-markdown code:not([class~="not-prose"]):not(.not-prose *)::before,
|
||||
.wm-markdown code:not([class~="not-prose"]):not(.not-prose *)::after {
|
||||
content: none !important;
|
||||
}
|
||||
.wm-markdown.prose code::before,
|
||||
.wm-markdown.prose code::after {
|
||||
content: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -144,15 +144,24 @@
|
||||
runnable && runnable.type === 'runnableByName' ? runnable.inlineScript?.refreshOn ?? [] : []
|
||||
|
||||
function refreshIfAutoRefresh(src: 'arg changed' | 'static changed') {
|
||||
// console.log(
|
||||
// 'refreshIfAutoRefresh',
|
||||
// src,
|
||||
// id,
|
||||
// iterContext ? $iterContext : undefined,
|
||||
// $rowContext ? $rowContext : undefined,
|
||||
// firstRefresh
|
||||
// )
|
||||
if (firstRefresh) {
|
||||
firstRefresh = false
|
||||
if (
|
||||
src == 'arg changed' &&
|
||||
args == undefined &&
|
||||
result != undefined &&
|
||||
Object.keys(runnableInputValues ?? {}).length == 0 &&
|
||||
Object.keys(extraQueryParams ?? {}).length == 0
|
||||
) {
|
||||
// console.debug(`Skipping refreshing ${id} because ${_src} (first)`)
|
||||
console.log('skipping refresh because first refresh')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import type { AppViewerContext, ListContext } from '../../types'
|
||||
import { isScriptByNameDefined, isScriptByPathDefined } from '../../utils'
|
||||
import NonRunnableComponent from './NonRunnableComponent.svelte'
|
||||
import RunnableComponent from './RunnableComponent.svelte'
|
||||
@@ -98,6 +98,8 @@
|
||||
|
||||
const { staticExporter, initialized, noBackend, componentControl, runnableComponents } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
const iterContext = getContext<ListContext>('ListWrapperContext')
|
||||
const rowContext = getContext<ListContext>('RowWrapperContext')
|
||||
|
||||
if (noBackend && componentInput?.type == 'runnable') {
|
||||
result = componentInput?.['value']
|
||||
@@ -113,15 +115,24 @@
|
||||
}
|
||||
})
|
||||
|
||||
const fullId = id + (extraKey ?? '')
|
||||
if (!(initializing && componentInput?.type === 'runnable' && isRunnableDefined(componentInput))) {
|
||||
initializing = false
|
||||
} else {
|
||||
initializing =
|
||||
initializing == undefined && Object.keys($initialized?.runnableInitialized ?? {}).includes(id)
|
||||
? false
|
||||
: undefined
|
||||
if (result == undefined && !initializing) {
|
||||
result = $initialized.runnableInitialized?.[id]
|
||||
if (
|
||||
(initializing == undefined || initializing == true) &&
|
||||
Object.keys($initialized?.runnableInitialized ?? {}).includes(fullId)
|
||||
) {
|
||||
initializing = false
|
||||
}
|
||||
|
||||
if (
|
||||
result == undefined &&
|
||||
!initializing &&
|
||||
iterContext == undefined &&
|
||||
rowContext == undefined
|
||||
) {
|
||||
result = $initialized.runnableInitialized?.[fullId]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,9 +302,12 @@
|
||||
on:argsChanged
|
||||
on:resultSet={(e) => {
|
||||
const res = e.detail
|
||||
if (initializing !== false) {
|
||||
if ($initialized?.runnableInitialized?.[fullId] === undefined) {
|
||||
console.log('resultSet', id)
|
||||
$initialized.runnableInitialized = { ...$initialized.runnableInitialized, [id]: res }
|
||||
$initialized.runnableInitialized = {
|
||||
...($initialized.runnableInitialized ?? {}),
|
||||
[fullId]: res
|
||||
}
|
||||
}
|
||||
|
||||
initializing = false
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
: undefined}
|
||||
pathTransformer={resolvedConfig?.type?.configuration?.s3?.pathTemplate}
|
||||
allowMultiple={resolvedConfigS3.allowMultiple}
|
||||
allowDelete={resolvedConfigS3.allowDelete}
|
||||
containerText={resolvedConfigS3.text}
|
||||
customResourcePath={resolvedConfigS3.resource}
|
||||
customResourceType="s3"
|
||||
|
||||
@@ -90,6 +90,10 @@
|
||||
// 32px (2rem) of top and bottom margin
|
||||
wrapperHeight - headerHeight - 64
|
||||
)
|
||||
|
||||
async function getMenuElements(): Promise<HTMLElement[]> {
|
||||
return Array.from(document.querySelectorAll('[data-menu]')) as HTMLElement[]
|
||||
}
|
||||
</script>
|
||||
|
||||
<InitializeComponent {id} />
|
||||
@@ -176,7 +180,11 @@
|
||||
<div
|
||||
style={css?.popup?.style}
|
||||
class={twMerge('mx-24 mt-8 bg-surface rounded-lg relative', css?.popup?.class)}
|
||||
use:clickOutside={false}
|
||||
use:clickOutside={{
|
||||
capture: false,
|
||||
stopPropagation: false,
|
||||
exclude: getMenuElements
|
||||
}}
|
||||
on:click_outside={(e) => {
|
||||
if ($mode !== 'dnd' && !unclickableOutside) {
|
||||
handleClickAway(e)
|
||||
|
||||
@@ -824,7 +824,21 @@
|
||||
on:hideRightPanel={() => hideRightPanel()}
|
||||
on:hideBottomPanel={() => hideBottomPanel()}
|
||||
on:showBottomPanel={() => showBottomPanel()}
|
||||
/>
|
||||
>
|
||||
<svelte:fragment
|
||||
slot="unsavedConfirmationModal"
|
||||
let:diffDrawer
|
||||
let:additionalExitAction
|
||||
let:getInitialAndModifiedValues
|
||||
>
|
||||
<slot
|
||||
name="unsavedConfirmationModal"
|
||||
{diffDrawer}
|
||||
{additionalExitAction}
|
||||
{getInitialAndModifiedValues}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</AppEditorHeader>
|
||||
{#if $mode === 'preview'}
|
||||
<SplitPanesWrapper>
|
||||
<div
|
||||
|
||||
@@ -62,7 +62,6 @@
|
||||
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { Sha256 } from '@aws-crypto/sha256-js'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
@@ -947,23 +946,26 @@
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
<TestJobLoader bind:this={testJobLoader} bind:isLoading={testIsLoading} bind:job />
|
||||
<UnsavedConfirmationModal
|
||||
{diffDrawer}
|
||||
getInitialAndModifiedValues={() => ({
|
||||
savedValue: savedApp,
|
||||
modifiedValue: {
|
||||
summary: $summary,
|
||||
value: $app,
|
||||
path: newEditedPath || savedApp?.draft?.path || savedApp?.path,
|
||||
policy,
|
||||
custom_path: customPath
|
||||
}
|
||||
})}
|
||||
additionalExitAction={() => {
|
||||
setTheme(priorDarkMode)
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if $$slots.unsavedConfirmationModal}
|
||||
<slot
|
||||
name="unsavedConfirmationModal"
|
||||
{diffDrawer}
|
||||
additionalExitAction={() => {
|
||||
setTheme(priorDarkMode)
|
||||
}}
|
||||
getInitialAndModifiedValues={() => ({
|
||||
savedValue: savedApp,
|
||||
modifiedValue: {
|
||||
summary: $summary,
|
||||
value: $app,
|
||||
path: newEditedPath || savedApp?.draft?.path || savedApp?.path,
|
||||
policy,
|
||||
custom_path: customPath
|
||||
}
|
||||
})}
|
||||
/>
|
||||
{/if}
|
||||
<DeployOverrideConfirmationModal
|
||||
bind:deployedBy
|
||||
bind:confirmCallback
|
||||
@@ -1553,18 +1555,21 @@
|
||||
on:selected={({ detail }) => {
|
||||
$app.fullscreen = detail === 'true'
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton
|
||||
icon={AlignHorizontalSpaceAround}
|
||||
value={'false'}
|
||||
tooltip="The max width is 1168px and the content stay centered instead of taking the full page width"
|
||||
iconProps={{ size: 16 }}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
tooltip="The width is of the app if the full width of its container"
|
||||
icon={Expand}
|
||||
value={'true'}
|
||||
iconProps={{ size: 16 }}
|
||||
{item}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
@@ -1578,40 +1583,46 @@
|
||||
setTheme(theme)
|
||||
}}
|
||||
selected={$app.darkMode === undefined ? 'auto' : $app.darkMode ? 'dark' : 'sun'}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton
|
||||
icon={SunMoon}
|
||||
value={'auto'}
|
||||
tooltip="The app mode between dark/light is automatic"
|
||||
iconProps={{ size: 16 }}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
icon={Sun}
|
||||
value={'sun'}
|
||||
tooltip="Force light mode"
|
||||
iconProps={{ size: 16 }}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
tooltip="Force dark mode"
|
||||
icon={Moon}
|
||||
value={'dark'}
|
||||
iconProps={{ size: 16 }}
|
||||
{item}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
<div class="flex flex-row gap-2">
|
||||
<ToggleButtonGroup class="h-[30px]" bind:selected={$breakpoint}>
|
||||
<ToggleButtonGroup class="h-[30px]" bind:selected={$breakpoint} let:item>
|
||||
<ToggleButton
|
||||
tooltip="Computer View"
|
||||
icon={Laptop2}
|
||||
value={'lg'}
|
||||
iconProps={{ size: 16 }}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
tooltip="Mobile View"
|
||||
icon={Smartphone}
|
||||
value={'sm'}
|
||||
iconProps={{ size: 16 }}
|
||||
{item}
|
||||
/>
|
||||
{#if $breakpoint === 'sm'}
|
||||
<Toggle
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
jobsById.set({})
|
||||
jobs.set([])
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton
|
||||
label="Editor"
|
||||
@@ -26,6 +27,7 @@
|
||||
disabled={loading}
|
||||
iconProps={{ size: 16 }}
|
||||
class="gap-2"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
label="Preview"
|
||||
@@ -36,5 +38,6 @@
|
||||
id="app-editor-preview-toggle"
|
||||
iconProps={{ size: 16 }}
|
||||
class="gap-2"
|
||||
{item}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user