mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-14 16:02:27 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca72c97a32 | ||
|
|
9a5baa8e53 | ||
|
|
0e3d835609 | ||
|
|
2a1bff3160 | ||
|
|
7a8a0bb163 | ||
|
|
deff3b0045 | ||
|
|
50ff183bae | ||
|
|
1b885ce2c2 | ||
|
|
cec82a9cd2 | ||
|
|
44ed4045f7 | ||
|
|
4fdca87de9 | ||
|
|
8a792c9c9d | ||
|
|
10b6b1dc04 | ||
|
|
9163060c90 | ||
|
|
2d572695ef | ||
|
|
ca8020cf82 | ||
|
|
84bd06e906 | ||
|
|
4b8f3580a4 | ||
|
|
0ea405415e | ||
|
|
d598731913 | ||
|
|
9c71503d74 | ||
|
|
5a96d64183 | ||
|
|
a017459e12 | ||
|
|
ce01fa1677 | ||
|
|
4a39e45476 | ||
|
|
60186b8c9f | ||
|
|
9f7aa01cac | ||
|
|
de00944f09 | ||
|
|
c1b220be26 | ||
|
|
50861fccc0 | ||
|
|
5e9870a8a9 | ||
|
|
1671005100 | ||
|
|
3d9ca62ab6 | ||
|
|
c3d49a352e | ||
|
|
7784c14726 | ||
|
|
e5e174ae95 | ||
|
|
1f09311a08 | ||
|
|
d832cd8b5f | ||
|
|
d9d63a4e31 | ||
|
|
d44976f35e | ||
|
|
02170032af | ||
|
|
de9a839af4 | ||
|
|
d9148eaa78 | ||
|
|
274eb78152 | ||
|
|
2774d394ad | ||
|
|
58194521b1 | ||
|
|
0d96cfb5a8 | ||
|
|
4c98410b83 | ||
|
|
8c6f5a320b | ||
|
|
8bc9a021a8 | ||
|
|
eff3e1c43a | ||
|
|
844acbf00b | ||
|
|
e0040fe3b3 | ||
|
|
d0bad936f0 | ||
|
|
63340e6ce2 | ||
|
|
c839bae160 | ||
|
|
253452013c | ||
|
|
af70339d07 | ||
|
|
a01c26f0e2 | ||
|
|
b1bcdae01d | ||
|
|
b1a4f2ab50 | ||
|
|
1ad54e887e | ||
|
|
a446660345 | ||
|
|
c21182f502 | ||
|
|
54698b3eef | ||
|
|
59e8004f99 | ||
|
|
1271a65b5a | ||
|
|
414b9c338b | ||
|
|
53909750ef | ||
|
|
60e7fa764c | ||
|
|
a0c86ef4c7 | ||
|
|
5805554ac8 | ||
|
|
bd7607d162 | ||
|
|
7985a7bafa | ||
|
|
13896a5741 | ||
|
|
e881ed5e00 | ||
|
|
d6a654aaff |
@@ -0,0 +1,58 @@
|
||||
name: Build windows executable for this branch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
SQLX_OFFLINE: true
|
||||
DISABLE_EMBEDDING: true
|
||||
RUST_LOG: info
|
||||
|
||||
jobs:
|
||||
cargo_build_windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Read EE repo commit hash
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ee_repo_ref = Get-Content .\backend\ee-repo-ref.txt
|
||||
echo "ee_repo_ref=$ee_repo_ref" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
|
||||
- name: Checkout windmill-ee-private repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: windmill-labs/windmill-ee-private
|
||||
path: ./windmill-ee-private
|
||||
ref: ${{ env.ee_repo_ref }}
|
||||
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Substitute EE code
|
||||
shell: bash
|
||||
run: |
|
||||
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
|
||||
|
||||
- name: Cargo build windows
|
||||
timeout-minutes: 90
|
||||
run: |
|
||||
vcpkg.exe install openssl-windows:x64-windows
|
||||
vcpkg.exe install openssl:x64-windows-static
|
||||
vcpkg.exe integrate install
|
||||
$env:VCPKGRS_DYNAMIC=1
|
||||
$env:OPENSSL_DIR="${Env:VCPKG_INSTALLATION_ROOT}\installed\x64-windows-static"
|
||||
mkdir frontend/build && cd backend
|
||||
New-Item -Path . -Name "windmill-api/openapi-deref.yaml" -ItemType "File" -Force
|
||||
cargo build --release --features=enterprise,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,tantivy,deno_core
|
||||
|
||||
- name: Rename binary with corresponding architecture
|
||||
run: |
|
||||
Rename-Item -Path ".\backend\target\release\windmill.exe" -NewName "windmill-ee.exe"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: windmill-ee-binary
|
||||
path: ./backend/target/release/windmill-ee.exe
|
||||
@@ -1,5 +1,54 @@
|
||||
# Changelog
|
||||
|
||||
## [1.423.2](https://github.com/windmill-labs/windmill/compare/v1.423.1...v1.423.2) (2024-11-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix intempestive expr type change in flow transform ([84bd06e](https://github.com/windmill-labs/windmill/commit/84bd06e90650e949c11e2b09447a7bad1ab60a95))
|
||||
|
||||
## [1.423.1](https://github.com/windmill-labs/windmill/compare/v1.423.0...v1.423.1) (2024-11-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **autoscaling:** autoscaling thresholds to be >= and not > ([de00944](https://github.com/windmill-labs/windmill/commit/de00944f09699ff33a382f5a14f515ccf90b2454))
|
||||
|
||||
## [1.423.0](https://github.com/windmill-labs/windmill/compare/v1.422.1...v1.423.0) (2024-11-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* s3 input available for public apps ([#4685](https://github.com/windmill-labs/windmill/issues/4685)) ([1671005](https://github.com/windmill-labs/windmill/commit/167100510032ab53cd609fb2c7629e67faceb093))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* prevent underflow for autoscaling scalein ([5e9870a](https://github.com/windmill-labs/windmill/commit/5e9870a8a993032ec7d45c62e0023008da42c74a))
|
||||
* support multiple pip-extra-index-url with commas ([50861fc](https://github.com/windmill-labs/windmill/commit/50861fccc0cd86d4c76120c3306adf94f375330e))
|
||||
|
||||
## [1.422.1](https://github.com/windmill-labs/windmill/compare/v1.422.0...v1.422.1) (2024-11-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix password inputs ([e5e174a](https://github.com/windmill-labs/windmill/commit/e5e174ae9516f4c6b94ceb6e258b467f5c9a1f1a))
|
||||
|
||||
## [1.422.0](https://github.com/windmill-labs/windmill/compare/v1.421.2...v1.422.0) (2024-11-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* expandable subflows in flows ([#4683](https://github.com/windmill-labs/windmill/issues/4683)) ([d44976f](https://github.com/windmill-labs/windmill/commit/d44976f35e45ade510d1ec220b5a1503e11f3db9))
|
||||
* **frontend:** critical alerts UI ([#4653](https://github.com/windmill-labs/windmill/issues/4653)) ([d9148ea](https://github.com/windmill-labs/windmill/commit/d9148eaa78680a93d81d71847a7df67e01f3c110))
|
||||
|
||||
## [1.421.2](https://github.com/windmill-labs/windmill/compare/v1.421.1...v1.421.2) (2024-11-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bash:** correctly propagate exit errors ([8bc9a02](https://github.com/windmill-labs/windmill/commit/8bc9a021a88391147c9170f56b5a0edddd55bc7d))
|
||||
|
||||
## [1.421.1](https://github.com/windmill-labs/windmill/compare/v1.421.0...v1.421.1) (2024-11-08)
|
||||
|
||||
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO alerts (alert_type, message) VALUES ('recovered_critical_error', $1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "00ce4ed3ca0eac7cb6283b047353a64b9e78c4beb423f04baef9a53fbf87e9f9"
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, alert_type, message, created_at, acknowledged \n FROM alerts \n WHERE acknowledged = $1\n ORDER BY created_at DESC \n LIMIT $2 OFFSET $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "alert_type",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "message",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "acknowledged",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bool",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "0b955f2cff82a2d4ba3840588143e08952f029480d4a42503ecc3c5e70437995"
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n flow_status AS \"flow_status!: Json<Box<RawValue>>\",\n raw_flow->'modules'->(flow_status->'step')::int AS \"module: Json<Box<RawValue>>\"\n FROM queue WHERE id = $1 AND workspace_id = $2 LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "flow_status!: Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "module: Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1e7ce0c140410ae799f9c0c5772e4be4506bfd238c88f1b1c3ddf39b29071446"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT raw_flow->'failure_module' != 'null'::jsonb\n FROM completed_job\n WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "31dd23f6768052e486668172e15eba1a3f9b6a8e5678a7ee8e5456ff4405d6f9"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE password SET login_type = $1 WHERE email = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "349396e8fdd96d45875110bc06767e6eb876792ec8f83e9b03c2fb46bb12e0b9"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO alerts (alert_type, message, acknowledged) VALUES ('recovered_critical_error', $1, $2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3a94ad52c6b7cde844fa868167248cd9ff63e5fdfa1d93d8fbec32a257b6b05e"
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO alerts (alert_type, message) VALUES ('critical_error', $1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4c0067c2135a259aea5cc2db60f7375a9a33671be8ef406427d90f67a98c9c9f"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO alerts (alert_type, message, acknowledged) VALUES ('critical_error', $1, $2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4d22084a5d9860832f30e8f08cbfa1848ed3c1336fa4790f45b8189c4ac97d91"
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "bool",
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+7
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH uuid_table as (\n select gen_random_uuid() as uuid from generate_series(1, $11)\n )\n INSERT INTO queue \n (id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id, concurrent_limit, concurrency_time_window_s, timeout)\n (SELECT uuid, $1, $2, $3, $4, ('{ \"uuid\": \"' || uuid || '\" }')::jsonb, $5, $6, $7, $8, $9, $10, $12, $13, $14 FROM uuid_table) \n RETURNING id",
|
||||
"query": "WITH uuid_table as (\n select gen_random_uuid() as uuid from generate_series(1, $11)\n )\n INSERT INTO queue \n (id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id, concurrent_limit, concurrency_time_window_s, timeout, raw_code, raw_lock, raw_flow, flow_status)\n (SELECT uuid, $1, $2, $3, $4, ('{ \"uuid\": \"' || uuid || '\" }')::jsonb, $5, $6, $7, $8, $9, $10, $12, $13, $14, $15, $16, $17, $18 FROM uuid_table) \n RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -72,12 +72,16 @@
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Int4"
|
||||
"Int4",
|
||||
"Text",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0a686ca61444d7ad7484071727aa039a6ea6697e5a49a633b767c052aa3e0a18"
|
||||
"hash": "a69ba7471d1a5faa145bebbd17d43e6dbe02e9e92ebbc31a50c99c3a04719284"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE alerts SET acknowledged = true WHERE acknowledged = false",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a7c5008aa7ea43d0afac7d9f19846976ed7af2e90270902f001115e023cb947d"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT raw_flow->'failure_module' != 'null'::jsonb\n FROM queue\n WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "aa6907b7266ee437dfbd77a9bf6c047fda88f3e72a0d10323a5e4020cf857c42"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT wm_version FROM worker_ping WHERE wm_version != $1 AND ping_at > now() - interval '5 minutes'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "wm_version",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ad03e5acf10ef94abc37cb9f56b1775c67f075c8bc83458e8be2e242347218d6"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT flow_version.value AS \"value: sqlx::types::Json<Box<RawValue>>\" FROM flow \n LEFT JOIN flow_version\n ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n WHERE flow.path = $1 AND flow.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ae543dfa106fa6ad4e9bf45cda1110d4702a80f455c63af6fcbd7cf45bbc4f7a"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE alerts SET acknowledged = true WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "be3ae231557e794172336bc27d725f862dcf039bbb3d75ced9d54c86f53d2580"
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, alert_type, message, created_at, acknowledged \n FROM alerts \n ORDER BY created_at DESC \n LIMIT $1 OFFSET $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "alert_type",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "message",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "acknowledged",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "cc5ab80241b88c5befea279f16c4ec68cec17b31dcd277b321f652917346496b"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT raw_flow->'modules'->($1)->'value'->>'type' = 'flow' FROM queue WHERE id = $2",
|
||||
"query": "SELECT raw_flow->'modules'->($1)::text->'value'->>'type' = 'flow' FROM queue WHERE id = $2 LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -19,5 +19,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "3e539fef054ad31bc1736e27276087775a721a6ee7ae35b03fd4ce3563ea3838"
|
||||
"hash": "de1abe57b6aa61155f747a3bcb98359f70eade6654b5a884915f07f3ef3fe15e"
|
||||
}
|
||||
Generated
+157
-124
@@ -96,9 +96,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.18"
|
||||
version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
||||
checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
@@ -576,7 +576,7 @@ dependencies = [
|
||||
"serde-aux",
|
||||
"serde_json",
|
||||
"sha2 0.10.8",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -659,7 +659,7 @@ dependencies = [
|
||||
"sha2 0.10.8",
|
||||
"smart-default",
|
||||
"smol_str",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"uuid 0.8.2",
|
||||
]
|
||||
@@ -692,7 +692,7 @@ dependencies = [
|
||||
"crc32fast",
|
||||
"log",
|
||||
"pin-project",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -1363,9 +1363,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "borsh"
|
||||
version = "1.5.2"
|
||||
version = "1.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5327f6c99920069d1fe374aa743be1af0031dea9f250852cdf1ae6a0861ee24"
|
||||
checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03"
|
||||
dependencies = [
|
||||
"borsh-derive",
|
||||
"cfg_aliases",
|
||||
@@ -1373,9 +1373,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "borsh-derive"
|
||||
version = "1.5.2"
|
||||
version = "1.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10aedd8f1a81a8aafbfde924b0e3061cd6fedd6f6bbcfc6a76e6fd426d7bfe26"
|
||||
checksum = "c2593a3b8b938bd68373196c9832f516be11fa487ef4ae745eb282e6a56a7244"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro-crate",
|
||||
@@ -1545,7 +1545,7 @@ dependencies = [
|
||||
"rand_distr",
|
||||
"rayon",
|
||||
"safetensors",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"yoke",
|
||||
"zip",
|
||||
]
|
||||
@@ -1562,7 +1562,7 @@ dependencies = [
|
||||
"rayon",
|
||||
"safetensors",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1598,9 +1598,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.37"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf"
|
||||
checksum = "1aeb932158bd710538c73702db6945cb68a8fb08c519e6e12706b94263b36db8"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
@@ -1714,9 +1714,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.20"
|
||||
version = "4.5.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
|
||||
checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -1724,9 +1724,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.20"
|
||||
version = "4.5.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
|
||||
checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -1748,9 +1748,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
|
||||
checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
@@ -1783,13 +1783,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "comfy-table"
|
||||
version = "7.1.1"
|
||||
version = "7.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7"
|
||||
checksum = "e0d05af1e006a2407bedef5af410552494ce5be9090444dbbcb57258c1af3d56"
|
||||
dependencies = [
|
||||
"strum 0.26.3",
|
||||
"strum_macros 0.26.4",
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1816,7 +1816,7 @@ dependencies = [
|
||||
"encode_unicode",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.14",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -1943,9 +1943,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.14"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
|
||||
checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -2080,9 +2080,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.3.0"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
|
||||
checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf"
|
||||
dependencies = [
|
||||
"csv-core",
|
||||
"itoa",
|
||||
@@ -2553,7 +2553,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"sourcemap 9.0.1",
|
||||
"sourcemap 9.1.0",
|
||||
"swc_atoms",
|
||||
"swc_common",
|
||||
"swc_config",
|
||||
@@ -2576,8 +2576,8 @@ dependencies = [
|
||||
"swc_visit",
|
||||
"swc_visit_macros",
|
||||
"text_lines",
|
||||
"thiserror",
|
||||
"unicode-width",
|
||||
"thiserror 1.0.69",
|
||||
"unicode-width 0.1.14",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -2715,7 +2715,7 @@ dependencies = [
|
||||
"strum 0.25.0",
|
||||
"strum_macros 0.25.3",
|
||||
"syn 2.0.87",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2725,7 +2725,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4889646c1ce8437a6fde3acb057fd7e2d039e62c61f5063fc125ed1ede114dc6"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -3654,7 +3654,7 @@ dependencies = [
|
||||
"rustls-pemfile 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -3808,7 +3808,7 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3900,7 +3900,7 @@ checksum = "5eb37859fda6792e95231aef1c5838f4043ec0ee352d8313421e311c606df612"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"strum 0.25.0",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"unic-ucd-category",
|
||||
]
|
||||
|
||||
@@ -4067,7 +4067,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
@@ -4560,15 +4560,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.17.8"
|
||||
version = "0.17.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3"
|
||||
checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281"
|
||||
dependencies = [
|
||||
"console",
|
||||
"instant",
|
||||
"number_prefix",
|
||||
"portable-atomic",
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.0",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5319,7 +5319,7 @@ dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"termcolor",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5345,7 +5345,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"socket2 0.5.7",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-util",
|
||||
@@ -5386,7 +5386,7 @@ dependencies = [
|
||||
"sha2 0.10.8",
|
||||
"smallvec",
|
||||
"subprocess",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
"uuid 1.11.0",
|
||||
"zstd 0.13.2",
|
||||
@@ -5578,7 +5578,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"sha2 0.10.8",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -5700,7 +5700,7 @@ dependencies = [
|
||||
"serde_with",
|
||||
"sha2 0.10.8",
|
||||
"subtle",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -6352,7 +6352,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"syn 2.0.87",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"typify",
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -6391,7 +6391,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"memchr",
|
||||
"parking_lot",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6428,9 +6428,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "psm"
|
||||
version = "0.1.23"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205"
|
||||
checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
@@ -6519,9 +6519,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.5"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684"
|
||||
checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"pin-project-lite",
|
||||
@@ -6530,26 +6530,29 @@ dependencies = [
|
||||
"rustc-hash 2.0.0",
|
||||
"rustls 0.23.16",
|
||||
"socket2 0.5.7",
|
||||
"thiserror",
|
||||
"thiserror 2.0.3",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.8"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6"
|
||||
checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.2.15",
|
||||
"rand 0.8.5",
|
||||
"ring 0.17.8",
|
||||
"rustc-hash 2.0.0",
|
||||
"rustls 0.23.16",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"thiserror 2.0.3",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6762,7 +6765,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6773,7 +6776,7 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.8",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
@@ -6788,9 +6791,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.8"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -7075,7 +7078,7 @@ dependencies = [
|
||||
"radix_fmt",
|
||||
"rand 0.8.5",
|
||||
"redis",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7184,16 +7187,16 @@ checksum = "f1adc9dfed5cc999077978cc7163b9282c5751c8d39827c4ea8c8c220ca5a440"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"toml 0.8.19",
|
||||
"toolchain_find",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.39"
|
||||
version = "0.38.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee"
|
||||
checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"errno",
|
||||
@@ -7290,6 +7293,9 @@ name = "rustls-pki-types"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-tokio-stream"
|
||||
@@ -7427,7 +7433,7 @@ dependencies = [
|
||||
"quick-xml 0.30.0",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"url",
|
||||
"uuid 1.11.0",
|
||||
]
|
||||
@@ -7539,9 +7545,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.12.0"
|
||||
version = "2.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
|
||||
checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -7579,9 +7585,9 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.214"
|
||||
version = "1.0.215"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
|
||||
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -7620,9 +7626,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.214"
|
||||
version = "1.0.215"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
|
||||
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -7680,7 +7686,7 @@ checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7691,7 +7697,7 @@ checksum = "8cac3f1e2ca2fe333923a1ae72caca910b98ed0630bb35ef6f8c8517d6e81afa"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7736,7 +7742,7 @@ dependencies = [
|
||||
"num-bigint",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"v8",
|
||||
]
|
||||
|
||||
@@ -7900,7 +7906,7 @@ checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
]
|
||||
|
||||
@@ -8043,9 +8049,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sourcemap"
|
||||
version = "9.0.1"
|
||||
version = "9.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86721155737d89818020dc35713595c6e7b21016073c101e6590c47ca58c2d16"
|
||||
checksum = "4d146f02f4bbbabbbe3da0f9cd3ea2ab779defc4ed1f070b5bd83ea48ed78811"
|
||||
dependencies = [
|
||||
"base64-simd 0.7.0",
|
||||
"bitvec",
|
||||
@@ -8114,7 +8120,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1008d95d2ec2d062959352527be30e10fec42a1aa5e5a48d990a5ff0fb9bdc0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8197,7 +8203,7 @@ dependencies = [
|
||||
"sha2 0.10.8",
|
||||
"smallvec",
|
||||
"sqlformat",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
@@ -8284,7 +8290,7 @@ dependencies = [
|
||||
"smallvec",
|
||||
"sqlx-core",
|
||||
"stringprep",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"tracing",
|
||||
"uuid 1.11.0",
|
||||
"whoami",
|
||||
@@ -8326,7 +8332,7 @@ dependencies = [
|
||||
"smallvec",
|
||||
"sqlx-core",
|
||||
"stringprep",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"tracing",
|
||||
"uuid 1.11.0",
|
||||
"whoami",
|
||||
@@ -8533,13 +8539,13 @@ dependencies = [
|
||||
"rustc-hash 1.1.0",
|
||||
"serde",
|
||||
"siphasher",
|
||||
"sourcemap 9.0.1",
|
||||
"sourcemap 9.1.0",
|
||||
"swc_allocator",
|
||||
"swc_atoms",
|
||||
"swc_eq_ignore_macros",
|
||||
"swc_visit",
|
||||
"tracing",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.14",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -8597,7 +8603,7 @@ dependencies = [
|
||||
"num-bigint",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"sourcemap 9.0.1",
|
||||
"sourcemap 9.1.0",
|
||||
"swc_allocator",
|
||||
"swc_atoms",
|
||||
"swc_common",
|
||||
@@ -8902,7 +8908,7 @@ dependencies = [
|
||||
"byteorder",
|
||||
"enum-as-inner",
|
||||
"libc",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -8993,7 +8999,7 @@ dependencies = [
|
||||
"tantivy-stacker",
|
||||
"tantivy-tokenizer-api",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
"uuid 1.11.0",
|
||||
"winapi",
|
||||
@@ -9139,18 +9145,38 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.68"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.68"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -9198,7 +9224,7 @@ dependencies = [
|
||||
"pretty-hex",
|
||||
"rustls-native-certs 0.6.3",
|
||||
"rustls-pemfile 1.0.4",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tokio-util",
|
||||
@@ -9324,7 +9350,7 @@ dependencies = [
|
||||
"rayon",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -9355,7 +9381,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"spm_precompiled",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"unicode-normalization-alignments",
|
||||
"unicode-segmentation",
|
||||
"unicode_categories",
|
||||
@@ -9457,7 +9483,7 @@ checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"futures-util",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -9703,7 +9729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
@@ -9853,7 +9879,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -9876,7 +9902,7 @@ dependencies = [
|
||||
"resolv-conf",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"trust-dns-proto",
|
||||
@@ -9903,7 +9929,7 @@ dependencies = [
|
||||
"native-tls",
|
||||
"rand 0.8.5",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
@@ -9954,7 +9980,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde_json",
|
||||
"syn 2.0.87",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
@@ -10116,6 +10142,12 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
@@ -10578,7 +10610,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -10620,7 +10652,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -10705,7 +10737,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -10723,7 +10755,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
@@ -10736,7 +10768,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rsmq_async",
|
||||
@@ -10751,7 +10783,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -10779,11 +10811,12 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest 0.12.9",
|
||||
"semver 1.0.23",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.8",
|
||||
"sqlx",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"tikv-jemalloc-ctl",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -10797,7 +10830,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"rsmq_async",
|
||||
@@ -10812,7 +10845,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -10834,7 +10867,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"lazy_static",
|
||||
@@ -10846,7 +10879,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -10855,7 +10888,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -10867,7 +10900,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -10879,7 +10912,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -10891,7 +10924,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.13.0",
|
||||
@@ -10902,7 +10935,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.13.0",
|
||||
@@ -10913,7 +10946,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -10931,7 +10964,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -10948,7 +10981,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -10960,7 +10993,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -10978,7 +11011,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.15",
|
||||
@@ -10999,7 +11032,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -11009,7 +11042,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -11042,7 +11075,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -11052,7 +11085,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -29,7 +29,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.421.1"
|
||||
version = "1.423.2"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -180,6 +180,7 @@ tokio-util = { version = "^0", features = ["io"] }
|
||||
json-pointer = "^0"
|
||||
itertools = "^0"
|
||||
regex = "^1"
|
||||
semver = "^1"
|
||||
|
||||
deno_fetch = "0.195.0"
|
||||
deno_tls = "0.158.0"
|
||||
|
||||
@@ -1 +1 @@
|
||||
6534b0f31fb4a75dd706fca2ce91e37e77e4ad02
|
||||
0d9c8813acd28848515c736e7b684220b5a785a3
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE alerts DROP COLUMN acknowledged;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Step 1: Add the new column 'acknowledged' to the 'alerts' table
|
||||
ALTER TABLE alerts
|
||||
ADD COLUMN acknowledged BOOLEAN DEFAULT NULL;
|
||||
|
||||
-- Step 2: Update all existing rows to set 'acknowledged' to true
|
||||
-- we don't want to pop up notifications to all users after migrations
|
||||
-- but only show new alerts from the point of the upgrade
|
||||
UPDATE alerts
|
||||
SET acknowledged = TRUE;
|
||||
+23
-16
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
use anyhow::Context;
|
||||
use git_version::git_version;
|
||||
use monitor::{
|
||||
reload_timeout_wait_result_setting, send_current_log_file_to_object_store,
|
||||
send_logs_to_object_store,
|
||||
@@ -31,18 +30,19 @@ use windmill_common::ee::{maybe_renew_license_key_on_start, LICENSE_KEY_ID, LICE
|
||||
|
||||
use windmill_common::{
|
||||
global_settings::{
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ERROR_CHANNELS_SETTING,
|
||||
CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING,
|
||||
ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
|
||||
EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING,
|
||||
JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING,
|
||||
OAUTH_SETTING, PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
|
||||
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING,
|
||||
CRITICAL_ERROR_CHANNELS_SETTING, CUSTOM_TAGS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING,
|
||||
DEFAULT_TAGS_WORKSPACES_SETTING, ENV_SETTINGS, EXPOSE_DEBUG_METRICS_SETTING,
|
||||
EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING,
|
||||
JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING,
|
||||
LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, PIP_INDEX_URL_SETTING,
|
||||
REQUEST_SIZE_LIMIT_SETTING, REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING,
|
||||
RETENTION_PERIOD_SECS_SETTING, SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, SMTP_SETTING,
|
||||
TIMEOUT_WAIT_RESULT_SETTING,
|
||||
},
|
||||
scripts::ScriptLang,
|
||||
stats_ee::schedule_stats,
|
||||
utils::{hostname, rd_string, Mode},
|
||||
utils::{hostname, rd_string, Mode, GIT_VERSION},
|
||||
worker::{reload_custom_tags_setting, HUB_CACHE_DIR, TMP_DIR, WORKER_GROUP},
|
||||
DB, METRICS_ENABLED,
|
||||
};
|
||||
@@ -67,16 +67,17 @@ use windmill_worker::{
|
||||
get_hub_script_content_and_requirements, BUN_BUNDLE_CACHE_DIR, BUN_CACHE_DIR,
|
||||
BUN_DEPSTAR_CACHE_DIR, DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, DENO_CACHE_DIR_NPM,
|
||||
GO_BIN_CACHE_DIR, GO_CACHE_DIR, LOCK_CACHE_DIR, PIP_CACHE_DIR, POWERSHELL_CACHE_DIR,
|
||||
RUST_CACHE_DIR, TAR_PIP_CACHE_DIR, TMP_LOGS_DIR, UV_CACHE_DIR,
|
||||
PY311_CACHE_DIR, RUST_CACHE_DIR, TAR_PIP_CACHE_DIR, TMP_LOGS_DIR, UV_CACHE_DIR,
|
||||
};
|
||||
|
||||
use crate::monitor::{
|
||||
initial_load, load_keep_job_dir, load_metrics_debug_enabled, load_require_preexisting_user,
|
||||
load_tag_per_workspace_enabled, load_tag_per_workspace_workspaces, monitor_db, monitor_pool,
|
||||
reload_base_url_setting, reload_bunfig_install_scopes_setting,
|
||||
reload_critical_error_channels_setting, reload_extra_pip_index_url_setting,
|
||||
reload_hub_base_url_setting, reload_job_default_timeout_setting, reload_jwt_secret_setting,
|
||||
reload_license_key, reload_npm_config_registry_setting, reload_pip_index_url_setting,
|
||||
reload_critical_alert_mute_ui_setting, reload_critical_error_channels_setting,
|
||||
reload_extra_pip_index_url_setting, reload_hub_base_url_setting,
|
||||
reload_job_default_timeout_setting, reload_jwt_secret_setting, reload_license_key,
|
||||
reload_npm_config_registry_setting, reload_pip_index_url_setting,
|
||||
reload_retention_period_setting, reload_scim_token_setting, reload_smtp_config,
|
||||
reload_worker_config,
|
||||
};
|
||||
@@ -84,7 +85,6 @@ use crate::monitor::{
|
||||
#[cfg(feature = "parquet")]
|
||||
use crate::monitor::reload_s3_cache_setting;
|
||||
|
||||
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
|
||||
const DEFAULT_NUM_WORKERS: usize = 1;
|
||||
const DEFAULT_PORT: u16 = 8000;
|
||||
const DEFAULT_SERVER_BIND_ADDR: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
|
||||
@@ -817,6 +817,12 @@ Windmill Community Edition {GIT_VERSION}
|
||||
tracing::error!(error = %e, "Could not reload jwt secret setting");
|
||||
}
|
||||
},
|
||||
CRITICAL_ALERT_MUTE_UI_SETTING => {
|
||||
tracing::info!("Critical alert UI setting changed");
|
||||
if let Err(e) = reload_critical_alert_mute_ui_setting(&db).await {
|
||||
tracing::error!(error = %e, "Could not reload critical alert UI setting");
|
||||
}
|
||||
},
|
||||
a @_ => {
|
||||
tracing::info!("Unrecognized Global Setting Change Payload: {:?}", a);
|
||||
}
|
||||
@@ -991,8 +997,9 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
|
||||
for x in [
|
||||
LOCK_CACHE_DIR,
|
||||
TMP_LOGS_DIR,
|
||||
PIP_CACHE_DIR,
|
||||
UV_CACHE_DIR,
|
||||
PY311_CACHE_DIR,
|
||||
PIP_CACHE_DIR,
|
||||
TAR_PIP_CACHE_DIR,
|
||||
DENO_CACHE_DIR,
|
||||
DENO_CACHE_DIR_DEPS,
|
||||
|
||||
+24
-4
@@ -39,7 +39,7 @@ use windmill_common::{
|
||||
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
|
||||
HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING,
|
||||
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
|
||||
PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING,
|
||||
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
|
||||
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
|
||||
},
|
||||
@@ -51,11 +51,11 @@ use windmill_common::{
|
||||
utils::{now_from_db, rd_string, report_critical_error, Mode},
|
||||
worker::{
|
||||
load_worker_config, make_pull_query, make_suspended_pull_query, reload_custom_tags_setting,
|
||||
DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, SMTP_CONFIG, WORKER_CONFIG,
|
||||
WORKER_GROUP,
|
||||
update_min_version, DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, SMTP_CONFIG,
|
||||
WORKER_CONFIG, WORKER_GROUP,
|
||||
},
|
||||
BASE_URL, CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, JOB_RETENTION_SECS,
|
||||
METRICS_DEBUG_ENABLED, METRICS_ENABLED,
|
||||
METRICS_DEBUG_ENABLED, METRICS_ENABLED, CRITICAL_ALERT_MUTE_UI_ENABLED
|
||||
};
|
||||
use windmill_queue::cancel_job;
|
||||
use windmill_worker::{
|
||||
@@ -131,6 +131,10 @@ pub async fn initial_load(
|
||||
tracing::error!("Error loading expose debug metrics: {e:#}");
|
||||
}
|
||||
|
||||
if let Err(e) = reload_critical_alert_mute_ui_setting(db).await {
|
||||
tracing::error!("Error loading critical alert mute ui setting: {e:#}");
|
||||
}
|
||||
|
||||
if let Err(e) = load_tag_per_workspace_enabled(db).await {
|
||||
tracing::error!("Error loading default tag per workpsace: {e:#}");
|
||||
}
|
||||
@@ -226,6 +230,17 @@ pub async fn load_tag_per_workspace_workspaces(db: &DB) -> error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reload_critical_alert_mute_ui_setting(db: &DB) -> error::Result<()> {
|
||||
let mute = load_value_from_global_settings(db, CRITICAL_ALERT_MUTE_UI_SETTING).await;
|
||||
match mute {
|
||||
Ok(Some(serde_json::Value::Bool(t))) => {
|
||||
CRITICAL_ALERT_MUTE_UI_ENABLED.store(t, Ordering::Relaxed);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn load_metrics_debug_enabled(db: &DB) -> error::Result<()> {
|
||||
let metrics_enabled = load_value_from_global_settings(db, EXPOSE_DEBUG_METRICS_SETTING).await;
|
||||
match metrics_enabled {
|
||||
@@ -1065,6 +1080,10 @@ pub async fn monitor_db(
|
||||
}
|
||||
};
|
||||
|
||||
let update_min_worker_version_f = async {
|
||||
update_min_version(db).await;
|
||||
};
|
||||
|
||||
join!(
|
||||
expired_items_f,
|
||||
zombie_jobs_f,
|
||||
@@ -1073,6 +1092,7 @@ pub async fn monitor_db(
|
||||
worker_groups_alerts_f,
|
||||
jobs_waiting_alerts_f,
|
||||
apply_autoscaling_f,
|
||||
update_min_worker_version_f,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1374
-657
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.421.1
|
||||
version: 1.423.2
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -190,7 +190,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/users/{username}:
|
||||
/w/{workspace}/users/get/{username}:
|
||||
get:
|
||||
summary: get user (require admin privilege)
|
||||
operationId: getUser
|
||||
@@ -283,6 +283,71 @@ paths:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/users/set_password_of/{user}:
|
||||
post:
|
||||
summary: set password for a specific user (require super admin)
|
||||
operationId: setPasswordForUser
|
||||
tags:
|
||||
- user
|
||||
parameters:
|
||||
- name: user
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: set password
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
required:
|
||||
- password
|
||||
responses:
|
||||
"200":
|
||||
description: password set
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/users/set_login_type/{user}:
|
||||
post:
|
||||
summary: set login type for a specific user (require super admin)
|
||||
operationId: setLoginTypeForUser
|
||||
tags:
|
||||
- user
|
||||
parameters:
|
||||
- name: user
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: set login type
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
login_type:
|
||||
type: string
|
||||
required:
|
||||
- login_type
|
||||
responses:
|
||||
"200":
|
||||
description: login type set
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/users/create:
|
||||
post:
|
||||
@@ -773,6 +838,78 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/settings/critical_alerts:
|
||||
get:
|
||||
summary: Get all critical alerts
|
||||
operationId: getCriticalAlerts
|
||||
tags:
|
||||
- setting
|
||||
parameters:
|
||||
- in: query
|
||||
name: page
|
||||
schema:
|
||||
type: integer
|
||||
default: 1
|
||||
description: The page number to retrieve (minimum value is 1)
|
||||
- in: query
|
||||
name: page_size
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
description: Number of alerts per page (maximum is 100)
|
||||
- in: query
|
||||
name: acknowledged
|
||||
schema:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: Filter by acknowledgment status; true for acknowledged, false for unacknowledged, and omit for all alerts
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully retrieved all critical alerts
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/CriticalAlert'
|
||||
|
||||
/settings/critical_alerts/{id}/acknowledge:
|
||||
post:
|
||||
summary: Acknowledge a critical alert
|
||||
operationId: acknowledgeCriticalAlert
|
||||
tags:
|
||||
- setting
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: The ID of the critical alert to acknowledge
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully acknowledged the critical alert
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
example: "Critical alert acknowledged"
|
||||
|
||||
/settings/critical_alerts/acknowledge_all:
|
||||
post:
|
||||
summary: Acknowledge all unacknowledged critical alerts
|
||||
operationId: acknowledgeAllCriticalAlerts
|
||||
tags:
|
||||
- setting
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully acknowledged all unacknowledged critical alerts.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
example: "All unacknowledged critical alerts acknowledged"
|
||||
|
||||
/settings/test_license_key:
|
||||
post:
|
||||
@@ -8116,6 +8253,23 @@ paths:
|
||||
- id
|
||||
- values
|
||||
|
||||
/workers/queue_counts:
|
||||
get:
|
||||
summary: get counts of jobs waiting for an executor per tag
|
||||
operationId: getCountsOfJobsWaitingPerTag
|
||||
tags:
|
||||
- worker
|
||||
responses:
|
||||
"200":
|
||||
description: queue counts
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: integer
|
||||
|
||||
|
||||
/configs/list_worker_groups:
|
||||
get:
|
||||
summary: list worker groups
|
||||
@@ -12114,6 +12268,10 @@ components:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: object
|
||||
s3_inputs:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
execution_mode:
|
||||
type: string
|
||||
enum: [viewer, publisher, anonymous]
|
||||
@@ -12721,3 +12879,23 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
CriticalAlert:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: Unique identifier for the alert
|
||||
alert_type:
|
||||
type: string
|
||||
description: Type of alert (e.g., critical_error)
|
||||
message:
|
||||
type: string
|
||||
description: The message content of the alert
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Time when the alert was created
|
||||
acknowledged:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: Acknowledgment status of the alert, can be true, false, or null if not set
|
||||
|
||||
@@ -7,6 +7,12 @@ use std::collections::HashMap;
|
||||
* Please see the included NOTICE for copyright information and
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
use crate::{job_helpers_ee::{
|
||||
get_random_file_name, get_s3_resource, get_workspace_s3_resource, upload_file_internal,
|
||||
UploadFileResponse,
|
||||
}, users::fetch_api_authed_from_permissioned_as};
|
||||
use crate::{
|
||||
db::{ApiAuthed, DB},
|
||||
resources::get_resource_value_interpolated_internal,
|
||||
@@ -23,7 +29,13 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
#[cfg(feature = "parquet")]
|
||||
use itertools::Itertools;
|
||||
use magic_crypt::MagicCryptTrait;
|
||||
#[cfg(feature = "parquet")]
|
||||
use object_store::{Attribute, Attributes};
|
||||
#[cfg(feature = "parquet")]
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, value::RawValue};
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -32,6 +44,8 @@ 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::{
|
||||
apps::ListAppQuery,
|
||||
db::UserDB,
|
||||
@@ -69,6 +83,7 @@ pub fn workspaced_service() -> Router {
|
||||
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("/public_app/:secret", get(get_public_app_by_secret))
|
||||
.route("/public_resource/*path", get(get_public_resource))
|
||||
}
|
||||
@@ -179,6 +194,14 @@ pub struct PolicyTriggerableInputs {
|
||||
allow_user_resources: AllowUserResources,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct S3Input {
|
||||
allowed_resources: Vec<String>,
|
||||
allow_user_resources: bool,
|
||||
allow_workspace_resource: bool,
|
||||
file_key_regex: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Policy {
|
||||
pub on_behalf_of: Option<String>,
|
||||
@@ -192,6 +215,7 @@ pub struct Policy {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub triggerables_v2: Option<HashMap<String, PolicyTriggerableInputs>>,
|
||||
pub execution_mode: ExecutionMode,
|
||||
pub s3_inputs: Option<Vec<S3Input>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -432,9 +456,7 @@ async fn get_latest_version(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
|
||||
) -> JsonResult<Option<AppHistory>> {
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let row = sqlx::query!(
|
||||
"SELECT a.id as app_id, av.id as version_id, dm.deployment_msg as deployment_msg
|
||||
@@ -457,7 +479,6 @@ async fn get_latest_version(
|
||||
} else {
|
||||
return Ok(Json(None));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async fn update_app_history(
|
||||
@@ -1067,6 +1088,49 @@ fn digest(code: &str) -> String {
|
||||
format!("rawscript/{:x}", result)
|
||||
}
|
||||
|
||||
async fn get_on_behalf_details_from_policy_and_authed(
|
||||
policy: &Policy,
|
||||
opt_authed: &Option<ApiAuthed>,
|
||||
) -> Result<(String, String, String)> {
|
||||
let (username, permissioned_as, email) = match policy.execution_mode {
|
||||
ExecutionMode::Anonymous => {
|
||||
let username = opt_authed
|
||||
.as_ref()
|
||||
.map(|a| a.username.clone())
|
||||
.unwrap_or_else(|| "anonymous".to_string());
|
||||
let (permissioned_as, email) = get_on_behalf_of(&policy)?;
|
||||
(username, permissioned_as, email)
|
||||
}
|
||||
ExecutionMode::Publisher => {
|
||||
let username = opt_authed
|
||||
.as_ref()
|
||||
.map(|a| a.username.clone())
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(
|
||||
"publisher execution mode requires authentication".to_string(),
|
||||
)
|
||||
})?;
|
||||
let (permissioned_as, email) = get_on_behalf_of(&policy)?;
|
||||
(username, permissioned_as, email)
|
||||
}
|
||||
ExecutionMode::Viewer => {
|
||||
let (username, email) = opt_authed
|
||||
.as_ref()
|
||||
.map(|a| (a.username.clone(), a.email.clone()))
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest("Required to be authed in viewer mode".to_string())
|
||||
})?;
|
||||
(
|
||||
username.clone(),
|
||||
username_to_permissioned_as(&username),
|
||||
email,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
Ok((username, permissioned_as, email))
|
||||
}
|
||||
|
||||
async fn execute_component(
|
||||
OptAuthed(opt_authed): OptAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
@@ -1129,6 +1193,7 @@ async fn execute_component(
|
||||
triggerables_v2: Some(hm),
|
||||
on_behalf_of: None,
|
||||
on_behalf_of_email: None,
|
||||
s3_inputs: None,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -1146,41 +1211,8 @@ async fn execute_component(
|
||||
}
|
||||
};
|
||||
|
||||
let (username, permissioned_as, email) = match policy.execution_mode {
|
||||
ExecutionMode::Anonymous => {
|
||||
let username = opt_authed
|
||||
.as_ref()
|
||||
.map(|a| a.username.clone())
|
||||
.unwrap_or_else(|| "anonymous".to_string());
|
||||
let (permissioned_as, email) = get_on_behalf_of(&policy)?;
|
||||
(username, permissioned_as, email)
|
||||
}
|
||||
ExecutionMode::Publisher => {
|
||||
let username = opt_authed
|
||||
.as_ref()
|
||||
.map(|a| a.username.clone())
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(
|
||||
"publisher execution mode requires authentication".to_string(),
|
||||
)
|
||||
})?;
|
||||
let (permissioned_as, email) = get_on_behalf_of(&policy)?;
|
||||
(username, permissioned_as, email)
|
||||
}
|
||||
ExecutionMode::Viewer => {
|
||||
let (username, email) = opt_authed
|
||||
.as_ref()
|
||||
.map(|a| (a.username.clone(), a.email.clone()))
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest("Required to be authed in viewer mode".to_string())
|
||||
})?;
|
||||
(
|
||||
username.clone(),
|
||||
username_to_permissioned_as(&username),
|
||||
email,
|
||||
)
|
||||
}
|
||||
};
|
||||
let (username, permissioned_as, email) =
|
||||
get_on_behalf_details_from_policy_and_authed(&policy, &opt_authed).await?;
|
||||
|
||||
let (job_payload, (args, job_id), tag) = match payload {
|
||||
ExecuteApp { args, component, raw_code: Some(raw_code), path: None, .. } => {
|
||||
@@ -1249,6 +1281,248 @@ async fn execute_component(
|
||||
Ok(uuid.to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "parquet"))]
|
||||
async fn upload_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 {
|
||||
file_key: Option<String>,
|
||||
file_extension: Option<String>,
|
||||
s3_resource_path: Option<String>,
|
||||
content_type: Option<String>,
|
||||
content_disposition: Option<String>,
|
||||
force_viewer_file_key_regex: Option<String>,
|
||||
force_viewer_allow_user_resources: Option<bool>,
|
||||
force_viewer_allow_workspace_resource: Option<bool>,
|
||||
force_viewer_allowed_resources: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
async fn upload_s3_file_from_app(
|
||||
OptAuthed(opt_authed): OptAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Query(query): Query<UploadFileToS3Query>,
|
||||
request: axum::extract::Request,
|
||||
) -> JsonResult<UploadFileResponse> {
|
||||
let policy = if let Some(file_key_regex) = query.force_viewer_file_key_regex {
|
||||
Some(Policy {
|
||||
execution_mode: ExecutionMode::Viewer,
|
||||
triggerables: None,
|
||||
triggerables_v2: None,
|
||||
on_behalf_of: None,
|
||||
on_behalf_of_email: None,
|
||||
s3_inputs: Some(vec![S3Input {
|
||||
file_key_regex: file_key_regex,
|
||||
allow_user_resources: query.force_viewer_allow_user_resources.unwrap_or(false),
|
||||
allow_workspace_resource: query
|
||||
.force_viewer_allow_workspace_resource
|
||||
.unwrap_or(false),
|
||||
allowed_resources: query
|
||||
.force_viewer_allowed_resources
|
||||
.map(|s| s.split(',').map(|s| s.to_string()).collect())
|
||||
.unwrap_or_default(),
|
||||
}]),
|
||||
})
|
||||
} else {
|
||||
let policy_o = sqlx::query_scalar!(
|
||||
"SELECT policy from app WHERE path = $1 AND workspace_id = $2",
|
||||
&path.0,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
|
||||
policy_o
|
||||
.map(|p| serde_json::from_value::<Policy>(p).map_err(to_anyhow))
|
||||
.transpose()?
|
||||
};
|
||||
|
||||
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 policy = policy.unwrap();
|
||||
let s3_inputs = policy.s3_inputs.as_ref().unwrap();
|
||||
|
||||
let (username, permissioned_as, email) =
|
||||
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, &w_id, &db, username)
|
||||
.await?;
|
||||
|
||||
if let Some(file_key) = query.file_key {
|
||||
// file key is provided => requires workspace, user or list policy and must match the regex
|
||||
let matching_s3_inputs = if let Some(ref s3_resource_path) = query.s3_resource_path {
|
||||
s3_inputs
|
||||
.iter()
|
||||
.filter(|s3_input| {
|
||||
s3_input.allowed_resources.contains(s3_resource_path)
|
||||
|| s3_input.allow_user_resources
|
||||
})
|
||||
.sorted_by_key(|i| i.allow_user_resources) // consider user resources last
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
s3_inputs
|
||||
.iter()
|
||||
.filter(|s3_input| s3_input.allow_workspace_resource)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let matched_input = matching_s3_inputs.iter().find(|s3_input| {
|
||||
match Regex::new(&s3_input.file_key_regex) {
|
||||
Ok(re) => re.is_match(&file_key),
|
||||
Err(e) => {
|
||||
tracing::error!("Error compiling regex: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(matched_input) = matched_input {
|
||||
if let Some(ref s3_resource_path) = query.s3_resource_path {
|
||||
if matched_input.allow_user_resources {
|
||||
if let Some(authed) = opt_authed {
|
||||
(
|
||||
Some(
|
||||
get_s3_resource(
|
||||
&authed,
|
||||
&db,
|
||||
Some(user_db),
|
||||
"",
|
||||
&w_id,
|
||||
s3_resource_path,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
file_key,
|
||||
)
|
||||
} else {
|
||||
return Err(Error::BadRequest(
|
||||
"User resources are not allowed without being logged in"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
(
|
||||
Some(
|
||||
get_s3_resource(
|
||||
&on_behalf_authed,
|
||||
&db,
|
||||
Some(user_db),
|
||||
"",
|
||||
&w_id,
|
||||
s3_resource_path,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
file_key,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
let (_, s3_resource_opt) =
|
||||
get_workspace_s3_resource(&on_behalf_authed, &db, None, "", &w_id, None)
|
||||
.await?;
|
||||
(s3_resource_opt, file_key)
|
||||
}
|
||||
} else {
|
||||
return Err(Error::BadRequest(
|
||||
"No matching s3 resource found for the given file key".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// no file key => requires unnamed upload policy => allow workspace resource and file_key_regex is empty
|
||||
let has_unnamed_policy = s3_inputs.iter().any(|s3_input| {
|
||||
s3_input.allow_workspace_resource && s3_input.file_key_regex.is_empty()
|
||||
});
|
||||
|
||||
if !has_unnamed_policy {
|
||||
return Err(Error::BadRequest(
|
||||
"no policy found for unnamed s3 file uplooad".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// for now, we place all files into `windmill_uploads` folder with a random name
|
||||
// TODO: make the folder configurable via the workspace settings
|
||||
let file_key = get_random_file_name(query.file_extension);
|
||||
|
||||
let (_, s3_resource_opt) =
|
||||
get_workspace_s3_resource(&on_behalf_authed, &db, None, "", &w_id, None).await?;
|
||||
|
||||
(s3_resource_opt, file_key)
|
||||
}
|
||||
} else {
|
||||
// backward compatibility (no policy)
|
||||
// if no policy but logged in, use the user's auth to get the s3 resource
|
||||
if let Some(authed) = opt_authed {
|
||||
let file_key = query
|
||||
.file_key
|
||||
.unwrap_or_else(|| get_random_file_name(query.file_extension));
|
||||
|
||||
if let Some(ref s3_resource_path) = query.s3_resource_path {
|
||||
(
|
||||
Some(
|
||||
get_s3_resource(
|
||||
&authed,
|
||||
&db,
|
||||
Some(user_db),
|
||||
"",
|
||||
&w_id,
|
||||
s3_resource_path,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
file_key,
|
||||
)
|
||||
} else {
|
||||
let (_, s3_resource) =
|
||||
get_workspace_s3_resource(&authed, &db, None, "", &w_id, None).await?;
|
||||
|
||||
(s3_resource, file_key)
|
||||
}
|
||||
} else {
|
||||
return Err(Error::BadRequest("Missing s3 policy".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let s3_resource = s3_resource_opt.ok_or(Error::InternalErr(
|
||||
"No files storage resource defined at the workspace level".to_string(),
|
||||
))?;
|
||||
let s3_client = build_object_store_client(&s3_resource).await?;
|
||||
|
||||
let options = Attributes::from_iter(vec![
|
||||
(
|
||||
Attribute::ContentType,
|
||||
query.content_type.unwrap_or_else(|| {
|
||||
mime_guess::from_path(&file_key)
|
||||
.first_or_octet_stream()
|
||||
.to_string()
|
||||
}),
|
||||
),
|
||||
(
|
||||
Attribute::ContentDisposition,
|
||||
query.content_disposition.unwrap_or("inline".to_string()),
|
||||
),
|
||||
])
|
||||
.into();
|
||||
|
||||
upload_file_internal(s3_client, &file_key, request, options).await?;
|
||||
|
||||
return Ok(Json(UploadFileResponse { file_key }));
|
||||
}
|
||||
|
||||
fn get_on_behalf_of(policy: &Policy) -> Result<(String, String)> {
|
||||
let permissioned_as = policy
|
||||
.on_behalf_of
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
use axum::Router;
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
use windmill_common::s3_helpers::StorageResourceType;
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
use crate::db::{ApiAuthed, DB};
|
||||
#[cfg(feature = "parquet")]
|
||||
use object_store::{ObjectStore, PutMultipartOpts};
|
||||
#[cfg(feature = "parquet")]
|
||||
use std::sync::Arc;
|
||||
use windmill_common::error;
|
||||
#[cfg(feature = "parquet")]
|
||||
use windmill_common::{db::UserDB, s3_helpers::ObjectStoreResource};
|
||||
#[derive(Serialize)]
|
||||
pub struct UploadFileResponse {
|
||||
pub file_key: String,
|
||||
}
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
@@ -21,3 +33,29 @@ pub async fn get_workspace_s3_resource<'c>(
|
||||
// implementation is not open source
|
||||
Ok((None, None))
|
||||
}
|
||||
|
||||
pub fn get_random_file_name(_file_extension: Option<String>) -> String {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub async fn get_s3_resource<'c>(
|
||||
_authed: &ApiAuthed,
|
||||
_db: &DB,
|
||||
_user_db: Option<UserDB>,
|
||||
_token: &str,
|
||||
_w_id: &str,
|
||||
_resource_path: &str,
|
||||
_resource_type: Option<StorageResourceType>,
|
||||
_job_id: Option<Uuid>,
|
||||
) -> error::Result<ObjectStoreResource> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub async fn upload_file_internal(
|
||||
_s3_client: Arc<dyn ObjectStore>,
|
||||
_file_key: &str,
|
||||
_request: axum::extract::Request,
|
||||
_options: PutMultipartOpts,
|
||||
) -> error::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
+274
-240
@@ -12,6 +12,7 @@ use quick_cache::sync::Cache;
|
||||
use serde_json::value::RawValue;
|
||||
use sqlx::Pool;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
#[cfg(feature = "prometheus")]
|
||||
use std::sync::atomic::Ordering;
|
||||
use tokio::io::AsyncReadExt;
|
||||
@@ -65,7 +66,7 @@ use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{self, to_anyhow, Error},
|
||||
flow_status::{Approval, FlowStatus, FlowStatusModule},
|
||||
flows::FlowValue,
|
||||
flows::{add_virtual_items_if_necessary, FlowValue},
|
||||
jobs::{script_path_to_payload, CompletedJob, JobKind, JobPayload, QueuedJob, RawCode},
|
||||
oauth2::HmacSha256,
|
||||
scripts::{ScriptHash, ScriptLang},
|
||||
@@ -600,7 +601,7 @@ async fn get_flow_job_debug_info(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
) -> error::Result<Response> {
|
||||
let job = get_queued_job(&id, w_id.as_str(), &db).await?;
|
||||
let job = get_queued_job_ex(&db, &w_id, id, false, None).await?;
|
||||
if let Some(job) = job {
|
||||
let is_flow = job.is_flow();
|
||||
if job.is_flow_step || !is_flow {
|
||||
@@ -734,6 +735,67 @@ fn generate_get_job_query(no_logs: bool, table: &str) -> String {
|
||||
{join}
|
||||
WHERE id = $1 AND {table}.workspace_id = $2");
|
||||
}
|
||||
pub async fn get_queued_job_ex(
|
||||
db: &DB,
|
||||
workspace_id: &str,
|
||||
job_id: Uuid,
|
||||
no_logs: bool,
|
||||
// first optional is if authed need to be checked, second is the opt_authed itself
|
||||
opt_authed: Option<&Option<ApiAuthed>>,
|
||||
) -> error::Result<Option<JobExtended<QueuedJob>>> {
|
||||
let query = if no_logs { &*GET_QUEUED_JOB_QUERY_NO_LOGS } else { &*GET_QUEUED_JOB_QUERY };
|
||||
let job = sqlx::query_as::<_, JobExtended<QueuedJob>>(query)
|
||||
.bind(job_id)
|
||||
.bind(workspace_id)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
if let Some(job) = job.as_ref() {
|
||||
if opt_authed.is_some_and(|x| x.is_none()) && job.created_by != "anonymous" {
|
||||
return Err(Error::BadRequest(
|
||||
"As a non logged in user, you can only see jobs ran by anonymous users".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(job)
|
||||
}
|
||||
pub async fn get_completed_job_ex(
|
||||
db: &DB,
|
||||
workspace_id: &str,
|
||||
job_id: Uuid,
|
||||
no_logs: bool,
|
||||
// first optional is if authed need to be checked, second is the opt_authed itself
|
||||
opt_authed: Option<&Option<ApiAuthed>>,
|
||||
) -> error::Result<Option<JobExtended<CompletedJob>>> {
|
||||
let query = if no_logs { &*GET_COMPLETED_JOB_QUERY_NO_LOGS } else { &*GET_COMPLETED_JOB_QUERY };
|
||||
let cjob = sqlx::query_as::<_, JobExtended<CompletedJob>>(query)
|
||||
.bind(job_id)
|
||||
.bind(workspace_id)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
if let Some(job) = cjob.as_ref() {
|
||||
if opt_authed.is_some_and(|x| x.is_none()) && job.created_by != "anonymous" {
|
||||
return Err(Error::BadRequest(
|
||||
"As a non logged in user, you can only see jobs ran by anonymous users".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut cjob) = cjob {
|
||||
let CompletedJobWithFormattedResult { mut cj, result } = format_completed_job_result(cjob.inner);
|
||||
cj.result = match result {
|
||||
Some(FormattedResult::RawValue(rv)) => rv,
|
||||
Some(FormattedResult::Vec(v)) => Some(to_raw_value(&v)),
|
||||
None => None,
|
||||
}.map(sqlx::types::Json);
|
||||
cjob.inner = cj;
|
||||
return Ok(Some(cjob));
|
||||
}
|
||||
|
||||
Ok(cjob)
|
||||
}
|
||||
pub async fn get_job_internal(
|
||||
db: &DB,
|
||||
workspace_id: &str,
|
||||
@@ -742,48 +804,18 @@ pub async fn get_job_internal(
|
||||
// first optional is if authed need to be checked, second is the opt_authed itself
|
||||
opt_authed: Option<&Option<ApiAuthed>>,
|
||||
) -> error::Result<Job> {
|
||||
let cjob_maybe = sqlx::query_as::<_, CompletedJob>(if no_logs {
|
||||
&*GET_COMPLETED_JOB_QUERY_NO_LOGS
|
||||
} else {
|
||||
&*GET_COMPLETED_JOB_QUERY
|
||||
})
|
||||
.bind(job_id)
|
||||
.bind(workspace_id)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.map(Job::CompletedJob);
|
||||
|
||||
if let Some(cjob) = cjob_maybe {
|
||||
Ok(match cjob {
|
||||
Job::CompletedJob(cjob) => {
|
||||
if opt_authed.is_some_and(|x| x.is_none()) && cjob.created_by != "anonymous" {
|
||||
return Err(Error::BadRequest(
|
||||
"As a non logged in user, you can only see jobs ran by anonymous users"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
Job::CompletedJobWithFormattedResult(format_completed_job_result(cjob))
|
||||
}
|
||||
cjob => cjob,
|
||||
})
|
||||
} else {
|
||||
let job_o = sqlx::query_as::<_, QueuedJob>(if no_logs {
|
||||
&*GET_QUEUED_JOB_QUERY_NO_LOGS
|
||||
} else {
|
||||
&*GET_QUEUED_JOB_QUERY
|
||||
})
|
||||
.bind(job_id)
|
||||
.bind(workspace_id)
|
||||
.fetch_optional(db)
|
||||
let cjob = get_completed_job_ex(db, workspace_id, job_id, no_logs, opt_authed.clone())
|
||||
.await?
|
||||
.map(Job::QueuedJob);
|
||||
let job: Job = not_found_if_none(job_o, "Job", job_id.to_string())?;
|
||||
if opt_authed.is_some_and(|x| x.is_none()) && job.created_by() != "anonymous" {
|
||||
return Err(Error::BadRequest(
|
||||
"As a non logged in user, you can only see jobs ran by anonymous users".to_string(),
|
||||
));
|
||||
.map(Job::CompletedJob);
|
||||
|
||||
match cjob {
|
||||
Some(cjob) => Ok(cjob),
|
||||
None => {
|
||||
let job_maybe = get_queued_job_ex(db, workspace_id, job_id, no_logs, opt_authed)
|
||||
.await?
|
||||
.map(Job::QueuedJob);
|
||||
not_found_if_none(job_maybe, "Job", job_id.to_string())
|
||||
}
|
||||
Ok(job)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1735,7 +1767,6 @@ async fn resume_suspended_job_internal(
|
||||
let trigger_email = match &parent_flow {
|
||||
Job::CompletedJob(job) => &job.email,
|
||||
Job::QueuedJob(job) => &job.email,
|
||||
Job::CompletedJobWithFormattedResult(job) => &job.cj.email,
|
||||
};
|
||||
conditionally_require_authed_user(authed.clone(), flow_status, trigger_email)?;
|
||||
|
||||
@@ -2010,7 +2041,6 @@ pub async fn get_suspended_job_flow(
|
||||
let trigger_email = match &flow {
|
||||
Job::CompletedJob(job) => &job.email,
|
||||
Job::QueuedJob(job) => &job.email,
|
||||
Job::CompletedJobWithFormattedResult(job) => &job.cj.email,
|
||||
};
|
||||
conditionally_require_authed_user(authed.clone(), flow_status.clone(), trigger_email)?;
|
||||
|
||||
@@ -2225,13 +2255,45 @@ pub async fn get_resume_urls(
|
||||
Ok(Json(res))
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Serialize)]
|
||||
pub struct JobExtended<T> {
|
||||
#[sqlx(flatten)]
|
||||
#[serde(flatten)]
|
||||
inner: T,
|
||||
|
||||
#[sqlx(skip)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub self_wait_time_ms: Option<i64>,
|
||||
#[sqlx(skip)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aggregate_wait_time_ms: Option<i64>,
|
||||
}
|
||||
|
||||
impl<T> JobExtended<T> {
|
||||
pub fn new(self_wait_time_ms: Option<i64>, aggregate_wait_time_ms: Option<i64>, inner: T) -> Self {
|
||||
Self { inner, self_wait_time_ms, aggregate_wait_time_ms }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for JobExtended<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for JobExtended<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Job {
|
||||
QueuedJob(QueuedJob),
|
||||
CompletedJob(CompletedJob),
|
||||
#[serde(rename = "CompletedJob")]
|
||||
CompletedJobWithFormattedResult(CompletedJobWithFormattedResult),
|
||||
QueuedJob(JobExtended<QueuedJob>),
|
||||
CompletedJob(JobExtended<CompletedJob>),
|
||||
}
|
||||
|
||||
impl Job {
|
||||
@@ -2239,27 +2301,6 @@ impl Job {
|
||||
match self {
|
||||
Job::QueuedJob(job) => &job.created_by,
|
||||
Job::CompletedJob(job) => &job.created_by,
|
||||
Job::CompletedJobWithFormattedResult(job) => &job.cj.created_by,
|
||||
}
|
||||
}
|
||||
pub fn raw_flow(&self) -> Option<FlowValue> {
|
||||
match self {
|
||||
Job::QueuedJob(job) => job
|
||||
.raw_flow
|
||||
.as_ref()
|
||||
.map(|rf| serde_json::from_str(rf.0.get()).ok())
|
||||
.flatten(),
|
||||
Job::CompletedJob(job) => job
|
||||
.raw_flow
|
||||
.as_ref()
|
||||
.map(|rf| serde_json::from_str(rf.0.get()).ok())
|
||||
.flatten(),
|
||||
Job::CompletedJobWithFormattedResult(job) => job
|
||||
.cj
|
||||
.raw_flow
|
||||
.as_ref()
|
||||
.map(|rf| serde_json::from_str(rf.0.get()).ok())
|
||||
.flatten(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2279,13 +2320,6 @@ impl Job {
|
||||
job.logs = Some(logs.to_string());
|
||||
}
|
||||
}
|
||||
Job::CompletedJobWithFormattedResult(job) => {
|
||||
if let Some(ref mut l) = job.cj.logs {
|
||||
l.push_str(logs);
|
||||
} else {
|
||||
job.cj.logs = Some(logs.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2293,7 +2327,6 @@ impl Job {
|
||||
match self {
|
||||
Job::QueuedJob(job) => job.logs.as_ref().map(|l| l.len()),
|
||||
Job::CompletedJob(job) => job.logs.as_ref().map(|l| l.len()),
|
||||
Job::CompletedJobWithFormattedResult(job) => job.cj.logs.as_ref().map(|l| l.len()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2301,7 +2334,6 @@ impl Job {
|
||||
match self {
|
||||
Job::QueuedJob(job) => job.logs.clone(),
|
||||
Job::CompletedJob(job) => job.logs.clone(),
|
||||
Job::CompletedJobWithFormattedResult(job) => job.cj.logs.clone(),
|
||||
}
|
||||
}
|
||||
pub fn flow_status(&self) -> Option<FlowStatus> {
|
||||
@@ -2316,19 +2348,12 @@ impl Job {
|
||||
.as_ref()
|
||||
.map(|rf| serde_json::from_str(rf.0.get()).ok())
|
||||
.flatten(),
|
||||
Job::CompletedJobWithFormattedResult(job) => job
|
||||
.cj
|
||||
.flow_status
|
||||
.as_ref()
|
||||
.map(|rf| serde_json::from_str(rf.0.get()).ok())
|
||||
.flatten(),
|
||||
}
|
||||
}
|
||||
pub fn is_flow_step(&self) -> bool {
|
||||
match self {
|
||||
Job::QueuedJob(job) => job.is_flow_step,
|
||||
Job::CompletedJob(job) => job.is_flow_step,
|
||||
Job::CompletedJobWithFormattedResult(job) => job.cj.is_flow_step,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2343,7 +2368,6 @@ impl Job {
|
||||
match self {
|
||||
Job::QueuedJob(job) => &job.job_kind,
|
||||
Job::CompletedJob(job) => &job.job_kind,
|
||||
Job::CompletedJobWithFormattedResult(job) => &job.cj.job_kind,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2351,7 +2375,6 @@ impl Job {
|
||||
match self {
|
||||
Job::QueuedJob(job) => job.id,
|
||||
Job::CompletedJob(job) => job.id,
|
||||
Job::CompletedJobWithFormattedResult(job) => job.cj.id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2359,7 +2382,6 @@ impl Job {
|
||||
match self {
|
||||
Job::QueuedJob(job) => &job.workspace_id,
|
||||
Job::CompletedJob(job) => &job.workspace_id,
|
||||
Job::CompletedJobWithFormattedResult(job) => &job.cj.workspace_id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2367,7 +2389,6 @@ impl Job {
|
||||
match self {
|
||||
Job::QueuedJob(job) => job.script_path.as_ref(),
|
||||
Job::CompletedJob(job) => job.script_path.as_ref(),
|
||||
Job::CompletedJobWithFormattedResult(job) => job.cj.script_path.as_ref(),
|
||||
}
|
||||
.map(String::as_str)
|
||||
.unwrap_or("tmp/main")
|
||||
@@ -2377,7 +2398,6 @@ impl Job {
|
||||
match self {
|
||||
Job::QueuedJob(job) => job.args.as_ref(),
|
||||
Job::CompletedJob(job) => job.args.as_ref(),
|
||||
Job::CompletedJobWithFormattedResult(job) => job.cj.args.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2426,10 +2446,6 @@ impl Job {
|
||||
job.self_wait_time_ms = self_wait_time;
|
||||
job.aggregate_wait_time_ms = aggregate_wait_time;
|
||||
}
|
||||
Job::CompletedJobWithFormattedResult(job) => {
|
||||
job.cj.self_wait_time_ms = self_wait_time;
|
||||
job.cj.aggregate_wait_time_ms = aggregate_wait_time;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -2557,86 +2573,82 @@ impl UnifiedJob {
|
||||
impl<'a> From<UnifiedJob> for Job {
|
||||
fn from(uj: UnifiedJob) -> Self {
|
||||
match uj.typ.as_ref() {
|
||||
"CompletedJob" => Job::CompletedJob(CompletedJob {
|
||||
workspace_id: uj.workspace_id,
|
||||
id: uj.id,
|
||||
parent_job: uj.parent_job,
|
||||
created_by: uj.created_by,
|
||||
created_at: uj.created_at,
|
||||
started_at: uj.started_at.unwrap_or(uj.created_at),
|
||||
duration_ms: uj.duration_ms.unwrap(),
|
||||
success: uj.success.unwrap(),
|
||||
script_hash: uj.script_hash,
|
||||
script_path: uj.script_path,
|
||||
args: None,
|
||||
result: None,
|
||||
logs: None,
|
||||
flow_status: None,
|
||||
deleted: uj.deleted,
|
||||
canceled: uj.canceled,
|
||||
canceled_by: uj.canceled_by,
|
||||
raw_code: None,
|
||||
canceled_reason: None,
|
||||
job_kind: uj.job_kind,
|
||||
schedule_path: uj.schedule_path,
|
||||
permissioned_as: uj.permissioned_as,
|
||||
raw_flow: None,
|
||||
is_flow_step: uj.is_flow_step,
|
||||
language: uj.language,
|
||||
is_skipped: uj.is_skipped,
|
||||
email: uj.email,
|
||||
visible_to_owner: uj.visible_to_owner,
|
||||
mem_peak: uj.mem_peak,
|
||||
tag: uj.tag,
|
||||
priority: uj.priority,
|
||||
labels: uj.labels,
|
||||
self_wait_time_ms: uj.self_wait_time_ms,
|
||||
aggregate_wait_time_ms: uj.aggregate_wait_time_ms,
|
||||
}),
|
||||
"QueuedJob" => Job::QueuedJob(QueuedJob {
|
||||
workspace_id: uj.workspace_id,
|
||||
id: uj.id,
|
||||
parent_job: uj.parent_job,
|
||||
created_by: uj.created_by,
|
||||
created_at: uj.created_at,
|
||||
started_at: uj.started_at,
|
||||
script_hash: uj.script_hash,
|
||||
script_path: uj.script_path,
|
||||
args: None,
|
||||
running: uj.running.unwrap(),
|
||||
scheduled_for: uj.scheduled_for.unwrap(),
|
||||
logs: None,
|
||||
flow_status: None,
|
||||
raw_code: None,
|
||||
raw_lock: None,
|
||||
canceled: uj.canceled,
|
||||
canceled_by: uj.canceled_by,
|
||||
canceled_reason: None,
|
||||
last_ping: None,
|
||||
job_kind: uj.job_kind,
|
||||
schedule_path: uj.schedule_path,
|
||||
permissioned_as: uj.permissioned_as,
|
||||
raw_flow: None,
|
||||
is_flow_step: uj.is_flow_step,
|
||||
language: uj.language,
|
||||
same_worker: false,
|
||||
pre_run_error: None,
|
||||
email: uj.email,
|
||||
visible_to_owner: uj.visible_to_owner,
|
||||
suspend: uj.suspend,
|
||||
mem_peak: uj.mem_peak,
|
||||
root_job: None,
|
||||
leaf_jobs: None,
|
||||
tag: uj.tag,
|
||||
concurrent_limit: uj.concurrent_limit,
|
||||
concurrency_time_window_s: uj.concurrency_time_window_s,
|
||||
timeout: None,
|
||||
flow_step_id: None,
|
||||
cache_ttl: None,
|
||||
priority: uj.priority,
|
||||
self_wait_time_ms: uj.self_wait_time_ms,
|
||||
aggregate_wait_time_ms: uj.aggregate_wait_time_ms,
|
||||
}),
|
||||
"CompletedJob" => Job::CompletedJob(JobExtended::new(uj.self_wait_time_ms, uj.aggregate_wait_time_ms, CompletedJob {
|
||||
workspace_id: uj.workspace_id,
|
||||
id: uj.id,
|
||||
parent_job: uj.parent_job,
|
||||
created_by: uj.created_by,
|
||||
created_at: uj.created_at,
|
||||
started_at: uj.started_at.unwrap_or(uj.created_at),
|
||||
duration_ms: uj.duration_ms.unwrap(),
|
||||
success: uj.success.unwrap(),
|
||||
script_hash: uj.script_hash,
|
||||
script_path: uj.script_path,
|
||||
args: None,
|
||||
result: None,
|
||||
logs: None,
|
||||
flow_status: None,
|
||||
deleted: uj.deleted,
|
||||
canceled: uj.canceled,
|
||||
canceled_by: uj.canceled_by,
|
||||
raw_code: None,
|
||||
canceled_reason: None,
|
||||
job_kind: uj.job_kind,
|
||||
schedule_path: uj.schedule_path,
|
||||
permissioned_as: uj.permissioned_as,
|
||||
raw_flow: None,
|
||||
is_flow_step: uj.is_flow_step,
|
||||
language: uj.language,
|
||||
is_skipped: uj.is_skipped,
|
||||
email: uj.email,
|
||||
visible_to_owner: uj.visible_to_owner,
|
||||
mem_peak: uj.mem_peak,
|
||||
tag: uj.tag,
|
||||
priority: uj.priority,
|
||||
labels: uj.labels,
|
||||
})),
|
||||
"QueuedJob" => Job::QueuedJob(JobExtended::new(uj.self_wait_time_ms, uj.aggregate_wait_time_ms, QueuedJob {
|
||||
workspace_id: uj.workspace_id,
|
||||
id: uj.id,
|
||||
parent_job: uj.parent_job,
|
||||
created_by: uj.created_by,
|
||||
created_at: uj.created_at,
|
||||
started_at: uj.started_at,
|
||||
script_hash: uj.script_hash,
|
||||
script_path: uj.script_path,
|
||||
args: None,
|
||||
running: uj.running.unwrap(),
|
||||
scheduled_for: uj.scheduled_for.unwrap(),
|
||||
logs: None,
|
||||
flow_status: None,
|
||||
raw_code: None,
|
||||
raw_lock: None,
|
||||
canceled: uj.canceled,
|
||||
canceled_by: uj.canceled_by,
|
||||
canceled_reason: None,
|
||||
last_ping: None,
|
||||
job_kind: uj.job_kind,
|
||||
schedule_path: uj.schedule_path,
|
||||
permissioned_as: uj.permissioned_as,
|
||||
raw_flow: None,
|
||||
is_flow_step: uj.is_flow_step,
|
||||
language: uj.language,
|
||||
same_worker: false,
|
||||
pre_run_error: None,
|
||||
email: uj.email,
|
||||
visible_to_owner: uj.visible_to_owner,
|
||||
suspend: uj.suspend,
|
||||
mem_peak: uj.mem_peak,
|
||||
root_job: None,
|
||||
leaf_jobs: None,
|
||||
tag: uj.tag,
|
||||
concurrent_limit: uj.concurrent_limit,
|
||||
concurrency_time_window_s: uj.concurrency_time_window_s,
|
||||
timeout: None,
|
||||
flow_step_id: None,
|
||||
cache_ttl: None,
|
||||
priority: uj.priority,
|
||||
})),
|
||||
t => panic!("job type {} not valid", t),
|
||||
}
|
||||
}
|
||||
@@ -3079,7 +3091,7 @@ pub async fn run_workflow_as_code(
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let job = get_queued_job(&job_id, &w_id, &db).await?;
|
||||
let job = get_queued_job_ex(&db, &w_id, job_id, true, None).await?;
|
||||
|
||||
if *CLOUD_HOSTED {
|
||||
tracing::info!("workflow_as_code_tracing id {i} ");
|
||||
@@ -3087,6 +3099,7 @@ pub async fn run_workflow_as_code(
|
||||
}
|
||||
|
||||
let job = not_found_if_none(job, "Queued Job", &job_id.to_string())?;
|
||||
let JobExtended { inner: job, .. } = job;
|
||||
let (job_payload, tag, _delete_after_use, timeout) = match job.job_kind {
|
||||
JobKind::Preview => (
|
||||
JobPayload::Code(RawCode {
|
||||
@@ -4275,18 +4288,26 @@ async fn run_flow_dependencies_job(
|
||||
wait_result
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct BatchRawScript {
|
||||
content: String,
|
||||
language: Option<ScriptLang>,
|
||||
lock: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct BatchInfo {
|
||||
kind: String,
|
||||
flow_value: Option<FlowValue>,
|
||||
path: Option<String>,
|
||||
rawscript: Option<BatchRawScript>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn add_batch_jobs(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||
Extension(_rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||
Path((w_id, n)): Path<(String, i32)>,
|
||||
Json(batch_info): Json<BatchInfo>,
|
||||
) -> error::JsonResult<Vec<Uuid>> {
|
||||
@@ -4302,6 +4323,10 @@ async fn add_batch_jobs(
|
||||
concurrent_limit,
|
||||
concurrent_time_window_s,
|
||||
timeout,
|
||||
raw_code,
|
||||
raw_lock,
|
||||
raw_flow,
|
||||
flow_status
|
||||
) = match batch_info.kind.as_str() {
|
||||
"script" => {
|
||||
if let Some(path) = batch_info.path {
|
||||
@@ -4329,6 +4354,10 @@ async fn add_batch_jobs(
|
||||
concurrent_limit,
|
||||
concurrency_time_window_s,
|
||||
timeout,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
@@ -4336,61 +4365,72 @@ async fn add_batch_jobs(
|
||||
))?
|
||||
}
|
||||
}
|
||||
"flow" => {
|
||||
let mut uuids: Vec<Uuid> = Vec::new();
|
||||
let payload = if let Some(ref fv) = batch_info.flow_value {
|
||||
JobPayload::RawFlow { value: fv.clone(), path: None, restarted_from: None }
|
||||
} else {
|
||||
if let Some(path) = batch_info.path.as_ref() {
|
||||
JobPayload::Flow {
|
||||
path: path.to_string(),
|
||||
dedicated_worker: None,
|
||||
apply_preprocessor: false,
|
||||
}
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"Path is required if no value is not provided"
|
||||
))?
|
||||
}
|
||||
};
|
||||
let mut tx = PushIsolationLevel::IsolatedRoot(db.clone(), rsmq);
|
||||
for _ in 0..n {
|
||||
let ehm = HashMap::new();
|
||||
let (uuid, ntx) = push(
|
||||
&db,
|
||||
tx,
|
||||
&w_id,
|
||||
payload.clone(),
|
||||
PushArgs::from(&ehm),
|
||||
authed.display_username(),
|
||||
&authed.email,
|
||||
username_to_permissioned_as(&authed.username),
|
||||
"rawscript" => {
|
||||
if let Some(rawscript) = batch_info.rawscript {
|
||||
(
|
||||
None,
|
||||
None,
|
||||
JobKind::Preview,
|
||||
rawscript.language,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
true,
|
||||
Some(rawscript.content),
|
||||
rawscript.lock,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
)
|
||||
.await?;
|
||||
tx = PushIsolationLevel::Transaction(ntx);
|
||||
uuids.push(uuid);
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"rawscript is required for `rawscript` kind"
|
||||
))?
|
||||
}
|
||||
match tx {
|
||||
PushIsolationLevel::Transaction(tx) => {
|
||||
tx.commit().await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
return Ok(Json(uuids));
|
||||
}
|
||||
"flow" => {
|
||||
let (mut value, job_kind, path) = if let Some(value) = batch_info.flow_value {
|
||||
(value, JobKind::FlowPreview, None)
|
||||
} else if let Some(path) = batch_info.path {
|
||||
let value_json = sqlx::query!(
|
||||
"SELECT flow_version.value AS \"value: sqlx::types::Json<Box<RawValue>>\" FROM flow
|
||||
LEFT JOIN flow_version
|
||||
ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
|
||||
WHERE flow.path = $1 AND flow.workspace_id = $2",
|
||||
&path, &w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.ok_or_else(|| Error::InternalErr(format!("not found flow at path {:?}", path)))?;
|
||||
let value =
|
||||
serde_json::from_str::<FlowValue>(value_json.value.get()).map_err(|err| {
|
||||
Error::InternalErr(format!(
|
||||
"could not convert json to flow for {path}: {err:?}"
|
||||
))
|
||||
})?;
|
||||
(value, JobKind::Flow, Some(path))
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"Path is required if no value is not provided"
|
||||
))?
|
||||
};
|
||||
add_virtual_items_if_necessary(&mut value.modules);
|
||||
let flow_status = FlowStatus::new(&value);
|
||||
(
|
||||
None, // script_hash
|
||||
path, // script_path
|
||||
job_kind, // job_kind
|
||||
None, // language
|
||||
None, // dedicated_worker
|
||||
value.concurrency_key.clone(), // custom_concurrency_key
|
||||
value.concurrent_limit.clone(), // concurrent_limit
|
||||
value.concurrency_time_window_s, // concurrency_time_window_s
|
||||
None, // timeout
|
||||
None, // raw_code
|
||||
None, // raw_lock
|
||||
Some(value), // raw_flow
|
||||
Some(flow_status), // flow_status
|
||||
)
|
||||
}
|
||||
"noop" => (
|
||||
None,
|
||||
@@ -4402,6 +4442,10 @@ async fn add_batch_jobs(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
_ => {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
@@ -4428,8 +4472,8 @@ async fn add_batch_jobs(
|
||||
select gen_random_uuid() as uuid from generate_series(1, $11)
|
||||
)
|
||||
INSERT INTO queue
|
||||
(id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id, concurrent_limit, concurrency_time_window_s, timeout)
|
||||
(SELECT uuid, $1, $2, $3, $4, ('{ "uuid": "' || uuid || '" }')::jsonb, $5, $6, $7, $8, $9, $10, $12, $13, $14 FROM uuid_table)
|
||||
(id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id, concurrent_limit, concurrency_time_window_s, timeout, raw_code, raw_lock, raw_flow, flow_status)
|
||||
(SELECT uuid, $1, $2, $3, $4, ('{ "uuid": "' || uuid || '" }')::jsonb, $5, $6, $7, $8, $9, $10, $12, $13, $14, $15, $16, $17, $18 FROM uuid_table)
|
||||
RETURNING id"#,
|
||||
hash.map(|h| h.0),
|
||||
path,
|
||||
@@ -4444,7 +4488,11 @@ async fn add_batch_jobs(
|
||||
n,
|
||||
concurrent_limit,
|
||||
concurrent_time_window_s,
|
||||
timeout
|
||||
timeout,
|
||||
raw_code,
|
||||
raw_lock,
|
||||
raw_flow.map(sqlx::types::Json) as Option<sqlx::types::Json<FlowValue>>,
|
||||
flow_status.map(sqlx::types::Json) as Option<sqlx::types::Json<FlowStatus>>
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
@@ -5023,24 +5071,10 @@ async fn get_completed_job<'a>(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, id)): Path<(String, Uuid)>,
|
||||
) -> error::Result<Response> {
|
||||
let job_o = sqlx::query_as::<_, CompletedJob>("SELECT id, workspace_id, parent_job, created_by, created_at, duration_ms, success, script_hash, script_path,
|
||||
CASE WHEN args is null or pg_column_size(args) < 90000 THEN args ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as args, CASE WHEN result is null or pg_column_size(result) < 90000 THEN result ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as result, logs, deleted, raw_code, canceled, canceled_by, canceled_reason, job_kind,
|
||||
schedule_path, permissioned_as, flow_status, raw_flow, is_flow_step, language, started_at, is_skipped,
|
||||
raw_lock, email, visible_to_owner, mem_peak, tag, priority, result->'wm_labels' as labels FROM completed_job WHERE id = $1 AND workspace_id = $2")
|
||||
.bind(id)
|
||||
.bind(&w_id)
|
||||
.fetch_optional(&db)
|
||||
let job_o = get_completed_job_ex(&db, &w_id, id, false, Some(&opt_authed))
|
||||
.await?;
|
||||
|
||||
let cj = not_found_if_none(job_o, "Completed Job", id.to_string())?;
|
||||
|
||||
if opt_authed.is_none() && cj.created_by != "anonymous" {
|
||||
return Err(Error::BadRequest(
|
||||
"As a non logged in user, you can only see jobs ran by anonymous users".to_string(),
|
||||
));
|
||||
}
|
||||
let cj = format_completed_job_result(cj);
|
||||
|
||||
let response = Json(cj).into_response();
|
||||
// let extra_log = query_scalar!(
|
||||
// "SELECT substr(logs, $1) as logs FROM large_logs WHERE workspace_id = $2 AND job_id = $3",
|
||||
|
||||
@@ -25,7 +25,6 @@ use argon2::Argon2;
|
||||
use axum::extract::DefaultBodyLimit;
|
||||
use axum::{middleware::from_extractor, routing::get, Extension, Router};
|
||||
use db::DB;
|
||||
use git_version::git_version;
|
||||
use http::HeaderValue;
|
||||
use reqwest::Client;
|
||||
use std::collections::HashMap;
|
||||
@@ -40,7 +39,7 @@ use tower_http::{
|
||||
};
|
||||
use windmill_common::db::UserDB;
|
||||
use windmill_common::worker::{ALL_TAGS, CLOUD_HOSTED};
|
||||
use windmill_common::{BASE_URL, INSTANCE_NAME};
|
||||
use windmill_common::{BASE_URL, INSTANCE_NAME, utils::GIT_VERSION};
|
||||
|
||||
use crate::scim_ee::has_scim_token;
|
||||
use windmill_common::error::AppError;
|
||||
@@ -93,9 +92,6 @@ mod workers;
|
||||
mod workspaces;
|
||||
mod workspaces_ee;
|
||||
|
||||
pub const GIT_VERSION: &str =
|
||||
git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
|
||||
|
||||
pub const DEFAULT_BODY_LIMIT: usize = 2097152 * 100; // 200MB
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
|
||||
@@ -58,7 +58,10 @@ pub fn global_service() -> Router {
|
||||
)
|
||||
.route("/renew_license_key", post(renew_license_key))
|
||||
.route("/customer_portal", post(create_customer_portal_session))
|
||||
.route("/test_critical_channels", post(test_critical_channels));
|
||||
.route("/test_critical_channels", post(test_critical_channels))
|
||||
.route("/critical_alerts", get(get_critical_alerts))
|
||||
.route("/critical_alerts/:id/acknowledge", post(acknowledge_critical_alert))
|
||||
.route("/critical_alerts/acknowledge_all", post(acknowledge_all_critical_alerts));
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
{
|
||||
@@ -430,3 +433,117 @@ pub async fn test_critical_channels(
|
||||
pub async fn test_critical_channels() -> Result<String> {
|
||||
Ok("Critical channels require EE".to_string())
|
||||
}
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CriticalAlert {
|
||||
id: i32,
|
||||
alert_type: String,
|
||||
message: String,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
acknowledged: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[derive(Deserialize)]
|
||||
pub struct AlertQueryParams {
|
||||
pub page: Option<i32>,
|
||||
pub page_size: Option<i32>,
|
||||
pub acknowledged: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn get_critical_alerts(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
Query(params): Query<AlertQueryParams>,
|
||||
) -> JsonResult<Vec<CriticalAlert>> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
|
||||
// Default pagination values if not provided
|
||||
let page = params.page.unwrap_or(1).max(1);
|
||||
let page_size = params.page_size.unwrap_or(10).min(100) as i64;
|
||||
let offset = ((page - 1) * page_size as i32) as i64;
|
||||
|
||||
let alerts = if let Some(acknowledged) = params.acknowledged {
|
||||
sqlx::query_as!(
|
||||
CriticalAlert,
|
||||
"SELECT id, alert_type, message, created_at, acknowledged
|
||||
FROM alerts
|
||||
WHERE acknowledged = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3",
|
||||
acknowledged,
|
||||
page_size,
|
||||
offset
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as!(
|
||||
CriticalAlert,
|
||||
"SELECT id, alert_type, message, created_at, acknowledged
|
||||
FROM alerts
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2",
|
||||
page_size,
|
||||
offset
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?
|
||||
};
|
||||
|
||||
Ok(Json(alerts))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
pub async fn get_critical_alerts() -> error::Error {
|
||||
error::Error::NotFound("Critical Alerts require EE".to_string())
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn acknowledge_critical_alert(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
Path(id): Path<i32>,
|
||||
) -> error::Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE alerts SET acknowledged = true WHERE id = $1",
|
||||
id
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
tracing::info!("Acknowledged critical alert with id: {}", id);
|
||||
Ok("Critical alert acknowledged".to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
pub async fn acknowledge_critical_alert() -> error::Error {
|
||||
error::Error::NotFound("Critical Alerts require EE".to_string())
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn acknowledge_all_critical_alerts(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
) -> error::Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE alerts SET acknowledged = true WHERE acknowledged = false"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
tracing::info!("Acknowledged all unacknowledged critical alerts");
|
||||
Ok("All unacknowledged critical alerts acknowledged".to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
pub async fn acknowledge_all_critical_alerts() -> error::Error {
|
||||
error::Error::NotFound("Critical Alerts require EE".to_string())
|
||||
}
|
||||
|
||||
@@ -90,6 +90,9 @@ pub fn global_service() -> Router {
|
||||
.route("/accept_invite", post(accept_invite))
|
||||
.route("/list_as_super_admin", get(list_users_as_super_admin))
|
||||
.route("/setpassword", post(set_password))
|
||||
.route("/set_password_of/:user", post(set_password_of_user))
|
||||
.route("/set_login_type/:user", post(set_login_type))
|
||||
|
||||
.route("/create", post(create_user))
|
||||
.route("/update/:user", post(update_user))
|
||||
.route("/delete/:user", delete(delete_user))
|
||||
@@ -746,10 +749,20 @@ pub async fn fetch_api_authed(
|
||||
username_override: String,
|
||||
) -> error::Result<ApiAuthed> {
|
||||
let permissioned_as = username_to_permissioned_as(username.as_str());
|
||||
fetch_api_authed_from_permissioned_as(permissioned_as, email, w_id, db, username_override).await
|
||||
}
|
||||
|
||||
pub async fn fetch_api_authed_from_permissioned_as(
|
||||
permissioned_as: String,
|
||||
email: String,
|
||||
w_id: &str,
|
||||
db: &DB,
|
||||
username_override: String,
|
||||
) -> error::Result<ApiAuthed> {
|
||||
let authed =
|
||||
fetch_authed_from_permissioned_as(permissioned_as, email.clone(), w_id, db).await?;
|
||||
Ok(ApiAuthed {
|
||||
username: username,
|
||||
username: authed.username,
|
||||
email: email,
|
||||
is_admin: authed.is_admin,
|
||||
is_operator: authed.is_operator,
|
||||
@@ -856,6 +869,12 @@ pub struct EditPassword {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditLoginType {
|
||||
pub login_type: String,
|
||||
}
|
||||
|
||||
|
||||
#[derive(FromRow, Serialize)]
|
||||
pub struct TruncatedToken {
|
||||
pub label: Option<String>,
|
||||
@@ -2018,7 +2037,52 @@ async fn set_password(
|
||||
authed: ApiAuthed,
|
||||
Json(ep): Json<EditPassword>,
|
||||
) -> Result<String> {
|
||||
crate::users_ee::set_password(db, argon2, authed, ep).await
|
||||
let email = authed.email.clone();
|
||||
crate::users_ee::set_password(db, argon2, authed, &email, ep).await
|
||||
}
|
||||
|
||||
async fn set_password_of_user(
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(argon2): Extension<Arc<Argon2<'_>>>,
|
||||
Path(email): Path<String>,
|
||||
authed: ApiAuthed,
|
||||
Json(ep): Json<EditPassword>,
|
||||
) -> Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
crate::users_ee::set_password(db, argon2, authed, &email, ep).await
|
||||
}
|
||||
|
||||
async fn set_login_type(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(email): Path<String>,
|
||||
authed: ApiAuthed,
|
||||
Json(et): Json<EditLoginType>,
|
||||
) -> Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE password SET login_type = $1 WHERE email = $2",
|
||||
et.login_type,
|
||||
email
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"users.set_login_type",
|
||||
ActionKind::Update,
|
||||
"global",
|
||||
Some(&email),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
Ok(format!("login type of {} updated to {}", email, et.login_type))
|
||||
|
||||
}
|
||||
|
||||
async fn login(
|
||||
|
||||
@@ -27,6 +27,7 @@ pub async fn set_password(
|
||||
_db: DB,
|
||||
_argon2: Arc<Argon2<'_>>,
|
||||
_authed: ApiAuthed,
|
||||
_user_email: &str,
|
||||
_ep: EditPassword,
|
||||
) -> Result<String> {
|
||||
Err(Error::InternalErr(
|
||||
|
||||
@@ -36,6 +36,7 @@ pub fn global_service() -> Router {
|
||||
)
|
||||
.route("/get_default_tags", get(get_default_tags))
|
||||
.route("/queue_metrics", get(get_queue_metrics))
|
||||
.route("/queue_counts", get(get_queue_counts))
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize)]
|
||||
@@ -176,3 +177,12 @@ async fn get_queue_metrics(
|
||||
|
||||
Ok(Json(queue_metrics))
|
||||
}
|
||||
|
||||
async fn get_queue_counts(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
) -> JsonResult<std::collections::HashMap<String, u32>> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
let queue_counts = windmill_common::queue::get_queue_counts(&db).await;
|
||||
Ok(Json(queue_counts))
|
||||
}
|
||||
@@ -58,6 +58,7 @@ async-stream.workspace = true
|
||||
const_format.workspace = true
|
||||
crc.workspace = true
|
||||
windmill-macros.workspace = true
|
||||
semver.workspace = true
|
||||
|
||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
tikv-jemalloc-ctl = { optional = true, workspace = true }
|
||||
|
||||
@@ -16,6 +16,7 @@ use rand::Rng;
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
more_serde::{default_empty_string, default_id, default_null, default_true, is_default},
|
||||
scripts::{Schema, ScriptHash, ScriptLang},
|
||||
};
|
||||
@@ -651,3 +652,29 @@ pub fn add_virtual_items_if_necessary(modules: &mut Vec<FlowModule>) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn has_failure_module<'c>(flow: sqlx::types::Uuid, db: &sqlx::Pool<sqlx::Postgres>, completed: bool) -> Result<bool, Error> {
|
||||
if completed {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT raw_flow->'failure_module' != 'null'::jsonb
|
||||
FROM completed_job
|
||||
WHERE id = $1",
|
||||
flow
|
||||
)
|
||||
} else {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT raw_flow->'failure_module' != 'null'::jsonb
|
||||
FROM queue
|
||||
WHERE id = $1",
|
||||
flow
|
||||
)
|
||||
}
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error during retrieval of has_failure_module: {e:#}"
|
||||
))
|
||||
})
|
||||
.map(|v| v.unwrap_or(false))
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ pub const AUTOMATE_USERNAME_CREATION_SETTING: &str = "automate_username_creation
|
||||
pub const HUB_BASE_URL_SETTING: &str = "hub_base_url";
|
||||
pub const HUB_ACCESSIBLE_URL_SETTING: &str = "hub_accessible_url";
|
||||
pub const CRITICAL_ERROR_CHANNELS_SETTING: &str = "critical_error_channels";
|
||||
pub const CRITICAL_ALERT_MUTE_UI_SETTING: &str = "critical_alert_mute_ui";
|
||||
pub const DEV_INSTANCE_SETTING: &str = "dev_instance";
|
||||
pub const JWT_SECRET_SETTING: &str = "jwt_secret";
|
||||
pub const EMAIL_DOMAIN_SETTING: &str = "email_domain";
|
||||
|
||||
@@ -108,13 +108,6 @@ pub struct QueuedJob {
|
||||
pub cache_ttl: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub priority: Option<i16>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[sqlx(skip)]
|
||||
pub self_wait_time_ms: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[sqlx(skip)]
|
||||
pub aggregate_wait_time_ms: Option<i64>,
|
||||
}
|
||||
|
||||
impl QueuedJob {
|
||||
@@ -198,8 +191,6 @@ impl Default for QueuedJob {
|
||||
flow_step_id: None,
|
||||
cache_ttl: None,
|
||||
priority: None,
|
||||
self_wait_time_ms: None,
|
||||
aggregate_wait_time_ms: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,13 +244,6 @@ pub struct CompletedJob {
|
||||
pub priority: Option<i16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub labels: Option<serde_json::Value>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[sqlx(skip)]
|
||||
pub self_wait_time_ms: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[sqlx(skip)]
|
||||
pub aggregate_wait_time_ms: Option<i64>,
|
||||
}
|
||||
|
||||
impl CompletedJob {
|
||||
|
||||
@@ -86,6 +86,8 @@ lazy_static::lazy_static! {
|
||||
pub static ref METRICS_ENABLED: AtomicBool = AtomicBool::new(std::env::var("METRICS_PORT").is_ok() || std::env::var("METRICS_ADDR").is_ok());
|
||||
pub static ref METRICS_DEBUG_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub static ref CRITICAL_ALERT_MUTE_UI_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub static ref BASE_URL: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
|
||||
pub static ref IS_READY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::{Pool, Postgres};
|
||||
use semver::Version;
|
||||
|
||||
pub const MAX_PER_PAGE: usize = 10000;
|
||||
pub const DEFAULT_PER_PAGE: usize = 1000;
|
||||
@@ -28,12 +29,19 @@ pub const DEFAULT_PER_PAGE: usize = 1000;
|
||||
pub const GIT_VERSION: &str =
|
||||
git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
use crate::CRITICAL_ALERT_MUTE_UI_ENABLED;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
|
||||
.user_agent("windmill/beta")
|
||||
.timeout(std::time::Duration::from_secs(20))
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.build().unwrap();
|
||||
pub static ref GIT_SEM_VERSION: Version = Version::parse(
|
||||
// skip first `v` character.
|
||||
GIT_VERSION.split_at(1).1
|
||||
).unwrap_or(Version::new(0, 1, 0));
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
@@ -244,9 +252,12 @@ pub fn generate_lock_id(database_name: &str) -> i64 {
|
||||
pub async fn report_critical_error(error_message: String, _db: DB) -> () {
|
||||
tracing::error!("CRITICAL ERROR: {error_message}");
|
||||
|
||||
let mute = CRITICAL_ALERT_MUTE_UI_ENABLED.load(Ordering::Relaxed);
|
||||
|
||||
if let Err(err) = sqlx::query!(
|
||||
"INSERT INTO alerts (alert_type, message) VALUES ('critical_error', $1)",
|
||||
error_message
|
||||
"INSERT INTO alerts (alert_type, message, acknowledged) VALUES ('critical_error', $1, $2)",
|
||||
error_message,
|
||||
mute
|
||||
)
|
||||
.execute(&_db)
|
||||
.await
|
||||
@@ -261,9 +272,12 @@ pub async fn report_critical_error(error_message: String, _db: DB) -> () {
|
||||
pub async fn report_recovered_critical_error(message: String, _db: DB) -> () {
|
||||
tracing::info!("RECOVERED CRITICAL ERROR: {message}");
|
||||
|
||||
let mute = CRITICAL_ALERT_MUTE_UI_ENABLED.load(Ordering::Relaxed);
|
||||
|
||||
if let Err(err) = sqlx::query!(
|
||||
"INSERT INTO alerts (alert_type, message) VALUES ('recovered_critical_error', $1)",
|
||||
message
|
||||
"INSERT INTO alerts (alert_type, message, acknowledged) VALUES ('recovered_critical_error', $1, $2)",
|
||||
message,
|
||||
mute
|
||||
)
|
||||
.execute(&_db)
|
||||
.await
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use const_format::concatcp;
|
||||
use itertools::Itertools;
|
||||
use regex::Regex;
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::{
|
||||
@@ -88,6 +89,7 @@ lazy_static::lazy_static! {
|
||||
.and_then(|x| x.parse::<bool>().ok())
|
||||
.unwrap_or(false);
|
||||
|
||||
pub static ref MIN_VERSION: Arc<RwLock<Version>> = Arc::new(RwLock::new(Version::new(0, 0, 0)));
|
||||
}
|
||||
|
||||
pub async fn make_suspended_pull_query(wc: &WorkerConfig) {
|
||||
@@ -308,6 +310,8 @@ fn parse_file<T: FromStr>(path: &str) -> Option<T> {
|
||||
pub struct PythonAnnotations {
|
||||
pub no_cache: bool,
|
||||
pub no_uv: bool,
|
||||
pub no_uv_install: bool,
|
||||
pub no_uv_compile: bool,
|
||||
}
|
||||
|
||||
#[annotations("//")]
|
||||
@@ -548,6 +552,31 @@ pub fn get_windmill_memory_usage() -> Option<i64> {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_min_version<'c, E: sqlx::Executor<'c, Database = sqlx::Postgres>>(executor: E) -> bool {
|
||||
use crate::utils::{GIT_VERSION, GIT_SEM_VERSION};
|
||||
|
||||
// fetch all pings with a different version than self from the last 5 minutes.
|
||||
let pings = sqlx::query_scalar!(
|
||||
"SELECT wm_version FROM worker_ping WHERE wm_version != $1 AND ping_at > now() - interval '5 minutes'",
|
||||
GIT_VERSION
|
||||
).fetch_all(executor).await.unwrap_or_default();
|
||||
|
||||
let cur_version = GIT_SEM_VERSION.clone();
|
||||
let min_version = pings
|
||||
.iter()
|
||||
.filter(|x| !x.is_empty())
|
||||
.filter_map(|x| semver::Version::parse(x.split_at(1).1).ok())
|
||||
.min()
|
||||
.unwrap_or_else(|| cur_version.clone());
|
||||
|
||||
if min_version != cur_version {
|
||||
tracing::info!("Minimal worker version: {min_version}");
|
||||
}
|
||||
|
||||
*MIN_VERSION.write().await = min_version.clone();
|
||||
min_version >= cur_version
|
||||
}
|
||||
|
||||
pub async fn update_ping(worker_instance: &str, worker_name: &str, ip: &str, db: &DB) {
|
||||
let (tags, dw) = {
|
||||
let wc = WORKER_CONFIG.read().await.clone();
|
||||
|
||||
@@ -71,6 +71,9 @@ use windmill_common::BASE_URL;
|
||||
#[cfg(feature = "cloud")]
|
||||
use windmill_common::users::SUPERADMIN_SYNC_EMAIL;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::flows::has_failure_module;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::worker::CLOUD_HOSTED;
|
||||
|
||||
@@ -482,13 +485,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[derive(Deserialize)]
|
||||
struct RawFlowFailureModule {
|
||||
#[cfg(feature = "enterprise")]
|
||||
failure_module: Option<Box<RawValue>>,
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub async fn add_completed_job_error<R: rsmq_async::RsmqConnection + Clone + Send>(
|
||||
db: &Pool<Postgres>,
|
||||
@@ -952,15 +948,7 @@ pub async fn add_completed_job<
|
||||
} else if !skip_downstream_error_handlers
|
||||
&& (matches!(queued_job.job_kind, JobKind::Script)
|
||||
|| matches!(queued_job.job_kind, JobKind::Flow)
|
||||
&& queued_job
|
||||
.raw_flow
|
||||
.as_ref()
|
||||
.and_then(|v| {
|
||||
serde_json::from_str::<RawFlowFailureModule>((**v).get())
|
||||
.ok()
|
||||
.and_then(|v| v.failure_module)
|
||||
})
|
||||
.is_none())
|
||||
&& !has_failure_module(job_id, db, true).await.unwrap_or(false))
|
||||
&& queued_job.parent_job.is_none()
|
||||
{
|
||||
let result = serde_json::from_str(
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
name: "python download pip"
|
||||
|
||||
mode: ONCE
|
||||
hostname: "python"
|
||||
log_level: ERROR
|
||||
time_limit: 900
|
||||
|
||||
rlimit_as: 2048
|
||||
rlimit_cpu: 1000
|
||||
rlimit_fsize: 1024
|
||||
rlimit_nofile: 64
|
||||
|
||||
envar: "HOME=/user"
|
||||
envar: "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH"
|
||||
|
||||
cwd: "/tmp"
|
||||
|
||||
clone_newnet: false
|
||||
clone_newuser: {CLONE_NEWUSER}
|
||||
|
||||
keep_caps: true
|
||||
keep_env: true
|
||||
|
||||
mount {
|
||||
src: "/bin"
|
||||
dst: "/bin"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/lib"
|
||||
dst: "/lib"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/lib64"
|
||||
dst: "/lib64"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/usr"
|
||||
dst: "/usr"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/etc"
|
||||
dst: "/etc"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/dev/null"
|
||||
dst: "/dev/null"
|
||||
is_bind: true
|
||||
rw: true
|
||||
}
|
||||
|
||||
mount {
|
||||
dst: "/tmp"
|
||||
fstype: "tmpfs"
|
||||
rw: true
|
||||
options: "size=500000000"
|
||||
}
|
||||
|
||||
|
||||
mount {
|
||||
src: "{WORKER_DIR}/download_deps.py.pip.sh"
|
||||
dst: "/download_deps.sh"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "{CACHE_DIR}"
|
||||
dst: "{CACHE_DIR}"
|
||||
is_bind: true
|
||||
rw: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "/dev/urandom"
|
||||
dst: "/dev/urandom"
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
exec_bin {
|
||||
path: "/bin/sh"
|
||||
arg: "/download_deps.sh"
|
||||
}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
#/bin/sh
|
||||
|
||||
INDEX_URL_ARG=$([ -z "$INDEX_URL" ] && echo ""|| echo "--index-url $INDEX_URL" )
|
||||
EXTRA_INDEX_URL_ARG=$([ -z "$EXTRA_INDEX_URL" ] && echo ""|| echo "--extra-index-url $EXTRA_INDEX_URL" )
|
||||
TRUSTED_HOST_ARG=$([ -z "$TRUSTED_HOST" ] && echo "" || echo "--trusted-host $TRUSTED_HOST")
|
||||
|
||||
if [ ! -z "$INDEX_URL" ]
|
||||
then
|
||||
echo "\$INDEX_URL is set to $INDEX_URL"
|
||||
fi
|
||||
|
||||
if [ ! -z "$EXTRA_INDEX_URL" ]
|
||||
then
|
||||
echo "\$EXTRA_INDEX_URL is set to $EXTRA_INDEX_URL"
|
||||
fi
|
||||
|
||||
if [ ! -z "$TRUSTED_HOST" ]
|
||||
then
|
||||
echo "\$TRUSTED_HOST is set to $TRUSTED_HOST"
|
||||
fi
|
||||
|
||||
CMD="/usr/local/bin/python3 -m pip install -v \"$REQ\" -I -t \"$TARGET\" --no-cache --no-color --no-deps --isolated --no-warn-conflicts --disable-pip-version-check $INDEX_URL_ARG $EXTRA_INDEX_URL_ARG $TRUSTED_HOST_ARG"
|
||||
echo $CMD
|
||||
eval $CMD
|
||||
@@ -19,6 +19,18 @@ then
|
||||
echo "\$TRUSTED_HOST is set to $TRUSTED_HOST"
|
||||
fi
|
||||
|
||||
CMD="/usr/local/bin/python3 -m pip install -v \"$REQ\" -I -t \"$TARGET\" --no-cache --no-color --no-deps --isolated --no-warn-conflicts --disable-pip-version-check $INDEX_URL_ARG $EXTRA_INDEX_URL_ARG $TRUSTED_HOST_ARG"
|
||||
CMD="/usr/local/bin/uv pip install
|
||||
\"$REQ\"
|
||||
--target \"$TARGET\"
|
||||
--no-cache
|
||||
--no-config
|
||||
--no-color
|
||||
--no-deps
|
||||
--link-mode=copy
|
||||
$INDEX_URL_ARG $EXTRA_INDEX_URL_ARG $TRUSTED_HOST_ARG
|
||||
--index-strategy unsafe-best-match
|
||||
--system
|
||||
"
|
||||
|
||||
echo $CMD
|
||||
eval $CMD
|
||||
|
||||
@@ -116,6 +116,8 @@ async fn handle_ansible_python_deps(
|
||||
job_dir,
|
||||
worker_dir,
|
||||
&mut Some(occupancy_metrics),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
additional_python_paths.append(&mut venv_path);
|
||||
|
||||
@@ -89,8 +89,18 @@ cat bp | tail -1 >> ./result2.out &
|
||||
# Run main.sh in the same process group
|
||||
{bash} ./main.sh "$@" 2>&1 | tee bp &
|
||||
|
||||
# Wait for all background processes to finish
|
||||
wait
|
||||
pid=$!
|
||||
|
||||
# Wait for main.sh to finish and capture its exit status
|
||||
wait $pid
|
||||
exit_status=$?
|
||||
|
||||
# Clean up the named pipe and background processes
|
||||
rm -f bp
|
||||
|
||||
|
||||
# Exit with the captured status
|
||||
exit $exit_status
|
||||
"#,
|
||||
bash = BIN_BASH.as_str(),
|
||||
);
|
||||
|
||||
@@ -20,13 +20,21 @@ use std::sync::Arc;
|
||||
pub async fn build_tar_and_push(
|
||||
s3_client: Arc<dyn ObjectStore>,
|
||||
folder: String,
|
||||
no_uv: bool,
|
||||
) -> error::Result<()> {
|
||||
use object_store::path::Path;
|
||||
|
||||
use crate::PY311_CACHE_DIR;
|
||||
|
||||
tracing::info!("Started building and pushing piptar {folder}");
|
||||
let start = Instant::now();
|
||||
let folder_name = folder.split("/").last().unwrap();
|
||||
let tar_path = format!("{PIP_CACHE_DIR}/{folder_name}_tar.tar",);
|
||||
let prefix = if no_uv {
|
||||
PIP_CACHE_DIR
|
||||
} else {
|
||||
PY311_CACHE_DIR
|
||||
};
|
||||
let tar_path = format!("{prefix}/{folder_name}_tar.tar",);
|
||||
|
||||
let tar_file = std::fs::File::create(&tar_path)?;
|
||||
let mut tar = tar::Builder::new(tar_file);
|
||||
@@ -46,7 +54,10 @@ pub async fn build_tar_and_push(
|
||||
// })?;
|
||||
if let Err(e) = s3_client
|
||||
.put(
|
||||
&Path::from(format!("/tar/pip/{folder_name}.tar")),
|
||||
&Path::from(format!(
|
||||
"/tar/{}/{folder_name}.tar",
|
||||
if no_uv { "pip" } else { "python_311" }
|
||||
)),
|
||||
std::fs::read(&tar_path)?.into(),
|
||||
)
|
||||
.await
|
||||
@@ -71,7 +82,11 @@ pub async fn build_tar_and_push(
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
pub async fn pull_from_tar(client: Arc<dyn ObjectStore>, folder: String) -> error::Result<()> {
|
||||
pub async fn pull_from_tar(
|
||||
client: Arc<dyn ObjectStore>,
|
||||
folder: String,
|
||||
no_uv: bool,
|
||||
) -> error::Result<()> {
|
||||
use windmill_common::s3_helpers::attempt_fetch_bytes;
|
||||
|
||||
let folder_name = folder.split("/").last().unwrap();
|
||||
@@ -79,7 +94,10 @@ pub async fn pull_from_tar(client: Arc<dyn ObjectStore>, folder: String) -> erro
|
||||
tracing::info!("Attempting to pull piptar {folder_name} from bucket");
|
||||
|
||||
let start = Instant::now();
|
||||
let tar_path = format!("tar/pip/{folder_name}.tar");
|
||||
let tar_path = format!(
|
||||
"tar/{}/{folder_name}.tar",
|
||||
if no_uv { "pip" } else { "python_311" }
|
||||
);
|
||||
let bytes = attempt_fetch_bytes(client, &tar_path).await?;
|
||||
|
||||
// tracing::info!("B: {target} {folder}");
|
||||
|
||||
@@ -48,6 +48,7 @@ use futures::{
|
||||
|
||||
use crate::common::{resolve_job_timeout, OccupancyMetrics};
|
||||
use crate::job_logger::{append_job_logs, append_with_limit, LARGE_LOG_THRESHOLD_SIZE};
|
||||
use crate::job_logger_ee::process_streaming_log_lines;
|
||||
use crate::{MAX_RESULT_SIZE, MAX_WAIT_FOR_SIGINT, MAX_WAIT_FOR_SIGTERM};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@@ -434,6 +435,8 @@ pub async fn handle_child(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async fn get_mem_peak(pid: Option<u32>, nsjail: bool) -> i32 {
|
||||
if pid.is_none() {
|
||||
return -1;
|
||||
@@ -689,19 +692,24 @@ fn child_joined_output_stream(
|
||||
|
||||
let stdout = BufReader::new(stdout).lines();
|
||||
let stderr = BufReader::new(stderr).lines();
|
||||
stream::select(lines_to_stream(stderr), lines_to_stream(stdout))
|
||||
stream::select(lines_to_stream(stderr, true), lines_to_stream(stdout, false))
|
||||
}
|
||||
|
||||
pub fn lines_to_stream<R: tokio::io::AsyncBufRead + Unpin>(
|
||||
mut lines: tokio::io::Lines<R>,
|
||||
stderr: bool,
|
||||
) -> impl futures::Stream<Item = io::Result<String>> {
|
||||
stream::poll_fn(move |cx| {
|
||||
std::pin::Pin::new(&mut lines)
|
||||
.poll_next_line(cx)
|
||||
.map(|result| result.transpose())
|
||||
.map(|result| {
|
||||
process_streaming_log_lines(result, stderr)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub fn process_status(status: ExitStatus) -> error::Result<()> {
|
||||
if status.success() {
|
||||
Ok(())
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
use itertools::Itertools;
|
||||
use swc_ecma_parser::lexer::util::CharExt;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
use object_store::path::Path;
|
||||
use regex::Regex;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
use windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS;
|
||||
|
||||
use windmill_common::error::{self};
|
||||
use windmill_common::worker::{CLOUD_HOSTED, TMP_DIR};
|
||||
use windmill_common::worker::CLOUD_HOSTED;
|
||||
|
||||
use windmill_queue::append_logs;
|
||||
|
||||
@@ -19,6 +10,12 @@ use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use windmill_common::DB;
|
||||
|
||||
use crate::job_logger_ee::default_disk_log_storage;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
use crate::job_logger_ee::s3_storage;
|
||||
|
||||
|
||||
pub enum CompactLogs {
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
NotEE,
|
||||
@@ -28,150 +25,7 @@ pub enum CompactLogs {
|
||||
S3,
|
||||
}
|
||||
|
||||
async fn compact_logs(
|
||||
job_id: Uuid,
|
||||
w_id: &str,
|
||||
db: &DB,
|
||||
nlogs: String,
|
||||
total_size: Arc<AtomicU32>,
|
||||
compact_kind: CompactLogs,
|
||||
_worker_name: &str,
|
||||
) -> error::Result<(String, String)> {
|
||||
let mut prev_logs = sqlx::query_scalar!(
|
||||
"SELECT logs FROM job_logs WHERE job_id = $1 AND workspace_id = $2",
|
||||
job_id,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
let size = prev_logs.char_indices().count() as i32;
|
||||
let nlogs_len = nlogs.char_indices().count();
|
||||
let to_keep_in_db = usize::max(
|
||||
usize::min(nlogs_len, 3000),
|
||||
nlogs_len % LARGE_LOG_THRESHOLD_SIZE,
|
||||
);
|
||||
let extra_split = to_keep_in_db < nlogs_len;
|
||||
let stored_in_storage_len = if extra_split {
|
||||
nlogs_len - to_keep_in_db
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let extra_to_newline = nlogs
|
||||
.chars()
|
||||
.skip(stored_in_storage_len)
|
||||
.find_position(|x| x.is_line_break())
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(to_keep_in_db);
|
||||
let stored_in_storage_to_newline = stored_in_storage_len + extra_to_newline;
|
||||
|
||||
let (append_to_storage, stored_in_db) = if extra_split {
|
||||
if stored_in_storage_to_newline == nlogs.len() {
|
||||
(nlogs.as_ref(), "".to_string())
|
||||
} else {
|
||||
let split_idx = nlogs
|
||||
.char_indices()
|
||||
.nth(stored_in_storage_to_newline)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(0);
|
||||
let (append_to_storage, stored_in_db) = nlogs.split_at(split_idx);
|
||||
// tracing::error!("{append_to_storage} ||||| {stored_in_db}");
|
||||
// tracing::error!(
|
||||
// "{:?} {:?} {} {}",
|
||||
// excess_prev_logs.lines().last(),
|
||||
// current_logs.lines().next(),
|
||||
// split_idx,
|
||||
// excess_size_modulo
|
||||
// );
|
||||
(append_to_storage, stored_in_db.to_string())
|
||||
}
|
||||
} else {
|
||||
// tracing::error!("{:?}", nlogs.lines().last());
|
||||
("", nlogs.to_string())
|
||||
};
|
||||
|
||||
let new_size_with_excess = size + stored_in_storage_to_newline as i32;
|
||||
|
||||
let new_size = total_size.fetch_add(
|
||||
new_size_with_excess as u32,
|
||||
std::sync::atomic::Ordering::SeqCst,
|
||||
) + new_size_with_excess as u32;
|
||||
|
||||
let path = format!(
|
||||
"logs/{job_id}/{}_{new_size}.txt",
|
||||
chrono::Utc::now().timestamp_millis()
|
||||
);
|
||||
|
||||
let mut new_current_logs = match compact_kind {
|
||||
CompactLogs::NoS3 => format!("\n[windmill] No object storage set in instance settings. Previous logs have been saved to disk at {path}"),
|
||||
CompactLogs::S3 => format!("\n[windmill] Previous logs have been saved to object storage at {path}"),
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
CompactLogs::NotEE => format!("\n[windmill] Previous logs have been saved to disk at {path}"),
|
||||
};
|
||||
new_current_logs.push_str(&stored_in_db);
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE job_logs SET logs = $1, log_offset = $2,
|
||||
log_file_index = array_append(coalesce(log_file_index, array[]::text[]), $3)
|
||||
WHERE workspace_id = $4 AND job_id = $5",
|
||||
new_current_logs,
|
||||
new_size as i32,
|
||||
path,
|
||||
w_id,
|
||||
job_id
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
prev_logs.push_str(&append_to_storage);
|
||||
|
||||
return Ok((prev_logs, path));
|
||||
}
|
||||
|
||||
async fn default_disk_log_storage(
|
||||
job_id: Uuid,
|
||||
w_id: &str,
|
||||
db: &DB,
|
||||
nlogs: String,
|
||||
total_size: Arc<AtomicU32>,
|
||||
compact_kind: CompactLogs,
|
||||
worker_name: &str,
|
||||
) {
|
||||
match compact_logs(
|
||||
job_id,
|
||||
&w_id,
|
||||
&db,
|
||||
nlogs,
|
||||
total_size,
|
||||
compact_kind,
|
||||
worker_name,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(e) => tracing::error!("Could not compact logs for job {job_id}: {e:?}",),
|
||||
Ok((prev_logs, path)) => {
|
||||
let path = format!("{}/{}", TMP_DIR, path);
|
||||
let splitted = &path.split("/").collect_vec();
|
||||
tokio::fs::create_dir_all(splitted.into_iter().take(splitted.len() - 1).join("/"))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Could not create logs directory: {e:?}",);
|
||||
e
|
||||
})
|
||||
.ok();
|
||||
let created = tokio::fs::File::create(&path).await;
|
||||
if let Err(e) = created {
|
||||
tracing::error!("Could not create logs file {path}: {e:?}",);
|
||||
return;
|
||||
}
|
||||
if let Err(e) = tokio::fs::write(&path, prev_logs).await {
|
||||
tracing::error!("Could not write to logs file {path}: {e:?}");
|
||||
} else {
|
||||
tracing::info!("Logs length of {job_id} has exceeded a threshold. Previous logs have been saved to disk at {path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn append_job_logs(
|
||||
job_id: Uuid,
|
||||
@@ -184,43 +38,7 @@ pub(crate) async fn append_job_logs(
|
||||
) -> () {
|
||||
if must_compact_logs {
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
if let Some(os) = OBJECT_STORE_CACHE_SETTINGS.read().await.clone() {
|
||||
match compact_logs(
|
||||
job_id,
|
||||
&w_id,
|
||||
&db,
|
||||
logs,
|
||||
total_size,
|
||||
CompactLogs::S3,
|
||||
&worker_name,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(e) => tracing::error!("Could not compact logs for job {job_id}: {e:?}",),
|
||||
Ok((prev_logs, path)) => {
|
||||
tracing::info!("Logs length of {job_id} has exceeded a threshold. Previous logs have been saved to object storage at {path}");
|
||||
let path2 = path.clone();
|
||||
if let Err(e) = os
|
||||
.put(&Path::from(path), prev_logs.to_string().into_bytes().into())
|
||||
.await
|
||||
{
|
||||
tracing::error!("Could not save logs to s3: {e:?}");
|
||||
}
|
||||
tracing::info!("Logs of {job_id} saved to object storage at {path2}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
default_disk_log_storage(
|
||||
job_id,
|
||||
&w_id,
|
||||
&db,
|
||||
logs,
|
||||
total_size,
|
||||
CompactLogs::NoS3,
|
||||
&worker_name,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
s3_storage(job_id, &w_id, &db, logs, total_size, &worker_name).await;
|
||||
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
{
|
||||
@@ -252,6 +70,7 @@ pub fn append_with_limit(dst: &mut String, src: &str, limit: &mut usize) {
|
||||
if *NO_LOGS_AT_ALL {
|
||||
return;
|
||||
}
|
||||
|
||||
let src_str;
|
||||
let src = {
|
||||
src_str = RE_00.replace_all(src, "");
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
use std::io;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::Arc;
|
||||
|
||||
use uuid::Uuid;
|
||||
use windmill_common::DB;
|
||||
|
||||
use crate::job_logger::CompactLogs;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
pub(crate) async fn s3_storage(_job_id: Uuid, _w_id: &String, _db: &sqlx::Pool<sqlx::Postgres>, _logs: &String, _total_size: &Arc<AtomicU32>, _worker_name: &String) {
|
||||
tracing::info!("Logs length of {job_id} has exceeded a threshold. Implementation to store excess on s3 in not OSS");
|
||||
}
|
||||
|
||||
pub(crate) async fn default_disk_log_storage(
|
||||
job_id: Uuid,
|
||||
_w_id: &str,
|
||||
_db: &DB,
|
||||
_nlogs: String,
|
||||
_total_size: Arc<AtomicU32>,
|
||||
_compact_kind: CompactLogs,
|
||||
_worker_name: &str,
|
||||
) {
|
||||
tracing::info!("Logs length of {job_id} has exceeded a threshold. Implementation to store excess on disk in not OSS");
|
||||
}
|
||||
|
||||
|
||||
pub(crate) fn process_streaming_log_lines(r: Result<Option<String>, io::Error>, _stderr: bool) -> Option<Result<String, io::Error>> {
|
||||
r.transpose()
|
||||
}
|
||||
@@ -29,7 +29,7 @@ mod rust_executor;
|
||||
mod worker;
|
||||
mod worker_flow;
|
||||
mod worker_lockfiles;
|
||||
|
||||
mod job_logger_ee;
|
||||
pub use worker::*;
|
||||
|
||||
pub use result_processor::handle_job_error;
|
||||
|
||||
@@ -42,6 +42,10 @@ lazy_static::lazy_static! {
|
||||
static ref USE_PIP_COMPILE: bool = std::env::var("USE_PIP_COMPILE")
|
||||
.ok().map(|flag| flag == "true").unwrap_or(false);
|
||||
|
||||
/// Use pip install
|
||||
static ref USE_PIP_INSTALL: bool = std::env::var("USE_PIP_INSTALL")
|
||||
.ok().map(|flag| flag == "true").unwrap_or(false);
|
||||
|
||||
|
||||
static ref RELATIVE_IMPORT_REGEX: Regex = Regex::new(r#"(import|from)\s(((u|f)\.)|\.)"#).unwrap();
|
||||
|
||||
@@ -50,6 +54,8 @@ lazy_static::lazy_static! {
|
||||
}
|
||||
|
||||
const NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT: &str = include_str!("../nsjail/download.py.config.proto");
|
||||
const NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT_FALLBACK: &str =
|
||||
include_str!("../nsjail/download.py.pip.config.proto");
|
||||
const NSJAIL_CONFIG_RUN_PYTHON3_CONTENT: &str = include_str!("../nsjail/run.python3.config.proto");
|
||||
const RELATIVE_PYTHON_LOADER: &str = include_str!("../loader.py");
|
||||
|
||||
@@ -66,8 +72,8 @@ use crate::{
|
||||
},
|
||||
handle_child::handle_child,
|
||||
AuthedClientBackgroundTask, DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV, LOCK_CACHE_DIR,
|
||||
NSJAIL_PATH, PATH_ENV, PIP_CACHE_DIR, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, PROXY_ENVS, TZ_ENV,
|
||||
UV_CACHE_DIR,
|
||||
NSJAIL_PATH, PATH_ENV, PIP_CACHE_DIR, PIP_EXTRA_INDEX_URL, PIP_INDEX_URL, PROXY_ENVS,
|
||||
PY311_CACHE_DIR, TZ_ENV, UV_CACHE_DIR,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -199,8 +205,11 @@ pub async fn uv_pip_compile(
|
||||
.clone()
|
||||
.map(handle_ephemeral_token);
|
||||
if let Some(url) = pip_extra_index_url.as_ref() {
|
||||
args.extend(["--extra-index-url", url, "--no-emit-index-url"]);
|
||||
pip_args.push(format!("--extra-index-url {}", url));
|
||||
url.split(",").for_each(|url| {
|
||||
args.extend(["--extra-index-url", url]);
|
||||
pip_args.push(format!("--extra-index-url {}", url));
|
||||
});
|
||||
args.push("--no-emit-index-url");
|
||||
}
|
||||
let pip_index_url = PIP_INDEX_URL
|
||||
.read()
|
||||
@@ -279,7 +288,9 @@ pub async fn uv_pip_compile(
|
||||
.clone()
|
||||
.map(handle_ephemeral_token);
|
||||
if let Some(url) = pip_extra_index_url.as_ref() {
|
||||
args.extend(["--extra-index-url", url]);
|
||||
url.split(",").for_each(|url| {
|
||||
args.extend(["--extra-index-url", url]);
|
||||
});
|
||||
}
|
||||
let pip_index_url = PIP_INDEX_URL
|
||||
.read()
|
||||
@@ -896,10 +907,10 @@ async fn handle_python_deps(
|
||||
.unwrap_or_else(|| vec![])
|
||||
.clone();
|
||||
|
||||
let annotations = windmill_common::worker::PythonAnnotations::parse(inner_content);
|
||||
let requirements = match requirements_o {
|
||||
Some(r) => r,
|
||||
None => {
|
||||
let annotation = windmill_common::worker::PythonAnnotations::parse(inner_content);
|
||||
let mut already_visited = vec![];
|
||||
|
||||
let requirements = windmill_parser_py_imports::parse_python_imports(
|
||||
@@ -924,8 +935,8 @@ async fn handle_python_deps(
|
||||
worker_name,
|
||||
w_id,
|
||||
occupancy_metrics,
|
||||
annotation.no_uv,
|
||||
annotation.no_cache,
|
||||
annotations.no_uv || annotations.no_uv_compile,
|
||||
annotations.no_cache,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -950,6 +961,8 @@ async fn handle_python_deps(
|
||||
job_dir,
|
||||
worker_dir,
|
||||
occupancy_metrics,
|
||||
annotations.no_uv || annotations.no_uv_install,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
additional_python_paths.append(&mut venv_path);
|
||||
@@ -961,6 +974,7 @@ lazy_static::lazy_static! {
|
||||
static ref PIP_SECRET_VARIABLE: Regex = Regex::new(r"\$\{PIP_SECRET:([^\s\}]+)\}").unwrap();
|
||||
}
|
||||
|
||||
/// pip install, include cached or pull from S3
|
||||
pub async fn handle_python_reqs(
|
||||
requirements: Vec<&str>,
|
||||
job_id: &Uuid,
|
||||
@@ -972,12 +986,22 @@ pub async fn handle_python_reqs(
|
||||
job_dir: &str,
|
||||
worker_dir: &str,
|
||||
occupancy_metrics: &mut Option<&mut OccupancyMetrics>,
|
||||
// TODO: Remove (Deprecated)
|
||||
mut no_uv_install: bool,
|
||||
is_ansible: bool,
|
||||
) -> error::Result<Vec<String>> {
|
||||
let mut req_paths: Vec<String> = vec![];
|
||||
let mut vars = vec![("PATH", PATH_ENV.as_str())];
|
||||
let pip_extra_index_url;
|
||||
let pip_index_url;
|
||||
|
||||
no_uv_install |= *USE_PIP_INSTALL;
|
||||
|
||||
if no_uv_install && !is_ansible {
|
||||
append_logs(&job_id, w_id, "\nFallback to pip (Deprecated!)\n", db).await;
|
||||
tracing::warn!("Fallback to pip");
|
||||
}
|
||||
|
||||
if !*DISABLE_NSJAIL {
|
||||
pip_extra_index_url = PIP_EXTRA_INDEX_URL
|
||||
.read()
|
||||
@@ -1008,10 +1032,21 @@ pub async fn handle_python_reqs(
|
||||
let _ = write_file(
|
||||
job_dir,
|
||||
"download.config.proto",
|
||||
&NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT
|
||||
.replace("{WORKER_DIR}", &worker_dir)
|
||||
.replace("{CACHE_DIR}", PIP_CACHE_DIR)
|
||||
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()),
|
||||
&(if no_uv_install {
|
||||
NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT_FALLBACK
|
||||
} else {
|
||||
NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT
|
||||
})
|
||||
.replace("{WORKER_DIR}", &worker_dir)
|
||||
.replace(
|
||||
"{CACHE_DIR}",
|
||||
if no_uv_install {
|
||||
PIP_CACHE_DIR
|
||||
} else {
|
||||
PY311_CACHE_DIR
|
||||
},
|
||||
)
|
||||
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()),
|
||||
)?;
|
||||
};
|
||||
|
||||
@@ -1021,8 +1056,14 @@ pub async fn handle_python_reqs(
|
||||
if req.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let py_prefix = if no_uv_install {
|
||||
PIP_CACHE_DIR
|
||||
} else {
|
||||
PY311_CACHE_DIR
|
||||
};
|
||||
|
||||
let venv_p = format!(
|
||||
"{PIP_CACHE_DIR}/{}",
|
||||
"{py_prefix}/{}",
|
||||
req.replace(' ', "").replace('/', "").replace(':', "")
|
||||
);
|
||||
if metadata(&venv_p).await.is_ok() {
|
||||
@@ -1068,7 +1109,10 @@ pub async fn handle_python_reqs(
|
||||
.map(|(req, venv_p)| {
|
||||
let os = os.clone();
|
||||
async move {
|
||||
if pull_from_tar(os, venv_p.clone()).await.is_ok() {
|
||||
if pull_from_tar(os, venv_p.clone(), no_uv_install)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
PullFromTar::Pulled(venv_p.to_string())
|
||||
} else {
|
||||
PullFromTar::NotPulled(req.to_string(), venv_p.to_string())
|
||||
@@ -1109,7 +1153,7 @@ pub async fn handle_python_reqs(
|
||||
|
||||
for (req, venv_p) in req_with_penv {
|
||||
let mut logs1 = String::new();
|
||||
logs1.push_str("\n\n--- PIP INSTALL ---\n");
|
||||
logs1.push_str("\n\n--- UV PIP INSTALL ---\n");
|
||||
logs1.push_str(&format!("\n{req} is being installed for the first time.\n It will be cached for all ulterior uses."));
|
||||
append_logs(&job_id, w_id, logs1, db).await;
|
||||
|
||||
@@ -1145,21 +1189,47 @@ pub async fn handle_python_reqs(
|
||||
#[cfg(windows)]
|
||||
let req = format!("{}", req);
|
||||
|
||||
let mut command_args = vec![
|
||||
PYTHON_PATH.as_str(),
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
&req,
|
||||
"-I",
|
||||
"--no-deps",
|
||||
"--no-color",
|
||||
"--isolated",
|
||||
"--no-warn-conflicts",
|
||||
"--disable-pip-version-check",
|
||||
"-t",
|
||||
venv_p.as_str(),
|
||||
];
|
||||
let mut command_args = if no_uv_install {
|
||||
vec![
|
||||
PYTHON_PATH.as_str(),
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
&req,
|
||||
"-I",
|
||||
"--no-deps",
|
||||
"--no-color",
|
||||
"--isolated",
|
||||
"--no-warn-conflicts",
|
||||
"--disable-pip-version-check",
|
||||
"-t",
|
||||
venv_p.as_str(),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
UV_PATH.as_str(),
|
||||
"pip",
|
||||
"install",
|
||||
&req,
|
||||
"--no-deps",
|
||||
"--no-color",
|
||||
// "-p",
|
||||
// "3.11",
|
||||
// Prevent uv from discovering configuration files.
|
||||
"--no-config",
|
||||
"--link-mode=copy",
|
||||
// TODO: Doublecheck it
|
||||
"--system",
|
||||
// Prefer main index over extra
|
||||
// https://docs.astral.sh/uv/pip/compatibility/#packages-that-exist-on-multiple-indexes
|
||||
// TODO: Use env variable that can be toggled from UI
|
||||
"--index-strategy",
|
||||
"unsafe-best-match",
|
||||
"--target",
|
||||
venv_p.as_str(),
|
||||
"--no-cache",
|
||||
]
|
||||
};
|
||||
let pip_extra_index_url = PIP_EXTRA_INDEX_URL
|
||||
.read()
|
||||
.await
|
||||
@@ -1167,7 +1237,9 @@ pub async fn handle_python_reqs(
|
||||
.map(handle_ephemeral_token);
|
||||
|
||||
if let Some(url) = pip_extra_index_url.as_ref() {
|
||||
command_args.extend(["--extra-index-url", url]);
|
||||
url.split(",").for_each(|url| {
|
||||
command_args.extend(["--extra-index-url", url]);
|
||||
});
|
||||
}
|
||||
let pip_index_url = PIP_INDEX_URL
|
||||
.read()
|
||||
@@ -1189,7 +1261,7 @@ pub async fn handle_python_reqs(
|
||||
|
||||
envs.push(("HOME", HOME_ENV.as_str()));
|
||||
|
||||
tracing::debug!("pip install command: {:?}", command_args);
|
||||
tracing::debug!("uv pip install command: {:?}", command_args);
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@@ -1200,7 +1272,12 @@ pub async fn handle_python_reqs(
|
||||
.envs(envs)
|
||||
.args([
|
||||
"-x",
|
||||
&format!("{}/pip-{}.lock", LOCK_CACHE_DIR, fssafe_req),
|
||||
&format!(
|
||||
"{}/{}-{}.lock",
|
||||
LOCK_CACHE_DIR,
|
||||
if no_uv_install { "pip" } else { "py311" },
|
||||
fssafe_req
|
||||
),
|
||||
"--command",
|
||||
&command_args.join(" "),
|
||||
])
|
||||
@@ -1211,16 +1288,20 @@ pub async fn handle_python_reqs(
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut pip_cmd = Command::new(PYTHON_PATH.as_str());
|
||||
pip_cmd
|
||||
.env_clear()
|
||||
let installer_path = if no_uv_install { command_args[0] } else { "uv" };
|
||||
let mut cmd: Command = Command::new(&installer_path);
|
||||
cmd.env_clear()
|
||||
.envs(envs)
|
||||
.envs(PROXY_ENVS.clone())
|
||||
.env("SystemRoot", SYSTEM_ROOT.as_str())
|
||||
.env(
|
||||
"TMP",
|
||||
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
|
||||
)
|
||||
.args(&command_args[1..])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
start_child_process(pip_cmd, PYTHON_PATH.as_str()).await?
|
||||
start_child_process(cmd, installer_path).await?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1233,7 +1314,7 @@ pub async fn handle_python_reqs(
|
||||
false,
|
||||
worker_name,
|
||||
&w_id,
|
||||
&format!("pip install {req}"),
|
||||
&format!("uv pip install {req}"),
|
||||
None,
|
||||
false,
|
||||
occupancy_metrics,
|
||||
@@ -1253,7 +1334,7 @@ pub async fn handle_python_reqs(
|
||||
tracing::warn!("S3 cache not available in the pro plan");
|
||||
} else {
|
||||
let venv_p = venv_p.clone();
|
||||
tokio::spawn(build_tar_and_push(os, venv_p));
|
||||
tokio::spawn(build_tar_and_push(os, venv_p, no_uv_install));
|
||||
}
|
||||
}
|
||||
req_paths.push(venv_p);
|
||||
|
||||
@@ -295,6 +295,7 @@ pub async fn process_result(
|
||||
message: format!("error during execution of the script:\n{}", err),
|
||||
name: "ExecutionErr".to_string(),
|
||||
step_id: job.flow_step_id.clone(),
|
||||
exit_code: None,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -608,6 +609,8 @@ pub struct SerializedError {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub step_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exit_code: Option<i32>,
|
||||
}
|
||||
pub fn extract_error_value(log_lines: &str, i: i32, step_id: Option<String>) -> Box<RawValue> {
|
||||
return to_raw_value(&SerializedError {
|
||||
@@ -617,5 +620,6 @@ pub fn extract_error_value(log_lines: &str, i: i32, step_id: Option<String>) ->
|
||||
),
|
||||
name: "ExecutionErr".to_string(),
|
||||
step_id,
|
||||
exit_code: Some(i),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -236,7 +236,14 @@ pub const TMP_LOGS_DIR: &str = concatcp!(TMP_DIR, "/logs");
|
||||
pub const ROOT_CACHE_NOMOUNT_DIR: &str = concatcp!(TMP_DIR, "/cache_nomount/");
|
||||
|
||||
pub const LOCK_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "lock");
|
||||
// Used as fallback now
|
||||
pub const PIP_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "pip");
|
||||
|
||||
// pub const PY310_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_310");
|
||||
pub const PY311_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_311");
|
||||
// pub const PY312_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_312");
|
||||
// pub const PY313_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_313");
|
||||
|
||||
pub const UV_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "uv");
|
||||
pub const TAR_PIP_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "tar/pip");
|
||||
pub const DENO_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "deno");
|
||||
@@ -257,6 +264,7 @@ const NUM_SECS_PING: u64 = 5;
|
||||
const NUM_SECS_READINGS: u64 = 60;
|
||||
|
||||
const INCLUDE_DEPS_PY_SH_CONTENT: &str = include_str!("../nsjail/download_deps.py.sh");
|
||||
const INCLUDE_DEPS_PY_SH_CONTENT_FALLBACK: &str = include_str!("../nsjail/download_deps.py.pip.sh");
|
||||
|
||||
pub const DEFAULT_CLOUD_TIMEOUT: u64 = 900;
|
||||
pub const DEFAULT_SELFHOSTED_TIMEOUT: u64 = 604800; // 7 days
|
||||
@@ -311,6 +319,7 @@ lazy_static::lazy_static! {
|
||||
.and_then(|x| x.parse::<bool>().ok())
|
||||
.unwrap_or(false);
|
||||
|
||||
// pub static ref DISABLE_NSJAIL: bool = false;
|
||||
pub static ref DISABLE_NSJAIL: bool = std::env::var("DISABLE_NSJAIL")
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<bool>().ok())
|
||||
@@ -740,6 +749,13 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
|
||||
"download_deps.py.sh",
|
||||
INCLUDE_DEPS_PY_SH_CONTENT,
|
||||
);
|
||||
|
||||
// TODO: Remove (Deprecated)
|
||||
let _ = write_file(
|
||||
&worker_dir,
|
||||
"download_deps.py.pip.sh",
|
||||
INCLUDE_DEPS_PY_SH_CONTENT_FALLBACK,
|
||||
);
|
||||
}
|
||||
|
||||
let mut last_ping = Instant::now() - Duration::from_secs(NUM_SECS_PING + 1);
|
||||
@@ -1903,8 +1919,10 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
}
|
||||
};
|
||||
if job.is_flow() {
|
||||
let flow = job.parse_raw_flow();
|
||||
handle_flow(
|
||||
job,
|
||||
flow,
|
||||
db,
|
||||
&client.get_authed().await,
|
||||
None,
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -49,7 +48,7 @@ use windmill_common::{
|
||||
Approval, BranchAllStatus, BranchChosen, FlowStatus, FlowStatusModule, RetryStatus,
|
||||
MAX_RETRY_ATTEMPTS, MAX_RETRY_INTERVAL,
|
||||
},
|
||||
flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform, Retry, Suspend},
|
||||
flows::{has_failure_module, FlowModule, FlowModuleValue, FlowValue, InputTransform, Retry, Suspend},
|
||||
};
|
||||
use windmill_queue::schedule::get_schedule_opt;
|
||||
use windmill_queue::{
|
||||
@@ -177,11 +176,6 @@ struct RecoveryObject {
|
||||
recover: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Deserialize)]
|
||||
pub struct RowFlowStatus {
|
||||
pub flow_status: sqlx::types::Json<Box<serde_json::value::RawValue>>,
|
||||
pub current_module: Option<sqlx::types::Json<Box<serde_json::value::RawValue>>>,
|
||||
}
|
||||
// #[instrument(level = "trace", skip_all)]
|
||||
pub async fn update_flow_status_after_job_completion_internal<
|
||||
R: rsmq_async::RsmqConnection + Send + Sync + Clone,
|
||||
@@ -207,6 +201,7 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
let (
|
||||
should_continue_flow,
|
||||
flow_job,
|
||||
flow_value,
|
||||
stop_early,
|
||||
skip_if_stop_early,
|
||||
nresult,
|
||||
@@ -215,35 +210,28 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
) = {
|
||||
// tracing::debug!("UPDATE FLOW STATUS: {flow:?} {success} {result:?} {w_id} {depth}");
|
||||
|
||||
let old_status_json = sqlx::query_as::<_, RowFlowStatus>(
|
||||
"SELECT flow_status, raw_flow->'modules'->(flow_status->'step')::int as current_module FROM queue WHERE id = $1 AND workspace_id = $2",
|
||||
let (old_status, current_module) = sqlx::query!(
|
||||
"SELECT
|
||||
flow_status AS \"flow_status!: Json<Box<RawValue>>\",
|
||||
raw_flow->'modules'->(flow_status->'step')::int AS \"module: Json<Box<RawValue>>\"
|
||||
FROM queue WHERE id = $1 AND workspace_id = $2 LIMIT 1",
|
||||
flow, w_id
|
||||
)
|
||||
.bind(flow)
|
||||
.bind(w_id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"fetching flow status {flow} while reporting {success} {result:?}: {e:#}"
|
||||
))
|
||||
})?;
|
||||
|
||||
let old_status = serde_json::from_str::<FlowStatus>(old_status_json.flow_status.get())
|
||||
.or_else(|e| {
|
||||
Err(Error::InternalErr(format!(
|
||||
"requiring status to be parsable as FlowStatus: {e:?}"
|
||||
)))
|
||||
})?;
|
||||
|
||||
let current_module = if let Some(x) = old_status_json.current_module {
|
||||
Some(serde_json::from_str::<FlowModule>(x.0.get()).or_else(|e| {
|
||||
Err(Error::InternalErr(format!(
|
||||
.map_err(|e| Error::InternalErr(
|
||||
format!("fetching flow status {flow} while reporting {success} {result:?}: {e:#}")
|
||||
))
|
||||
.and_then(|record| Ok((
|
||||
serde_json::from_str::<FlowStatus>(record.flow_status.0.get()).map_err(|e| Error::InternalErr(
|
||||
format!("requiring current module to be parsable as FlowStatus: {e:?}")
|
||||
))?,
|
||||
record.module.map(|json| {
|
||||
serde_json::from_str::<FlowModule>(json.0.get()).map_err(|e| Error::InternalErr(format!(
|
||||
"requiring current module to be parsable as FlowModule: {e:?}"
|
||||
)))
|
||||
})?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}).transpose()?,
|
||||
)))?;
|
||||
|
||||
let module_step = Step::from_i32_and_len(old_status.step, old_status.modules.len());
|
||||
|
||||
@@ -303,16 +291,15 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
|
||||
let is_flow = if let Some(step) = step {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT raw_flow->'modules'->($1)->'value'->>'type' = 'flow' FROM queue WHERE id = $2",
|
||||
step as i32,
|
||||
&flow
|
||||
"SELECT raw_flow->'modules'->($1)::text->'value'->>'type' = 'flow' FROM queue WHERE id = $2 LIMIT 1",
|
||||
step as i32, flow
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!("error during retrieval of step's type: {e:#}"))
|
||||
})?
|
||||
.unwrap_or(false)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!("error during retrieval of step's type: {e:#}"))
|
||||
})?
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
@@ -938,10 +925,7 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
.unwrap_or_else(|| "none".to_string());
|
||||
tracing::info!(id = %flow_job.id, root_id = %job_root, "update flow status");
|
||||
|
||||
let module = get_module(&flow_job, &module_step);
|
||||
// tracing::error!(
|
||||
// "UPDATE FLOW STATUS 3: {module:#?} {unrecoverable} {} {is_last_step} {success} {skip_error_handler} is_failure_step {is_failure_step}", flow_job.canceled
|
||||
// );
|
||||
let flow_value = flow_job.parse_raw_flow();
|
||||
|
||||
let should_continue_flow = match success {
|
||||
_ if stop_early => false,
|
||||
@@ -953,7 +937,14 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
}
|
||||
false
|
||||
if next_retry(
|
||||
&module.and_then(|m| m.retry.clone()).unwrap_or_default(),
|
||||
flow_value
|
||||
.as_ref()
|
||||
.and_then(|value| match module_step {
|
||||
Step::PreprocessorStep => value.preprocessor_module.as_ref().and_then(|m| m.retry.as_ref()),
|
||||
Step::Step(i) => value.modules.get(i).as_ref().and_then(|m| m.retry.as_ref()),
|
||||
Step::FailureStep => value.failure_module.as_ref().and_then(|m| m.retry.as_ref()),
|
||||
})
|
||||
.unwrap_or(&Retry::default()),
|
||||
&old_status.retry,
|
||||
)
|
||||
.is_some() =>
|
||||
@@ -963,7 +954,7 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
false
|
||||
if !is_failure_step
|
||||
&& !skip_error_handler
|
||||
&& has_failure_module(flow, db).await? =>
|
||||
&& has_failure_module(flow, db, false).await? =>
|
||||
{
|
||||
true
|
||||
}
|
||||
@@ -975,6 +966,7 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
(
|
||||
should_continue_flow,
|
||||
flow_job,
|
||||
flow_value,
|
||||
stop_early,
|
||||
skip_if_stop_early,
|
||||
nresult,
|
||||
@@ -1042,13 +1034,11 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
let args_hash =
|
||||
hash_args(db, client, w_id, job_id_for_status, &flow_job.args).await;
|
||||
let flow_path = flow_job.script_path();
|
||||
let version_hash = if let Some(rc) = flow_job.raw_flow.as_ref() {
|
||||
use std::hash::Hasher;
|
||||
let mut s = DefaultHasher::new();
|
||||
serde_json::to_string(&rc.0)
|
||||
.unwrap_or_default()
|
||||
.hash(&mut s);
|
||||
format!("flow_{}", hex::encode(s.finish().to_be_bytes()))
|
||||
let version_hash = if let Some(sqlx::types::Json(s)) = flow_job.raw_flow.as_ref() {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = DefaultHasher::new();
|
||||
s.get().hash(&mut h);
|
||||
format!("flow_{}", hex::encode(h.finish().to_be_bytes()))
|
||||
} else {
|
||||
"flow_unknown".to_string()
|
||||
};
|
||||
@@ -1107,6 +1097,7 @@ pub async fn update_flow_status_after_job_completion_internal<
|
||||
tracing::debug!(id = %flow_job.id, "start handle flow");
|
||||
match handle_flow(
|
||||
flow_job.clone(),
|
||||
flow_value,
|
||||
db,
|
||||
client,
|
||||
Some(nresult.clone()),
|
||||
@@ -1239,19 +1230,6 @@ async fn retrieve_flow_jobs_results(
|
||||
Ok(to_raw_value(&results))
|
||||
}
|
||||
|
||||
fn get_module(flow_job: &QueuedJob, module_step: &Step) -> Option<FlowModule> {
|
||||
let raw_flow = flow_job.parse_raw_flow();
|
||||
if let Some(raw_flow) = raw_flow {
|
||||
match module_step {
|
||||
Step::PreprocessorStep => raw_flow.preprocessor_module.map(|x| *x.clone()),
|
||||
Step::Step(i) => raw_flow.modules.get(*i).map(|x| x.clone()),
|
||||
Step::FailureStep => raw_flow.failure_module.map(|x| *x.clone()),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn compute_skip_branchall_failure<'c>(
|
||||
job: &Uuid,
|
||||
branch: usize,
|
||||
@@ -1290,23 +1268,6 @@ async fn compute_skip_branchall_failure<'c>(
|
||||
}))
|
||||
}
|
||||
|
||||
async fn has_failure_module<'c>(flow: Uuid, db: &DB) -> Result<bool, Error> {
|
||||
sqlx::query_scalar::<_, Option<bool>>(
|
||||
"SELECT raw_flow->'failure_module' != 'null'::jsonb
|
||||
FROM queue
|
||||
WHERE id = $1",
|
||||
)
|
||||
.bind(flow)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!(
|
||||
"error during retrieval of has_failure_module: {e:#}"
|
||||
))
|
||||
})
|
||||
.map(|v| v.unwrap_or(false))
|
||||
}
|
||||
|
||||
// async fn retrieve_cleanup_module<'c>(flow_uuid: Uuid, db: &DB) -> Result<FlowCleanupModule, Error> {
|
||||
// tracing::warn!("Retrieving cleanup module of flow {}", flow_uuid);
|
||||
// let raw_value = sqlx::query_scalar!(
|
||||
@@ -1523,6 +1484,7 @@ async fn transform_input(
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub async fn handle_flow<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
flow_job: Arc<QueuedJob>,
|
||||
flow_value: Option<FlowValue>,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
client: &AuthedClient,
|
||||
last_result: Option<Arc<Box<RawValue>>>,
|
||||
@@ -1531,8 +1493,7 @@ pub async fn handle_flow<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
rsmq: Option<R>,
|
||||
job_completed_tx: Sender<SendResult>,
|
||||
) -> anyhow::Result<()> {
|
||||
let flow = flow_job
|
||||
.parse_raw_flow()
|
||||
let flow = flow_value
|
||||
.with_context(|| "Unable to parse flow definition")?;
|
||||
let status = flow_job
|
||||
.parse_flow_status()
|
||||
|
||||
@@ -1312,6 +1312,8 @@ async fn python_dep(
|
||||
job_dir,
|
||||
worker_dir,
|
||||
occupancy_metrics,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DenoLandProvider } from "https://deno.land/x/cliffy@v0.25.7/command/upg
|
||||
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";
|
||||
|
||||
import { VERSION, createBenchScript, getFlowPayload, login } from "./lib.ts";
|
||||
|
||||
@@ -172,8 +173,8 @@ export async function main({
|
||||
kind: "script",
|
||||
path: "f/benchmarks/" + kind,
|
||||
});
|
||||
} else if (["2steps"].includes(kind)) {
|
||||
nStepsFlow = 2;
|
||||
} else if (["2steps", "bigscriptinflow"].includes(kind)) {
|
||||
nStepsFlow = kind == "2steps" ? 2 : 1;
|
||||
const payload = getFlowPayload(kind);
|
||||
body = JSON.stringify({
|
||||
kind: "flow",
|
||||
@@ -194,6 +195,15 @@ export async function main({
|
||||
kind: "script",
|
||||
path: kind.substr(7),
|
||||
});
|
||||
} else if (kind == "bigrawscript") {
|
||||
noVerify = true;
|
||||
body = JSON.stringify({
|
||||
kind: "rawscript",
|
||||
rawscript: {
|
||||
language: api.RawScript.language.BASH,
|
||||
content: "# let's bloat that bash script, 3.. 2.. 1.. BOOM\n".repeat(25000) + "echo \"$WM_FLOW_JOB_ID\"\n",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unknown script pattern " + kind);
|
||||
}
|
||||
@@ -246,7 +256,7 @@ export async function main({
|
||||
} else {
|
||||
const elapsed = start ? Date.now() - start : 0;
|
||||
completedJobs = await getCompletedJobsCount();
|
||||
if (kind === "2steps" || kind.startsWith("flow:")) {
|
||||
if (nStepsFlow > 0) {
|
||||
completedJobs = Math.floor(completedJobs / (nStepsFlow + 1));
|
||||
}
|
||||
const avgThr = ((completedJobs / elapsed) * 1000).toFixed(2);
|
||||
|
||||
+18
-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.421.1";
|
||||
export const VERSION = "v1.423.2";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
@@ -235,6 +235,23 @@ export const getFlowPayload = (flowPattern: string): api.FlowPreview => {
|
||||
],
|
||||
},
|
||||
};
|
||||
} else if (flowPattern == "bigscriptinflow") {
|
||||
return {
|
||||
path: "bigscriptinflow",
|
||||
args: {},
|
||||
value: {
|
||||
modules: [
|
||||
{
|
||||
value: {
|
||||
input_transforms: {},
|
||||
language: api.RawScript.language.BASH,
|
||||
type: "rawscript",
|
||||
content: "# let's bloat that bash script, 3.. 2.. 1.. BOOM\n".repeat(25000) + "echo \"$WM_FLOW_JOB_ID\"\n",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
path: "2steps",
|
||||
|
||||
@@ -30,5 +30,13 @@
|
||||
{
|
||||
"kind": "nativets",
|
||||
"jobs": 2000
|
||||
},
|
||||
{
|
||||
"kind": "bigrawscript",
|
||||
"jobs": 10000
|
||||
},
|
||||
{
|
||||
"kind": "bigscriptinflow",
|
||||
"jobs": 10000
|
||||
}
|
||||
]
|
||||
+1
-1
@@ -60,7 +60,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.421.1";
|
||||
export const VERSION = "1.423.2";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
@@ -26,7 +26,10 @@
|
||||
]);
|
||||
in {
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = buildInputs ++ (with pkgs; [ git go xcaddy sqlx-cli ]);
|
||||
buildInputs = buildInputs ++ (with pkgs; [
|
||||
git xcaddy sqlx-cli flock rust-analyzer
|
||||
deno python3 python3Packages.pip go bun uv
|
||||
]);
|
||||
packages = [
|
||||
(pkgs.writeScriptBin "wm-caddy" ''
|
||||
cd ./frontend
|
||||
@@ -56,6 +59,9 @@
|
||||
sqlx database create
|
||||
wm-migrate
|
||||
'')
|
||||
(pkgs.writeScriptBin "wm-bench" ''
|
||||
deno run -A benchmarks/main.ts -e admin@windmill.dev -p changeme $*
|
||||
'')
|
||||
(pkgs.writeScriptBin "wm" ''
|
||||
cd ./frontend
|
||||
npm run dev $*
|
||||
@@ -68,6 +74,12 @@
|
||||
DATABASE_URL = "postgres://postgres:changeme@127.0.0.1:5432/";
|
||||
REMOTE = "http://127.0.0.1:8000";
|
||||
REMOTE_LSP = "http://127.0.0.1:3001";
|
||||
DENO_PATH = "${pkgs.deno}/bin/deno";
|
||||
PYTHON_PATH = "${pkgs.python3}/bin/python3";
|
||||
GO_PATH = "${pkgs.go}/bin/go";
|
||||
BUN_PATH = "${pkgs.bun}/bin/bun";
|
||||
UV_PATH = "${pkgs.uv}/bin/uv";
|
||||
FLOCK_PATH = "${pkgs.flock}/bin/flock";
|
||||
};
|
||||
packages.default = self.packages.${system}.windmill;
|
||||
packages.windmill-client = pkgs.stdenv.mkDerivation {
|
||||
@@ -82,7 +94,7 @@
|
||||
npm config set strict-ssl false
|
||||
cd frontend
|
||||
npm install --verbose
|
||||
npm run generate-backend-client-mac
|
||||
npm run ${if pkgs.stdenv.isDarwin then "generate-backend-client-mac" else "generate-backend-client"}
|
||||
NODE_OPTIONS="--max-old-space-size=8192" npm run build
|
||||
'';
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.421.1",
|
||||
"version": "1.423.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.421.1",
|
||||
"version": "1.423.2",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-js": "^4.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.421.1",
|
||||
"version": "1.423.2",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -418,6 +418,9 @@
|
||||
bind:selected={value}
|
||||
options={itemsType?.multiselect ?? []}
|
||||
selectedOptionsDraggable={true}
|
||||
on:open={() => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else if itemsType?.enum != undefined && Array.isArray(itemsType?.enum) && Array.isArray(value)}
|
||||
@@ -428,6 +431,9 @@
|
||||
bind:selected={value}
|
||||
options={itemsType?.enum ?? []}
|
||||
selectedOptionsDraggable={true}
|
||||
on:open={() => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -816,6 +822,12 @@
|
||||
bind:value
|
||||
{enum_}
|
||||
{autofocus}
|
||||
on:focus={() => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
on:blur={(e) => {
|
||||
dispatch('blur')
|
||||
}}
|
||||
enumLabels={extra['enumLabels']}
|
||||
/>
|
||||
</div>
|
||||
@@ -884,14 +896,16 @@
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex flex-row w-full items-center justify-between relative">
|
||||
{#if password || extra?.['password'] == true}
|
||||
{#if value && typeof value == 'string' && value?.startsWith('$var:')}
|
||||
<input type="text" bind:value />
|
||||
{:else if onlyMaskPassword}
|
||||
<Password
|
||||
{disabled}
|
||||
bind:password={value}
|
||||
placeholder={placeholder ?? defaultValue ?? ''}
|
||||
/>
|
||||
{#if onlyMaskPassword}
|
||||
{#if value && typeof value == 'string' && value?.startsWith('$var:')}
|
||||
<input type="text" bind:value />
|
||||
{:else}
|
||||
<Password
|
||||
{disabled}
|
||||
bind:password={value}
|
||||
placeholder={placeholder ?? defaultValue ?? ''}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<PasswordArgInput {disabled} bind:value />
|
||||
{/if}
|
||||
|
||||
@@ -72,11 +72,13 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col max-w-2xl p-2">
|
||||
{#if isConflict}
|
||||
<span class="text-sm mb-2 leading-6 font-semibold"
|
||||
>{isConflict ? 'Fix username conflict' : 'Change username'}</span
|
||||
>Fix username conflict</span
|
||||
>
|
||||
{/if}
|
||||
|
||||
<span class="text-xs mb-1 leading-6"
|
||||
<span class="text-sm font-semibold mb-1 leading-6"
|
||||
>{isConflict ? 'Auto-generated instance username' : 'New username'}</span
|
||||
>
|
||||
<input
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import FlowGraphViewerStep from './FlowGraphViewerStep.svelte'
|
||||
import FlowGraphV2 from './graph/FlowGraphV2.svelte'
|
||||
import { dfs } from './flows/dfs'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
export let flow: {
|
||||
summary: string
|
||||
@@ -22,6 +23,7 @@
|
||||
export let noGraph = false
|
||||
export let triggerNode = false
|
||||
export let stepDetail: FlowModule | string | undefined = undefined
|
||||
export let workspace: string | undefined = $workspaceStore
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
@@ -37,6 +39,7 @@
|
||||
path={flow?.path}
|
||||
{download}
|
||||
minHeight={400}
|
||||
{workspace}
|
||||
modules={flow?.value?.modules}
|
||||
failureModule={flow?.value?.failure_module}
|
||||
preprocessorModule={flow?.value?.preprocessor_module}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import Alert from './common/alert/Alert.svelte'
|
||||
import FlowGraphViewerStep from './FlowGraphViewerStep.svelte'
|
||||
import FlowGraphV2 from './graph/FlowGraphV2.svelte'
|
||||
import { buildPrefix } from './graph/graphBuilder'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -74,6 +75,10 @@
|
||||
|
||||
export let wideResults = false
|
||||
export let hideFlowResult = false
|
||||
export let workspace: string | undefined = $workspaceStore
|
||||
export let prefix: string | undefined = undefined
|
||||
export let subflowParentsGlobalModuleStates: Writable<Record<string, GraphModuleState>>[] = []
|
||||
export let subflowParentsDurationStatuses: Writable<Record<string, DurationStatus>>[] = []
|
||||
|
||||
let jobResults: any[] =
|
||||
flowJobIds?.flowJobs?.map((x, id) => `iter #${id + 1} not loaded by frontend yet`) ?? []
|
||||
@@ -83,6 +88,7 @@
|
||||
|
||||
let localModuleStates: Writable<Record<string, GraphModuleState>> = writable({})
|
||||
let localDurationStatuses: Writable<Record<string, DurationStatus>> = writable({})
|
||||
let expandedSubflows: Record<string, FlowModule[]> = {}
|
||||
|
||||
export let job: Job | undefined = undefined
|
||||
|
||||
@@ -94,6 +100,24 @@
|
||||
// }
|
||||
// }
|
||||
|
||||
function updateModuleStates(
|
||||
moduleState: Writable<Record<string, GraphModuleState>>,
|
||||
key: string,
|
||||
newValue: GraphModuleState,
|
||||
keepType: boolean | undefined
|
||||
) {
|
||||
moduleState.update((x) => {
|
||||
if (keepType && (x[key]?.type == 'Success' || x[key]?.type == 'Failure')) {
|
||||
newValue.type = x[key].type
|
||||
}
|
||||
x[key] = newValue
|
||||
return x
|
||||
})
|
||||
}
|
||||
|
||||
function buildSubflowKey(key: string, prefix: string | undefined) {
|
||||
return prefix ? 'subflow:' + prefix + key : key
|
||||
}
|
||||
function setModuleState(
|
||||
key: string,
|
||||
value: Partial<GraphModuleState>,
|
||||
@@ -102,15 +126,14 @@
|
||||
) {
|
||||
let newValue = { ...($localModuleStates[key] ?? {}), ...value }
|
||||
if (!deepEqual($localModuleStates[key], value) || force) {
|
||||
;[localModuleStates, ...globalModuleStates].forEach((s) => {
|
||||
s.update((x) => {
|
||||
if (keepType && (x[key]?.type == 'Success' || x[key]?.type == 'Failure')) {
|
||||
newValue.type = x[key].type
|
||||
}
|
||||
x[key] = newValue
|
||||
return x
|
||||
})
|
||||
})
|
||||
;[localModuleStates, ...globalModuleStates].forEach((s) =>
|
||||
updateModuleStates(s, key, newValue, keepType)
|
||||
)
|
||||
if (prefix) {
|
||||
subflowParentsGlobalModuleStates.forEach((s) =>
|
||||
updateModuleStates(s, buildSubflowKey(key, prefix), newValue, keepType)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +147,14 @@
|
||||
return x
|
||||
})
|
||||
})
|
||||
if (prefix) {
|
||||
subflowParentsDurationStatuses.forEach((s) => {
|
||||
s.update((x) => {
|
||||
x[buildSubflowKey(key, prefix)].byJob[id] = value
|
||||
return x
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,15 +162,25 @@
|
||||
if ($localDurationStatuses[modId] == undefined) {
|
||||
$localDurationStatuses[modId] = { byJob: {} }
|
||||
}
|
||||
let prefixed = modId
|
||||
globalDurationStatuses.forEach((x) =>
|
||||
x.update((x) => {
|
||||
if (x[prefixed] == undefined) {
|
||||
x[prefixed] = { byJob: {} }
|
||||
if (x[modId] == undefined) {
|
||||
x[modId] = { byJob: {} }
|
||||
}
|
||||
return x
|
||||
})
|
||||
)
|
||||
if (prefix) {
|
||||
subflowParentsDurationStatuses.forEach((x) =>
|
||||
x.update((x) => {
|
||||
let key = buildSubflowKey(modId, prefix)
|
||||
if (x[key] == undefined) {
|
||||
x[key] = { byJob: {} }
|
||||
}
|
||||
return x
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let innerModules: FlowStatusModule[] = []
|
||||
@@ -368,7 +409,7 @@
|
||||
if (globalRefreshes) {
|
||||
let modId = flowJobIds?.moduleId
|
||||
if (modId) {
|
||||
globalRefreshes[modId] = async (loopJob) => {
|
||||
globalRefreshes[buildSubflowKey(modId, prefix)] = async (loopJob) => {
|
||||
setIteration(loopJob.index, loopJob.job, false, modId ?? '')
|
||||
refresh(true, loopJob)
|
||||
}
|
||||
@@ -631,6 +672,36 @@
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function allModulesForTimeline(
|
||||
modules: FlowModule[],
|
||||
expandedSubflows: Record<string, FlowModule[]>
|
||||
): string[] {
|
||||
const ids = dfs(modules, (x) => x.id)
|
||||
|
||||
function rec(ids: string[], prefix: string | undefined): string[] {
|
||||
return ids.concat(
|
||||
ids.flatMap((id) => {
|
||||
let fms = expandedSubflows[id]
|
||||
let oid = id.split(':').pop()
|
||||
if (!oid) {
|
||||
return []
|
||||
}
|
||||
let nprefix = buildPrefix(prefix, oid)
|
||||
return fms
|
||||
? rec(
|
||||
dfs(fms, (x) =>
|
||||
x.id.startsWith('subflow:') ? x.id : buildSubflowKey(x.id, nprefix)
|
||||
),
|
||||
nprefix
|
||||
)
|
||||
: []
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return rec(ids, undefined)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if notAnonynmous}
|
||||
@@ -803,6 +874,9 @@
|
||||
job={storedListJobs[j]}
|
||||
globalModuleStates={[localModuleStates, ...globalModuleStates]}
|
||||
globalDurationStatuses={[localDurationStatuses, ...globalDurationStatuses]}
|
||||
{prefix}
|
||||
{subflowParentsGlobalModuleStates}
|
||||
{subflowParentsDurationStatuses}
|
||||
render={forloop_selected == loopJobId && selected == 'sequence' && render}
|
||||
reducedPolling={flowJobIds?.flowJobs.length && flowJobIds?.flowJobs.length > 20}
|
||||
{workspaceId}
|
||||
@@ -880,6 +954,9 @@
|
||||
{childFlow}
|
||||
globalModuleStates={[localModuleStates, ...globalModuleStates]}
|
||||
globalDurationStatuses={[localDurationStatuses, ...globalDurationStatuses]}
|
||||
{prefix}
|
||||
{subflowParentsGlobalModuleStates}
|
||||
{subflowParentsDurationStatuses}
|
||||
render={failedRetry == retry_selected && render}
|
||||
reducedPolling={false}
|
||||
{workspaceId}
|
||||
@@ -895,6 +972,17 @@
|
||||
bind:refresh={recursiveRefresh[mod.job ?? '']}
|
||||
globalModuleStates={[]}
|
||||
globalDurationStatuses={[]}
|
||||
prefix={buildPrefix(prefix, mod.id ?? '')}
|
||||
subflowParentsGlobalModuleStates={[
|
||||
localModuleStates,
|
||||
...globalModuleStates,
|
||||
...subflowParentsGlobalModuleStates
|
||||
]}
|
||||
subflowParentsDurationStatuses={[
|
||||
localDurationStatuses,
|
||||
...globalDurationStatuses,
|
||||
...subflowParentsDurationStatuses
|
||||
]}
|
||||
render={selected == 'sequence' && render}
|
||||
{workspaceId}
|
||||
jobId={mod.job}
|
||||
@@ -915,6 +1003,9 @@
|
||||
globalDurationStatuses={[localDurationStatuses, ...globalDurationStatuses]}
|
||||
render={selected == 'sequence' && render}
|
||||
{workspaceId}
|
||||
{prefix}
|
||||
{subflowParentsGlobalModuleStates}
|
||||
{subflowParentsDurationStatuses}
|
||||
jobId={mod.job}
|
||||
innerModule={mod.flow_jobs ? job.raw_flow?.modules[i]?.value : undefined}
|
||||
flowJobIds={mod.flow_jobs
|
||||
@@ -973,6 +1064,7 @@
|
||||
minHeight={wrapperHeight}
|
||||
success={jobId != undefined && isSuccess(job?.['success'])}
|
||||
flowModuleStates={$localModuleStates}
|
||||
bind:expandedSubflows
|
||||
on:select={(e) => {
|
||||
if (rightColumnSelect != 'node_definition') {
|
||||
rightColumnSelect = 'node_status'
|
||||
@@ -1004,11 +1096,13 @@
|
||||
selectedForloopIndex: detail.index
|
||||
})
|
||||
globalRefreshes[detail.moduleId]?.({ job: detail.id, index: detail.index })
|
||||
// console.log('selectedIteration', prefix, detail.moduleId, globalRefreshes)
|
||||
}}
|
||||
modules={job.raw_flow?.modules ?? []}
|
||||
failureModule={job.raw_flow?.failure_module}
|
||||
preprocessorModule={job.raw_flow?.preprocessor_module}
|
||||
allowSimplifiedPoll={false}
|
||||
{workspace}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -1033,7 +1127,10 @@
|
||||
aggregateWaitTime={job?.aggregate_wait_time_ms}
|
||||
flowDone={job?.['success'] != undefined}
|
||||
bind:this={flowTimeline}
|
||||
flowModules={dfs(job.raw_flow?.modules ?? [], (x) => x.id)}
|
||||
flowModules={allModulesForTimeline(
|
||||
job?.raw_flow?.modules ?? [],
|
||||
expandedSubflows ?? {}
|
||||
)}
|
||||
durationStatuses={localDurationStatuses}
|
||||
/>
|
||||
{:else if rightColumnSelect == 'node_status'}
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
{/if}
|
||||
|
||||
<div class="px-2 py-2 grid grid-cols-6 w-full">
|
||||
<div class="truncate">{k}</div>
|
||||
<div class="truncate">{k.startsWith('subflow:') ? k.substring(8) : k}</div>
|
||||
<div class="col-span-5 flex min-h-6">
|
||||
{#if min && total}
|
||||
<div class="flex flex-col gap-2 w-full p-2 ml-4">
|
||||
|
||||
@@ -238,7 +238,7 @@
|
||||
setPropertyType(arg?.value)
|
||||
codeInjectionDetected = checkCodeInjection(arg?.value) != undefined
|
||||
} else if (propertyType == 'javascript' && focused) {
|
||||
setPropertyType(arg?.expr)
|
||||
// setPropertyType(arg?.expr)
|
||||
$inputMatches = checkCodeInjection(arg?.expr)
|
||||
}
|
||||
}
|
||||
@@ -279,7 +279,7 @@
|
||||
class={twMerge(
|
||||
'pl-2 pt-2 pb-2 ml-2 relative hover:bg-surface hover:shadow-md transition-all duration-200',
|
||||
$propPickerConfig?.propName == argName
|
||||
? 'bg-surface border-l-4 border-blue-500 shadow-md rounded-l-md z-2000'
|
||||
? 'bg-surface border-l-4 border-blue-500 shadow-md rounded-l-md z-50 '
|
||||
: 'hover:rounded-md',
|
||||
$$props.class
|
||||
)}
|
||||
@@ -565,10 +565,13 @@
|
||||
</div>
|
||||
|
||||
{#if $propPickerConfig?.propName == argName}
|
||||
<div class="text-blue-500 mt-2" in:fade={{ duration: 200 }}>
|
||||
<div
|
||||
class="text-blue-500 absolute top-2 lg:-right-2.5 -right-1"
|
||||
in:fade={{ duration: 200 }}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
width="16"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
{#if keys.length > 0}
|
||||
{#each keys as argName (argName)}
|
||||
{#if (!filter || filter.includes(argName)) && Object.keys(schema.properties ?? {}).includes(argName)}
|
||||
<div class="z-10 pt-2 relative">
|
||||
<div class="pt-2 relative">
|
||||
<InputTransformForm
|
||||
{previousModuleId}
|
||||
bind:arg={args[argName]}
|
||||
|
||||
@@ -5,17 +5,36 @@
|
||||
import Popup from './common/popup/Popup.svelte'
|
||||
import { offset, flip, shift } from 'svelte-floating-ui/dom'
|
||||
import ChangeInstanceUsernameInner from './ChangeInstanceUsernameInner.svelte'
|
||||
import { UserService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
|
||||
export let value: string | undefined
|
||||
export let email: string
|
||||
export let username: string | undefined = undefined
|
||||
export let automateUsernameCreation: boolean = false
|
||||
export let login_type: string
|
||||
|
||||
let password: string = ''
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function save() {
|
||||
function saveName() {
|
||||
dispatch('save', value)
|
||||
}
|
||||
async function savePassword() {
|
||||
if (password.length < 5) {
|
||||
sendUserToast('Password must be at least 5 characters long', true)
|
||||
return
|
||||
}
|
||||
await UserService.setPasswordForUser({user: email, requestBody:{ password }})
|
||||
sendUserToast(`Password updated for ${email}`)
|
||||
}
|
||||
|
||||
async function saveLoginType() {
|
||||
await UserService.setLoginTypeForUser({user: email, requestBody:{ login_type }})
|
||||
sendUserToast(`Login type updated for ${email}`)
|
||||
dispatch('refresh')
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popup
|
||||
@@ -34,7 +53,7 @@
|
||||
<ChangeInstanceUsernameInner {email} {username} on:renamed />
|
||||
{/if}
|
||||
<label class="block text-primary">
|
||||
<div class="pb-1 text-xs text-secondary">Name</div>
|
||||
<div class="pb-2 text-sm font-semibold text-primary">Name</div>
|
||||
<div class="flex w-full">
|
||||
<input
|
||||
type="text"
|
||||
@@ -44,7 +63,7 @@
|
||||
on:keydown|stopPropagation
|
||||
on:keypress|stopPropagation={({ key }) => {
|
||||
if (key === 'Enter') {
|
||||
save()
|
||||
saveName()
|
||||
close(null)
|
||||
}
|
||||
}}
|
||||
@@ -57,12 +76,78 @@
|
||||
btnClasses="mt-2 "
|
||||
aria-label="Save ID"
|
||||
on:click={() => {
|
||||
save()
|
||||
saveName()
|
||||
close(null)
|
||||
}}
|
||||
>
|
||||
Update name
|
||||
</Button>
|
||||
</label>
|
||||
<label class="block text-primary">
|
||||
<div class="pb-2 text-sm font-semibold text-primary">Password</div>
|
||||
<div class="flex w-full">
|
||||
<input
|
||||
type="password"
|
||||
bind:value={password}
|
||||
class="!w-auto grow"
|
||||
on:click|stopPropagation={() => {}}
|
||||
on:keydown|stopPropagation
|
||||
on:keypress|stopPropagation={({ key }) => {
|
||||
if (key === 'Enter') {
|
||||
savePassword()
|
||||
close(null)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
buttonType="button"
|
||||
btnClasses="mt-2 "
|
||||
aria-label="Save ID"
|
||||
on:click={() => {
|
||||
savePassword()
|
||||
close(null)
|
||||
}}
|
||||
>
|
||||
Update password
|
||||
</Button>
|
||||
</label>
|
||||
<label class="block text-primary">
|
||||
<div class="pb-2 text-sm font-semibold text-primary">Login type</div>
|
||||
<div class="text-xs text-secondary mb-2">
|
||||
Must match exact SSO name, "password" or "saml". Examples: password, google, saml, microsoft
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={login_type}
|
||||
class="!w-auto grow"
|
||||
on:click|stopPropagation={() => {}}
|
||||
on:keydown|stopPropagation
|
||||
on:keypress|stopPropagation={({ key }) => {
|
||||
if (key === 'Enter') {
|
||||
saveLoginType()
|
||||
close(null)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
buttonType="button"
|
||||
btnClasses="mt-2 "
|
||||
aria-label="Save login type"
|
||||
on:click={() => {
|
||||
saveLoginType()
|
||||
close(null)
|
||||
}}
|
||||
>
|
||||
Update login type
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
@@ -61,6 +61,18 @@
|
||||
export let render = true
|
||||
export let title: string | undefined = undefined
|
||||
export let placeholder: string | undefined = undefined
|
||||
export let appPath: string | undefined = undefined
|
||||
export let computeS3ForceViewerPolicies:
|
||||
| (() =>
|
||||
| {
|
||||
allowed_resources: string[]
|
||||
allow_user_resources: boolean
|
||||
allow_workspace_resource: boolean
|
||||
file_key_regex: string
|
||||
}
|
||||
| undefined)
|
||||
| undefined = undefined
|
||||
export let workspace: string | undefined = undefined
|
||||
|
||||
let oneOfSelected: string | undefined = undefined
|
||||
async function updateOneOfSelected(oneOf: SchemaProperty[] | undefined) {
|
||||
@@ -428,6 +440,9 @@
|
||||
.toLowerCase() == 's3object'}
|
||||
<div class="flex flex-col w-full gap-1">
|
||||
<FileUpload
|
||||
{appPath}
|
||||
computeForceViewerPolicies={computeS3ForceViewerPolicies}
|
||||
{workspace}
|
||||
allowMultiple={false}
|
||||
randomFileKey={true}
|
||||
on:addition={(evt) => {
|
||||
|
||||
@@ -16,6 +16,18 @@
|
||||
export let defaultValues: Record<string, any> = {}
|
||||
export let dynamicEnums: Record<string, any> = {}
|
||||
export let disabled: boolean = false
|
||||
export let appPath: string | undefined = undefined
|
||||
export let computeS3ForceViewerPolicies:
|
||||
| (() =>
|
||||
| {
|
||||
allowed_resources: string[]
|
||||
allow_user_resources: boolean
|
||||
allow_workspace_resource: boolean
|
||||
file_key_regex: string
|
||||
}
|
||||
| undefined)
|
||||
| undefined = undefined
|
||||
export let workspace: string | undefined = undefined
|
||||
|
||||
let inputCheck: { [id: string]: boolean } = {}
|
||||
let errors: { [id: string]: string } = {}
|
||||
@@ -104,6 +116,9 @@
|
||||
{displayType}
|
||||
{css}
|
||||
disabled={disabled || schema.properties[argName].disabled}
|
||||
{appPath}
|
||||
{computeS3ForceViewerPolicies}
|
||||
{workspace}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
@@ -10,41 +10,53 @@
|
||||
let path = ''
|
||||
let password = value && !value.startsWith('$var:') ? value : ''
|
||||
|
||||
let isGenerating = false
|
||||
async function generateValue() {
|
||||
let npath =
|
||||
'u/' +
|
||||
($userStore?.username ?? $userStore?.email)?.split('@')[0] +
|
||||
'/secret_arg/' +
|
||||
generateRandomString(12)
|
||||
let nvalue = '$var:' + npath
|
||||
await VariableService.createVariable({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
value: password,
|
||||
is_secret: true,
|
||||
path: npath,
|
||||
description: '',
|
||||
expires_at: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7).toISOString()
|
||||
}
|
||||
})
|
||||
path = npath
|
||||
value = nvalue
|
||||
if (isGenerating) return
|
||||
isGenerating = true
|
||||
try {
|
||||
let npath =
|
||||
'u/' +
|
||||
($userStore?.username ?? $userStore?.email)?.split('@')[0] +
|
||||
'/secret_arg/' +
|
||||
generateRandomString(12)
|
||||
let nvalue = '$var:' + npath
|
||||
await VariableService.createVariable({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
value: password,
|
||||
is_secret: true,
|
||||
path: npath,
|
||||
description: 'Ephemeral secret variable',
|
||||
expires_at: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7).toISOString()
|
||||
}
|
||||
})
|
||||
path = npath
|
||||
value = nvalue
|
||||
debouncedUpdate()
|
||||
} finally {
|
||||
isGenerating = false
|
||||
}
|
||||
}
|
||||
|
||||
async function updateValue() {
|
||||
await VariableService.updateVariable({
|
||||
workspace: $workspaceStore!,
|
||||
path: path,
|
||||
requestBody: {
|
||||
value: password
|
||||
}
|
||||
})
|
||||
try {
|
||||
await VariableService.updateVariable({
|
||||
workspace: $workspaceStore!,
|
||||
path: path,
|
||||
requestBody: {
|
||||
value: password
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
generateValue()
|
||||
}
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
function debouncedUpdate() {
|
||||
timeout && clearTimeout(timeout)
|
||||
setTimeout(updateValue, 500)
|
||||
timeout = setTimeout(updateValue, 500)
|
||||
}
|
||||
|
||||
$: password && debouncedUpdate()
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import InstanceNameEditor from './InstanceNameEditor.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { instanceSettingsSelectedTab } from '$lib/stores';
|
||||
let drawer: Drawer
|
||||
let filter = ''
|
||||
|
||||
@@ -56,6 +57,9 @@
|
||||
|
||||
let tab: 'users' | string = 'users'
|
||||
|
||||
$: $instanceSettingsSelectedTab, tab = $instanceSettingsSelectedTab
|
||||
$: tab, instanceSettingsSelectedTab.set(tab)
|
||||
|
||||
let nbDisplayed = 50
|
||||
|
||||
let instanceSettings
|
||||
@@ -272,9 +276,13 @@
|
||||
<td>
|
||||
<div class="flex flex-row gap-x-1 justify-end">
|
||||
<InstanceNameEditor
|
||||
login_type={login_type}
|
||||
value={name}
|
||||
{username}
|
||||
{email}
|
||||
on:refresh={() => {
|
||||
listUsers(activeOnly)
|
||||
}}
|
||||
on:save={(e) => {
|
||||
updateName(e.detail, email)
|
||||
}}
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { computeWorkspaceS3FileInputPolicy } from '../../editor/appUtilsS3'
|
||||
import { defaultIfEmptyString } from '$lib/utils'
|
||||
import { userStore } from '$lib/stores'
|
||||
|
||||
export let id: string
|
||||
export let componentInput: AppInput | undefined
|
||||
@@ -26,8 +29,16 @@
|
||||
export let configuration: RichConfigurations
|
||||
export let customCss: ComponentCustomCSS<'schemaformcomponent'> | undefined = undefined
|
||||
|
||||
const { worldStore, connectingInput, app, selectedComponent, componentControl } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
const {
|
||||
worldStore,
|
||||
connectingInput,
|
||||
app,
|
||||
selectedComponent,
|
||||
componentControl,
|
||||
appPath,
|
||||
isEditor,
|
||||
workspace
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
const iterContext = getContext<ListContext>('ListWrapperContext')
|
||||
const listInputs: ListInputs | undefined = getContext<ListInputs>('ListInputs')
|
||||
|
||||
@@ -100,6 +111,14 @@
|
||||
previousDefault = structuredClone(resolvedConfig.defaultValues)
|
||||
args = previousDefault ?? {}
|
||||
}
|
||||
|
||||
function computeS3ForceViewerPolicies() {
|
||||
if (!isEditor) {
|
||||
return undefined
|
||||
}
|
||||
const policy = computeWorkspaceS3FileInputPolicy()
|
||||
return policy
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each Object.keys(components['schemaformcomponent'].initialData.configuration) as key (key)}
|
||||
@@ -140,6 +159,9 @@
|
||||
bind:this={schemaForm}
|
||||
displayType={Boolean(resolvedConfig.displayType)}
|
||||
largeGap={Boolean(resolvedConfig.largeGap)}
|
||||
appPath={defaultIfEmptyString(appPath, `u/${$userStore?.username ?? 'unknown'}/newapp`)}
|
||||
{computeS3ForceViewerPolicies}
|
||||
{workspace}
|
||||
{css}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
import { get } from 'svelte/store'
|
||||
import RefreshButton from '$lib/components/apps/components/helpers/RefreshButton.svelte'
|
||||
import { ctxRegex } from '../../utils'
|
||||
import { computeWorkspaceS3FileInputPolicy } from '../../editor/appUtilsS3'
|
||||
|
||||
// Component props
|
||||
export let id: string
|
||||
@@ -671,6 +672,14 @@
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function computeS3ForceViewerPolicies() {
|
||||
if (!isEditor) {
|
||||
return undefined
|
||||
}
|
||||
const policy = computeWorkspaceS3FileInputPolicy()
|
||||
return policy
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each Object.entries(fields ?? {}) as [key, v] (key)}
|
||||
@@ -754,6 +763,9 @@
|
||||
<div class="px-2 h-fit min-h-0">
|
||||
<LightweightSchemaForm
|
||||
schema={schemaStripped}
|
||||
appPath={defaultIfEmptyString(appPath, `u/${$userStore?.username ?? 'unknown'}/newapp`)}
|
||||
{computeS3ForceViewerPolicies}
|
||||
{workspace}
|
||||
bind:this={schemaForm}
|
||||
bind:args
|
||||
on:inputClicked={handleInputClick}
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import FileUpload from '$lib/components/common/fileUpload/FileUpload.svelte'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import { computeS3FileInputPolicy } from '../../editor/appUtilsS3'
|
||||
import { defaultIfEmptyString } from '$lib/utils'
|
||||
import { userStore } from '$lib/stores'
|
||||
|
||||
export let id: string
|
||||
export let configuration: RichConfigurations
|
||||
@@ -77,6 +80,15 @@
|
||||
{/if}
|
||||
*/
|
||||
let forceDisplayUploads: boolean = false
|
||||
|
||||
const { appPath, isEditor } = getContext<AppViewerContext>('AppViewerContext')
|
||||
function computeForceViewerPolicies() {
|
||||
if (!isEditor) {
|
||||
return undefined
|
||||
}
|
||||
const policy = computeS3FileInputPolicy((configuration as any)?.type?.configuration?.s3, $app)
|
||||
return policy
|
||||
}
|
||||
</script>
|
||||
|
||||
<InitializeComponent {id} />
|
||||
@@ -139,5 +151,7 @@
|
||||
outputs.result.set(value)
|
||||
}}
|
||||
{forceDisplayUploads}
|
||||
appPath={defaultIfEmptyString(appPath, `u/${$userStore?.username ?? 'unknown'}/newapp`)}
|
||||
{computeForceViewerPolicies}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
import ToggleEnable from '$lib/components/common/toggleButton-v2/ToggleEnable.svelte'
|
||||
import HideButton from './settingsPanel/HideButton.svelte'
|
||||
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
|
||||
import { computeS3FileInputPolicy, computeWorkspaceS3FileInputPolicy } from './appUtilsS3'
|
||||
|
||||
async function hash(message) {
|
||||
try {
|
||||
@@ -195,8 +196,12 @@
|
||||
}
|
||||
|
||||
async function computeTriggerables() {
|
||||
const items = allItems($app.grid, $app.subgrids)
|
||||
|
||||
console.log('items', items)
|
||||
|
||||
const allTriggers: ([string, TriggerableV2] | undefined)[] = (await Promise.all(
|
||||
allItems($app.grid, $app.subgrids)
|
||||
items
|
||||
.flatMap((x) => {
|
||||
let c = x.data as AppComponent
|
||||
let r: { input: AppInput | undefined; id: string }[] = [
|
||||
@@ -296,6 +301,44 @@
|
||||
allTriggers.filter(Boolean) as [string, TriggerableV2][]
|
||||
)
|
||||
policy.triggerables_v2 = ntriggerables
|
||||
|
||||
const s3_inputs = items
|
||||
.filter((x) => (x.data as AppComponent).type === 's3fileinputcomponent')
|
||||
.map((x) => {
|
||||
const c = x.data as AppComponent
|
||||
const config = c.configuration as any
|
||||
return computeS3FileInputPolicy(config?.type?.configuration?.s3, $app)
|
||||
})
|
||||
.filter(Boolean) as {
|
||||
allowed_resources: string[]
|
||||
allow_user_resources: boolean
|
||||
file_key_regex: string
|
||||
}[]
|
||||
|
||||
if (
|
||||
items.findIndex((x) => {
|
||||
const c = x.data as AppComponent
|
||||
if (c.type === 'schemaformcomponent') {
|
||||
return (
|
||||
Object.values((c.componentInput as any)?.value?.properties ?? {}).findIndex(
|
||||
(p: any) => p?.type === 'object' && p?.format === 'resource-s3_object'
|
||||
) !== -1
|
||||
)
|
||||
} else if (c.type === 'formbuttoncomponent' || c.type === 'formcomponent') {
|
||||
return (
|
||||
Object.values((c.componentInput as any)?.fields ?? {}).findIndex(
|
||||
(p: any) => p?.fieldType === 'object' && p?.format === 'resource-s3_object'
|
||||
) !== -1
|
||||
)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) !== -1
|
||||
) {
|
||||
s3_inputs.push(computeWorkspaceS3FileInputPolicy())
|
||||
}
|
||||
|
||||
policy.s3_inputs = s3_inputs
|
||||
}
|
||||
|
||||
async function processRunnable(
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { AppInput, EvalInputV2 } from '../inputType'
|
||||
import type { App } from '../types'
|
||||
import { collectOneOfFields } from './appUtils'
|
||||
|
||||
function filenameExprToRegex(template: string) {
|
||||
const filenameEscaped = template.replaceAll('${file.name}', '<file_name>') // replace filename with placeholder
|
||||
const escapedTemplate = filenameEscaped
|
||||
.slice(1, -1) // remove quotes
|
||||
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex special characters
|
||||
const regexPattern = escapedTemplate.replaceAll('<file_name>', '[^/]+') // replace filename placeholder with regex pattern
|
||||
return `^${regexPattern}$`
|
||||
}
|
||||
|
||||
function staticToRegex(str: string) {
|
||||
return `^${str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`
|
||||
}
|
||||
|
||||
function checkIfExprIsString(input: string) {
|
||||
return /^(['"`])[^'"`]*\1$/g.test(input)
|
||||
}
|
||||
|
||||
function checkIfEvalIsStringWithFilename(input: EvalInputV2) {
|
||||
if (input.connections.length > 0) {
|
||||
return false
|
||||
} else {
|
||||
return checkIfExprIsString(input.expr.replaceAll('${file.name}', ''))
|
||||
}
|
||||
}
|
||||
|
||||
function removeResourcePrefix(resource: string) {
|
||||
return resource.replace(/^\$res:/, '')
|
||||
}
|
||||
|
||||
export function computeWorkspaceS3FileInputPolicy() {
|
||||
return {
|
||||
allow_workspace_resource: true,
|
||||
allowed_resources: [],
|
||||
allow_user_resources: false,
|
||||
file_key_regex: ''
|
||||
}
|
||||
}
|
||||
|
||||
export function computeS3FileInputPolicy(s3Config: any, app: App) {
|
||||
const resourceInput = s3Config?.resource as AppInput | undefined
|
||||
const pathTemplateInput = s3Config?.pathTemplate as AppInput | undefined
|
||||
|
||||
const allow_workspace_resource =
|
||||
!resourceInput || (resourceInput.type === 'static' && !resourceInput.value)
|
||||
const allowed_resources: string[] = resourceInput
|
||||
? resourceInput.type === 'static'
|
||||
? resourceInput.value
|
||||
? [removeResourcePrefix(resourceInput.value)]
|
||||
: []
|
||||
: collectOneOfFields(
|
||||
{
|
||||
s3_resource: resourceInput
|
||||
},
|
||||
app
|
||||
).s3_resource?.map((s) => removeResourcePrefix(s)) ?? []
|
||||
: []
|
||||
|
||||
const allow_user_resources =
|
||||
(resourceInput?.type === 'evalv2' && resourceInput?.allowUserResources) ?? false
|
||||
let file_key_regex = '^.*$'
|
||||
if (pathTemplateInput) {
|
||||
if (pathTemplateInput.type === 'static') {
|
||||
file_key_regex = staticToRegex(pathTemplateInput.value)
|
||||
} else if (
|
||||
pathTemplateInput.type === 'evalv2' &&
|
||||
checkIfEvalIsStringWithFilename(pathTemplateInput)
|
||||
) {
|
||||
file_key_regex = filenameExprToRegex(pathTemplateInput.expr)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allow_workspace_resource,
|
||||
allowed_resources,
|
||||
allow_user_resources,
|
||||
file_key_regex
|
||||
}
|
||||
}
|
||||
@@ -193,7 +193,7 @@
|
||||
{:else if componentInput?.type === 'user'}
|
||||
<span class="text-2xs italic text-tertiary">Field's value is set by the user</span>
|
||||
{/if}
|
||||
{#if (componentInput?.type === 'evalv2' || componentInput?.type === 'connected' || componentInput?.type === 'user') && fieldType == 'object' && format?.startsWith('resource-')}
|
||||
{#if (componentInput?.type === 'evalv2' || componentInput?.type === 'connected' || componentInput?.type === 'user') && ((fieldType == 'object' && format?.startsWith('resource-') && format !== 'resource-s3_object') || fieldType == 'resource')}
|
||||
<div class="flex flex-row items-center">
|
||||
<Toggle
|
||||
size="xs"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { HelpersService } from '$lib/gen'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import { Ban, CheckCheck, FileWarning, Files, RefreshCcw, Trash } from 'lucide-svelte'
|
||||
@@ -27,6 +27,17 @@
|
||||
export let defaultValue: string | undefined = undefined
|
||||
export let workspace: string | undefined = undefined
|
||||
export let fileUploads: Writable<FileUploadData[]> = writable([])
|
||||
export let appPath: string | undefined = undefined
|
||||
export let computeForceViewerPolicies:
|
||||
| (() =>
|
||||
| {
|
||||
allowed_resources: string[]
|
||||
allow_user_resources: boolean
|
||||
allow_workspace_resource: boolean
|
||||
file_key_regex: string
|
||||
}
|
||||
| undefined)
|
||||
| undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -115,6 +126,26 @@
|
||||
params.append('content_type', fileToUpload.type)
|
||||
}
|
||||
|
||||
if (computeForceViewerPolicies !== undefined) {
|
||||
const forceViewerPolicies = computeForceViewerPolicies()
|
||||
|
||||
if (forceViewerPolicies) {
|
||||
params.append(
|
||||
'force_viewer_allowed_resources',
|
||||
forceViewerPolicies.allowed_resources.join(',')
|
||||
)
|
||||
params.append(
|
||||
'force_viewer_allow_user_resources',
|
||||
JSON.stringify(forceViewerPolicies.allow_user_resources)
|
||||
)
|
||||
params.append(
|
||||
'force_viewer_allow_workspace_resource',
|
||||
JSON.stringify(forceViewerPolicies.allow_workspace_resource)
|
||||
)
|
||||
params.append('force_viewer_file_key_regex', forceViewerPolicies.file_key_regex)
|
||||
}
|
||||
}
|
||||
|
||||
// let response = await fetch(
|
||||
// `/api/w/${$workspaceStore}/job_helpers/multipart_upload_s3_file?${params.toString()}`,
|
||||
// {
|
||||
@@ -158,9 +189,16 @@
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
xhr?.open(
|
||||
'POST',
|
||||
`/api/w/${workspace ?? $workspaceStore}/job_helpers/upload_s3_file?${params.toString()}`,
|
||||
appPath
|
||||
? `/api/w/${
|
||||
workspace ?? $workspaceStore
|
||||
}/apps_u/upload_s3_file/${appPath}?${params.toString()}`
|
||||
: `/api/w/${
|
||||
workspace ?? $workspaceStore
|
||||
}/job_helpers/upload_s3_file?${params.toString()}`,
|
||||
true
|
||||
)
|
||||
xhr?.setRequestHeader('Content-Type', 'application/octet-stream')
|
||||
@@ -309,7 +347,7 @@
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#if fileUpload.progress === 100 && !fileUpload.cancelled}
|
||||
{#if fileUpload.progress === 100 && !fileUpload.cancelled && $userStore}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="red"
|
||||
|
||||
@@ -78,6 +78,10 @@
|
||||
newItem={newFlow}
|
||||
isFlow={true}
|
||||
/>
|
||||
{:else if $selectedId.startsWith('subflow:')}
|
||||
<div class="p-4"
|
||||
>Selected step is witin an expanded subflow and is not directly editable in the flow editor</div
|
||||
>
|
||||
{:else}
|
||||
{@const dup = checkDup($flowStore.value.modules)}
|
||||
{#if dup}
|
||||
|
||||
@@ -307,7 +307,7 @@
|
||||
{/if}
|
||||
|
||||
{#if inlineScripts?.length > 0}
|
||||
<div class="pb-0 flex flex-row items-center gap-2">
|
||||
<div class="pb-0 flex flex-row items-center gap-2 -mt-[3px]">
|
||||
<div class=" text-2xs font-light text-secondary ml-2"
|
||||
>New {selectedKind != 'script' ? selectedKind + ' ' : ''}script</div
|
||||
>
|
||||
|
||||
@@ -253,6 +253,10 @@
|
||||
forceReload++
|
||||
await reload(flowModule)
|
||||
}
|
||||
if (flowModule.value.type == 'flow') {
|
||||
forceReload++
|
||||
await reload(flowModule)
|
||||
}
|
||||
}}
|
||||
on:createScriptFromInlineScript={async () => {
|
||||
const [module, state] = await createScriptFromInlineScript(
|
||||
@@ -361,7 +365,9 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else if flowModule.value.type === 'flow'}
|
||||
<FlowPathViewer path={flowModule.value.path} />
|
||||
{#key forceReload}
|
||||
<FlowPathViewer path={flowModule.value.path} />
|
||||
{/key}
|
||||
{/if}
|
||||
</Pane>
|
||||
<Pane bind:size={editorSettingsPanelSize} minSize={20}>
|
||||
@@ -378,13 +384,14 @@
|
||||
class={advancedSelected === 'runtime' ? 'h-[calc(100%-68px)]' : 'h-[calc(100%-34px)]'}
|
||||
>
|
||||
{#if selected === 'inputs' && (flowModule.value.type == 'rawscript' || flowModule.value.type == 'script' || flowModule.value.type == 'flow')}
|
||||
<div class="h-full overflow-auto px-2 bg-surface" id="flow-editor-step-input">
|
||||
<div class="h-full overflow-auto bg-surface" id="flow-editor-step-input">
|
||||
<PropPickerWrapper
|
||||
pickableProperties={stepPropPicker.pickableProperties}
|
||||
error={failureModule}
|
||||
noPadding
|
||||
>
|
||||
<InputTransformSchemaForm
|
||||
class="px-1 xl:px-2"
|
||||
bind:this={inputTransformSchemaForm}
|
||||
pickableProperties={stepPropPicker.pickableProperties}
|
||||
schema={$flowStateStore[$selectedId]?.schema ?? {}}
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
import {
|
||||
Bed,
|
||||
Database,
|
||||
ExternalLink,
|
||||
Gauge,
|
||||
GitFork,
|
||||
Pen,
|
||||
PhoneIncoming,
|
||||
RefreshCcw,
|
||||
Repeat,
|
||||
Save,
|
||||
Square,
|
||||
@@ -139,6 +141,32 @@
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if module.value.type === 'flow'}
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
on:click={async () => {
|
||||
if (module.value.type == 'flow') {
|
||||
window.open(`/flows/edit/${module.value.path}`, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
}}
|
||||
startIcon={{ icon: Pen }}
|
||||
iconOnly={false}
|
||||
>
|
||||
Edit <ExternalLink size={12} />
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
on:click={async () => {
|
||||
dispatch('reload')
|
||||
}}
|
||||
startIcon={{
|
||||
icon: RefreshCcw
|
||||
}}
|
||||
iconOnly={true}
|
||||
/>
|
||||
{/if}
|
||||
<div class="px-0.5" />
|
||||
{#if module.value.type === 'rawscript'}
|
||||
<FlowModuleWorkerTagSelect bind:tag={module.value.tag} />
|
||||
|
||||
@@ -276,8 +276,13 @@
|
||||
</Popover>
|
||||
|
||||
<div class="flex items-center space-x-2 relative max-w-[25%]" bind:clientWidth={idBadgeWidth}>
|
||||
{#if id && id !== 'preprocessor' && !id.startsWith('failure')}
|
||||
<Badge color="indigo" wrapperClass="max-w-full" baseClass="max-w-full truncate" title={id}>
|
||||
{#if id && id !== 'preprocessor' && !id.startsWith('failure') && !id.startsWith('subflow:')}
|
||||
<Badge
|
||||
color="indigo"
|
||||
wrapperClass="max-w-full"
|
||||
baseClass="max-w-full truncate !px-1"
|
||||
title={id}
|
||||
>
|
||||
<span class="max-w-full text-2xs truncate">{id}</span></Badge
|
||||
>
|
||||
{#if deletable}
|
||||
@@ -290,6 +295,10 @@ hover:border-blue-700 hover:!visible {hover ? '' : '!hidden'}"
|
||||
title="Edit Id"><Pencil size={14} /></button
|
||||
>
|
||||
{/if}
|
||||
{:else if id?.startsWith('subflow:')}
|
||||
<Badge color="blue" wrapperClass="max-w-full" baseClass="!px-1" title={id}>
|
||||
<span class="max-w-full text-2xs truncate">{id.substring('subflow:'.length)}</span></Badge
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
import { getDependentComponents } from '../flowExplorer'
|
||||
import type { FlowCopilotContext } from '$lib/components/copilot/flow'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { copilotInfo, tutorialsToDo } from '$lib/stores'
|
||||
import { copilotInfo, tutorialsToDo, workspaceStore } from '$lib/stores'
|
||||
|
||||
import FlowTutorials from '$lib/components/FlowTutorials.svelte'
|
||||
import { ignoredTutorials } from '$lib/components/tutorials/ignoredTutorials'
|
||||
@@ -44,6 +44,7 @@
|
||||
export let disableSettings = false
|
||||
export let newFlow: boolean = false
|
||||
export let smallErrorHandler = false
|
||||
export let workspace: string | undefined = $workspaceStore
|
||||
|
||||
let flowTutorials: FlowTutorials | undefined = undefined
|
||||
|
||||
@@ -345,6 +346,7 @@
|
||||
preprocessorModule={$flowStore.value?.preprocessor_module}
|
||||
{selectedId}
|
||||
{flowInputsStore}
|
||||
{workspace}
|
||||
on:delete={({ detail }) => {
|
||||
let e = detail.detail
|
||||
dependents = getDependentComponents(e.id, $flowStore)
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
export let modules: FlowModule[]
|
||||
export let moving: string | undefined = undefined
|
||||
export let duration_ms: number | undefined = undefined
|
||||
export let isTrigger: boolean = false
|
||||
|
||||
export let retries: number | undefined = undefined
|
||||
export let flowJobs:
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<div class="truncate text-2xs text-center"><pre>{preLabel}</pre></div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if id && !hideId}
|
||||
{#if id && !hideId && !id?.startsWith('subflow:')}
|
||||
<div class="flex items-center shrink min-w-0">
|
||||
<Badge color="indigo" wrapperClass="w-full" baseClass="max-w-full" title={id}>
|
||||
<span class="max-w-full text-2xs truncate">{id}</span>
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
{alwaysOn}
|
||||
{displayContext}
|
||||
{error}
|
||||
previousId={pickableProperties?.previousId}
|
||||
{pickableProperties}
|
||||
allowCopy={!notSelectable && !$propPickerConfig}
|
||||
on:select={({ detail }) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { type FlowModule } from '../../gen'
|
||||
import { FlowService, type FlowModule } from '../../gen'
|
||||
import { NODE, type GraphModuleState } from '.'
|
||||
import { createEventDispatcher, getContext, onDestroy, onMount, setContext } from 'svelte'
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
import FlowYamlEditor from '../flows/header/FlowYamlEditor.svelte'
|
||||
import BranchOneEndNode from './renderers/nodes/branchOneEndNode.svelte'
|
||||
import type { TriggerContext } from '../triggers'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import SubflowBound from './renderers/nodes/SubflowBound.svelte'
|
||||
|
||||
export let success: boolean | undefined = undefined
|
||||
export let modules: FlowModule[] | undefined = []
|
||||
@@ -65,6 +67,7 @@
|
||||
undefined
|
||||
)
|
||||
export let triggerNode = false
|
||||
export let workspace: string = $workspaceStore ?? 'NO_WORKSPACE'
|
||||
|
||||
let useDataflow: Writable<boolean | undefined> = writable<boolean | undefined>(false)
|
||||
|
||||
@@ -85,6 +88,8 @@
|
||||
|
||||
let simplifiableFlow: SimplifiableFlow | undefined = undefined
|
||||
|
||||
export let expandedSubflows: Record<string, FlowModule[]> = {}
|
||||
|
||||
if (triggerContext && allowSimplifiedPoll) {
|
||||
if (isSimplifiable(modules)) {
|
||||
triggerContext?.simplifiedPoll?.set(true)
|
||||
@@ -201,6 +206,15 @@
|
||||
},
|
||||
simplifyFlow: (detail) => {
|
||||
triggerContext?.simplifiedPoll.set(detail)
|
||||
},
|
||||
expandSubflow: async (id: string, path: string) => {
|
||||
const flow = await FlowService.getFlowByPath({ workspace: workspace, path })
|
||||
expandedSubflows[id] = flow.value.modules
|
||||
expandedSubflows = expandedSubflows
|
||||
},
|
||||
minimizeSubflow: (id: string) => {
|
||||
delete expandedSubflows[id]
|
||||
expandedSubflows = expandedSubflows
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +236,8 @@
|
||||
$selectedId,
|
||||
moving,
|
||||
simplifiableFlow,
|
||||
triggerNode ? path : undefined
|
||||
triggerNode ? path : undefined,
|
||||
expandedSubflows
|
||||
)
|
||||
|
||||
const nodes = writable<Node[]>([])
|
||||
@@ -230,69 +245,6 @@
|
||||
|
||||
let height = 0
|
||||
|
||||
// function removeInputNode(nodes, edges, id) {
|
||||
// const inputNode = nodes.find((node) => node.id === id)
|
||||
// if (!inputNode) return { nodes, edges }
|
||||
|
||||
// // Find edges connected to the input node
|
||||
// const connectedEdges = edges.filter((edge) => edge.source === id || edge.target === id)
|
||||
|
||||
// // Remove the input node
|
||||
// let updatedNodes = nodes.filter((node) => node.id !== id)
|
||||
|
||||
// // Remove edges connected to the input node
|
||||
// let updatedEdges = edges.filter((edge) => edge.source !== id && edge.target !== id)
|
||||
|
||||
// // Create new edges from the input node's parent to its children
|
||||
// const inputEdges = connectedEdges.filter((edge) => edge.target === id)
|
||||
// const outputEdges = connectedEdges.filter((edge) => edge.source === id)
|
||||
|
||||
// inputEdges.forEach((inputEdge) => {
|
||||
// outputEdges.forEach((outputEdge) => {
|
||||
// const newEdge = {
|
||||
// id: `edge:${inputEdge.source}->${outputEdge.target}`,
|
||||
// source: inputEdge.source,
|
||||
// target: outputEdge.target,
|
||||
// type: 'empty',
|
||||
// data: {
|
||||
// ...outputEdge.data,
|
||||
// sourceId: inputEdge.source,
|
||||
// targetId: outputEdge.target
|
||||
// }
|
||||
// }
|
||||
// updatedEdges.push(newEdge)
|
||||
// })
|
||||
// })
|
||||
|
||||
// // Update parent ids of the nodes
|
||||
// updatedNodes = updatedNodes.map((node) => {
|
||||
// if (node.data && node.data.parentIds && node.data.parentIds.includes(id)) {
|
||||
// const updatedParentIds = node.data.parentIds.filter((parentId) => parentId !== id)
|
||||
// if (inputNode.data && inputNode.data.parentIds) {
|
||||
// updatedParentIds.push(...inputNode.data.parentIds)
|
||||
// }
|
||||
// return {
|
||||
// ...node,
|
||||
// data: {
|
||||
// ...node.data,
|
||||
// parentIds: [...new Set(updatedParentIds)] // Remove duplicates
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return node
|
||||
// })
|
||||
|
||||
// return { nodes: updatedNodes, edges: updatedEdges }
|
||||
// }
|
||||
|
||||
// function processGraph(graph, simplifiable) {
|
||||
// let newGraph = { nodes: graph.nodes, edges: graph.edges }
|
||||
// newGraph = removeInputNode(newGraph.nodes, newGraph.edges, 'Input')
|
||||
// newGraph = removeInputNode(newGraph.nodes, newGraph.edges, simplifiable.forLoopNode.id)
|
||||
// newGraph = removeInputNode(newGraph.nodes, newGraph.edges, simplifiable.triggerNode.id)
|
||||
// return newGraph
|
||||
// }
|
||||
|
||||
function isSimplifiable(modules: FlowModule[] | undefined): boolean {
|
||||
if (!modules || modules?.length !== 2) {
|
||||
return false
|
||||
@@ -313,7 +265,7 @@
|
||||
|
||||
$nodes = layoutNodes(newGraph.nodes)
|
||||
$edges = newGraph.edges
|
||||
height = Math.max(...$nodes.map((n) => n.position.y + NODE.height + 40), minHeight)
|
||||
height = Math.max(...$nodes.map((n) => n.position.y + NODE.height + 100), minHeight)
|
||||
}
|
||||
|
||||
$: (graph || allowSimplifiedPoll) && updateStores()
|
||||
@@ -330,6 +282,7 @@
|
||||
whileLoopEnd: ForLoopEndNode,
|
||||
branchOneStart: BranchOneStart,
|
||||
branchOneEnd: BranchOneEndNode,
|
||||
subflowBound: SubflowBound,
|
||||
noBranch: NoBranchNode,
|
||||
trigger: TriggersNode
|
||||
} as any
|
||||
|
||||
@@ -15,6 +15,8 @@ export type GraphEventHandlers = {
|
||||
selectedIteration: (detail, moduleId: string) => void
|
||||
changeId: (newId: string) => void
|
||||
simplifyFlow: (detail: boolean) => void
|
||||
expandSubflow: (id: string, path: string) => void
|
||||
minimizeSubflow: (id: string) => void
|
||||
}
|
||||
|
||||
export type SimplifiableFlow = { simplifiedFlow: boolean }
|
||||
@@ -26,6 +28,10 @@ export function isTriggerStep(module: FlowModule | undefined): boolean {
|
||||
module.value.is_trigger === true
|
||||
)
|
||||
}
|
||||
|
||||
export function buildPrefix(prefix: string | undefined, id: string): string {
|
||||
return (prefix ?? '') + id + ':'
|
||||
}
|
||||
export function graphBuilder(
|
||||
modules: FlowModule[] | undefined,
|
||||
extra: Record<string, any>,
|
||||
@@ -37,7 +43,8 @@ export function graphBuilder(
|
||||
selectedId: string | undefined,
|
||||
moving: string | undefined,
|
||||
simplifiableFlow: SimplifiableFlow | undefined,
|
||||
flowPathForTriggerNode: string | undefined
|
||||
flowPathForTriggerNode: string | undefined,
|
||||
expandedSubflows: Record<string, FlowModule[]>
|
||||
// triggerProps?: {
|
||||
// path?: string
|
||||
// flowIsSimplifiable?: boolean
|
||||
@@ -61,6 +68,10 @@ export function graphBuilder(
|
||||
throw new Error(`Duplicated node detected: ${module.id}`)
|
||||
}
|
||||
|
||||
if (module.id.startsWith('subflow:')) {
|
||||
extra.insertable = false
|
||||
}
|
||||
|
||||
nodes.push({
|
||||
id: module.id,
|
||||
data: {
|
||||
@@ -114,6 +125,7 @@ export function graphBuilder(
|
||||
function addEdge(
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
prefix: string | undefined,
|
||||
options?: {
|
||||
disableInsert?: boolean
|
||||
customId?: string
|
||||
@@ -154,7 +166,7 @@ export function graphBuilder(
|
||||
// If the index is -1, it means that the target module is not in the modules array, so we set it to the length of the array
|
||||
index: index >= 0 ? index : mods?.length ?? 0,
|
||||
...extra,
|
||||
insertable: extra.insertable && !options?.disableInsert
|
||||
insertable: extra.insertable && !options?.disableInsert && prefix == undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -188,11 +200,11 @@ export function graphBuilder(
|
||||
}
|
||||
nodes.push(triggerNode)
|
||||
if (preprocessorModule != null && preprocessorModule != undefined) {
|
||||
addEdge('Trigger', preprocessorModule.id, {
|
||||
addEdge('Trigger', preprocessorModule.id, undefined, {
|
||||
type: 'empty'
|
||||
})
|
||||
} else {
|
||||
addEdge('Trigger', 'Input', {
|
||||
addEdge('Trigger', 'Input', undefined, {
|
||||
type: 'empty'
|
||||
})
|
||||
}
|
||||
@@ -220,16 +232,25 @@ export function graphBuilder(
|
||||
beforeNode: Node,
|
||||
nextNode: Node | undefined,
|
||||
simplifiedTriggerView: boolean,
|
||||
prefix: string | undefined,
|
||||
currentOffset = 0,
|
||||
disableMoveIds: string[] = [],
|
||||
parentIndex?: string,
|
||||
branchChosen?: boolean
|
||||
) {
|
||||
if (prefix != undefined) {
|
||||
modules.forEach((m) => {
|
||||
if (!m['oid']) {
|
||||
m['oid'] = m.id
|
||||
}
|
||||
m.id = 'subflow:' + prefix + m['oid']
|
||||
})
|
||||
}
|
||||
let previousId: string | undefined = undefined
|
||||
|
||||
if (modules.length === 0) {
|
||||
if (nextNode) {
|
||||
addEdge(beforeNode.id, nextNode.id, {
|
||||
addEdge(beforeNode.id, nextNode.id, prefix, {
|
||||
subModules: modules,
|
||||
disableMoveIds
|
||||
})
|
||||
@@ -239,8 +260,8 @@ export function graphBuilder(
|
||||
const localDisableMoveIds = [...disableMoveIds, module.id]
|
||||
|
||||
// Add the edge between the previous node and the current one
|
||||
if (index > 0 && previousId) {
|
||||
addEdge(previousId, module.id, {
|
||||
if (index > 0 && previousId && expandedSubflows[module.id] == undefined) {
|
||||
addEdge(previousId, module.id, prefix, {
|
||||
subModules: modules,
|
||||
disableMoveIds
|
||||
})
|
||||
@@ -284,10 +305,10 @@ export function graphBuilder(
|
||||
|
||||
nodes.push(startNode)
|
||||
|
||||
addEdge(module.id, startNode.id, {
|
||||
addEdge(module.id, startNode.id, prefix, {
|
||||
type: 'empty'
|
||||
})
|
||||
addEdge(startNode.id, endNode.id, {
|
||||
addEdge(startNode.id, endNode.id, prefix, {
|
||||
type: 'empty'
|
||||
})
|
||||
} else {
|
||||
@@ -311,7 +332,7 @@ export function graphBuilder(
|
||||
|
||||
nodes.push(startNode)
|
||||
|
||||
addEdge(module.id, startNode.id, {
|
||||
addEdge(module.id, startNode.id, prefix, {
|
||||
type: 'empty'
|
||||
})
|
||||
|
||||
@@ -320,6 +341,7 @@ export function graphBuilder(
|
||||
startNode,
|
||||
endNode,
|
||||
false,
|
||||
prefix,
|
||||
currentOffset,
|
||||
localDisableMoveIds,
|
||||
parentIndex ? `${parentIndex}-${index}-${branchIndex}` : `${index}-${branchIndex}`
|
||||
@@ -349,11 +371,11 @@ export function graphBuilder(
|
||||
}
|
||||
|
||||
if (!simplifiedTriggerView) {
|
||||
addEdge(module.id, startNode.id, {
|
||||
addEdge(module.id, startNode.id, prefix, {
|
||||
type: 'empty'
|
||||
})
|
||||
} else if (previousId) {
|
||||
addEdge(previousId, startNode.id, {
|
||||
addEdge(previousId, startNode.id, prefix, {
|
||||
type: 'empty'
|
||||
})
|
||||
}
|
||||
@@ -383,6 +405,7 @@ export function graphBuilder(
|
||||
startNode,
|
||||
endNode,
|
||||
false,
|
||||
prefix,
|
||||
currentOffset + 25,
|
||||
localDisableMoveIds,
|
||||
parentIndex
|
||||
@@ -406,7 +429,7 @@ export function graphBuilder(
|
||||
position: { x: -1, y: -1 },
|
||||
type: 'whileLoopStart'
|
||||
}
|
||||
addEdge(module.id, startNode.id, {
|
||||
addEdge(module.id, startNode.id, prefix, {
|
||||
type: 'empty'
|
||||
})
|
||||
|
||||
@@ -427,6 +450,7 @@ export function graphBuilder(
|
||||
startNode,
|
||||
endNode,
|
||||
false,
|
||||
prefix,
|
||||
currentOffset + 25,
|
||||
localDisableMoveIds,
|
||||
parentIndex
|
||||
@@ -465,7 +489,7 @@ export function graphBuilder(
|
||||
|
||||
nodes.push(defaultBranch)
|
||||
|
||||
addEdge(module.id, defaultBranch.id, { type: 'empty' })
|
||||
addEdge(module.id, defaultBranch.id, prefix, { type: 'empty' })
|
||||
|
||||
const branchChosen = extra.flowModuleStates?.[module.id]?.branchChosen
|
||||
processModules(
|
||||
@@ -473,6 +497,7 @@ export function graphBuilder(
|
||||
defaultBranch,
|
||||
endNode,
|
||||
false,
|
||||
prefix,
|
||||
currentOffset,
|
||||
localDisableMoveIds,
|
||||
parentIndex ? `${parentIndex}-${index}` : index.toString(),
|
||||
@@ -500,13 +525,14 @@ export function graphBuilder(
|
||||
|
||||
nodes.push(startNode)
|
||||
|
||||
addEdge(module.id, startNode.id, { type: 'empty' })
|
||||
addEdge(module.id, startNode.id, prefix, { type: 'empty' })
|
||||
|
||||
processModules(
|
||||
branch.modules,
|
||||
startNode,
|
||||
endNode,
|
||||
false,
|
||||
prefix,
|
||||
currentOffset,
|
||||
localDisableMoveIds,
|
||||
parentIndex ? `${parentIndex}-${index}` : index.toString(),
|
||||
@@ -516,13 +542,72 @@ export function graphBuilder(
|
||||
|
||||
previousId = endNode.id
|
||||
} else {
|
||||
addNode(module, currentOffset, 'module', modules)
|
||||
let expanded = expandedSubflows[module.id]
|
||||
if (expanded) {
|
||||
const startId = `${module.id}-subflow-start`
|
||||
const idWithoutPrefix = module.id.startsWith('subflow:')
|
||||
? module.id.substring(8)
|
||||
: module.id
|
||||
const startNode = {
|
||||
id: startId,
|
||||
data: {
|
||||
offset: currentOffset,
|
||||
label: `Start of subflow ${idWithoutPrefix}`,
|
||||
id: startId,
|
||||
subflowId: module.id,
|
||||
modules: modules,
|
||||
eventHandlers: eventHandlers,
|
||||
...extra
|
||||
},
|
||||
position: { x: -1, y: -1 },
|
||||
type: 'subflowBound'
|
||||
}
|
||||
|
||||
previousId = module.id
|
||||
nodes.push(startNode)
|
||||
|
||||
if (previousId) {
|
||||
addEdge(previousId!, startNode.id, prefix, { type: 'empty' })
|
||||
} else {
|
||||
addEdge(beforeNode.id, startNode.id, prefix, { type: 'empty' })
|
||||
}
|
||||
|
||||
const endId = `${module.id}-subflow-end`
|
||||
const endNode = {
|
||||
id: endId,
|
||||
data: {
|
||||
offset: currentOffset,
|
||||
label: `End of subflow ${idWithoutPrefix}`,
|
||||
id: endId,
|
||||
subflowId: module.id,
|
||||
modules: modules,
|
||||
eventHandlers: eventHandlers,
|
||||
...extra
|
||||
},
|
||||
position: { x: -1, y: -1 },
|
||||
type: 'subflowBound'
|
||||
}
|
||||
|
||||
nodes.push(endNode)
|
||||
|
||||
processModules(
|
||||
expanded,
|
||||
startNode,
|
||||
endNode,
|
||||
false,
|
||||
buildPrefix(prefix, module['oid'] ?? module.id),
|
||||
currentOffset,
|
||||
localDisableMoveIds
|
||||
)
|
||||
|
||||
previousId = endNode.id
|
||||
} else {
|
||||
addNode(module, currentOffset, 'module', modules)
|
||||
previousId = module.id
|
||||
}
|
||||
}
|
||||
|
||||
if (index === 0) {
|
||||
addEdge(beforeNode.id, module.id, {
|
||||
if (index === 0 && expandedSubflows[module.id] == undefined) {
|
||||
addEdge(beforeNode.id, module.id, prefix, {
|
||||
subModules: modules,
|
||||
disableMoveIds,
|
||||
disableInsert: simplifiedTriggerView
|
||||
@@ -530,7 +615,7 @@ export function graphBuilder(
|
||||
}
|
||||
|
||||
if (index === modules.length - 1 && previousId && nextNode) {
|
||||
addEdge(previousId, nextNode.id, {
|
||||
addEdge(previousId, nextNode.id, prefix, {
|
||||
subModules: modules,
|
||||
disableMoveIds
|
||||
})
|
||||
@@ -540,9 +625,9 @@ export function graphBuilder(
|
||||
}
|
||||
|
||||
if (simplifiableFlow?.simplifiedFlow === true && triggerNode) {
|
||||
processModules(modules, triggerNode, undefined, true)
|
||||
processModules(modules, triggerNode, undefined, true, undefined)
|
||||
} else {
|
||||
processModules(modules, inputNode, resultNode, false)
|
||||
processModules(modules, inputNode, resultNode, false, undefined)
|
||||
}
|
||||
|
||||
if (failureModule) {
|
||||
@@ -558,14 +643,14 @@ export function graphBuilder(
|
||||
|
||||
Object.entries(toAdd).forEach((x) => {
|
||||
addNode({ ...failureModule, id: x[1] }, 0, 'module')
|
||||
addEdge(x[0], x[1], { type: 'empty' })
|
||||
addEdge(x[0], x[1], undefined, { type: 'empty' })
|
||||
})
|
||||
}
|
||||
|
||||
if (preprocessorModule) {
|
||||
addNode(preprocessorModule, 0, 'module')
|
||||
const id = JSON.parse(JSON.stringify(preprocessorModule.id))
|
||||
addEdge(id, 'Input', { type: 'empty' })
|
||||
addEdge(id, 'Input', undefined, { type: 'empty' })
|
||||
}
|
||||
|
||||
if (failureModule && !extra.flowModuleStates) {
|
||||
@@ -598,7 +683,7 @@ export function graphBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
addEdge(pid, selectedId!, {
|
||||
addEdge(pid, selectedId!, undefined, {
|
||||
customId: `dep-${pid}-${selectedId}-${input}-${index}`,
|
||||
type: 'dataflowedge'
|
||||
})
|
||||
@@ -608,7 +693,7 @@ export function graphBuilder(
|
||||
Object.entries(deps.dependents).forEach((x, i) => {
|
||||
let pid = x[0]
|
||||
|
||||
addEdge(selectedId!, pid, {
|
||||
addEdge(selectedId!, pid, undefined, {
|
||||
customId: `dep-${selectedId}-${pid}-${i}`,
|
||||
type: 'dataflowedge'
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import MapItem from '$lib/components/flows/map/MapItem.svelte'
|
||||
import type { FlowModule, FlowModuleValue } from '$lib/gen'
|
||||
import { GitBranchPlus } from 'lucide-svelte'
|
||||
import { GitBranchPlus, Maximize2 } from 'lucide-svelte'
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import type { GraphEventHandlers } from '../../graphBuilder'
|
||||
import type { GraphModuleState } from '../../model'
|
||||
@@ -46,6 +46,19 @@
|
||||
</script>
|
||||
|
||||
<NodeWrapper offset={data.offset} let:darkMode>
|
||||
{#if data.module.value.type == 'flow'}
|
||||
<button
|
||||
title="Unexpand subflow"
|
||||
class="z-50 absolute -top-[10px] right-[25px] rounded-full h-[20px] w-[20px] center-center text-primary bg-surface duration-150 hover:bg-surface-hover"
|
||||
on:click|preventDefault|stopPropagation={() => {
|
||||
if (data.module.value.type == 'flow') {
|
||||
data.eventHandlers.expandSubflow(data.module.id, data.module.value.path)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Maximize2 size={12} />
|
||||
</button>
|
||||
{/if}
|
||||
<MapItem
|
||||
mod={data.module}
|
||||
insertable={data.insertable}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import { Minimize2 } from 'lucide-svelte'
|
||||
import type { GraphModuleState } from '../../model'
|
||||
import { getStateColor } from '../../util'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import type { GraphEventHandlers } from '../../graphBuilder'
|
||||
|
||||
export let data: {
|
||||
label: string
|
||||
preLabel: string | undefined
|
||||
insertable: boolean
|
||||
flowModuleStates: Record<string, GraphModuleState> | undefined
|
||||
subflowId: string
|
||||
id: string
|
||||
modules: FlowModule[]
|
||||
selected: boolean
|
||||
eventHandlers: GraphEventHandlers
|
||||
offset: number
|
||||
}
|
||||
</script>
|
||||
|
||||
<NodeWrapper let:darkMode offset={data.offset}>
|
||||
<VirtualItem
|
||||
label={data.label}
|
||||
preLabel={data.preLabel}
|
||||
selectable
|
||||
selected={data.selected}
|
||||
bgColor={getStateColor(undefined, darkMode)}
|
||||
borderColor={undefined}
|
||||
on:select={() => {
|
||||
data.eventHandlers.select(data.id)
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
title="Unexpand subflow"
|
||||
class="z-50 absolute -top-[10px] right-[25px] rounded-full h-[20px] w-[20px] center-center text-primary bg-surface duration-150 hover:bg-surface-hover"
|
||||
on:click|preventDefault|stopPropagation={() => {
|
||||
data.eventHandlers.minimizeSubflow(data.subflowId)
|
||||
}}
|
||||
>
|
||||
<Minimize2 size={12} />
|
||||
</button>
|
||||
</NodeWrapper>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user