Compare commits
23 Commits
rf/emptyLo
...
rf/dockerN
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a5b8e8344 | ||
|
|
ebc82dbe58 | ||
|
|
b3603d8720 | ||
|
|
61a3c81d5d | ||
|
|
d229d469a1 | ||
|
|
2f5fdd6b3f | ||
|
|
efe43ca3a8 | ||
|
|
37394d6d53 | ||
|
|
f353b91407 | ||
|
|
431074d249 | ||
|
|
8d2ddad9e6 | ||
|
|
c2a9ce46be | ||
|
|
9d06c152ee | ||
|
|
9ba4b071c8 | ||
|
|
734b6c0587 | ||
|
|
490114d133 | ||
|
|
af1b85f7d0 | ||
|
|
8f1343e155 | ||
|
|
31e002ad41 | ||
|
|
68596701f1 | ||
|
|
1a438e9751 | ||
|
|
e2953862af | ||
|
|
857adf5b63 |
62
.claude/settings.json
Normal file
62
.claude/settings.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Read(**/*.rs)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(head:*)",
|
||||
"Bash(tail:*)",
|
||||
"Bash(less:*)",
|
||||
"Bash(more:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(wc:*)",
|
||||
"Bash(diff:*)",
|
||||
"Bash(file:*)",
|
||||
"Bash(stat:*)",
|
||||
"Bash(tree:*)",
|
||||
"Bash(pwd)",
|
||||
"Bash(which:*)",
|
||||
"Bash(whereis:*)",
|
||||
"Bash(echo:*)",
|
||||
"Bash(git status:*)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git branch:*)",
|
||||
"Bash(git show:*)",
|
||||
"Bash(git blame:*)"
|
||||
],
|
||||
"deny": [
|
||||
"Read(.env)",
|
||||
"Read(.env.*)",
|
||||
"Read(**/.env)",
|
||||
"Read(**/.env.*)",
|
||||
"Read(**/secrets/**)",
|
||||
"Read(**/*.pem)",
|
||||
"Read(**/*.key)",
|
||||
"Read(**/credentials.json)",
|
||||
"Read(**/*secret*)",
|
||||
"Edit(.env)",
|
||||
"Edit(.env.*)",
|
||||
"Edit(**/.env)",
|
||||
"Edit(**/.env.*)"
|
||||
],
|
||||
"ask": [
|
||||
"Bash(rm:*)",
|
||||
"Bash(rmdir:*)",
|
||||
"Bash(mv:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(chown:*)",
|
||||
"Bash(truncate:*)",
|
||||
"Bash(shred:*)",
|
||||
"Bash(unlink:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git reset:*)",
|
||||
"Bash(git revert:*)",
|
||||
"Bash(git checkout:*)",
|
||||
"Bash(git merge:*)",
|
||||
"Bash(git rebase:*)"
|
||||
]
|
||||
},
|
||||
"enableAllProjectMcpServers": true
|
||||
}
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,5 +1,32 @@
|
||||
# Changelog
|
||||
|
||||
## [1.592.1](https://github.com/windmill-labs/windmill/compare/v1.592.0...v1.592.1) (2025-12-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ai:** improve share system prompts ([490114d](https://github.com/windmill-labs/windmill/commit/490114d133a08ef7f61ed216796b01fbec32a677))
|
||||
|
||||
## [1.592.0](https://github.com/windmill-labs/windmill/compare/v1.591.4...v1.592.0) (2025-12-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ai:** standardize and improve system prompts ([#7346](https://github.com/windmill-labs/windmill/issues/7346)) ([31e002a](https://github.com/windmill-labs/windmill/commit/31e002ad411bfbf08c933700bf5ae12b253ac0b8))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bun:** deployment error on workspace dependencies ([#7355](https://github.com/windmill-labs/windmill/issues/7355)) ([6859670](https://github.com/windmill-labs/windmill/commit/68596701f1e9da460d7bd3246dc797fca1e66a62))
|
||||
* clear datetime input in schedule sets input to null ([#7358](https://github.com/windmill-labs/windmill/issues/7358)) ([8f1343e](https://github.com/windmill-labs/windmill/commit/8f1343e155620b047e746da40653ec627f97a1a3))
|
||||
|
||||
## [1.591.4](https://github.com/windmill-labs/windmill/compare/v1.591.3...v1.591.4) (2025-12-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **app:** fix raw scripts forbidden by policy if no args ([857adf5](https://github.com/windmill-labs/windmill/commit/857adf5b63fe243736366c38a7e573678552a99b))
|
||||
|
||||
## [1.591.3](https://github.com/windmill-labs/windmill/compare/v1.591.2...v1.591.3) (2025-12-11)
|
||||
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ RUN mkdir /backend
|
||||
COPY /backend/windmill-api/openapi.yaml /backend/windmill-api/openapi.yaml
|
||||
COPY /openflow.openapi.yaml /openflow.openapi.yaml
|
||||
COPY /backend/windmill-api/build_openapi.sh /backend/windmill-api/build_openapi.sh
|
||||
COPY /system_prompts/auto-generated /system_prompts/auto-generated
|
||||
|
||||
RUN cd /backend/windmill-api && . ./build_openapi.sh
|
||||
COPY /backend/parsers/windmill-parser-wasm/pkg/ /backend/parsers/windmill-parser-wasm/pkg/
|
||||
|
||||
19
backend/.sqlx/query-034a8519198daf30e0eb8a74ed92f896c83bb39e1cb52fe3c29c1a224c3859c2.json
generated
Normal file
19
backend/.sqlx/query-034a8519198daf30e0eb8a74ed92f896c83bb39e1cb52fe3c29c1a224c3859c2.json
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_diff SET has_changes = true, exists_in_source = $5, exists_in_fork = $6\n WHERE path = $3 AND kind = $4 AND (\n (source_workspace_id = $1 AND fork_workspace_id = $2)\n OR (source_workspace_id = $2 AND fork_workspace_id =$1)\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "034a8519198daf30e0eb8a74ed92f896c83bb39e1cb52fe3c29c1a224c3859c2"
|
||||
}
|
||||
22
backend/.sqlx/query-03669873e4e3b22c737d5170821f677925474aad885bf1c0780bdb978225517e.json
generated
Normal file
22
backend/.sqlx/query-03669873e4e3b22c737d5170821f677925474aad885bf1c0780bdb978225517e.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE schedule SET enabled = false WHERE workspace_id = $1 AND enabled = true RETURNING path",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "03669873e4e3b22c737d5170821f677925474aad885bf1c0780bdb978225517e"
|
||||
}
|
||||
@@ -46,11 +46,11 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
|
||||
59
backend/.sqlx/query-0b8e5fe95f4a2855678ca041b50405b698a368626da42dd9f4ce9d0681d016a1.json
generated
Normal file
59
backend/.sqlx/query-0b8e5fe95f4a2855678ca041b50405b698a368626da42dd9f4ce9d0681d016a1.json
generated
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT path, kind, ahead, behind, has_changes, exists_in_source, exists_in_fork FROM workspace_diff\n WHERE source_workspace_id = $1 AND fork_workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "kind",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "ahead",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "behind",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "has_changes",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "exists_in_source",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "exists_in_fork",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "0b8e5fe95f4a2855678ca041b50405b698a368626da42dd9f4ce9d0681d016a1"
|
||||
}
|
||||
17
backend/.sqlx/query-0f689b9bd1c9a24f3c6cdafef0215f102122665bc3cc15718831b991052b4caf.json
generated
Normal file
17
backend/.sqlx/query-0f689b9bd1c9a24f3c6cdafef0215f102122665bc3cc15718831b991052b4caf.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM workspace_diff WHERE path = $3 AND kind = $4 AND (\n (source_workspace_id = $1 AND fork_workspace_id = $2)\n OR (source_workspace_id = $2 AND fork_workspace_id =$1)\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0f689b9bd1c9a24f3c6cdafef0215f102122665bc3cc15718831b991052b4caf"
|
||||
}
|
||||
23
backend/.sqlx/query-2d1ba3c92c0385c530934082284cf548a50d533dc1bef58dfd0ecc163c9920f3.json
generated
Normal file
23
backend/.sqlx/query-2d1ba3c92c0385c530934082284cf548a50d533dc1bef58dfd0ecc163c9920f3.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT path FROM script\n WHERE workspace_id = $1 AND path = ANY($2) AND archived = false",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2d1ba3c92c0385c530934082284cf548a50d533dc1bef58dfd0ecc163c9920f3"
|
||||
}
|
||||
35
backend/.sqlx/query-2d5ff8fc102ae0c452c1f9cd5cd30fd0a3b4e6e746c659da1767edafd139d45e.json
generated
Normal file
35
backend/.sqlx/query-2d5ff8fc102ae0c452c1f9cd5cd30fd0a3b4e6e746c659da1767edafd139d45e.json
generated
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value, is_secret, description\n FROM variable\n WHERE workspace_id = $1 AND path = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "is_secret",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "description",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2d5ff8fc102ae0c452c1f9cd5cd30fd0a3b4e6e746c659da1767edafd139d45e"
|
||||
}
|
||||
22
backend/.sqlx/query-4d04e436d46530f2f4c9dbc5c6c472581b9d90d330957539c0019ec2e7d9a68a.json
generated
Normal file
22
backend/.sqlx/query-4d04e436d46530f2f4c9dbc5c6c472581b9d90d330957539c0019ec2e7d9a68a.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id FROM workspace WHERE parent_workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4d04e436d46530f2f4c9dbc5c6c472581b9d90d330957539c0019ec2e7d9a68a"
|
||||
}
|
||||
23
backend/.sqlx/query-50ef2eac143273c7b7b7414b9be3c89130c709a57aca712e2af996bd4a4fa101.json
generated
Normal file
23
backend/.sqlx/query-50ef2eac143273c7b7b7414b9be3c89130c709a57aca712e2af996bd4a4fa101.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT path FROM app\n WHERE workspace_id = $1 AND path = ANY($2)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "50ef2eac143273c7b7b7414b9be3c89130c709a57aca712e2af996bd4a4fa101"
|
||||
}
|
||||
23
backend/.sqlx/query-5169d793f3820095839bb69d6c7c8cbea692ef6c1a838b970f382ee72db492d3.json
generated
Normal file
23
backend/.sqlx/query-5169d793f3820095839bb69d6c7c8cbea692ef6c1a838b970f382ee72db492d3.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT path FROM resource\n WHERE workspace_id = $1 AND path = ANY($2)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5169d793f3820095839bb69d6c7c8cbea692ef6c1a838b970f382ee72db492d3"
|
||||
}
|
||||
35
backend/.sqlx/query-66a9cb11fcc4757a3b35154840b703b87daa538cf3a72007649bbb074f56131e.json
generated
Normal file
35
backend/.sqlx/query-66a9cb11fcc4757a3b35154840b703b87daa538cf3a72007649bbb074f56131e.json
generated
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT app.summary, app.policy, app_version.value\n FROM app\n JOIN app_version\n ON app_version.id = app.versions[array_upper(app.versions, 1)]\n WHERE app.workspace_id = $1 AND app.path = $2 AND COALESCE(app.draft_only, false) = false",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "policy",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "value",
|
||||
"type_info": "Json"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "66a9cb11fcc4757a3b35154840b703b87daa538cf3a72007649bbb074f56131e"
|
||||
}
|
||||
35
backend/.sqlx/query-819c233915383e89af1bcf1a56c5f67c4e1fc217f216f609e36a9944a7807b33.json
generated
Normal file
35
backend/.sqlx/query-819c233915383e89af1bcf1a56c5f67c4e1fc217f216f609e36a9944a7807b33.json
generated
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value, description, resource_type\n FROM resource\n WHERE workspace_id = $1 AND path = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "resource_type",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "819c233915383e89af1bcf1a56c5f67c4e1fc217f216f609e36a9944a7807b33"
|
||||
}
|
||||
17
backend/.sqlx/query-90a1797f8d5ef7f9b67557c1fb919d1165860c5daf0de76fb2ac2201de11553c.json
generated
Normal file
17
backend/.sqlx/query-90a1797f8d5ef7f9b67557c1fb919d1165860c5daf0de76fb2ac2201de11553c.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace_diff (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n SELECT $1, unnest($2::varchar[]), $3, $4, 0, 1, NULL\n ON CONFLICT (source_workspace_id, fork_workspace_id, path, kind)\n DO UPDATE SET\n behind = workspace_diff.behind + 1,\n has_changes = NULL",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"VarcharArray",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "90a1797f8d5ef7f9b67557c1fb919d1165860c5daf0de76fb2ac2201de11553c"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE folder SET owners = array_remove(owners, $1::varchar) WHERE name = $2 AND workspace_id = $3 RETURNING name",
|
||||
"query": "UPDATE folder SET owners = array_remove(owners, $1::varchar) WHERE name = $2 AND workspace_id = $3 AND $1 = ANY(owners) RETURNING name",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -11,7 +11,7 @@
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
@@ -20,5 +20,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e500a422fa986faf5c612c08f4a139e10b25a3106551e7c92f43dcf9758a5ec7"
|
||||
"hash": "a3ca0af5d84acba93776828c090a79bf6554180b9db4951b69f7070f769b2962"
|
||||
}
|
||||
59
backend/.sqlx/query-a6fe41e36e06c88e1387abaea17107ceab2c5b17258958bde03b3e492bb2790e.json
generated
Normal file
59
backend/.sqlx/query-a6fe41e36e06c88e1387abaea17107ceab2c5b17258958bde03b3e492bb2790e.json
generated
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT hash, created_at, content, summary, description, lock, schema\n FROM script\n WHERE workspace_id = $1 AND path = $2 AND archived = false\n ORDER BY created_at DESC\n LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "hash",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "content",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "summary",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "lock",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "schema",
|
||||
"type_info": "Json"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a6fe41e36e06c88e1387abaea17107ceab2c5b17258958bde03b3e492bb2790e"
|
||||
}
|
||||
17
backend/.sqlx/query-ac3001bd72248efe36ae3c96af5450f0721f888d18475fae862d675d40779a30.json
generated
Normal file
17
backend/.sqlx/query-ac3001bd72248efe36ae3c96af5450f0721f888d18475fae862d675d40779a30.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO workspace_diff (source_workspace_id, fork_workspace_id, path, kind, ahead, behind, has_changes)\n VALUES ($1, $2, $3, $4, 1, 0, NULL)\n ON CONFLICT (source_workspace_id, fork_workspace_id, path, kind)\n DO UPDATE SET\n ahead = workspace_diff.ahead + 1,\n has_changes = NULL",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ac3001bd72248efe36ae3c96af5450f0721f888d18475fae862d675d40779a30"
|
||||
}
|
||||
15
backend/.sqlx/query-ad288f9f242f930fc571d04d5ba217f653437f60c890b6b73059fdaa7eb45302.json
generated
Normal file
15
backend/.sqlx/query-ad288f9f242f930fc571d04d5ba217f653437f60c890b6b73059fdaa7eb45302.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE workspace_settings\n SET git_app_installations = (\n SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)\n FROM jsonb_array_elements(git_app_installations) AS elem\n WHERE (elem->>'installation_id')::bigint != $1\n )\n WHERE workspace_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ad288f9f242f930fc571d04d5ba217f653437f60c890b6b73059fdaa7eb45302"
|
||||
}
|
||||
15
backend/.sqlx/query-af6aea925527c486e56375b6cd215b0e36938a5853285be485647bc8cc846973.json
generated
Normal file
15
backend/.sqlx/query-af6aea925527c486e56375b6cd215b0e36938a5853285be485647bc8cc846973.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM workspace_diff WHERE has_changes = false AND (\n (source_workspace_id = $1 AND fork_workspace_id = $2)\n OR (source_workspace_id = $2 AND fork_workspace_id =$1)\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "af6aea925527c486e56375b6cd215b0e36938a5853285be485647bc8cc846973"
|
||||
}
|
||||
49
backend/.sqlx/query-b11034489786626184e2f6b7f99469a945a4bbfe0e6a33c5a3e009dadd56d52c.json
generated
Normal file
49
backend/.sqlx/query-b11034489786626184e2f6b7f99469a945a4bbfe0e6a33c5a3e009dadd56d52c.json
generated
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, changed_by, changed_at, change_type, member_affected\n FROM group_permission_history\n WHERE workspace_id = $1 AND group_name = $2\n ORDER BY id DESC\n LIMIT $3 OFFSET $4",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "changed_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "changed_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "change_type",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "member_affected",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b11034489786626184e2f6b7f99469a945a4bbfe0e6a33c5a3e009dadd56d52c"
|
||||
}
|
||||
49
backend/.sqlx/query-b24eeafc2fc26664ef38d15d3cb47ca19549bcd9372d7a61ec2e28d82897bacf.json
generated
Normal file
49
backend/.sqlx/query-b24eeafc2fc26664ef38d15d3cb47ca19549bcd9372d7a61ec2e28d82897bacf.json
generated
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, changed_by, changed_at, change_type, affected\n FROM folder_permission_history\n WHERE workspace_id = $1 AND folder_name = $2\n ORDER BY id DESC\n LIMIT $3 OFFSET $4",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "changed_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "changed_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "change_type",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "affected",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b24eeafc2fc26664ef38d15d3cb47ca19549bcd9372d7a61ec2e28d82897bacf"
|
||||
}
|
||||
22
backend/.sqlx/query-b9c520fdc29b17977bf1c0b30311c536ebea8a76b75cda188ec91fcc0fc274a7.json
generated
Normal file
22
backend/.sqlx/query-b9c520fdc29b17977bf1c0b30311c536ebea8a76b75cda188ec91fcc0fc274a7.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id FROM v2_job_queue WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b9c520fdc29b17977bf1c0b30311c536ebea8a76b75cda188ec91fcc0fc274a7"
|
||||
}
|
||||
23
backend/.sqlx/query-c5897802334f09596a0297f9b9a1e24f3c6f8b1628965438a9360c073d70924e.json
generated
Normal file
23
backend/.sqlx/query-c5897802334f09596a0297f9b9a1e24f3c6f8b1628965438a9360c073d70924e.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT path FROM flow\n WHERE workspace_id = $1 AND path = ANY($2) AND archived = false",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c5897802334f09596a0297f9b9a1e24f3c6f8b1628965438a9360c073d70924e"
|
||||
}
|
||||
41
backend/.sqlx/query-dc6ffc9946325e794f3a9d1cf621a9c07d211351195c399f6ff25c1dc7adb01a.json
generated
Normal file
41
backend/.sqlx/query-dc6ffc9946325e794f3a9d1cf621a9c07d211351195c399f6ff25c1dc7adb01a.json
generated
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value, summary, description, schema\n FROM flow\n WHERE workspace_id = $1 AND path = $2 AND archived = false",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "summary",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "schema",
|
||||
"type_info": "Json"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "dc6ffc9946325e794f3a9d1cf621a9c07d211351195c399f6ff25c1dc7adb01a"
|
||||
}
|
||||
18
backend/.sqlx/query-f1206b00c4b81e59943385646d653330efba9cd1e731621f7efc91f04567841f.json
generated
Normal file
18
backend/.sqlx/query-f1206b00c4b81e59943385646d653330efba9cd1e731621f7efc91f04567841f.json
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO folder_permission_history\n (workspace_id, folder_name, changed_by, change_type, affected)\n VALUES ($1, $2, $3, $4, $5)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f1206b00c4b81e59943385646d653330efba9cd1e731621f7efc91f04567841f"
|
||||
}
|
||||
18
backend/.sqlx/query-f95358255e55d68dd453d173e23ab3cb1f1c2ba5b1cfc78f706b0b014f045477.json
generated
Normal file
18
backend/.sqlx/query-f95358255e55d68dd453d173e23ab3cb1f1c2ba5b1cfc78f706b0b014f045477.json
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO group_permission_history\n (workspace_id, group_name, changed_by, change_type, member_affected)\n VALUES ($1, $2, $3, $4, $5)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f95358255e55d68dd453d173e23ab3cb1f1c2ba5b1cfc78f706b0b014f045477"
|
||||
}
|
||||
23
backend/.sqlx/query-ff24cfb1f603b4ea612e48cee5880fc516207c8a474446a1ee09e8324f85ba09.json
generated
Normal file
23
backend/.sqlx/query-ff24cfb1f603b4ea612e48cee5880fc516207c8a474446a1ee09e8324f85ba09.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT path FROM variable\n WHERE workspace_id = $1 AND path = ANY($2)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ff24cfb1f603b4ea612e48cee5880fc516207c8a474446a1ee09e8324f85ba09"
|
||||
}
|
||||
60
backend/Cargo.lock
generated
60
backend/Cargo.lock
generated
@@ -2166,9 +2166,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.54"
|
||||
version = "0.1.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
|
||||
checksum = "d49d74c227b6cc9f3c51a2c7c667a05b6453f7f0f952a5f8e4493bb9e731d68e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
@@ -14147,7 +14147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3beec919fbdf99d719de8eda6adae3281f8a5b71ae40431f44dc7423053d34"
|
||||
dependencies = [
|
||||
"loki-api",
|
||||
"reqwest 0.12.24",
|
||||
"reqwest 0.11.27",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"snap",
|
||||
@@ -15165,7 +15165,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"aws-sdk-config",
|
||||
@@ -15227,7 +15227,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -15348,7 +15348,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -15363,7 +15363,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -15377,7 +15377,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -15396,7 +15396,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15489,7 +15489,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -15504,7 +15504,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -15528,7 +15528,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -15544,7 +15544,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -15553,7 +15553,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15565,7 +15565,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15577,7 +15577,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -15589,7 +15589,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15601,7 +15601,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15613,7 +15613,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -15624,7 +15624,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15635,7 +15635,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15648,7 +15648,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15672,7 +15672,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15686,7 +15686,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -15703,7 +15703,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15717,7 +15717,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15736,7 +15736,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -15747,7 +15747,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15784,7 +15784,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -15794,7 +15794,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -33,7 +33,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.591.3"
|
||||
version = "1.592.1"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
55e8296c3758b031e50d7ebdaef7c33666f02385
|
||||
4897449185f1af0f0323c931df288b5942b38fb8
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Add down migration script here
|
||||
DROP INDEX IF EXISTS idx_group_perm_history_workspace_group;
|
||||
DROP TABLE IF EXISTS group_permission_history;
|
||||
|
||||
DROP INDEX IF EXISTS idx_folder_perm_history_workspace_folder;
|
||||
DROP TABLE IF EXISTS folder_permission_history;
|
||||
@@ -0,0 +1,90 @@
|
||||
-- Add up migration script here
|
||||
|
||||
-- Folder permission changes history
|
||||
CREATE TABLE IF NOT EXISTS folder_permission_history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
workspace_id VARCHAR(50) NOT NULL,
|
||||
folder_name VARCHAR(255) NOT NULL,
|
||||
changed_by VARCHAR(50) NOT NULL,
|
||||
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
change_type VARCHAR(50) NOT NULL,
|
||||
affected VARCHAR(100),
|
||||
FOREIGN KEY (workspace_id, folder_name) REFERENCES folder(workspace_id, name) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_folder_perm_history_workspace_folder
|
||||
ON folder_permission_history(workspace_id, folder_name, id DESC);
|
||||
|
||||
-- Group permission changes history
|
||||
CREATE TABLE IF NOT EXISTS group_permission_history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
workspace_id VARCHAR(50) NOT NULL,
|
||||
group_name VARCHAR(255) NOT NULL,
|
||||
changed_by VARCHAR(50) NOT NULL,
|
||||
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
change_type VARCHAR(50) NOT NULL,
|
||||
member_affected VARCHAR(100),
|
||||
FOREIGN KEY (workspace_id, group_name) REFERENCES group_(workspace_id, name) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_group_perm_history_workspace_group
|
||||
ON group_permission_history(workspace_id, group_name, id DESC);
|
||||
|
||||
GRANT ALL ON TABLE folder_permission_history TO windmill_user;
|
||||
GRANT ALL ON TABLE group_permission_history TO windmill_user;
|
||||
GRANT ALL ON TABLE folder_permission_history TO windmill_admin;
|
||||
GRANT ALL ON TABLE group_permission_history TO windmill_admin;
|
||||
|
||||
-- Enable RLS on folder_permission_history
|
||||
ALTER TABLE folder_permission_history ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Admin policies (windmill_admin can always do everything)
|
||||
CREATE POLICY admin_all ON folder_permission_history FOR ALL TO windmill_admin USING (true) WITH CHECK (true);
|
||||
CREATE POLICY admin_all ON group_permission_history FOR ALL TO windmill_admin USING (true) WITH CHECK (true);
|
||||
|
||||
-- Enable RLS on group_permission_history
|
||||
ALTER TABLE group_permission_history ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS policies for folder_permission_history
|
||||
-- Anyone can insert
|
||||
CREATE POLICY allow_insert ON folder_permission_history FOR INSERT TO windmill_user WITH CHECK (true);
|
||||
|
||||
-- Select requires being in extra_perms (as user or via group)
|
||||
CREATE POLICY see_extra_perms_user ON folder_permission_history FOR SELECT TO windmill_user
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM folder f
|
||||
WHERE f.workspace_id = folder_permission_history.workspace_id
|
||||
AND f.name = folder_permission_history.folder_name
|
||||
AND f.extra_perms ? CONCAT('u/', current_setting('session.user'))
|
||||
));
|
||||
|
||||
CREATE POLICY see_extra_perms_groups ON folder_permission_history FOR SELECT TO windmill_user
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM folder f
|
||||
WHERE f.workspace_id = folder_permission_history.workspace_id
|
||||
AND f.name = folder_permission_history.folder_name
|
||||
AND f.extra_perms ?| regexp_split_to_array(current_setting('session.pgroups'), ',')::text[]
|
||||
));
|
||||
|
||||
-- RLS policies for group_permission_history
|
||||
-- Anyone can insert
|
||||
CREATE POLICY allow_insert ON group_permission_history FOR INSERT TO windmill_user WITH CHECK (true);
|
||||
|
||||
-- Select requires being in extra_perms (as user or via group)
|
||||
CREATE POLICY see_extra_perms_user ON group_permission_history FOR SELECT TO windmill_user
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM group_ g
|
||||
WHERE g.workspace_id = group_permission_history.workspace_id
|
||||
AND g.name = group_permission_history.group_name
|
||||
AND (g.extra_perms ->> CONCAT('u/', current_setting('session.user')))::boolean
|
||||
));
|
||||
|
||||
CREATE POLICY see_extra_perms_groups ON group_permission_history FOR SELECT TO windmill_user
|
||||
USING (EXISTS (
|
||||
SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f
|
||||
WHERE g.workspace_id = group_permission_history.workspace_id
|
||||
AND g.name = group_permission_history.group_name
|
||||
AND SPLIT_PART(f.key, '/', 1) = 'g'
|
||||
AND f.key = ANY(regexp_split_to_array(current_setting('session.pgroups'), ',')::text[])
|
||||
AND f.value::boolean
|
||||
));
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add down migration script here
|
||||
DROP TABLE IF EXISTS skip_workspace_diff_tally;
|
||||
DROP TABLE IF EXISTS workspace_diff;
|
||||
@@ -0,0 +1,24 @@
|
||||
-- Add up migration script here
|
||||
CREATE TABLE workspace_diff (
|
||||
source_workspace_id VARCHAR(50) NOT NULL,
|
||||
fork_workspace_id VARCHAR(50) NOT NULL,
|
||||
path VARCHAR(255) NOT NULL,
|
||||
kind VARCHAR(50) NOT NULL,
|
||||
ahead INTEGER NOT NULL DEFAULT 0,
|
||||
behind INTEGER NOT NULL DEFAULT 0,
|
||||
has_changes BOOLEAN DEFAULT NULL,
|
||||
exists_in_source BOOLEAN DEFAULT NULL,
|
||||
exists_in_fork BOOLEAN DEFAULT NULL,
|
||||
PRIMARY KEY (source_workspace_id, fork_workspace_id, path, kind)
|
||||
);
|
||||
|
||||
-- Create table to track workspaces that should be excluded from diff tallying
|
||||
-- Old workspaces that are linked but have already diverged need to be skipped
|
||||
CREATE TABLE skip_workspace_diff_tally (
|
||||
workspace_id VARCHAR(50) PRIMARY KEY,
|
||||
added_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Populate with all existing workspaces to exclude them from new tallying logic
|
||||
INSERT INTO skip_workspace_diff_tally (workspace_id)
|
||||
SELECT id FROM workspace;
|
||||
@@ -1 +1,2 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE email_trigger DROP COLUMN enabled;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.591.3
|
||||
version: 1.592.1
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -2024,6 +2024,52 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/compare/{target_workspace_id}:
|
||||
get:
|
||||
operationId: compareWorkspaces
|
||||
summary: Compare two workspaces
|
||||
description: Compares the current workspace with a target workspace to find differences in scripts, flows, apps, resources, and variables. Returns information about items that are ahead, behind, or in conflict.
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: target_workspace_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The ID of the workspace to compare with
|
||||
responses:
|
||||
"200":
|
||||
description: Workspace comparison results
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/WorkspaceComparison"
|
||||
|
||||
/w/{workspace}/workspaces/reset_diff_tally/{fork_workspace_id}:
|
||||
post:
|
||||
operationId: resetDiffTally
|
||||
summary: Resets the ahead and behind deployement counter after a deployement
|
||||
description: This endpoint should be called after a fork deployement
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: fork_workspace_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The ID of the workspace to compare with
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/users/exists/{email}:
|
||||
get:
|
||||
summary: exists email
|
||||
@@ -12975,6 +13021,40 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/groups_history/get/{name}:
|
||||
get:
|
||||
summary: get group permission history
|
||||
operationId: getGroupPermissionHistory
|
||||
tags:
|
||||
- group
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
responses:
|
||||
"200":
|
||||
description: group permission history
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
changed_by:
|
||||
type: string
|
||||
changed_at:
|
||||
type: string
|
||||
format: date-time
|
||||
change_type:
|
||||
type: string
|
||||
member_affected:
|
||||
type: string
|
||||
nullable: true
|
||||
|
||||
/w/{workspace}/folders/list:
|
||||
get:
|
||||
summary: list folders
|
||||
@@ -13238,6 +13318,40 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/folders_history/get/{name}:
|
||||
get:
|
||||
summary: get folder permission history
|
||||
operationId: getFolderPermissionHistory
|
||||
tags:
|
||||
- folder
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
responses:
|
||||
"200":
|
||||
description: folder permission history
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
changed_by:
|
||||
type: string
|
||||
changed_at:
|
||||
type: string
|
||||
format: date-time
|
||||
change_type:
|
||||
type: string
|
||||
affected:
|
||||
type: string
|
||||
nullable: true
|
||||
|
||||
/workers/list:
|
||||
get:
|
||||
summary: list workers
|
||||
@@ -20927,6 +21041,108 @@ components:
|
||||
type: boolean
|
||||
description: Whether operators can view workers page
|
||||
|
||||
WorkspaceComparison:
|
||||
type: object
|
||||
required:
|
||||
- all_ahead_items_visible
|
||||
- all_behind_items_visible
|
||||
- skipped_comparison
|
||||
- diffs
|
||||
- summary
|
||||
properties:
|
||||
all_ahead_items_visible:
|
||||
type: boolean
|
||||
description: All items with changes ahead are visible by the user of the request.
|
||||
all_behind_items_visible:
|
||||
type: boolean
|
||||
description: All items with changes behind are visible by the user of the request.
|
||||
skipped_comparison:
|
||||
type: boolean
|
||||
description: Whether the comparison was skipped. This happens with old forks that where not being kept track of
|
||||
diffs:
|
||||
type: array
|
||||
description: List of differences found between workspaces
|
||||
items:
|
||||
$ref: "#/components/schemas/WorkspaceItemDiff"
|
||||
summary:
|
||||
$ref: "#/components/schemas/CompareSummary"
|
||||
description: Summary statistics of the comparison
|
||||
|
||||
WorkspaceItemDiff:
|
||||
type: object
|
||||
required:
|
||||
- kind
|
||||
- path
|
||||
- ahead
|
||||
- behind
|
||||
- has_changes
|
||||
- exists_in_source
|
||||
- exists_in_fork
|
||||
properties:
|
||||
kind:
|
||||
type: string
|
||||
enum: ["script", "flow", "app", "resource", "variable"]
|
||||
description: Type of the item
|
||||
path:
|
||||
type: string
|
||||
description: Path of the item in the workspace
|
||||
ahead:
|
||||
type: integer
|
||||
description: Number of versions source is ahead of target
|
||||
behind:
|
||||
type: integer
|
||||
description: Number of versions source is behind target
|
||||
has_changes:
|
||||
type: boolean
|
||||
description: Whether the item has any differences
|
||||
exists_in_source:
|
||||
type: boolean
|
||||
description: If the item exists in the source workspace
|
||||
exists_in_fork:
|
||||
type: boolean
|
||||
description: If the item exists in the fork workspace
|
||||
|
||||
CompareSummary:
|
||||
type: object
|
||||
required:
|
||||
- total_diffs
|
||||
- total_ahead
|
||||
- total_behind
|
||||
- scripts_changed
|
||||
- flows_changed
|
||||
- apps_changed
|
||||
- resources_changed
|
||||
- variables_changed
|
||||
- conflicts
|
||||
properties:
|
||||
total_diffs:
|
||||
type: integer
|
||||
description: Total number of items with differences
|
||||
total_ahead:
|
||||
type: integer
|
||||
description: Total number of ahead changes
|
||||
total_behind:
|
||||
type: integer
|
||||
description: Total number of behind changes
|
||||
scripts_changed:
|
||||
type: integer
|
||||
description: Number of scripts with differences
|
||||
flows_changed:
|
||||
type: integer
|
||||
description: Number of flows with differences
|
||||
apps_changed:
|
||||
type: integer
|
||||
description: Number of apps with differences
|
||||
resources_changed:
|
||||
type: integer
|
||||
description: Number of resources with differences
|
||||
variables_changed:
|
||||
type: integer
|
||||
description: Number of variables with differences
|
||||
conflicts:
|
||||
type: integer
|
||||
description: Number of items that are both ahead and behind (conflicts)
|
||||
|
||||
TeamInfo:
|
||||
type: object
|
||||
required:
|
||||
@@ -21002,6 +21218,9 @@ components:
|
||||
per_page:
|
||||
type: number
|
||||
description: Number of repositories loaded per page
|
||||
error:
|
||||
type: string
|
||||
description: Error message if token retrieval failed
|
||||
required:
|
||||
- installation_id
|
||||
- account_id
|
||||
|
||||
68
backend/windmill-api/src/folder_history.rs
Normal file
68
backend/windmill-api/src/folder_history.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Author: Ruben Fiszel
|
||||
* Copyright: Windmill Labs, Inc 2022
|
||||
* This file and its contents are licensed under the AGPLv3 License.
|
||||
* Please see the included NOTICE for copyright information and
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::JsonResult,
|
||||
utils::{paginate, Pagination},
|
||||
};
|
||||
|
||||
use serde::Serialize;
|
||||
use sqlx::FromRow;
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new().route("/get/:name", get(get_folder_permission_history))
|
||||
}
|
||||
|
||||
#[derive(Serialize, FromRow)]
|
||||
pub struct FolderPermissionChange {
|
||||
pub id: i64,
|
||||
pub changed_by: String,
|
||||
pub changed_at: chrono::DateTime<chrono::Utc>,
|
||||
pub change_type: String,
|
||||
pub affected: Option<String>,
|
||||
}
|
||||
|
||||
async fn get_folder_permission_history(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
) -> JsonResult<Vec<FolderPermissionChange>> {
|
||||
// Check if user is owner of the folder (before starting transaction for performance)
|
||||
crate::folders::require_is_owner(&authed, &name)?;
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let (per_page, offset) = paginate(pagination);
|
||||
|
||||
let history = sqlx::query_as!(
|
||||
FolderPermissionChange,
|
||||
"SELECT id, changed_by, changed_at, change_type, affected
|
||||
FROM folder_permission_history
|
||||
WHERE workspace_id = $1 AND folder_name = $2
|
||||
ORDER BY id DESC
|
||||
LIMIT $3 OFFSET $4",
|
||||
w_id,
|
||||
name,
|
||||
per_page as i64,
|
||||
offset as i64
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(axum::Json(history))
|
||||
}
|
||||
@@ -261,6 +261,17 @@ async fn create_folder(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
log_folder_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
&ng.name,
|
||||
&authed.username,
|
||||
"create",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
@@ -332,6 +343,10 @@ async fn update_folder(
|
||||
|
||||
sqlb.set("edited_at", "now()");
|
||||
|
||||
// Track whether permission-related fields are being updated
|
||||
let owners_changed = ng.owners.is_some();
|
||||
let extra_perms_changed = ng.extra_perms.is_some();
|
||||
|
||||
if !authed.is_admin {
|
||||
let prefixed_username = format!("u/{}", authed.username);
|
||||
if ng.owners.as_ref().is_some_and(|x| {
|
||||
@@ -418,6 +433,31 @@ async fn update_folder(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Log permission changes if owners or extra_perms were updated
|
||||
if owners_changed {
|
||||
log_folder_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
&name,
|
||||
&authed.username,
|
||||
"update_owners",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if extra_perms_changed {
|
||||
log_folder_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
&name,
|
||||
&authed.username,
|
||||
"update_acl",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
@@ -675,6 +715,17 @@ async fn add_owner(
|
||||
Some([("owner", owner.as_str())].into()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
log_folder_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
&name,
|
||||
&authed.username,
|
||||
"grant_admin",
|
||||
Some(&owner),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(
|
||||
@@ -697,8 +748,8 @@ async fn remove_owner(
|
||||
not_found_if_none(get_folderopt(&mut tx, &w_id, &name).await?, "Folder", &name)?;
|
||||
require_is_owner(&authed, &name)?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE folder SET owners = array_remove(owners, $1::varchar) WHERE name = $2 AND workspace_id = $3 RETURNING name",
|
||||
let folder = sqlx::query!(
|
||||
"UPDATE folder SET owners = array_remove(owners, $1::varchar) WHERE name = $2 AND workspace_id = $3 AND $1 = ANY(owners) RETURNING name",
|
||||
owner,
|
||||
&name,
|
||||
&w_id,
|
||||
@@ -706,18 +757,29 @@ async fn remove_owner(
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if folder.is_none() && write.is_none() {
|
||||
return Ok(format!("Owner {} is already not a member of folder {}", owner, name));
|
||||
}
|
||||
|
||||
if let Some(write) = write {
|
||||
sqlx::query(&format!(
|
||||
"UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \
|
||||
true) WHERE name = $2 AND workspace_id = $3 RETURNING extra_perms"
|
||||
))
|
||||
let old_write = sqlx::query_scalar::<_, Option<bool>>(&format!(
|
||||
"UPDATE folder SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \
|
||||
true) FROM (SELECT (extra_perms->>'{owner}')::boolean as old_val FROM folder WHERE name = $2 AND workspace_id = $3) old \
|
||||
WHERE name = $2 AND workspace_id = $3 RETURNING old.old_val"
|
||||
))
|
||||
.bind(write)
|
||||
.bind(&name)
|
||||
.bind(&w_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
.await?
|
||||
.flatten();
|
||||
|
||||
if folder.is_none() && old_write.is_none_or(|ow| ow == write) {
|
||||
return Ok(format!("Owner {} is already not a member of folder {} and write permission was already {}", owner, name, write));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
@@ -728,6 +790,14 @@ async fn remove_owner(
|
||||
Some([("owner", owner.as_str())].into()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let change_type = match write {
|
||||
Some(true) => "grant_writer_only",
|
||||
Some(false) => "grant_viewer_only",
|
||||
None => "revoke_all",
|
||||
};
|
||||
log_folder_permission_change(&mut *tx, &w_id, &name, &authed.username, change_type, Some(&owner)).await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(
|
||||
@@ -737,3 +807,26 @@ async fn remove_owner(
|
||||
|
||||
Ok(format!("Removed {} to folder {}", owner, name))
|
||||
}
|
||||
|
||||
pub async fn log_folder_permission_change<'c, E: sqlx::Executor<'c, Database = Postgres>>(
|
||||
db: E,
|
||||
workspace_id: &str,
|
||||
folder_name: &str,
|
||||
changed_by: &str,
|
||||
change_type: &str,
|
||||
affected: Option<&str>,
|
||||
) -> Result<()> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO folder_permission_history
|
||||
(workspace_id, folder_name, changed_by, change_type, affected)
|
||||
VALUES ($1, $2, $3, $4, $5)",
|
||||
workspace_id,
|
||||
folder_name,
|
||||
changed_by,
|
||||
change_type,
|
||||
affected
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ async fn add_granular_acl(
|
||||
"UPDATE {kind} SET extra_perms = jsonb_set(extra_perms, $1, to_jsonb($2), \
|
||||
true) WHERE {identifier} = $3 AND workspace_id = $4 RETURNING extra_perms"
|
||||
))
|
||||
.bind(vec![owner])
|
||||
.bind(vec![owner.clone()])
|
||||
.bind(write.unwrap_or(false))
|
||||
.bind(path)
|
||||
.bind(&w_id)
|
||||
@@ -124,6 +124,39 @@ async fn add_granular_acl(
|
||||
.await?;
|
||||
|
||||
let _ = not_found_if_none(obj_o, &kind, &path)?;
|
||||
|
||||
if kind == "folder" {
|
||||
let change_type = if write.unwrap_or(false) {
|
||||
"grant_read"
|
||||
} else {
|
||||
"grant_write"
|
||||
};
|
||||
crate::folders::log_folder_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
path,
|
||||
&authed.username,
|
||||
change_type,
|
||||
Some(&owner),
|
||||
)
|
||||
.await?;
|
||||
} else if kind == "group_" {
|
||||
let change_type = if write.unwrap_or(false) {
|
||||
"grant_admin"
|
||||
} else {
|
||||
"grant_member_only"
|
||||
};
|
||||
crate::groups::log_group_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
path,
|
||||
&authed.username,
|
||||
change_type,
|
||||
Some(&owner),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
match kind {
|
||||
@@ -190,7 +223,7 @@ async fn remove_granular_acl(
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(GranularAcl { owner, write: _ }): Json<GranularAcl>,
|
||||
Json(GranularAcl { owner, .. }): Json<GranularAcl>,
|
||||
) -> Result<String> {
|
||||
let path = path.to_path();
|
||||
|
||||
@@ -225,73 +258,104 @@ async fn remove_granular_acl(
|
||||
require_owner_of_path(&authed, path)?;
|
||||
}
|
||||
|
||||
let obj_o = sqlx::query_scalar::<_, serde_json::Value>(&format!(
|
||||
"UPDATE {kind} SET extra_perms = extra_perms - $1 WHERE {identifier} = $2 AND \
|
||||
workspace_id = $3 RETURNING extra_perms"
|
||||
let obj_o = sqlx::query_scalar::<_, bool>(&format!(
|
||||
"WITH old AS (
|
||||
SELECT extra_perms->$1 as old_write FROM {kind}
|
||||
WHERE {identifier} = $2 AND workspace_id = $3 AND extra_perms ? $1
|
||||
)
|
||||
UPDATE {kind} SET extra_perms = extra_perms - $1
|
||||
WHERE {identifier} = $2 AND workspace_id = $3 AND extra_perms ? $1
|
||||
RETURNING (SELECT old_write FROM old)::bool"
|
||||
))
|
||||
.bind(owner)
|
||||
.bind(&owner)
|
||||
.bind(path)
|
||||
.bind(&w_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let _ = not_found_if_none(obj_o, &kind, &path)?;
|
||||
tx.commit().await?;
|
||||
|
||||
match kind {
|
||||
"folder" => {
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
// Only log if something was actually removed (obj_o is Some)
|
||||
if let Some(write) = obj_o {
|
||||
// Log permission changes for folders and groups
|
||||
if kind == "folder" {
|
||||
let change_type = if write { "revoke_write" } else { "revoke_read" };
|
||||
crate::folders::log_folder_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
DeployedObject::Folder { path: format!("f/{}", path) },
|
||||
Some(format!("Folder '{}' changed permissions", path)),
|
||||
true,
|
||||
path,
|
||||
&authed.username,
|
||||
change_type,
|
||||
Some(&owner),
|
||||
)
|
||||
.await?
|
||||
.await?;
|
||||
} else if kind == "group_" {
|
||||
crate::groups::log_group_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
path,
|
||||
&authed.username,
|
||||
"revoke_admin",
|
||||
Some(&owner),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
match kind {
|
||||
"folder" => {
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Folder { path: format!("f/{}", path) },
|
||||
Some(format!("Folder '{}' changed permissions", path)),
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
// "app" => {
|
||||
// handle_deployment_metadata(
|
||||
// &authed.email,
|
||||
// &authed.username,
|
||||
// &db,
|
||||
// &w_id,
|
||||
// DeployedObject::App { path: path.to_string(), parent_path: None, version: 0 },
|
||||
// Some(format!("App '{}' changed permissions", path)),
|
||||
// // true,
|
||||
// )
|
||||
// .await?
|
||||
// }
|
||||
// "script" => {
|
||||
// handle_deployment_metadata(
|
||||
// &authed.email,
|
||||
// &authed.username,
|
||||
// &db,
|
||||
// &w_id,
|
||||
// DeployedObject::Script {
|
||||
// path: path.to_string(),
|
||||
// parent_path: None,
|
||||
// hash: ScriptHash(0),
|
||||
// },
|
||||
// Some(format!("Script '{}' changed permissions", path)),
|
||||
// // true,
|
||||
// )
|
||||
// .await?
|
||||
// }
|
||||
// "flow" => {
|
||||
// handle_deployment_metadata(
|
||||
// &authed.email,
|
||||
// &authed.username,
|
||||
// &db,
|
||||
// &w_id,
|
||||
// DeployedObject::Flow { path: path.to_string(), parent_path: None },
|
||||
// Some(format!("Flow '{}' changed permissions", path)),
|
||||
// // true,
|
||||
// )
|
||||
// .await?
|
||||
// }
|
||||
_ => (),
|
||||
}
|
||||
// "app" => {
|
||||
// handle_deployment_metadata(
|
||||
// &authed.email,
|
||||
// &authed.username,
|
||||
// &db,
|
||||
// &w_id,
|
||||
// DeployedObject::App { path: path.to_string(), parent_path: None, version: 0 },
|
||||
// Some(format!("App '{}' changed permissions", path)),
|
||||
// // true,
|
||||
// )
|
||||
// .await?
|
||||
// }
|
||||
// "script" => {
|
||||
// handle_deployment_metadata(
|
||||
// &authed.email,
|
||||
// &authed.username,
|
||||
// &db,
|
||||
// &w_id,
|
||||
// DeployedObject::Script {
|
||||
// path: path.to_string(),
|
||||
// parent_path: None,
|
||||
// hash: ScriptHash(0),
|
||||
// },
|
||||
// Some(format!("Script '{}' changed permissions", path)),
|
||||
// // true,
|
||||
// )
|
||||
// .await?
|
||||
// }
|
||||
// "flow" => {
|
||||
// handle_deployment_metadata(
|
||||
// &authed.email,
|
||||
// &authed.username,
|
||||
// &db,
|
||||
// &w_id,
|
||||
// DeployedObject::Flow { path: path.to_string(), parent_path: None },
|
||||
// Some(format!("Flow '{}' changed permissions", path)),
|
||||
// // true,
|
||||
// )
|
||||
// .await?
|
||||
// }
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok("Successfully removed granular acl".to_string())
|
||||
|
||||
66
backend/windmill-api/src/group_history.rs
Normal file
66
backend/windmill-api/src/group_history.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Author: Ruben Fiszel
|
||||
* Copyright: Windmill Labs, Inc 2022
|
||||
* This file and its contents are licensed under the AGPLv3 License.
|
||||
* Please see the included NOTICE for copyright information and
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{Error, JsonResult},
|
||||
utils::{paginate, Pagination},
|
||||
};
|
||||
|
||||
use serde::Serialize;
|
||||
use sqlx::FromRow;
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new().route("/get/:name", get(get_group_permission_history))
|
||||
}
|
||||
|
||||
#[derive(Serialize, FromRow)]
|
||||
pub struct GroupPermissionChange {
|
||||
pub id: i64,
|
||||
pub changed_by: String,
|
||||
pub changed_at: chrono::DateTime<chrono::Utc>,
|
||||
pub change_type: String,
|
||||
pub member_affected: Option<String>,
|
||||
}
|
||||
|
||||
async fn get_group_permission_history(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
) -> JsonResult<Vec<GroupPermissionChange>> {
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let (per_page, offset) = paginate(pagination);
|
||||
|
||||
let history = sqlx::query_as!(
|
||||
GroupPermissionChange,
|
||||
"SELECT id, changed_by, changed_at, change_type, member_affected
|
||||
FROM group_permission_history
|
||||
WHERE workspace_id = $1 AND group_name = $2
|
||||
ORDER BY id DESC
|
||||
LIMIT $3 OFFSET $4",
|
||||
w_id,
|
||||
name,
|
||||
per_page as i64,
|
||||
offset as i64
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(axum::Json(history))
|
||||
}
|
||||
@@ -271,6 +271,16 @@ async fn create_group(
|
||||
)
|
||||
.await?;
|
||||
|
||||
log_group_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
&ng.name,
|
||||
&authed.username,
|
||||
"create",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
@@ -542,6 +552,17 @@ async fn update_group(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
log_group_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
&name,
|
||||
&authed.username,
|
||||
"update_summary",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
@@ -572,7 +593,7 @@ async fn add_user(
|
||||
|
||||
not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?;
|
||||
|
||||
sqlx::query!(
|
||||
let result = sqlx::query!(
|
||||
"INSERT INTO usr_to_group (workspace_id, usr, group_) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
|
||||
&w_id,
|
||||
user_username,
|
||||
@@ -581,6 +602,10 @@ async fn add_user(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Ok(format!("{} is already a member of group {}", user_username, name));
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
@@ -591,6 +616,17 @@ async fn add_user(
|
||||
Some([("user", user_username.as_str())].into()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
log_group_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
&name,
|
||||
&authed.username,
|
||||
"add_member",
|
||||
Some(&user_username),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
@@ -851,6 +887,16 @@ async fn remove_user(
|
||||
)
|
||||
.await?;
|
||||
|
||||
log_group_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
&name,
|
||||
&authed.username,
|
||||
"remove_member",
|
||||
Some(&user_username),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
@@ -981,3 +1027,26 @@ async fn overwrite_igroups() -> JsonResult<String> {
|
||||
"This feature is only available in the enterprise version".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn log_group_permission_change<'c, E: sqlx::Executor<'c, Database = Postgres>>(
|
||||
db: E,
|
||||
workspace_id: &str,
|
||||
group_name: &str,
|
||||
changed_by: &str,
|
||||
change_type: &str,
|
||||
member_affected: Option<&str>,
|
||||
) -> Result<()> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO group_permission_history
|
||||
(workspace_id, group_name, changed_by, change_type, member_affected)
|
||||
VALUES ($1, $2, $3, $4, $5)",
|
||||
workspace_id,
|
||||
group_name,
|
||||
changed_by,
|
||||
change_type,
|
||||
member_affected
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -94,8 +94,10 @@ mod favorite;
|
||||
mod flow_conversations;
|
||||
pub mod flows;
|
||||
mod folders;
|
||||
mod folder_history;
|
||||
mod granular_acls;
|
||||
mod groups;
|
||||
mod group_history;
|
||||
#[cfg(feature = "private")]
|
||||
pub mod indexer_ee;
|
||||
mod indexer_oss;
|
||||
@@ -466,7 +468,9 @@ pub async fn run_server(
|
||||
flow_conversations::workspaced_service(),
|
||||
)
|
||||
.nest("/folders", folders::workspaced_service())
|
||||
.nest("/folders_history", folder_history::workspaced_service())
|
||||
.nest("/groups", groups::workspaced_service())
|
||||
.nest("/groups_history", group_history::workspaced_service())
|
||||
.nest("/inputs", inputs::workspaced_service())
|
||||
.nest("/job_metrics", job_metrics::workspaced_service())
|
||||
.nest("/job_helpers", job_helpers_service)
|
||||
|
||||
@@ -29,7 +29,7 @@ use regex::Regex;
|
||||
|
||||
use hex;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use uuid::Uuid;
|
||||
use windmill_audit::audit_oss::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
@@ -172,11 +172,16 @@ pub fn workspaced_service() -> Router {
|
||||
post(acknowledge_all_critical_alerts),
|
||||
)
|
||||
.route("/critical_alerts/mute", post(mute_critical_alerts))
|
||||
.route("/operator_settings", post(update_operator_settings))
|
||||
.route(
|
||||
"/create_workspace_fork_branch",
|
||||
post(create_workspace_fork_branch),
|
||||
)
|
||||
.route("/operator_settings", post(update_operator_settings));
|
||||
.route(
|
||||
"/reset_diff_tally/:fork_workspace_id",
|
||||
post(reset_workspace_diffs),
|
||||
)
|
||||
.route("/compare/:target_workspace_id", get(compare_workspaces));
|
||||
|
||||
#[cfg(all(feature = "stripe", feature = "enterprise"))]
|
||||
{
|
||||
@@ -3322,11 +3327,73 @@ async fn archive_workspace(
|
||||
authed: ApiAuthed,
|
||||
) -> Result<String> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
// Step 1: Disable all schedules and clear their queued jobs
|
||||
let mut tx = db.begin().await?;
|
||||
let disabled_schedules = sqlx::query_scalar!(
|
||||
"UPDATE schedule SET enabled = false WHERE workspace_id = $1 AND enabled = true RETURNING path",
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let schedules_count = disabled_schedules.len();
|
||||
tracing::info!(
|
||||
"Disabled {} schedules in workspace {}",
|
||||
schedules_count,
|
||||
w_id
|
||||
);
|
||||
|
||||
// Clear all schedule-related jobs using the existing clear_schedule function
|
||||
for schedule_path in &disabled_schedules {
|
||||
crate::schedule::clear_schedule(&mut tx, schedule_path, &w_id).await?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
// Step 2: Get all remaining queued jobs for this workspace (non-schedule jobs)
|
||||
let jobs_to_cancel =
|
||||
sqlx::query_scalar!("SELECT id FROM v2_job_queue WHERE workspace_id = $1", &w_id)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
|
||||
let jobs_count = jobs_to_cancel.len();
|
||||
tracing::info!(
|
||||
"Found {} remaining jobs to cancel in workspace {}",
|
||||
jobs_count,
|
||||
w_id
|
||||
);
|
||||
|
||||
// Step 3: Cancel all remaining jobs using the existing cancel_jobs function
|
||||
let canceled_count = if !jobs_to_cancel.is_empty() {
|
||||
let axum::Json(canceled_jobs) = crate::jobs::cancel_jobs(
|
||||
jobs_to_cancel,
|
||||
&db,
|
||||
&authed.username,
|
||||
&w_id,
|
||||
false, // force_cancel
|
||||
)
|
||||
.await?;
|
||||
|
||||
let count = canceled_jobs.len();
|
||||
tracing::info!("Canceled {} jobs in workspace {}", count, w_id);
|
||||
count
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Step 4: Archive the workspace
|
||||
let mut tx = db.begin().await?;
|
||||
sqlx::query!("UPDATE workspace SET deleted = true WHERE id = $1", &w_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let mut audit_params = HashMap::new();
|
||||
audit_params.insert("disabled_schedules", schedules_count.to_string());
|
||||
audit_params.insert("canceled_jobs", canceled_count.to_string());
|
||||
let audit_params_refs: HashMap<&str, &str> =
|
||||
audit_params.iter().map(|(k, v)| (*k, v.as_str())).collect();
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
@@ -3334,12 +3401,15 @@ async fn archive_workspace(
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&authed.email),
|
||||
None,
|
||||
Some(audit_params_refs),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!("Archived workspace {}", &w_id))
|
||||
Ok(format!(
|
||||
"Archived workspace {}, disabled {} schedules and canceled {} jobs",
|
||||
&w_id, schedules_count, canceled_count
|
||||
))
|
||||
}
|
||||
|
||||
async fn leave_workspace(
|
||||
@@ -4042,3 +4112,611 @@ async fn update_operator_settings(
|
||||
|
||||
Ok("Operator settings updated successfully".to_string())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct WorkspaceComparison {
|
||||
pub all_ahead_items_visible: bool,
|
||||
pub all_behind_items_visible: bool,
|
||||
pub skipped_comparison: bool,
|
||||
pub diffs: Vec<WorkspaceDiffRow>,
|
||||
pub summary: CompareSummary,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct CompareSummary {
|
||||
pub total_diffs: usize,
|
||||
pub total_ahead: usize,
|
||||
pub total_behind: usize,
|
||||
pub scripts_changed: usize,
|
||||
pub flows_changed: usize,
|
||||
pub apps_changed: usize,
|
||||
pub resources_changed: usize,
|
||||
pub variables_changed: usize,
|
||||
pub conflicts: usize, // Items that are both ahead and behind
|
||||
}
|
||||
|
||||
async fn reset_workspace_diffs(
|
||||
authed: ApiAuthed,
|
||||
Path((w_id, target_workspace_id)): Path<(String, String)>,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
) -> JsonResult<()> {
|
||||
// Needed to compute the has_changes: Option<bool>. Otherwise it will be None, and the query will not hit the items
|
||||
let _ = compare_workspaces(
|
||||
authed,
|
||||
Path((w_id.clone(), target_workspace_id.clone())),
|
||||
Extension(db.clone()),
|
||||
Extension(user_db),
|
||||
)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM workspace_diff WHERE has_changes = false AND (
|
||||
(source_workspace_id = $1 AND fork_workspace_id = $2)
|
||||
OR (source_workspace_id = $2 AND fork_workspace_id =$1)
|
||||
)",
|
||||
target_workspace_id,
|
||||
w_id,
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
pub struct WorkspaceDiffRow {
|
||||
kind: String,
|
||||
path: String,
|
||||
ahead: i32,
|
||||
behind: i32,
|
||||
has_changes: Option<bool>,
|
||||
exists_in_source: Option<bool>,
|
||||
exists_in_fork: Option<bool>,
|
||||
}
|
||||
|
||||
async fn compare_workspaces(
|
||||
authed: ApiAuthed,
|
||||
Path((source_workspace_id, fork_workspace_id)): Path<(String, String)>,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
) -> JsonResult<WorkspaceComparison> {
|
||||
// require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let skipped_comparison: bool = sqlx::query_scalar(
|
||||
"SELECT EXISTS(
|
||||
SELECT 1 FROM skip_workspace_diff_tally
|
||||
WHERE workspace_id = $1
|
||||
)",
|
||||
)
|
||||
.bind(&fork_workspace_id)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
|
||||
if skipped_comparison {
|
||||
return Ok(Json(WorkspaceComparison {
|
||||
all_ahead_items_visible: true,
|
||||
all_behind_items_visible: true,
|
||||
skipped_comparison,
|
||||
diffs: vec![],
|
||||
summary: Default::default(),
|
||||
}));
|
||||
}
|
||||
|
||||
let diff_items = sqlx::query_as!(
|
||||
WorkspaceDiffRow,
|
||||
"SELECT path, kind, ahead, behind, has_changes, exists_in_source, exists_in_fork FROM workspace_diff
|
||||
WHERE source_workspace_id = $1 AND fork_workspace_id = $2",
|
||||
source_workspace_id,
|
||||
fork_workspace_id,
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
|
||||
let mut confirmed_diffs = vec![];
|
||||
for item in diff_items {
|
||||
if let Some(has_changes) = item.has_changes {
|
||||
if has_changes {
|
||||
confirmed_diffs.push(item);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let item_comparison = match item.kind.as_str() {
|
||||
"script" => Some(
|
||||
compare_two_scripts(&db, &source_workspace_id, &fork_workspace_id, &item.path)
|
||||
.await?,
|
||||
),
|
||||
"flow" => Some(
|
||||
compare_two_flows(&db, &source_workspace_id, &fork_workspace_id, &item.path)
|
||||
.await?,
|
||||
),
|
||||
"app" => Some(
|
||||
compare_two_apps(&db, &source_workspace_id, &fork_workspace_id, &item.path).await?,
|
||||
),
|
||||
"resource" => Some(
|
||||
compare_two_resources(&db, &source_workspace_id, &fork_workspace_id, &item.path)
|
||||
.await?,
|
||||
),
|
||||
"variable" => Some(
|
||||
compare_two_variables(&db, &source_workspace_id, &fork_workspace_id, &item.path)
|
||||
.await?,
|
||||
),
|
||||
k => {
|
||||
tracing::error!("Received unrecognized item kind `{k}` with path: `{}` while computing diff of {fork_workspace_id} and {source_workspace_id} workspaces. Skipping this item", item.path);
|
||||
None
|
||||
// Some(ItemComparison {
|
||||
// has_changes: true,
|
||||
// exists_in_source: true,
|
||||
// exists_in_fork: true,
|
||||
// })
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(item_comparison) = item_comparison {
|
||||
if item_comparison.has_changes {
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_diff SET has_changes = true, exists_in_source = $5, exists_in_fork = $6
|
||||
WHERE path = $3 AND kind = $4 AND (
|
||||
(source_workspace_id = $1 AND fork_workspace_id = $2)
|
||||
OR (source_workspace_id = $2 AND fork_workspace_id =$1)
|
||||
)",
|
||||
source_workspace_id,
|
||||
fork_workspace_id,
|
||||
item.path,
|
||||
item.kind,
|
||||
item_comparison.exists_in_source,
|
||||
item_comparison.exists_in_fork,
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
confirmed_diffs.push(WorkspaceDiffRow {
|
||||
has_changes: Some(item_comparison.has_changes),
|
||||
exists_in_source: Some(item_comparison.exists_in_source),
|
||||
exists_in_fork: Some(item_comparison.exists_in_fork),
|
||||
..item
|
||||
});
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"DELETE FROM workspace_diff WHERE path = $3 AND kind = $4 AND (
|
||||
(source_workspace_id = $1 AND fork_workspace_id = $2)
|
||||
OR (source_workspace_id = $2 AND fork_workspace_id =$1)
|
||||
)",
|
||||
source_workspace_id,
|
||||
fork_workspace_id,
|
||||
item.path,
|
||||
item.kind,
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let visible_diffs = filter_visible_diffs(
|
||||
&confirmed_diffs,
|
||||
&source_workspace_id,
|
||||
&fork_workspace_id,
|
||||
user_db.begin(&authed).await?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let summary = CompareSummary {
|
||||
total_diffs: visible_diffs.len(),
|
||||
total_ahead: visible_diffs
|
||||
.iter()
|
||||
.map(|s| s.ahead)
|
||||
.fold(0, |acc, s| acc + s.try_into().unwrap_or(0)),
|
||||
total_behind: visible_diffs
|
||||
.iter()
|
||||
.map(|s| s.behind)
|
||||
.fold(0, |acc, s| acc + s.try_into().unwrap_or(0)),
|
||||
scripts_changed: visible_diffs.iter().filter(|s| s.kind == "script").count(),
|
||||
flows_changed: visible_diffs.iter().filter(|s| s.kind == "flow").count(),
|
||||
apps_changed: visible_diffs.iter().filter(|s| s.kind == "app").count(),
|
||||
resources_changed: visible_diffs
|
||||
.iter()
|
||||
.filter(|s| s.kind == "resource")
|
||||
.count(),
|
||||
variables_changed: visible_diffs
|
||||
.iter()
|
||||
.filter(|s| s.kind == "variable")
|
||||
.count(),
|
||||
conflicts: visible_diffs
|
||||
.iter()
|
||||
.filter(|s| s.ahead > 0 && s.behind > 0)
|
||||
.count(),
|
||||
};
|
||||
|
||||
let all_ahead_items_visible = summary.total_ahead
|
||||
== confirmed_diffs
|
||||
.iter()
|
||||
.map(|s| s.ahead)
|
||||
.fold(0, |acc, s| acc + s.try_into().unwrap_or(0));
|
||||
let all_behind_items_visible = summary.total_behind
|
||||
== confirmed_diffs
|
||||
.iter()
|
||||
.map(|s| s.behind)
|
||||
.fold(0, |acc, s| acc + s.try_into().unwrap_or(0));
|
||||
|
||||
return Ok(Json(WorkspaceComparison {
|
||||
all_ahead_items_visible,
|
||||
all_behind_items_visible,
|
||||
skipped_comparison: false,
|
||||
diffs: visible_diffs,
|
||||
summary,
|
||||
}));
|
||||
}
|
||||
|
||||
async fn filter_visible_diffs(
|
||||
confirmed_diffs: &[WorkspaceDiffRow],
|
||||
source_workspace_id: &str,
|
||||
fork_workspace_id: &str,
|
||||
mut tx: Transaction<'static, Postgres>,
|
||||
) -> Result<Vec<WorkspaceDiffRow>> {
|
||||
// Step 1: Group paths by (workspace, kind)
|
||||
let mut source_items: HashMap<&str, Vec<&str>> = HashMap::new();
|
||||
let mut fork_items: HashMap<&str, Vec<&str>> = HashMap::new();
|
||||
|
||||
for diff in confirmed_diffs {
|
||||
if diff.exists_in_source.unwrap_or(false) {
|
||||
source_items.entry(&diff.kind).or_default().push(&diff.path);
|
||||
}
|
||||
if diff.exists_in_fork.unwrap_or(false) {
|
||||
fork_items.entry(&diff.kind).or_default().push(&diff.path);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Batch query for each (workspace, kind) combination
|
||||
let source_visible = query_visible_items(&mut tx, source_workspace_id, &source_items).await?;
|
||||
let fork_visible = query_visible_items(&mut tx, fork_workspace_id, &fork_items).await?;
|
||||
|
||||
// Step 3: Filter diffs based on visibility
|
||||
let visible_diffs: Vec<WorkspaceDiffRow> = confirmed_diffs
|
||||
.iter()
|
||||
.filter(|diff| {
|
||||
let v = (diff.kind.to_string(), diff.path.to_string());
|
||||
let source_ok = !diff.exists_in_source.unwrap_or(false) || source_visible.contains(&v);
|
||||
let fork_ok = !diff.exists_in_fork.unwrap_or(false) || fork_visible.contains(&v);
|
||||
source_ok && fork_ok
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
Ok(visible_diffs)
|
||||
}
|
||||
|
||||
async fn query_visible_items<'c>(
|
||||
tx: &mut Transaction<'c, Postgres>,
|
||||
workspace_id: &str,
|
||||
items_by_kind: &HashMap<&str, Vec<&str>>,
|
||||
) -> Result<HashSet<(String, String)>> {
|
||||
let mut visible = HashSet::new();
|
||||
|
||||
for (kind, paths) in items_by_kind {
|
||||
let paths_vec: Vec<String> = paths.iter().map(|s| s.to_string()).collect();
|
||||
|
||||
let results = match *kind {
|
||||
"script" => {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT path FROM script
|
||||
WHERE workspace_id = $1 AND path = ANY($2) AND archived = false",
|
||||
workspace_id,
|
||||
&paths_vec
|
||||
)
|
||||
.fetch_all(&mut **tx)
|
||||
.await?
|
||||
}
|
||||
"flow" => {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT path FROM flow
|
||||
WHERE workspace_id = $1 AND path = ANY($2) AND archived = false",
|
||||
workspace_id,
|
||||
&paths_vec
|
||||
)
|
||||
.fetch_all(&mut **tx)
|
||||
.await?
|
||||
}
|
||||
"app" => {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT path FROM app
|
||||
WHERE workspace_id = $1 AND path = ANY($2)",
|
||||
workspace_id,
|
||||
&paths_vec
|
||||
)
|
||||
.fetch_all(&mut **tx)
|
||||
.await?
|
||||
}
|
||||
"resource" => {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT path FROM resource
|
||||
WHERE workspace_id = $1 AND path = ANY($2)",
|
||||
workspace_id,
|
||||
&paths_vec
|
||||
)
|
||||
.fetch_all(&mut **tx)
|
||||
.await?
|
||||
}
|
||||
"variable" => {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT path FROM variable
|
||||
WHERE workspace_id = $1 AND path = ANY($2)",
|
||||
workspace_id,
|
||||
&paths_vec
|
||||
)
|
||||
.fetch_all(&mut **tx)
|
||||
.await?
|
||||
}
|
||||
_ => vec![], // Unknown kind
|
||||
};
|
||||
|
||||
for path in results {
|
||||
visible.insert((kind.to_string(), path));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(visible)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ItemComparison {
|
||||
has_changes: bool,
|
||||
exists_in_source: bool,
|
||||
exists_in_fork: bool,
|
||||
}
|
||||
|
||||
async fn compare_two_scripts(
|
||||
db: &DB,
|
||||
source_workspace_id: &str,
|
||||
fork_workspace_id: &str,
|
||||
path: &str,
|
||||
) -> Result<ItemComparison> {
|
||||
// Get latest script from each workspace
|
||||
let source_script = sqlx::query!(
|
||||
"SELECT hash, created_at, content, summary, description, lock, schema
|
||||
FROM script
|
||||
WHERE workspace_id = $1 AND path = $2 AND archived = false
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1",
|
||||
source_workspace_id,
|
||||
path
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let target_script = sqlx::query!(
|
||||
"SELECT hash, created_at, content, summary, description, lock, schema
|
||||
FROM script
|
||||
WHERE workspace_id = $1 AND path = $2 AND archived = false
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1",
|
||||
fork_workspace_id,
|
||||
path
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let mut has_changes = false;
|
||||
|
||||
// Check metadata differences
|
||||
if let (Some(source), Some(target)) = (&source_script, &target_script) {
|
||||
if source.content != target.content
|
||||
|| source.summary != target.summary
|
||||
|| source.description != target.description
|
||||
|| source.lock != target.lock
|
||||
|| source.schema != target.schema
|
||||
{
|
||||
has_changes = true;
|
||||
}
|
||||
} else if source_script.is_some() || target_script.is_some() {
|
||||
// The script exists in one of source or target, but not the other, this is considered as a change
|
||||
has_changes = true
|
||||
}
|
||||
|
||||
return Ok(ItemComparison {
|
||||
has_changes,
|
||||
exists_in_source: source_script.is_some(),
|
||||
exists_in_fork: target_script.is_some(),
|
||||
});
|
||||
}
|
||||
|
||||
async fn compare_two_flows(
|
||||
db: &DB,
|
||||
source_workspace_id: &str,
|
||||
fork_workspace_id: &str,
|
||||
path: &str,
|
||||
) -> Result<ItemComparison> {
|
||||
// Get latest flow from each workspace
|
||||
let source_flow = sqlx::query!(
|
||||
"SELECT value, summary, description, schema
|
||||
FROM flow
|
||||
WHERE workspace_id = $1 AND path = $2 AND archived = false",
|
||||
source_workspace_id,
|
||||
path
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let target_flow = sqlx::query!(
|
||||
"SELECT value, summary, description, schema
|
||||
FROM flow
|
||||
WHERE workspace_id = $1 AND path = $2 AND archived = false",
|
||||
fork_workspace_id,
|
||||
path
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let mut has_changes = false;
|
||||
|
||||
// Check metadata differences
|
||||
if let (Some(source), Some(target)) = (&source_flow, &target_flow) {
|
||||
if source.value != target.value
|
||||
|| source.summary != target.summary
|
||||
|| source.description != target.description
|
||||
|| source.schema != target.schema
|
||||
{
|
||||
has_changes = true;
|
||||
}
|
||||
} else if source_flow.is_some() || target_flow.is_some() {
|
||||
// The flow exists in one of source or target, but not the other, this is considered as a change
|
||||
has_changes = true
|
||||
}
|
||||
|
||||
return Ok(ItemComparison {
|
||||
has_changes,
|
||||
exists_in_source: source_flow.is_some(),
|
||||
exists_in_fork: target_flow.is_some(),
|
||||
});
|
||||
}
|
||||
|
||||
async fn compare_two_apps(
|
||||
db: &DB,
|
||||
source_workspace_id: &str,
|
||||
fork_workspace_id: &str,
|
||||
path: &str,
|
||||
) -> Result<ItemComparison> {
|
||||
// Get app with its latest version data from source workspace
|
||||
let source_app = sqlx::query!(
|
||||
"SELECT app.summary, app.policy, app_version.value
|
||||
FROM app
|
||||
JOIN app_version
|
||||
ON app_version.id = app.versions[array_upper(app.versions, 1)]
|
||||
WHERE app.workspace_id = $1 AND app.path = $2 AND COALESCE(app.draft_only, false) = false",
|
||||
source_workspace_id,
|
||||
path
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let target_app = sqlx::query!(
|
||||
"SELECT app.summary, app.policy, app_version.value
|
||||
FROM app
|
||||
JOIN app_version
|
||||
ON app_version.id = app.versions[array_upper(app.versions, 1)]
|
||||
WHERE app.workspace_id = $1 AND app.path = $2 AND COALESCE(app.draft_only, false) = false",
|
||||
fork_workspace_id,
|
||||
path
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let mut has_changes = false;
|
||||
|
||||
// Check metadata and content differences
|
||||
if let (Some(source), Some(target)) = (&source_app, &target_app) {
|
||||
if source.summary != target.summary
|
||||
|| source.policy != target.policy
|
||||
|| source.value != target.value
|
||||
{
|
||||
has_changes = true;
|
||||
}
|
||||
} else if source_app.is_some() || target_app.is_some() {
|
||||
// The app exists in one of source or target, but not the other, this is considered as a change
|
||||
has_changes = true
|
||||
}
|
||||
|
||||
return Ok(ItemComparison {
|
||||
has_changes,
|
||||
exists_in_source: source_app.is_some(),
|
||||
exists_in_fork: target_app.is_some(),
|
||||
});
|
||||
}
|
||||
|
||||
async fn compare_two_resources(
|
||||
db: &DB,
|
||||
source_workspace_id: &str,
|
||||
fork_workspace_id: &str,
|
||||
path: &str,
|
||||
) -> Result<ItemComparison> {
|
||||
// Get resource from each workspace
|
||||
let source_resource = sqlx::query!(
|
||||
"SELECT value, description, resource_type
|
||||
FROM resource
|
||||
WHERE workspace_id = $1 AND path = $2",
|
||||
source_workspace_id,
|
||||
path
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let target_resource = sqlx::query!(
|
||||
"SELECT value, description, resource_type
|
||||
FROM resource
|
||||
WHERE workspace_id = $1 AND path = $2",
|
||||
fork_workspace_id,
|
||||
path
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let mut has_changes = false;
|
||||
|
||||
// Check metadata differences
|
||||
if let (Some(source), Some(target)) = (&source_resource, &target_resource) {
|
||||
if source.value != target.value
|
||||
|| source.description != target.description
|
||||
|| source.resource_type != target.resource_type
|
||||
{
|
||||
has_changes = true;
|
||||
}
|
||||
} else if source_resource.is_some() || target_resource.is_some() {
|
||||
// The resource exists in one of source or target, but not the other, this is considered as a change
|
||||
has_changes = true
|
||||
}
|
||||
|
||||
return Ok(ItemComparison {
|
||||
has_changes,
|
||||
exists_in_source: source_resource.is_some(),
|
||||
exists_in_fork: target_resource.is_some(),
|
||||
});
|
||||
}
|
||||
|
||||
async fn compare_two_variables(
|
||||
db: &DB,
|
||||
source_workspace_id: &str,
|
||||
fork_workspace_id: &str,
|
||||
path: &str,
|
||||
) -> Result<ItemComparison> {
|
||||
// Get variable from each workspace
|
||||
let source_variable = sqlx::query!(
|
||||
"SELECT value, is_secret, description
|
||||
FROM variable
|
||||
WHERE workspace_id = $1 AND path = $2",
|
||||
source_workspace_id,
|
||||
path
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let target_variable = sqlx::query!(
|
||||
"SELECT value, is_secret, description
|
||||
FROM variable
|
||||
WHERE workspace_id = $1 AND path = $2",
|
||||
fork_workspace_id,
|
||||
path
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let mut has_changes = false;
|
||||
|
||||
// Check metadata differences
|
||||
if let (Some(source), Some(target)) = (&source_variable, &target_variable) {
|
||||
if source.is_secret != target.is_secret
|
||||
|| source.value != target.value
|
||||
|| source.description != target.description
|
||||
{
|
||||
has_changes = true;
|
||||
}
|
||||
} else if source_variable.is_some() || target_variable.is_some() {
|
||||
// The variable exists in one of source or target, but not the other, this is considered as a change
|
||||
has_changes = true
|
||||
}
|
||||
|
||||
return Ok(ItemComparison {
|
||||
has_changes,
|
||||
exists_in_source: source_variable.is_some(),
|
||||
exists_in_fork: target_variable.is_some(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ lazy_static! {
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct AgentAuth {
|
||||
pub worker_group: String,
|
||||
pub suffix: Option<String>,
|
||||
pub suffix: Option<String>,
|
||||
pub tags: Vec<String>,
|
||||
pub exp: Option<usize>,
|
||||
}
|
||||
|
||||
@@ -102,4 +102,30 @@ impl DeployedObject {
|
||||
DeployedObject::Key { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_kind(&self) -> String {
|
||||
match self {
|
||||
DeployedObject::Script { .. } => "script",
|
||||
DeployedObject::Flow { .. } => "flow",
|
||||
DeployedObject::App { .. } => "app",
|
||||
DeployedObject::Folder { .. } => "folder",
|
||||
DeployedObject::Resource { .. } => "resource",
|
||||
DeployedObject::Variable { .. } => "variable",
|
||||
DeployedObject::Schedule { .. } => "schedule",
|
||||
DeployedObject::ResourceType { .. } => "resource_type",
|
||||
DeployedObject::User { .. } => "user",
|
||||
DeployedObject::Group { .. } => "group",
|
||||
DeployedObject::HttpTrigger { .. } => "http_trigger",
|
||||
DeployedObject::WebsocketTrigger { .. } => "websocket_trigger",
|
||||
DeployedObject::KafkaTrigger { .. } => "kafka_trigger",
|
||||
DeployedObject::NatsTrigger { .. } => "nats_trigger",
|
||||
DeployedObject::PostgresTrigger { .. } => "postgres_trigger",
|
||||
DeployedObject::MqttTrigger { .. } => "mqtt_trigger",
|
||||
DeployedObject::SqsTrigger { .. } => "sqs_trigger",
|
||||
DeployedObject::GcpTrigger { .. } => "gcp_trigger",
|
||||
DeployedObject::EmailTrigger { .. } => "email_trigger",
|
||||
DeployedObject::Settings { .. } => "settings",
|
||||
DeployedObject::Key { .. } => "key",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,6 +756,7 @@ pub async fn add_completed_job_error(
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.warn_after_seconds(10)
|
||||
.await?;
|
||||
Ok(result)
|
||||
}
|
||||
@@ -812,6 +813,7 @@ pub async fn add_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
has_stream,
|
||||
from_cache,
|
||||
)
|
||||
.warn_after_seconds(10)
|
||||
})
|
||||
.retry(
|
||||
ConstantBuilder::default()
|
||||
@@ -872,7 +874,7 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
) -> windmill_common::error::Result<(Option<Uuid>, i64, bool)> {
|
||||
// let start = std::time::Instant::now();
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
let mut tx = db.begin().warn_after_seconds(10).await?;
|
||||
|
||||
let job_id = queued_job.id;
|
||||
// tracing::error!("1 {:?}", start.elapsed());
|
||||
@@ -927,6 +929,7 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
/* $10 */ result_columns as Option<&Vec<String>>,
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.warn_after_seconds(10)
|
||||
.await
|
||||
.map_err(|e| Error::internal_err(format!("Could not add completed job {job_id}: {e:#}")))?;
|
||||
|
||||
@@ -938,6 +941,7 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
job_id
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.warn_after_seconds(10)
|
||||
.await
|
||||
.map_err(|e| Error::internal_err(format!("Could not add completed job {job_id}: {e:#}")))?
|
||||
.unwrap_or(false);
|
||||
@@ -963,6 +967,7 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
labels as Vec<String>
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.warn_after_seconds(10)
|
||||
.await
|
||||
.map_err(|e| Error::InternalErr(format!("Could not update job labels: {e:#}")))?;
|
||||
}
|
||||
@@ -986,6 +991,7 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
parent_job
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.warn_after_seconds(10)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
tracing::error!(
|
||||
@@ -998,7 +1004,7 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
// tracing::error!("Added completed job {:#?}", queued_job);
|
||||
|
||||
let mut _skip_downstream_error_handlers = false;
|
||||
tx = delete_job(tx, &job_id).await?;
|
||||
tx = delete_job(tx, &job_id).warn_after_seconds(10).await?;
|
||||
// tracing::error!("3 {:?}", start.elapsed());
|
||||
|
||||
if queued_job.is_flow_step() {
|
||||
@@ -1019,13 +1025,14 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
&queued_job.workspace_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.warn_after_seconds(10)
|
||||
.await?;
|
||||
if flow_is_done {
|
||||
let r = sqlx::query_scalar!(
|
||||
"UPDATE parallel_monitor_lock SET last_ping = now() WHERE parent_flow_id = $1 and job_id = $2 RETURNING 1",
|
||||
parent_job,
|
||||
&queued_job.id
|
||||
).fetch_optional(&mut *tx).await?;
|
||||
).fetch_optional(&mut *tx).warn_after_seconds(10).await?;
|
||||
if r.is_some() {
|
||||
tracing::info!(
|
||||
"parallel flow iteration is done, setting parallel monitor last ping lock for job {}",
|
||||
@@ -1039,8 +1046,9 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
let schedule_path = queued_job.schedule_path().unwrap();
|
||||
let script_path = queued_job.runnable_path.as_ref().unwrap();
|
||||
|
||||
let schedule =
|
||||
get_schedule_opt(&mut *tx, &queued_job.workspace_id, &schedule_path).await?;
|
||||
let schedule = get_schedule_opt(&mut *tx, &queued_job.workspace_id, &schedule_path)
|
||||
.warn_after_seconds(10)
|
||||
.await?;
|
||||
|
||||
if let Some(schedule) = schedule {
|
||||
#[cfg(feature = "enterprise")]
|
||||
@@ -1072,6 +1080,7 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
&queued_job.workspace_id
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.warn_after_seconds(10)
|
||||
.await?
|
||||
.flatten()
|
||||
.unwrap_or(false);
|
||||
@@ -1084,6 +1093,7 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
&script_path,
|
||||
&queued_job.workspace_id,
|
||||
))
|
||||
.warn_after_seconds(10)
|
||||
.await
|
||||
{
|
||||
match err {
|
||||
@@ -1106,6 +1116,7 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
queued_job.started_at.unwrap_or(chrono::Utc::now()),
|
||||
queued_job.priority,
|
||||
)
|
||||
.warn_after_seconds(10)
|
||||
.await
|
||||
{
|
||||
if !success {
|
||||
@@ -1122,6 +1133,7 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
err
|
||||
),
|
||||
)
|
||||
.warn_after_seconds(10)
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
@@ -1148,6 +1160,7 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
queued_job.id.hyphenated().to_string(),
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.warn_after_seconds(10)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::internal_err(format!(
|
||||
@@ -1162,6 +1175,7 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
queued_job.id,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.warn_after_seconds(10)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
@@ -1174,15 +1188,17 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
|
||||
sqlx::query!("DELETE FROM job_perms WHERE job_id = $1", job_id)
|
||||
.execute(&mut *tx)
|
||||
.warn_after_seconds(10)
|
||||
.await?;
|
||||
|
||||
if !success || has_stream {
|
||||
sqlx::query!("DELETE FROM job_result_stream_v2 WHERE job_id = $1", job_id)
|
||||
.execute(&mut *tx)
|
||||
.warn_after_seconds(10)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
tx.commit().warn_after_seconds(10).await?;
|
||||
|
||||
tracing::info!(
|
||||
%job_id,
|
||||
|
||||
@@ -27,6 +27,7 @@ use windmill_common::{
|
||||
client::AuthedClient,
|
||||
s3_helpers::BundleFormat,
|
||||
scripts::{id_to_codebase_info, CodebaseInfo},
|
||||
utils::WarnAfterExt,
|
||||
workspace_dependencies::WorkspaceDependenciesPrefetched,
|
||||
};
|
||||
|
||||
@@ -367,6 +368,7 @@ pub async fn install_bun_lockfile(
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.warn_after_seconds(10)
|
||||
.await?;
|
||||
} else {
|
||||
Box::into_pin(child_process.wait()).await?;
|
||||
|
||||
@@ -161,6 +161,7 @@ async fn process_jc(
|
||||
bench,
|
||||
)
|
||||
.instrument(span)
|
||||
.warn_after_seconds(10)
|
||||
.await;
|
||||
|
||||
if let Some(root_job) = root_job {
|
||||
@@ -291,6 +292,7 @@ pub fn start_background_processor(
|
||||
#[cfg(feature = "benchmark")]
|
||||
&mut bench,
|
||||
)
|
||||
.warn_after_seconds(10)
|
||||
.await;
|
||||
|
||||
if is_init_script_and_failure {
|
||||
@@ -544,6 +546,7 @@ pub async fn handle_receive_completed_job(
|
||||
#[cfg(feature = "benchmark")]
|
||||
bench,
|
||||
)
|
||||
.warn_after_seconds(10)
|
||||
.await;
|
||||
|
||||
match processed_completed_job {
|
||||
@@ -807,6 +810,7 @@ pub async fn handle_job_error(
|
||||
err_json.clone(),
|
||||
worker_name,
|
||||
)
|
||||
.warn_after_seconds(10)
|
||||
.await
|
||||
};
|
||||
|
||||
@@ -850,7 +854,9 @@ pub async fn handle_job_error(
|
||||
if let Err(err) = updated_flow {
|
||||
if let Some(parent_job_id) = job.parent_job {
|
||||
if let Ok(Some(parent_job)) =
|
||||
get_mini_completed_job(&parent_job_id, &job.workspace_id, db).await
|
||||
get_mini_completed_job(&parent_job_id, &job.workspace_id, db)
|
||||
.warn_after_seconds(10)
|
||||
.await
|
||||
{
|
||||
let e = json!({"message": err.to_string(), "name": "InternalErr"});
|
||||
append_logs(
|
||||
@@ -870,6 +876,7 @@ pub async fn handle_job_error(
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.warn_after_seconds(10)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2723,8 +2723,11 @@ async fn capture_dependency_job(
|
||||
.await?
|
||||
}
|
||||
ScriptLang::Bun | ScriptLang::Bunnative => {
|
||||
let wd_exist = workspace_dependencies.get_bun()?.is_some();
|
||||
// TODO: move inside gen_bun_lockfile
|
||||
write_file(job_dir, "main.ts", job_raw_code)?;
|
||||
if !wd_exist {
|
||||
write_file(job_dir, "main.ts", job_raw_code)?;
|
||||
}
|
||||
if let Some(lock) = gen_bun_lockfile(
|
||||
mem_peak,
|
||||
canceled_by,
|
||||
@@ -2743,20 +2746,22 @@ async fn capture_dependency_job(
|
||||
)
|
||||
.await?
|
||||
{
|
||||
crate::bun_executor::prebundle_bun_script(
|
||||
job_raw_code,
|
||||
&lock,
|
||||
script_path,
|
||||
job_id,
|
||||
w_id,
|
||||
Some(&db),
|
||||
&job_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
&token,
|
||||
&mut Some(occupancy_metrics),
|
||||
)
|
||||
.await?;
|
||||
if !wd_exist {
|
||||
crate::bun_executor::prebundle_bun_script(
|
||||
job_raw_code,
|
||||
&lock,
|
||||
script_path,
|
||||
job_id,
|
||||
w_id,
|
||||
Some(&db),
|
||||
&job_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
&token,
|
||||
&mut Some(occupancy_metrics),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
lock
|
||||
} else {
|
||||
|
||||
@@ -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.591.3";
|
||||
export const VERSION = "v1.592.1";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
4
cli/deno.lock
generated
4
cli/deno.lock
generated
@@ -65,6 +65,7 @@
|
||||
"jsr:@windmill-labs/cliffy-table@1.0.0-rc.5": "1.0.0-rc.5",
|
||||
"jsr:@windmill-labs/cliffy-table@^1.0.0-rc.5": "1.0.0-rc.5",
|
||||
"jsr:@windmill-labs/shared-utils@1.0.10": "1.0.10",
|
||||
"jsr:@windmill-labs/shared-utils@1.0.11": "1.0.11",
|
||||
"jsr:@windmill-labs/shared-utils@1.0.3": "1.0.3",
|
||||
"jsr:@windmill-labs/shared-utils@1.0.5": "1.0.5",
|
||||
"jsr:@windmill-labs/shared-utils@1.0.6": "1.0.6",
|
||||
@@ -363,6 +364,9 @@
|
||||
},
|
||||
"@windmill-labs/shared-utils@1.0.10": {
|
||||
"integrity": "bd1993eb8d693c8ba49da1618f82ff4601eeb59011b2cac13e664291f7a299d8"
|
||||
},
|
||||
"@windmill-labs/shared-utils@1.0.11": {
|
||||
"integrity": "4878a841480ad98213759495d72d40be1aebbbacc693f8aa9fc649127722580b"
|
||||
}
|
||||
},
|
||||
"npm": {
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
loadRunnablesFromBackend,
|
||||
writeRunnableToBackend,
|
||||
} from "./raw_apps.ts";
|
||||
import { replaceInlineScripts, AppFile as NormalAppFile } from "./apps.ts";
|
||||
import { replaceInlineScripts, AppFile as NormalAppFile } from "./app.ts";
|
||||
import {
|
||||
newPathAssigner,
|
||||
newRawAppPathAssigner,
|
||||
|
||||
@@ -28,7 +28,7 @@ import * as wmill from "../../../gen/services.gen.ts";
|
||||
import { resolveWorkspace } from "../../core/context.ts";
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
import { GLOBAL_CONFIG_OPT } from "../../core/conf.ts";
|
||||
import { replaceInlineScripts } from "./apps.ts";
|
||||
import { replaceInlineScripts } from "./app.ts";
|
||||
import { Runnable } from "./metadata.ts";
|
||||
import {
|
||||
APP_BACKEND_FOLDER,
|
||||
|
||||
@@ -15,7 +15,7 @@ import path from "node:path";
|
||||
|
||||
import { GlobalOptions, isSuperset } from "../../types.ts";
|
||||
|
||||
import { replaceInlineScripts, repopulateFields } from "./apps.ts";
|
||||
import { replaceInlineScripts, repopulateFields } from "./app.ts";
|
||||
import { createBundle, detectFrameworks } from "./bundle.ts";
|
||||
import { APP_BACKEND_FOLDER } from "./app_metadata.ts";
|
||||
import { writeIfChanged } from "../../utils/utils.ts";
|
||||
|
||||
@@ -1,2 +1,60 @@
|
||||
export { pullGitSyncSettings, pushGitSyncSettings } from "./index.ts";
|
||||
export { default } from "./index.ts";
|
||||
import { Command } from "../../../deps.ts";
|
||||
import { pullGitSyncSettings } from "./pull.ts";
|
||||
import { pushGitSyncSettings } from "./push.ts";
|
||||
|
||||
const command = new Command()
|
||||
.description(
|
||||
"Manage git-sync settings between local wmill.yaml and Windmill backend",
|
||||
)
|
||||
.command("pull")
|
||||
.description(
|
||||
"Pull git-sync settings from Windmill backend to local wmill.yaml",
|
||||
)
|
||||
.option(
|
||||
"--repository <repo:string>",
|
||||
"Specify repository path (e.g., u/user/repo)",
|
||||
)
|
||||
.option(
|
||||
"--default",
|
||||
"Write settings to top-level defaults instead of overrides",
|
||||
)
|
||||
.option("--replace", "Replace existing settings (non-interactive mode)")
|
||||
.option(
|
||||
"--override",
|
||||
"Add branch-specific override (non-interactive mode)",
|
||||
)
|
||||
.option("--diff", "Show differences without applying changes")
|
||||
.option("--json-output", "Output in JSON format")
|
||||
.option(
|
||||
"--with-backend-settings <json:string>",
|
||||
"Use provided JSON settings instead of querying backend (for testing)",
|
||||
)
|
||||
.option("--yes", "Skip interactive prompts and use default behavior")
|
||||
.option(
|
||||
"--promotion <branch:string>",
|
||||
"Use promotionOverrides from the specified branch instead of regular overrides"
|
||||
)
|
||||
.action(pullGitSyncSettings as any)
|
||||
.command("push")
|
||||
.description(
|
||||
"Push git-sync settings from local wmill.yaml to Windmill backend",
|
||||
)
|
||||
.option(
|
||||
"--repository <repo:string>",
|
||||
"Specify repository path (e.g., u/user/repo)",
|
||||
)
|
||||
.option("--diff", "Show what would be pushed without applying changes")
|
||||
.option("--json-output", "Output in JSON format")
|
||||
.option(
|
||||
"--with-backend-settings <json:string>",
|
||||
"Use provided JSON settings instead of querying backend (for testing)",
|
||||
)
|
||||
.option("--yes", "Skip interactive prompts and use default behavior")
|
||||
.option(
|
||||
"--promotion <branch:string>",
|
||||
"Use promotionOverrides from the specified branch instead of regular overrides"
|
||||
)
|
||||
.action(pushGitSyncSettings as any);
|
||||
|
||||
export { pullGitSyncSettings, pushGitSyncSettings };
|
||||
export default command;
|
||||
|
||||
@@ -1,60 +1,2 @@
|
||||
import { Command } from "../../../deps.ts";
|
||||
import { pullGitSyncSettings } from "./pull.ts";
|
||||
import { pushGitSyncSettings } from "./push.ts";
|
||||
|
||||
const command = new Command()
|
||||
.description(
|
||||
"Manage git-sync settings between local wmill.yaml and Windmill backend",
|
||||
)
|
||||
.command("pull")
|
||||
.description(
|
||||
"Pull git-sync settings from Windmill backend to local wmill.yaml",
|
||||
)
|
||||
.option(
|
||||
"--repository <repo:string>",
|
||||
"Specify repository path (e.g., u/user/repo)",
|
||||
)
|
||||
.option(
|
||||
"--default",
|
||||
"Write settings to top-level defaults instead of overrides",
|
||||
)
|
||||
.option("--replace", "Replace existing settings (non-interactive mode)")
|
||||
.option(
|
||||
"--override",
|
||||
"Add branch-specific override (non-interactive mode)",
|
||||
)
|
||||
.option("--diff", "Show differences without applying changes")
|
||||
.option("--json-output", "Output in JSON format")
|
||||
.option(
|
||||
"--with-backend-settings <json:string>",
|
||||
"Use provided JSON settings instead of querying backend (for testing)",
|
||||
)
|
||||
.option("--yes", "Skip interactive prompts and use default behavior")
|
||||
.option(
|
||||
"--promotion <branch:string>",
|
||||
"Use promotionOverrides from the specified branch instead of regular overrides"
|
||||
)
|
||||
.action(pullGitSyncSettings as any)
|
||||
.command("push")
|
||||
.description(
|
||||
"Push git-sync settings from local wmill.yaml to Windmill backend",
|
||||
)
|
||||
.option(
|
||||
"--repository <repo:string>",
|
||||
"Specify repository path (e.g., u/user/repo)",
|
||||
)
|
||||
.option("--diff", "Show what would be pushed without applying changes")
|
||||
.option("--json-output", "Output in JSON format")
|
||||
.option(
|
||||
"--with-backend-settings <json:string>",
|
||||
"Use provided JSON settings instead of querying backend (for testing)",
|
||||
)
|
||||
.option("--yes", "Skip interactive prompts and use default behavior")
|
||||
.option(
|
||||
"--promotion <branch:string>",
|
||||
"Use promotionOverrides from the specified branch instead of regular overrides"
|
||||
)
|
||||
.action(pushGitSyncSettings as any);
|
||||
|
||||
export { pullGitSyncSettings, pushGitSyncSettings };
|
||||
export default command;
|
||||
export { pullGitSyncSettings, pushGitSyncSettings } from "./gitsync-settings.ts";
|
||||
export { default } from "./gitsync-settings.ts";
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SCRIPT_GUIDANCE } from "../../guidance/script_guidance.ts";
|
||||
import { FLOW_GUIDANCE } from "../../guidance/flow_guidance.ts";
|
||||
import { getActiveWorkspaceOrFallback } from "../workspace/workspace.ts";
|
||||
import { generateRTNamespace } from "../resource-type/resource-type.ts";
|
||||
import { CLI_COMMANDS } from "../../guidance/prompts.ts";
|
||||
|
||||
export interface InitOptions {
|
||||
useDefault?: boolean;
|
||||
@@ -240,28 +241,12 @@ async function initAction(opts: InitOptions) {
|
||||
try {
|
||||
const scriptGuidanceContent = SCRIPT_GUIDANCE;
|
||||
const flowGuidanceContent = FLOW_GUIDANCE;
|
||||
const cliCommandsContent = CLI_COMMANDS;
|
||||
|
||||
// Create .cursor/rules directory
|
||||
await Deno.mkdir(".cursor/rules", { recursive: true });
|
||||
|
||||
// Create windmill.mdc file
|
||||
if (!(await Deno.stat(".cursor/rules/script.mdc").catch(() => null))) {
|
||||
// Create AGENTS.md file
|
||||
if (!(await Deno.stat("AGENTS.md").catch(() => null))) {
|
||||
await Deno.writeTextFile(
|
||||
".cursor/rules/script.mdc",
|
||||
scriptGuidanceContent
|
||||
);
|
||||
log.info(colors.green("Created .cursor/rules/script.mdc"));
|
||||
}
|
||||
|
||||
if (!(await Deno.stat(".cursor/rules/flow.mdc").catch(() => null))) {
|
||||
await Deno.writeTextFile(".cursor/rules/flow.mdc", flowGuidanceContent);
|
||||
log.info(colors.green("Created .cursor/rules/flow.mdc"));
|
||||
}
|
||||
|
||||
// Create CLAUDE.md file
|
||||
if (!(await Deno.stat("CLAUDE.md").catch(() => null))) {
|
||||
await Deno.writeTextFile(
|
||||
"CLAUDE.md",
|
||||
"AGENTS.md",
|
||||
`
|
||||
You are a helpful assistant that can help with Windmill scripts and flows creation.
|
||||
|
||||
@@ -270,8 +255,17 @@ ${scriptGuidanceContent}
|
||||
|
||||
## Flow Guidance
|
||||
${flowGuidanceContent}
|
||||
`
|
||||
|
||||
## CLI Commands
|
||||
${cliCommandsContent}
|
||||
`
|
||||
);
|
||||
log.info(colors.green("Created AGENTS.md"));
|
||||
}
|
||||
|
||||
// Create CLAUDE.md file, referencing AGENTS.md
|
||||
if (!(await Deno.stat("CLAUDE.md").catch(() => null))) {
|
||||
await Deno.writeTextFile("CLAUDE.md", "Instructions are in @AGENTS.md");
|
||||
log.info(colors.green("Created CLAUDE.md"));
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -75,7 +75,7 @@ import {
|
||||
} from "../../../windmill-utils-internal/src/path-utils/path-assigner.ts";
|
||||
import { extractInlineScripts as extractInlineScriptsForFlows } from "../../../windmill-utils-internal/src/inline-scripts/extractor.ts";
|
||||
import { generateFlowLockInternal } from "../flow/flow_metadata.ts";
|
||||
import { isExecutionModeAnonymous } from "../app/apps.ts";
|
||||
import { isExecutionModeAnonymous } from "../app/app.ts";
|
||||
import {
|
||||
APP_BACKEND_FOLDER,
|
||||
generateAppLocksInternal,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AIConfig, Config, GlobalSetting } from "../../gen/types.gen.ts";
|
||||
import { compareInstanceObjects, InstanceSyncOptions } from "../commands/instance/instance.ts";
|
||||
import { isSuperset } from "../types.ts";
|
||||
import { deepEqual } from "../utils/utils.ts";
|
||||
import { removeWorkerPrefix } from "../commands/worker-groups/worker_groups.ts";
|
||||
import { removeWorkerPrefix } from "../commands/worker-groups/worker-groups.ts";
|
||||
import { decrypt, encrypt } from "../utils/local_encryption.ts";
|
||||
|
||||
export interface SimplifiedSettings {
|
||||
|
||||
@@ -1,434 +1,17 @@
|
||||
export const FLOW_GUIDANCE = `
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
// CLI Flow Guidance - Uses centralized prompts from system_prompts/
|
||||
import * as prompts from "./prompts.ts";
|
||||
|
||||
# System Prompt: OpenFlow Workflow Generator
|
||||
|
||||
You are an expert at creating OpenFlow YAML specifications for Windmill workflows.
|
||||
// CLI-specific introduction
|
||||
const CLI_INTRO = `You are an expert at creating OpenFlow YAML specifications for Windmill workflows.
|
||||
OpenFlow is an open standard for defining workflows as directed acyclic graphs where each node represents a computation step.
|
||||
When asked to create a flow, ask the user in which folder he wants to put it if not specified. Then create a new folder in the specified folder, that ends with \`.flow\`. It should contain a \`.yaml\` file that contains the flow definition.
|
||||
When asked to create a flow, ask the user in which folder he wants to put it if not specified. Then create a new folder in the specified folder, that ends with \`.flow\`. It should contain a \`.yaml\` file that contains the flow definition.
|
||||
For rawscript type module in the flow, the content key should start with "!inline" followed by the path of the script containing the code. It should be put in the same folder as the flow.
|
||||
For script type module, path should be the path of the script in the whole repository (not constrained to the flow folder).
|
||||
You do not need to create .lock and .yaml files manually. Instead, you should run \`wmill flow generate-locks --yes\` to create them.
|
||||
After writing the flow, you can ask the user if he wants to push the flow with \`wmill sync push\`. Both should be run at the root of the repository.
|
||||
You do not need to create .lock and .yaml files manually. Instead, you should run \`wmill flow generate-locks --yes\` to create them.`;
|
||||
|
||||
## OpenFlow Structure
|
||||
// Assemble complete flow guidance
|
||||
export const FLOW_GUIDANCE = `
|
||||
${CLI_INTRO}
|
||||
|
||||
Every OpenFlow workflow must follow this root structure:
|
||||
|
||||
\`\`\`yaml
|
||||
summary: "Brief one-line description"
|
||||
description: "Optional detailed description"
|
||||
value:
|
||||
modules: [] # Array of workflow steps
|
||||
# Optional properties:
|
||||
failure_module: {} # Error handler
|
||||
preprocessor_module: {} # Runs before first step
|
||||
same_worker: false # Force same worker execution
|
||||
concurrent_limit: 0 # Limit concurrent executions
|
||||
concurrency_key: "string" # Custom concurrency grouping
|
||||
concurrency_time_window_s: 0
|
||||
custom_debounce_key: "key"
|
||||
debounce_delay_s: 0
|
||||
skip_expr: "javascript_expression" # Skip workflow condition
|
||||
cache_ttl: 0 # Cache results duration
|
||||
priority: 0 # Execution priority
|
||||
early_return: "javascript_expression" # Early termination condition
|
||||
schema: # JSON Schema for workflow inputs
|
||||
type: object
|
||||
properties: {}
|
||||
required: []
|
||||
\`\`\`
|
||||
|
||||
## Module Types
|
||||
|
||||
### 1. RawScript (Inline Code)
|
||||
\`\`\`yaml
|
||||
id: unique_step_id
|
||||
value:
|
||||
type: rawscript
|
||||
content: '!inline inline_script_1.inline_script.ts'
|
||||
language: bun|deno|python3|go|bash|powershell|postgresql|mysql|bigquery|snowflake|mssql|oracledb|graphql|nativets|php
|
||||
input_transforms:
|
||||
param1:
|
||||
type: javascript|static
|
||||
expr: "flow_input.name" # or for static: value: "fixed_value"
|
||||
# Optional properties:
|
||||
path: "optional/path"
|
||||
lock: "dependency_lock_content"
|
||||
tag: "version_tag"
|
||||
concurrent_limit: 0
|
||||
concurrency_time_window_s: 0
|
||||
custom_concurrency_key: "key"
|
||||
custom_debounce_key: "key"
|
||||
debounce_delay_s: 0
|
||||
is_trigger: false
|
||||
assets: []
|
||||
\`\`\`
|
||||
|
||||
### 2. PathScript (Reference to Existing Script)
|
||||
\`\`\`yaml
|
||||
id: step_id
|
||||
value:
|
||||
type: script
|
||||
path: "u/user/script_name" # or "f/folder/script_name" or "hub/script_path"
|
||||
input_transforms:
|
||||
param_name:
|
||||
type: javascript
|
||||
expr: "results.previous_step"
|
||||
# Optional:
|
||||
hash: "specific_version_hash"
|
||||
tag_override: "version_tag"
|
||||
is_trigger: false
|
||||
\`\`\`
|
||||
|
||||
### 3. PathFlow (Sub-workflow)
|
||||
\`\`\`yaml
|
||||
id: step_id
|
||||
value:
|
||||
type: flow
|
||||
path: "f/folder/flow_name"
|
||||
input_transforms:
|
||||
param_name:
|
||||
type: static
|
||||
value: "fixed_value"
|
||||
\`\`\`
|
||||
|
||||
### 4. ForLoop
|
||||
\`\`\`yaml
|
||||
id: loop_step
|
||||
value:
|
||||
type: forloopflow
|
||||
iterator:
|
||||
type: javascript
|
||||
expr: "flow_input.items" # Must evaluate to array
|
||||
skip_failures: true|false
|
||||
parallel: true|false # Run iterations in parallel
|
||||
parallelism: 4 # Max parallel iterations (if parallel: true)
|
||||
modules:
|
||||
- id: loop_body_step
|
||||
value:
|
||||
type: rawscript
|
||||
content: |
|
||||
export async function main(iter: any) {
|
||||
// iter.value contains current item
|
||||
// iter.index contains current index
|
||||
return iter.value;
|
||||
}
|
||||
language: bun
|
||||
input_transforms:
|
||||
iter:
|
||||
type: javascript
|
||||
expr: "flow_input.iter"
|
||||
\`\`\`
|
||||
|
||||
### 5. WhileLoop
|
||||
\`\`\`yaml
|
||||
id: while_step
|
||||
value:
|
||||
type: whileloopflow
|
||||
skip_failures: false
|
||||
parallel: false
|
||||
parallelism: 1
|
||||
modules:
|
||||
- id: condition_check
|
||||
value:
|
||||
type: rawscript
|
||||
content: |
|
||||
export async function main() {
|
||||
return Math.random() > 0.5; // Continue condition
|
||||
}
|
||||
language: bun
|
||||
input_transforms: {}
|
||||
\`\`\`
|
||||
|
||||
### 6. Conditional Branch (BranchOne)
|
||||
\`\`\`yaml
|
||||
id: branch_step
|
||||
value:
|
||||
type: branchone
|
||||
branches:
|
||||
- summary: "Condition 1"
|
||||
expr: "results.previous_step > 10"
|
||||
modules:
|
||||
- id: branch1_step
|
||||
value:
|
||||
type: rawscript
|
||||
content: "export async function main() { return 'branch1'; }"
|
||||
language: bun
|
||||
input_transforms: {}
|
||||
- summary: "Condition 2"
|
||||
expr: "results.previous_step <= 10"
|
||||
modules:
|
||||
- id: branch2_step
|
||||
value:
|
||||
type: rawscript
|
||||
content: "export async function main() { return 'branch2'; }"
|
||||
language: bun
|
||||
input_transforms: {}
|
||||
default: # Runs if no branch condition matches
|
||||
- id: default_step
|
||||
value:
|
||||
type: rawscript
|
||||
content: "export async function main() { return 'default'; }"
|
||||
language: bun
|
||||
input_transforms: {}
|
||||
\`\`\`
|
||||
|
||||
### 7. Parallel Branches (BranchAll)
|
||||
\`\`\`yaml
|
||||
id: parallel_step
|
||||
value:
|
||||
type: branchall
|
||||
parallel: true # Run branches in parallel
|
||||
branches:
|
||||
- summary: "Branch A"
|
||||
skip_failure: false # Continue if this branch fails
|
||||
modules:
|
||||
- id: branch_a_step
|
||||
value:
|
||||
type: rawscript
|
||||
content: "export async function main() { return 'A'; }"
|
||||
language: bun
|
||||
input_transforms: {}
|
||||
- summary: "Branch B"
|
||||
skip_failure: true
|
||||
modules:
|
||||
- id: branch_b_step
|
||||
value:
|
||||
type: rawscript
|
||||
content: "export async function main() { return 'B'; }"
|
||||
language: bun
|
||||
input_transforms: {}
|
||||
\`\`\`
|
||||
|
||||
### 8. Identity (Pass-through)
|
||||
\`\`\`yaml
|
||||
id: identity_step
|
||||
value:
|
||||
type: identity
|
||||
flow: false # Set to true if this represents a sub-flow
|
||||
\`\`\`
|
||||
|
||||
## Input Transforms & Data Flow
|
||||
|
||||
### JavaScript Expressions
|
||||
Reference data using these variables in \`expr\` fields:
|
||||
- \`flow_input.property_name\` - Access workflow inputs
|
||||
- \`results.step_id\` - Access outputs from previous steps
|
||||
- \`results.step_id.property\` - Access specific properties
|
||||
- \`flow_input.iter.value\` - Current iteration value (in loops)
|
||||
- \`flow_input.iter.index\` - Current iteration index (in loops)
|
||||
|
||||
### Static Values
|
||||
\`\`\`yaml
|
||||
input_transforms:
|
||||
param_name:
|
||||
type: static
|
||||
value: "fixed_string" # Can be string, number, boolean, object, array
|
||||
\`\`\`
|
||||
|
||||
### Resource References
|
||||
\`\`\`yaml
|
||||
input_transforms:
|
||||
database:
|
||||
type: static
|
||||
value: "$res:f/folder/my_database" # Reference to stored resource
|
||||
\`\`\`
|
||||
|
||||
## Advanced Module Properties
|
||||
|
||||
### Error Handling & Control Flow
|
||||
\`\`\`yaml
|
||||
id: step_id
|
||||
value: # ... module definition
|
||||
# Control flow options:
|
||||
stop_after_if:
|
||||
expr: "results.step_id.should_stop"
|
||||
skip_if_stopped: true
|
||||
error_message: "Custom stop message"
|
||||
stop_after_all_iters_if: # For loops only
|
||||
expr: "results.step_id.should_stop_loop"
|
||||
skip_if_stopped: false
|
||||
skip_if:
|
||||
expr: "results.step_id.should_skip"
|
||||
sleep:
|
||||
type: javascript
|
||||
expr: "flow_input.delay_seconds"
|
||||
continue_on_error: false # Continue workflow if this step fails
|
||||
delete_after_use: false # Clean up results after use
|
||||
|
||||
# Execution control:
|
||||
cache_ttl: 3600 # Cache results for 1 hour
|
||||
timeout: 300 # Step timeout in seconds
|
||||
priority: 0 # Higher numbers = higher priority
|
||||
mock:
|
||||
enabled: false
|
||||
return_value: "mocked_result"
|
||||
|
||||
# Suspend/Approval:
|
||||
suspend:
|
||||
required_events: 1 # Number of resume events needed
|
||||
timeout: 86400 # Timeout in seconds
|
||||
resume_form:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
approved:
|
||||
type: boolean
|
||||
user_auth_required: true
|
||||
user_groups_required:
|
||||
type: static
|
||||
value: ["admin"]
|
||||
self_approval_disabled: false
|
||||
hide_cancel: false
|
||||
continue_on_disapprove_timeout: false
|
||||
|
||||
# Retry configuration:
|
||||
retry:
|
||||
constant:
|
||||
attempts: 3
|
||||
seconds: 5
|
||||
# OR exponential backoff:
|
||||
# exponential:
|
||||
# attempts: 3
|
||||
# multiplier: 2
|
||||
# seconds: 1
|
||||
# random_factor: 10 # 0-100% jitter
|
||||
\`\`\`
|
||||
|
||||
## Special Modules
|
||||
|
||||
### Failure Handler (Error Handler)
|
||||
\`\`\`yaml
|
||||
value:
|
||||
failure_module:
|
||||
id: failure
|
||||
value:
|
||||
type: rawscript
|
||||
content: |
|
||||
export async function main(error: any) {
|
||||
// error.message, error.step_id, error.name, error.stack
|
||||
console.log("Flow failed:", error.message);
|
||||
return error;
|
||||
}
|
||||
language: bun
|
||||
input_transforms: {}
|
||||
\`\`\`
|
||||
|
||||
### Preprocessor
|
||||
\`\`\`yaml
|
||||
value:
|
||||
preprocessor_module:
|
||||
id: preprocessor
|
||||
value:
|
||||
type: rawscript
|
||||
content: |
|
||||
export async function main() {
|
||||
console.log("Flow starting...");
|
||||
return "preprocessed";
|
||||
}
|
||||
language: bun
|
||||
input_transforms: {}
|
||||
\`\`\`
|
||||
|
||||
## Schema Definition
|
||||
\`\`\`yaml
|
||||
schema:
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema"
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: "User name"
|
||||
default: ""
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
count:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
database:
|
||||
type: object
|
||||
format: "resource-postgresql" # Resource type reference
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required: ["name", "email"]
|
||||
order: ["name", "email", "count"] # UI field order
|
||||
\`\`\`
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Step IDs**: Use descriptive, unique identifiers (alphanumeric + underscores)
|
||||
2. **Data Flow**: Chain steps using \`results.step_id\` references
|
||||
3. **Error Handling**: Add failure_module for critical workflows
|
||||
4. **Languages**: Use \`bun\` for TypeScript (fastest), \`python3\` for Python
|
||||
5. **Resources**: Store credentials/configs as resources, reference with \`$res:path\`
|
||||
6. **Loops**: Prefer \`parallel: true\` for independent iterations
|
||||
7. **Branching**: Use \`branchone\` for if/else logic, \`branchall\` for parallel processing
|
||||
8. **Schemas**: Always define input schemas for better UX and validation
|
||||
|
||||
## Example Complete Workflow
|
||||
\`\`\`yaml
|
||||
summary: "Process user data"
|
||||
description: "Validates user input, processes data, and sends notifications"
|
||||
value:
|
||||
modules:
|
||||
- id: validate_input
|
||||
value:
|
||||
type: rawscript
|
||||
content: '!inline inline_script_0.inline_script.ts'
|
||||
# script at path inline_script_0.inline_script.ts will contain
|
||||
# export async function main(email: string, name: string) {
|
||||
# if (!email.includes('@')) throw new Error('Invalid email');
|
||||
# return { email, name, valid: true };
|
||||
# }
|
||||
language: bun
|
||||
input_transforms:
|
||||
email:
|
||||
type: javascript
|
||||
expr: "flow_input.email"
|
||||
name:
|
||||
type: javascript
|
||||
expr: "flow_input.name"
|
||||
- id: process_data
|
||||
value:
|
||||
type: script
|
||||
path: "f/shared/data_processor"
|
||||
input_transforms:
|
||||
user_data:
|
||||
type: javascript
|
||||
expr: "results.validate_input"
|
||||
- id: send_notification
|
||||
value:
|
||||
type: rawscript
|
||||
content: '!inline inline_script_1.inline_script.ts'
|
||||
# script at path inline_script_1.inline_script.ts will contain
|
||||
# export async function main(processed_data: any) {
|
||||
# console.log("Sending notification for:", processed_data.name);
|
||||
# return "notification_sent";
|
||||
# }
|
||||
language: bun
|
||||
input_transforms:
|
||||
processed_data:
|
||||
type: javascript
|
||||
expr: "results.process_data"
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: "User email address"
|
||||
name:
|
||||
type: string
|
||||
description: "User full name"
|
||||
required: ["email", "name"]
|
||||
\`\`\`
|
||||
|
||||
When generating OpenFlow YAML, ensure proper indentation, valid YAML syntax, and logical step dependencies. Always include meaningful summaries and proper input transforms to connect workflow steps.
|
||||
${prompts.FLOW_PROMPT}
|
||||
`;
|
||||
|
||||
2624
cli/src/guidance/prompts.ts
Normal file
2624
cli/src/guidance/prompts.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -1,439 +1,15 @@
|
||||
export const SCRIPT_GUIDANCE = `
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
// CLI Script Guidance - Uses centralized prompts from system_prompts/
|
||||
import * as prompts from "./prompts.ts";
|
||||
|
||||
# Windmill Script Writing Guide - Universal System Prompt
|
||||
|
||||
You are a coding assistant for the Windmill platform. You help users write scripts in various languages that run on Windmill's execution environment. Each script should be placed in a folder. Ask the user in which folder he wants the script to be located at before starting coding.
|
||||
// CLI-specific introduction
|
||||
const CLI_INTRO = `Each script should be placed in a folder. Ask the user in which folder he wants the script to be located at before starting coding.
|
||||
After writing a script, you do not need to create .lock and .yaml files manually. Instead, you can run \`wmill script generate-metadata\` bash command. This command takes no arguments. After writing the script, you can ask the user if he wants to push the script with \`wmill sync push\`. Both should be run at the root of the repository.
|
||||
|
||||
## General Principles
|
||||
You can use \`wmill resource-type list --schema\` to list all resource types available. You should use that to know the type of the resource you need to use in your script. You can use grep if the output is too long.`;
|
||||
|
||||
On Windmill, scripts are executed in isolated environments with specific conventions:
|
||||
// Assemble complete script guidance
|
||||
export const SCRIPT_GUIDANCE = `
|
||||
${CLI_INTRO}
|
||||
|
||||
- Scripts must export a main function
|
||||
- Do not call the main function
|
||||
- Libraries are installed automatically - do not show installation instructions
|
||||
- Credentials and configuration are stored in resources and passed as parameters
|
||||
- The windmill client (wmill) provides APIs for interacting with the platform
|
||||
- You can use \`wmill resource-type list --schema\` to list all resource types available. You should use that to know the type of the resource you need to use in your script. You can use grep if the output is too long.
|
||||
|
||||
## Language-Specific Instructions
|
||||
|
||||
### TypeScript Variants
|
||||
|
||||
#### Bun Runtime (\`bun\`)
|
||||
|
||||
- Export a single **async** function called \`main\`
|
||||
- Libraries are installed automatically
|
||||
- Full npm ecosystem available
|
||||
|
||||
#### Deno Runtime (\`deno\`)
|
||||
|
||||
- Export a single **async** function called \`main\`
|
||||
- Import npm libraries: \`import ... from "npm:{package}";\`
|
||||
- Import deno libraries normally
|
||||
- Libraries are installed automatically
|
||||
|
||||
#### TypeScript Resource Types & Windmill Client
|
||||
|
||||
**Resource Types:**
|
||||
On Windmill, credentials and configuration are stored in resources and passed as parameters to main.
|
||||
If you need credentials, add a parameter to \`main\` with the corresponding resource type inside the \`RT\` namespace: \`RT.Stripe\`.
|
||||
Only use them if needed to satisfy instructions. Always use the RT namespace.
|
||||
|
||||
**Windmill Client (\`import * as wmill from "windmill-client"\`):**
|
||||
|
||||
\`\`\`typescript
|
||||
// Resource operations
|
||||
wmill.getResource(path?: string, undefinedIfEmpty?: boolean): Promise<any>
|
||||
wmill.setResource(value: any, path?: string, initializeToTypeIfNotExist?: string): Promise<void>
|
||||
|
||||
// State management (persistent across executions)
|
||||
wmill.getState(): Promise<any>
|
||||
wmill.setState(state: any): Promise<void>
|
||||
|
||||
// Variables
|
||||
wmill.getVariable(path: string): Promise<string>
|
||||
wmill.setVariable(path: string, value: string, isSecretIfNotExist?: boolean, descriptionIfNotExist?: string): Promise<void>
|
||||
|
||||
// Script execution
|
||||
wmill.runScript(path?: string | null, hash_?: string | null, args?: Record<string, any> | null, verbose?: boolean): Promise<any>
|
||||
wmill.runScriptAsync(path: string | null, hash_: string | null, args: Record<string, any> | null, scheduledInSeconds?: number | null): Promise<string>
|
||||
wmill.waitJob(jobId: string, verbose?: boolean): Promise<any>
|
||||
wmill.getResult(jobId: string): Promise<any>
|
||||
wmill.getRootJobId(jobId?: string): Promise<string>
|
||||
|
||||
// S3 file operations (if S3 is configured)
|
||||
wmill.loadS3File(s3object: S3Object, s3ResourcePath?: string | undefined): Promise<Uint8Array | undefined>
|
||||
wmill.writeS3File(s3object: S3Object | undefined, fileContent: string | Blob, s3ResourcePath?: string | undefined): Promise<S3Object>
|
||||
|
||||
// Flow operations
|
||||
wmill.setFlowUserState(key: string, value: any, errorIfNotPossible?: boolean): Promise<void>
|
||||
wmill.getFlowUserState(key: string, errorIfNotPossible?: boolean): Promise<any>
|
||||
wmill.getResumeUrls(approver?: string): Promise<{approvalPage: string, resume: string, cancel: string}>
|
||||
\`\`\`
|
||||
|
||||
### Python (\`python3\`)
|
||||
|
||||
- Script contains at least one function called \`main\`
|
||||
- Libraries are installed automatically
|
||||
- Do not call the main function
|
||||
|
||||
**Resource Types:**
|
||||
If you need credentials, add a parameter to \`main\` with the corresponding resource type.
|
||||
**Redefine** the type of needed resources before the main function as TypedDict (only include if actually needed).
|
||||
Resource type name must be **IN LOWERCASE**.
|
||||
If an import conflicts with a resource type name, **rename the imported object, not the type name**.
|
||||
Import TypedDict from typing **if using it**.
|
||||
|
||||
**Windmill Client (\`import wmill\`):**
|
||||
|
||||
\`\`\`python
|
||||
# Resource operations
|
||||
wmill.get_resource(path: str, none_if_undefined: bool = False) -> dict | None
|
||||
wmill.set_resource(path: str, value: Any, resource_type: str = "any") -> None
|
||||
|
||||
# State management
|
||||
wmill.get_state() -> Any
|
||||
wmill.set_state(value: Any) -> None
|
||||
wmill.get_flow_user_state(key: str) -> Any
|
||||
wmill.set_flow_user_state(key: str, value: Any) -> None
|
||||
|
||||
# Variables
|
||||
wmill.get_variable(path: str) -> str
|
||||
wmill.set_variable(path: str, value: str, is_secret: bool = False) -> None
|
||||
|
||||
# Script execution
|
||||
wmill.run_script(path: str = None, hash_: str = None, args: dict = None, timeout = None, verbose: bool = False) -> Any
|
||||
wmill.run_script_async(path: str = None, hash_: str = None, args: dict = None, scheduled_in_secs: int = None) -> str
|
||||
wmill.wait_job(job_id: str, timeout = None, verbose: bool = False) -> Any
|
||||
wmill.get_result(job_id: str) -> Any
|
||||
|
||||
# S3 operations
|
||||
wmill.load_s3_file(s3object: S3Object | str, s3_resource_path: str | None = None) -> bytes
|
||||
wmill.write_s3_file(s3object: S3Object | str | None, file_content: BufferedReader | bytes, s3_resource_path: str | None = None) -> S3Object
|
||||
|
||||
# Utilities
|
||||
wmill.get_workspace() -> str
|
||||
wmill.whoami() -> dict
|
||||
wmill.set_progress(value: int, job_id: Optional[str] = None) -> None
|
||||
\`\`\`
|
||||
|
||||
### PHP (\`php\`)
|
||||
|
||||
- Script must start with \`<?php\`
|
||||
- Contains at least one function called \`main\`
|
||||
- **Redefine** resource types before main function (only if needed)
|
||||
- Check if class exists using \`class_exists\` before defining types
|
||||
- Resource type name must be exactly as specified
|
||||
|
||||
**Resource Types:**
|
||||
If you need credentials, add a parameter to \`main\` with the corresponding resource type.
|
||||
**Redefine** the type of needed resources before the main function.
|
||||
Before defining each type, check if the class already exists using class_exists.
|
||||
The resource type name has to be exactly as specified.
|
||||
|
||||
**Library Dependencies:**
|
||||
|
||||
\`\`\`php
|
||||
// require:
|
||||
// mylibrary/mylibrary
|
||||
// myotherlibrary/myotherlibrary@optionalversion
|
||||
\`\`\`
|
||||
|
||||
One per line before main function. Autoload already included.
|
||||
|
||||
### Rust (\`rust\`)
|
||||
|
||||
\`\`\`rust
|
||||
use anyhow::anyhow;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct ReturnType {
|
||||
// ...
|
||||
}
|
||||
|
||||
fn main(...) -> anyhow::Result<ReturnType>
|
||||
\`\`\`
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
\`\`\`\`rust
|
||||
//! \`\`\`cargo
|
||||
//! [dependencies]
|
||||
//! anyhow = "1.0.86"
|
||||
//! \`\`\`
|
||||
\`\`\`\`
|
||||
|
||||
Serde already included. For async functions, keep main sync and create runtime inside.
|
||||
|
||||
### Go (\`go\`)
|
||||
|
||||
- File package must be "inner"
|
||||
- Export single function called \`main\`
|
||||
- Return type: \`({return_type}, error)\`
|
||||
|
||||
### Bash (\`bash\`)
|
||||
|
||||
- Do not include "#!/bin/bash"
|
||||
- Arguments: \`var1="$1"\`, \`var2="$2"\`, etc.
|
||||
|
||||
### SQL Variants
|
||||
|
||||
#### PostgreSQL (\`postgresql\`)
|
||||
|
||||
- Arguments: \`$1::{type}\`, \`$2::{type}\`, etc.
|
||||
- Name parameters: \`-- $1 name1\` or \`-- $2 name = default\`
|
||||
|
||||
#### MySQL (\`mysql\`)
|
||||
|
||||
- Arguments: \`?\` placeholders
|
||||
- Name parameters: \`-- ? name1 ({type})\` or \`-- ? name2 ({type}) = default\`
|
||||
|
||||
#### BigQuery (\`bigquery\`)
|
||||
|
||||
- Arguments: \`@name1\`, \`@name2\`, etc.
|
||||
- Name parameters: \`-- @name1 ({type})\` or \`-- @name2 ({type}) = default\`
|
||||
|
||||
#### Snowflake (\`snowflake\`)
|
||||
|
||||
- Arguments: \`?\` placeholders
|
||||
- Name parameters: \`-- ? name1 ({type})\` or \`-- ? name2 ({type}) = default\`
|
||||
|
||||
#### Microsoft SQL Server (\`mssql\`)
|
||||
|
||||
- Arguments: \`@P1\`, \`@P2\`, etc.
|
||||
- Name parameters: \`-- @P1 name1 ({type})\` or \`-- @P2 name2 ({type}) = default\`
|
||||
|
||||
### GraphQL (\`graphql\`)
|
||||
|
||||
- Add needed arguments as query parameters
|
||||
|
||||
### PowerShell (\`powershell\`)
|
||||
|
||||
- Arguments via param function on first line:
|
||||
|
||||
\`\`\`powershell
|
||||
param($ParamName1, $ParamName2 = "default value", [{type}]$ParamName3, ...)
|
||||
\`\`\`
|
||||
|
||||
### C# (\`csharp\`)
|
||||
|
||||
- Public static Main method inside a class
|
||||
- NuGet packages: \`#r "nuget: PackageName, Version"\` at top
|
||||
- Method signature: \`public static ReturnType Main(parameter types...)\`
|
||||
|
||||
### Java (\`java\`)
|
||||
|
||||
- Main public class with \`public static main()\` method
|
||||
- Dependencies: \`//requirements://groupId:artifactId:version\` at top
|
||||
- Method signature: \`public static Object main(parameter types...)\`
|
||||
|
||||
## Supported Languages
|
||||
|
||||
\`bunnative\`, \`nativets\`, \`bun\`, \`deno\`, \`python3\`, \`php\`, \`rust\`, \`go\`, \`bash\`, \`postgresql\`, \`mysql\`, \`bigquery\`, \`snowflake\`, \`mssql\`, \`graphql\`, \`powershell\`, \`csharp\`, \`java\`
|
||||
|
||||
Always follow the specific conventions for the language being used and include only necessary dependencies and resource types.
|
||||
|
||||
# Windmill CLI Commands Summary
|
||||
|
||||
## Core Commands
|
||||
|
||||
### \`wmill init\`
|
||||
|
||||
Bootstrap a new Windmill project with a \`wmill.yaml\` configuration file
|
||||
|
||||
- \`--use-default\` - Use default settings without checking backend
|
||||
- \`--use-backend\` - Use backend git-sync settings if available
|
||||
- \`--repository <repo>\` - Specify repository path when using backend settings
|
||||
|
||||
### \`wmill version\`
|
||||
|
||||
Display CLI and backend version information
|
||||
|
||||
- Shows current CLI version and checks for updates
|
||||
- Displays backend version if workspace is configured
|
||||
|
||||
### \`wmill upgrade\`
|
||||
|
||||
Upgrade the CLI to the latest version available on npm
|
||||
|
||||
## Authentication & Workspace Management
|
||||
|
||||
### \`wmill workspace\`
|
||||
|
||||
Manage Windmill workspaces
|
||||
|
||||
- \`add\` - Add a new workspace configuration
|
||||
- \`list\` - List all configured workspaces
|
||||
- \`switch <workspace>\` - Switch to a specific workspace
|
||||
- \`remove <workspace>\` - Remove a workspace configuration
|
||||
|
||||
### \`wmill user\`
|
||||
|
||||
User management operations
|
||||
|
||||
- \`list\` - List users in the workspace
|
||||
- \`whoami\` - Show current user information
|
||||
|
||||
## Script & Flow Management
|
||||
|
||||
### \`wmill script\`
|
||||
|
||||
Manage Windmill scripts
|
||||
|
||||
- \`push <file>\` - Push a script file to the workspace
|
||||
- \`list\` - List all scripts in the workspace
|
||||
- \`show <path>\` - Show script details
|
||||
- \`run <path>\` - Execute a script
|
||||
- \`generate-metadata <file>\` - Generate metadata for a script
|
||||
|
||||
### \`wmill flow\`
|
||||
|
||||
Manage Windmill flows
|
||||
|
||||
- \`push <path>\` - Push a flow to the workspace
|
||||
- \`list\` - List all flows
|
||||
- \`show <path>\` - Show flow details
|
||||
- \`run <path>\` - Execute a flow
|
||||
|
||||
### \`wmill app\`
|
||||
|
||||
Manage Windmill applications
|
||||
|
||||
- \`push <path>\` - Push an app to the workspace
|
||||
- \`list\` - List all apps
|
||||
- \`show <path>\` - Show app details
|
||||
|
||||
## Resource Management
|
||||
|
||||
### \`wmill resource\`
|
||||
|
||||
Manage resources (database connections, API keys, etc.)
|
||||
|
||||
- \`list\` - List all resources
|
||||
- \`push <file>\` - Push a resource definition
|
||||
- \`show <path>\` - Show resource details
|
||||
|
||||
### \`wmill resource-type\`
|
||||
|
||||
Manage custom resource types
|
||||
|
||||
- Operations for defining and managing custom resource schemas
|
||||
|
||||
### \`wmill variable\`
|
||||
|
||||
Manage workspace variables and secrets
|
||||
|
||||
- \`list\` - List all variables
|
||||
- \`push <file>\` - Push a variable definition
|
||||
- \`show <path>\` - Show variable details
|
||||
|
||||
## Scheduling & Automation
|
||||
|
||||
### \`wmill schedule\`
|
||||
|
||||
Manage scheduled jobs
|
||||
|
||||
- \`list\` - List all schedules
|
||||
- \`push <file>\` - Push a schedule definition
|
||||
- Operations for managing cron-based job scheduling
|
||||
|
||||
### \`wmill trigger\`
|
||||
|
||||
Manage event triggers
|
||||
|
||||
- Operations for managing webhooks and event-based triggers
|
||||
|
||||
## Synchronization
|
||||
|
||||
### \`wmill sync\`
|
||||
|
||||
Synchronize local files with Windmill workspace
|
||||
|
||||
- \`pull\` - Download resources from workspace to local files
|
||||
- \`push\` - Upload local files to workspace
|
||||
- Supports bidirectional sync with conflict resolution
|
||||
- Works with \`wmill.yaml\` configuration
|
||||
|
||||
### \`wmill gitsync-settings\`
|
||||
|
||||
Manage git synchronization settings
|
||||
|
||||
- Configure automatic git sync for the workspace
|
||||
- Pull/push git sync configurations
|
||||
|
||||
## Development Tools
|
||||
|
||||
### \`wmill dev\`
|
||||
|
||||
Start development mode with live reloading
|
||||
|
||||
- Watches local files for changes
|
||||
- Automatically syncs changes to workspace
|
||||
- Provides real-time feedback during development
|
||||
|
||||
### \`wmill hub\`
|
||||
|
||||
Interact with Windmill Hub
|
||||
|
||||
- \`pull\` - Pull resources from the public Windmill Hub
|
||||
- Access community-shared scripts, flows, and resource types
|
||||
|
||||
## Infrastructure Management
|
||||
|
||||
### \`wmill instance\`
|
||||
|
||||
Manage Windmill instance settings (Enterprise)
|
||||
|
||||
- Configure instance-level settings
|
||||
- Manage global configurations
|
||||
|
||||
### \`wmill worker-groups\`
|
||||
|
||||
Manage worker groups for job execution
|
||||
|
||||
- Configure and manage worker pool settings
|
||||
|
||||
### \`wmill workers\`
|
||||
|
||||
Manage individual workers
|
||||
|
||||
- Monitor and configure worker instances
|
||||
|
||||
### \`wmill queues\`
|
||||
|
||||
Manage job queues
|
||||
|
||||
- Monitor and configure job execution queues
|
||||
|
||||
## Utility Commands
|
||||
|
||||
### \`wmill folder\`
|
||||
|
||||
Manage workspace folders and organization
|
||||
|
||||
- Operations for organizing resources into folders
|
||||
|
||||
### \`wmill completions\`
|
||||
|
||||
Generate shell completion scripts
|
||||
|
||||
- Support for bash, zsh, fish, and PowerShell
|
||||
|
||||
## Global Options
|
||||
|
||||
All commands support these global options:
|
||||
|
||||
- \`--workspace <workspace>\` - Specify target workspace
|
||||
- \`--token <token>\` - Specify API token
|
||||
- \`--base-url <url>\` - Specify Windmill instance URL
|
||||
- \`--config-dir <dir>\` - Custom configuration directory
|
||||
- \`--debug/--verbose\` - Enable debug logging
|
||||
- \`--show-diffs\` - Show detailed diff information during sync
|
||||
|
||||
The CLI uses a \`wmill.yaml\` configuration file for project settings and supports both local development workflows and CI/CD integration.
|
||||
`;
|
||||
${prompts.SCRIPT_PROMPT}
|
||||
`;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
log,
|
||||
} from "../deps.ts";
|
||||
import flow from "./commands/flow/flow.ts";
|
||||
import app from "./commands/app/apps.ts";
|
||||
import app from "./commands/app/app.ts";
|
||||
import script from "./commands/script/script.ts";
|
||||
import workspace, {
|
||||
getActiveWorkspace,
|
||||
@@ -22,7 +22,7 @@ import trigger from "./commands/trigger/trigger.ts";
|
||||
import sync from "./commands/sync/sync.ts";
|
||||
import gitsyncSettings from "./commands/gitsync-settings/gitsync-settings.ts";
|
||||
import instance from "./commands/instance/instance.ts";
|
||||
import workerGroups from "./commands/worker-groups/worker_groups.ts";
|
||||
import workerGroups from "./commands/worker-groups/worker-groups.ts";
|
||||
|
||||
import dev from "./commands/dev/dev.ts";
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
@@ -70,7 +70,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.591.3";
|
||||
export const VERSION = "1.592.1";
|
||||
|
||||
export const WM_FORK_PREFIX = "wm-fork";
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
yamlParseContent,
|
||||
yamlStringify,
|
||||
} from "../deps.ts";
|
||||
import { pushApp } from "./commands/app/apps.ts";
|
||||
import { pushApp } from "./commands/app/app.ts";
|
||||
import { pushFolder } from "./commands/folder/folder.ts";
|
||||
import { pushFlow } from "./commands/flow/flow.ts";
|
||||
import { pushResource } from "./commands/resource/resource.ts";
|
||||
|
||||
@@ -27,29 +27,23 @@ export class LockfileGenerationError extends Error {
|
||||
|
||||
export async function generateAllMetadata() {}
|
||||
|
||||
export async function getRawWorkspaceDependencies(): Promise<
|
||||
Record<string, string>
|
||||
> {
|
||||
export async function getRawWorkspaceDependencies(): Promise<Record<string, string>> {
|
||||
const rawWorkspaceDeps: Record<string, string> = {};
|
||||
|
||||
|
||||
try {
|
||||
for await (const entry of Deno.readDir("dependencies")) {
|
||||
if (entry.isDirectory) continue;
|
||||
|
||||
|
||||
const filePath = `dependencies/${entry.name}`;
|
||||
const content = await Deno.readTextFile(filePath);
|
||||
|
||||
|
||||
// Find matching language
|
||||
for (const lang of workspaceDependenciesLanguages) {
|
||||
if (entry.name.endsWith(lang.filename)) {
|
||||
// Check if out of sync
|
||||
const contentHash = await generateHash(content + filePath);
|
||||
const isUpToDate = await checkifMetadataUptodate(
|
||||
filePath,
|
||||
contentHash,
|
||||
undefined
|
||||
);
|
||||
|
||||
const isUpToDate = await checkifMetadataUptodate(filePath, contentHash, undefined);
|
||||
|
||||
if (!isUpToDate) {
|
||||
rawWorkspaceDeps[filePath] = content;
|
||||
}
|
||||
@@ -60,24 +54,19 @@ export async function getRawWorkspaceDependencies(): Promise<
|
||||
} catch {
|
||||
// dependencies directory doesn't exist
|
||||
}
|
||||
return rawWorkspaceDeps;
|
||||
return rawWorkspaceDeps;
|
||||
}
|
||||
|
||||
export function workspaceDependenciesPathToLanguageAndFilename(
|
||||
path: string
|
||||
): { name: string | undefined; language: ScriptLanguage } | undefined {
|
||||
const relativePath = path.replace("dependencies/", "");
|
||||
for (const { filename, language } of workspaceDependenciesLanguages) {
|
||||
if (relativePath.endsWith(filename)) {
|
||||
return {
|
||||
name:
|
||||
relativePath === filename
|
||||
? undefined
|
||||
: relativePath.replace("." + filename, ""),
|
||||
language,
|
||||
};
|
||||
export function workspaceDependenciesPathToLanguageAndFilename(path: string): { name: string | undefined, language: ScriptLanguage } | undefined {
|
||||
const relativePath = path.replace("dependencies/", "");
|
||||
for (const { filename, language } of workspaceDependenciesLanguages) {
|
||||
if (relativePath.endsWith(filename)) {
|
||||
return {
|
||||
name: relativePath === filename ? undefined : relativePath.replace("." + filename, ""),
|
||||
language
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// on windows, when using powershell, blue is not readable
|
||||
@@ -108,27 +97,24 @@ export async function generateScriptMetadataInternal(
|
||||
|
||||
// Filter workspace dependencies to only include those matching the script's language
|
||||
const filteredRawWorkspaceDependencies: Record<string, string> = {};
|
||||
for (const [depPath, depContent] of Object.entries(
|
||||
rawWorkspaceDependencies
|
||||
)) {
|
||||
for (const [depPath, depContent] of Object.entries(rawWorkspaceDependencies)) {
|
||||
const depInfo = workspaceDependenciesPathToLanguageAndFilename(depPath);
|
||||
if (depInfo && depInfo.language === language) {
|
||||
filteredRawWorkspaceDependencies[depPath] = depContent;
|
||||
}
|
||||
}
|
||||
|
||||
const metadataWithType = await parseMetadataFile(remotePath, undefined);
|
||||
const metadataWithType = await parseMetadataFile(
|
||||
remotePath,
|
||||
undefined,
|
||||
);
|
||||
|
||||
// read script content
|
||||
const scriptContent = await Deno.readTextFile(scriptPath);
|
||||
const metadataContent = await Deno.readTextFile(metadataWithType.path);
|
||||
|
||||
|
||||
// Note: rawWorkspaceDependencies are now passed in as parameter instead of being searched hierarchically
|
||||
let hash = await generateScriptHash(
|
||||
filteredRawWorkspaceDependencies,
|
||||
scriptContent,
|
||||
metadataContent
|
||||
);
|
||||
let hash = await generateScriptHash(filteredRawWorkspaceDependencies, scriptContent, metadataContent);
|
||||
|
||||
if (await checkifMetadataUptodate(remotePath, hash, undefined)) {
|
||||
if (!noStaleMessage) {
|
||||
@@ -174,7 +160,7 @@ export async function generateScriptMetadataInternal(
|
||||
} else {
|
||||
metadataParsedContent.lock = "";
|
||||
}
|
||||
} else if (!opts.schemaOnly || metadataParsedContent.lock != "") {
|
||||
} else {
|
||||
metadataParsedContent.lock =
|
||||
"!inline " + remotePath.replaceAll(SEP, "/") + ".script.lock";
|
||||
}
|
||||
@@ -244,13 +230,12 @@ async function updateScriptLock(
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(rawWorkspaceDependencies).length > 0) {
|
||||
const dependencyPaths = Object.keys(rawWorkspaceDependencies).join(", ");
|
||||
log.info(
|
||||
`Generating script lock for ${remotePath} with raw workspace dependencies: ${dependencyPaths}`
|
||||
);
|
||||
}
|
||||
|
||||
if (Object.keys(rawWorkspaceDependencies).length > 0) {
|
||||
const dependencyPaths = Object.keys(rawWorkspaceDependencies).join(', ');
|
||||
log.info(`Generating script lock for ${remotePath} with raw workspace dependencies: ${dependencyPaths}`);
|
||||
}
|
||||
|
||||
// generate the script lock running a dependency job in Windmill and update it inplace
|
||||
// TODO: update this once the client is released
|
||||
const extraHeaders = getHeaders();
|
||||
@@ -271,10 +256,8 @@ async function updateScriptLock(
|
||||
script_path: remotePath,
|
||||
},
|
||||
],
|
||||
raw_workspace_dependencies:
|
||||
Object.keys(rawWorkspaceDependencies).length > 0
|
||||
? rawWorkspaceDependencies
|
||||
: null,
|
||||
raw_workspace_dependencies: Object.keys(rawWorkspaceDependencies).length > 0
|
||||
? rawWorkspaceDependencies : null,
|
||||
entrypoint: remotePath,
|
||||
}),
|
||||
}
|
||||
@@ -560,7 +543,7 @@ export async function parseMetadataFile(
|
||||
workspaceRemote: Workspace;
|
||||
schemaOnly?: boolean;
|
||||
rawWorkspaceDependencies: Record<string, string>;
|
||||
codebases: SyncCodebase[];
|
||||
codebases: SyncCodebase[]
|
||||
})
|
||||
| undefined
|
||||
): Promise<{ isJson: boolean; payload: any; path: string }> {
|
||||
@@ -594,11 +577,7 @@ export async function parseMetadataFile(
|
||||
metadataFilePath = scriptPath + ".script.yaml";
|
||||
let scriptInitialMetadata = defaultScriptMetadata();
|
||||
const lockPath = scriptPath + ".script.lock";
|
||||
|
||||
if (generateMetadataIfMissing && !generateMetadataIfMissing?.schemaOnly) {
|
||||
log.info(`Using inline lock for ${metadataFilePath}`);
|
||||
scriptInitialMetadata.lock = "!inline " + lockPath;
|
||||
}
|
||||
scriptInitialMetadata.lock = "!inline " + lockPath;
|
||||
const scriptInitialMetadataYaml = yamlStringify(
|
||||
scriptInitialMetadata as Record<string, any>,
|
||||
yamlOptions
|
||||
@@ -607,14 +586,11 @@ export async function parseMetadataFile(
|
||||
await Deno.writeTextFile(metadataFilePath, scriptInitialMetadataYaml, {
|
||||
createNew: true,
|
||||
});
|
||||
await Deno.writeTextFile(lockPath, "", {
|
||||
createNew: true,
|
||||
});
|
||||
|
||||
if (generateMetadataIfMissing) {
|
||||
if (!generateMetadataIfMissing.schemaOnly) {
|
||||
await Deno.writeTextFile(lockPath, "", {
|
||||
createNew: true,
|
||||
});
|
||||
}
|
||||
|
||||
log.info(
|
||||
(await blueColor())(
|
||||
`Generating lockfile and schema for ${metadataFilePath}`
|
||||
@@ -714,9 +690,7 @@ export async function generateScriptHash(
|
||||
newMetadataContent: string
|
||||
) {
|
||||
return await generateHash(
|
||||
JSON.stringify(rawWorkspaceDependencies) +
|
||||
scriptContent +
|
||||
newMetadataContent
|
||||
JSON.stringify(rawWorkspaceDependencies) + scriptContent + newMetadataContent
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
48
docker/DockerfileNsjailSlim
Normal file
48
docker/DockerfileNsjailSlim
Normal file
@@ -0,0 +1,48 @@
|
||||
FROM debian:bookworm-slim AS nsjail
|
||||
|
||||
WORKDIR /nsjail
|
||||
|
||||
RUN apt-get -y update \
|
||||
&& apt-get install -y \
|
||||
bison=2:3.8.* \
|
||||
flex=2.6.* \
|
||||
g++=4:12.2.* \
|
||||
gcc=4:12.2.* \
|
||||
git=1:2.39.* \
|
||||
libprotobuf-dev=3.21.* \
|
||||
libnl-route-3-dev=3.7.* \
|
||||
make=4.3-4.1 \
|
||||
pkg-config=1.8.* \
|
||||
protobuf-compiler=3.21.*
|
||||
|
||||
|
||||
RUN git clone -b master --single-branch https://github.com/google/nsjail.git . && git checkout dccf911fd2659e7b08ce9507c25b2b38ec2c5800
|
||||
RUN make
|
||||
|
||||
FROM alpine:3.14 AS oracledb-client
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Oracle DB Client for amd64
|
||||
COPY --from=ghcr.io/oracle/oraclelinux9-instantclient:23 /usr/lib/oracle/23/client64/lib /opt/oracle/23/amd64/lib
|
||||
|
||||
# Oracle DB Client for arm64
|
||||
RUN mkdir -p /opt/oracle/23/arm64 \
|
||||
&& cd /opt/oracle/23/arm64 \
|
||||
&& wget https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linux-arm64.zip \
|
||||
&& unzip instantclient-basiclite-linux-arm64.zip && rm instantclient-basiclite-linux-arm64.zip && mv instantclient* ./lib
|
||||
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||
mv /opt/oracle/23/arm64/lib /opt/oracle/23/lib; \
|
||||
else \
|
||||
mv /opt/oracle/23/amd64/lib /opt/oracle/23/lib; \
|
||||
fi
|
||||
|
||||
FROM ghcr.io/windmill-labs/windmill-ee:dev
|
||||
|
||||
|
||||
# iptables
|
||||
RUN apt-get install -y iptables
|
||||
|
||||
COPY --from=nsjail /nsjail/nsjail /bin/nsjail
|
||||
|
||||
20
docker/DockerfileOnlyNsjail
Normal file
20
docker/DockerfileOnlyNsjail
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM debian:bookworm-slim AS nsjail
|
||||
|
||||
WORKDIR /nsjail
|
||||
|
||||
RUN apt-get -y update \
|
||||
&& apt-get install -y \
|
||||
bison=2:3.8.* \
|
||||
flex=2.6.* \
|
||||
g++=4:12.2.* \
|
||||
gcc=4:12.2.* \
|
||||
git=1:2.39.* \
|
||||
libprotobuf-dev=3.21.* \
|
||||
libnl-route-3-dev=3.7.* \
|
||||
make=4.3-4.1 \
|
||||
pkg-config=1.8.* \
|
||||
protobuf-compiler=3.21.*
|
||||
|
||||
|
||||
RUN git clone -b master --single-branch https://github.com/google/nsjail.git . && git checkout dccf911fd2659e7b08ce9507c25b2b38ec2c5800
|
||||
RUN make
|
||||
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@@ -14,4 +14,5 @@ static/tsdocs/
|
||||
static/ui_builder/
|
||||
ui_builder.tar.gz
|
||||
ui_builder_serve/
|
||||
src/lib/components/copilot/chat/flow/__tests__/eval/results/
|
||||
src/lib/components/copilot/chat/__tests__/flow/results/
|
||||
src/lib/components/copilot/chat/__tests__/app/results/
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.591.3",
|
||||
"version": "1.592.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.591.3",
|
||||
"version": "1.592.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.591.3",
|
||||
"version": "1.592.1",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -20,7 +20,7 @@ console.log('Running postinstall for root project');
|
||||
|
||||
import { x } from 'tar'
|
||||
|
||||
const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-b4fcf00.tar.gz'
|
||||
const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-6dcfccf.tar.gz'
|
||||
const outputTarPath = path.join(process.cwd(), 'ui_builder.tar.gz')
|
||||
const extractTo = path.join(process.cwd(), 'static/ui_builder/')
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
node ./scripts/untar_ui_builder.js
|
||||
# node ./scripts/untar_ui_builder.js
|
||||
|
||||
mkdir ui_builder_serve || true
|
||||
cp -r static/ui_builder ui_builder_serve/ui_builder || true
|
||||
rm -rf static/ui_builder || true
|
||||
# mkdir ui_builder_serve || true
|
||||
# cp -r static/ui_builder ui_builder_serve/ui_builder || true
|
||||
# rm -rf static/ui_builder || true
|
||||
python3 -c "
|
||||
import os
|
||||
os.chdir('ui_builder_serve')
|
||||
|
||||
1007
frontend/src/lib/components/CompareWorkspaces.svelte
Normal file
1007
frontend/src/lib/components/CompareWorkspaces.svelte
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
import { format, isValid, parse } from 'date-fns'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
|
||||
export let value: string | undefined = undefined
|
||||
export let value: string | null | undefined = undefined
|
||||
export let autofocus: boolean | null = false
|
||||
export let minDate: string | undefined = undefined
|
||||
export let maxDate: string | undefined = undefined
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function computeDate(value: string | undefined) {
|
||||
function computeDate(value: string | null | undefined) {
|
||||
if (dateFormat === undefined) {
|
||||
dateFormat = defaultDateFormat
|
||||
}
|
||||
@@ -83,6 +83,9 @@
|
||||
on:change={() => {
|
||||
if (date) {
|
||||
updateValue(date)
|
||||
} else {
|
||||
value = null
|
||||
dispatch('change', value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
interface Props {
|
||||
// import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
value?: string | undefined
|
||||
value?: string | null | undefined
|
||||
clearable?: boolean
|
||||
autofocus?: boolean | null
|
||||
useDropdown?: boolean
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
// let format: 'local' | 'utc' = 'local'
|
||||
|
||||
function parseValue(value: string | undefined = undefined) {
|
||||
function parseValue(value: string | null | undefined = undefined) {
|
||||
let dateFromValue: Date | undefined = value ? new Date(value) : undefined
|
||||
if (!isValidDate(dateFromValue)) {
|
||||
date = undefined
|
||||
@@ -76,6 +76,13 @@
|
||||
let initialTime = untrack(() => time)
|
||||
|
||||
function parseDateAndTime(date: string | undefined, time: string | undefined) {
|
||||
// Handle cleared date - if date is empty string (user cleared) but value still has a value
|
||||
if (date === '' && value) {
|
||||
value = null
|
||||
dispatchIfMounted('change', value)
|
||||
return
|
||||
}
|
||||
|
||||
if (date && time && (initialDate != date || initialTime != time)) {
|
||||
let newDate = new Date(timezone === 'local' ? `${date}T${time}` : `${date}T${time}Z`)
|
||||
if (newDate.toString() === 'Invalid Date') return
|
||||
@@ -178,7 +185,7 @@
|
||||
wrapperClasses="h-full"
|
||||
{disabled}
|
||||
on:click={() => {
|
||||
value = undefined
|
||||
value = null
|
||||
dispatch('clear')
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import Select from './select/Select.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
import TextInput from './text_input/TextInput.svelte'
|
||||
import PermissionHistory from './PermissionHistory.svelte'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
@@ -90,6 +91,7 @@
|
||||
role: getRole(x)
|
||||
}
|
||||
})
|
||||
reloadHistory++
|
||||
} catch (e) {
|
||||
folderNotFound = true
|
||||
}
|
||||
@@ -144,6 +146,8 @@
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
let reloadHistory = $state(0)
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={newGroup}>
|
||||
@@ -351,7 +355,7 @@
|
||||
{/if}</td
|
||||
>
|
||||
<td class="flex items-center justify-end">
|
||||
{#if can_write && (owner_name != 'u/' + $userStore?.username || $userStore?.is_admin)}
|
||||
{#if (can_write && owner_name != 'u/' + $userStore?.username) || $userStore?.is_admin}
|
||||
<Button
|
||||
variant="subtle"
|
||||
destructive
|
||||
@@ -378,7 +382,7 @@
|
||||
loadFolder()
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
{:else if can_write && owner_name == 'u/' + $userStore?.username}
|
||||
<span class="text-primary text-xs">cannot remove yourself</span>
|
||||
{/if}</td
|
||||
>
|
||||
@@ -441,4 +445,20 @@
|
||||
{/if}
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
{#if reloadHistory > 0}
|
||||
{#key reloadHistory}
|
||||
<PermissionHistory
|
||||
{name}
|
||||
fetchHistory={async (workspace, folderName, page, perPage) => {
|
||||
return await FolderService.getFolderPermissionHistory({
|
||||
workspace,
|
||||
name: folderName,
|
||||
page,
|
||||
perPage
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
192
frontend/src/lib/components/ForkWorkspaceBanner.svelte
Normal file
192
frontend/src/lib/components/ForkWorkspaceBanner.svelte
Normal file
@@ -0,0 +1,192 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore, userWorkspaces } from '$lib/stores'
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
import type { WorkspaceComparison } from '$lib/gen'
|
||||
import { Button } from './common'
|
||||
import { AlertTriangle, GitFork } from 'lucide-svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import { onMount, untrack } from 'svelte'
|
||||
|
||||
let loading = $state(false)
|
||||
let comparison: WorkspaceComparison | undefined = $state(undefined)
|
||||
let error: string | undefined = $state(undefined)
|
||||
|
||||
let isFork = $derived($workspaceStore?.startsWith('wm-fork-') ?? false)
|
||||
let currentWorkspaceData = $derived($userWorkspaces.find((w) => w.id === $workspaceStore))
|
||||
let parentWorkspaceId = $derived(currentWorkspaceData?.parent_workspace_id)
|
||||
let parentWorkspaceData = $derived($userWorkspaces.find((w) => w.id === parentWorkspaceId))
|
||||
|
||||
$effect(() => {
|
||||
;[$workspaceStore, parentWorkspaceId]
|
||||
untrack(() => {
|
||||
if (isFork && $workspaceStore) {
|
||||
checkForChanges()
|
||||
} else {
|
||||
comparison = undefined
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
if (isFork && $workspaceStore) {
|
||||
checkForChanges()
|
||||
} else {
|
||||
comparison = undefined
|
||||
}
|
||||
})
|
||||
|
||||
async function checkForChanges() {
|
||||
if (!$workspaceStore || !parentWorkspaceId) {
|
||||
return
|
||||
}
|
||||
|
||||
loading = true
|
||||
error = undefined
|
||||
|
||||
try {
|
||||
// Compare with parent workspace
|
||||
const result = await WorkspaceService.compareWorkspaces({
|
||||
workspace: parentWorkspaceId,
|
||||
targetWorkspaceId: $workspaceStore
|
||||
})
|
||||
|
||||
comparison = result
|
||||
} catch (e) {
|
||||
console.error('Failed to compare workspaces:', e)
|
||||
error = `Failed to check for changes: ${e}`
|
||||
// Still show banner if there's an error, but with error message
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function openComparisonDrawer() {
|
||||
if (parentWorkspaceId && $workspaceStore) {
|
||||
goto('/forks/compare?workspace_id=' + encodeURIComponent($workspaceStore), {
|
||||
replaceState: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function forkAheadBehindMessage(
|
||||
changesAhead: number,
|
||||
changesBehind: number,
|
||||
) {
|
||||
let msg: string[] = []
|
||||
if (changesAhead > 0 || changesBehind > 0) {
|
||||
msg.push('This fork is ')
|
||||
if (changesAhead > 0)
|
||||
msg.push(`${changesAhead} change${changesAhead > 1 ? 's' : ''} ahead of `)
|
||||
if (changesAhead > 0 && changesBehind > 0) msg.push('and ')
|
||||
if (changesBehind > 0)
|
||||
msg.push(`${changesBehind} change${changesBehind > 1 ? 's' : ''} behind `)
|
||||
}
|
||||
return msg.join('')
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isFork}
|
||||
<div class="w-full bg-blue-50 dark:bg-blue-900/20 border-b border-blue-200 dark:border-blue-800">
|
||||
<div class="px-4 py-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<GitFork class="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
<div class="text-sm">
|
||||
<span class="font-medium text-blue-900 dark:text-blue-100">
|
||||
Fork of <b>{parentWorkspaceData?.name}</b> ({parentWorkspaceId})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<span class="text-xs text-blue-600 dark:text-blue-400"> Checking for changes... </span>
|
||||
{:else if error}
|
||||
<span class="text-xs text-red-600 dark:text-red-400">
|
||||
{error}
|
||||
</span>
|
||||
{:else if comparison}
|
||||
<div class="flex items-center gap-4 text-xs">
|
||||
{#if comparison.summary.total_diffs > 0}
|
||||
<span>
|
||||
{forkAheadBehindMessage(
|
||||
comparison.summary.total_ahead,
|
||||
comparison.summary.total_behind,
|
||||
)}
|
||||
<span class="font-semibold underline">{parentWorkspaceId}</span> over {comparison.summary
|
||||
.total_diffs} items:
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if comparison.summary.scripts_changed > 0}
|
||||
<span class="text-blue-700 dark:text-blue-300">
|
||||
{comparison.summary.scripts_changed} script{comparison.summary
|
||||
.scripts_changed !== 1
|
||||
? 's'
|
||||
: ''}
|
||||
</span>
|
||||
{/if}
|
||||
{#if comparison.summary.flows_changed > 0}
|
||||
<span class="text-blue-700 dark:text-blue-300">
|
||||
{comparison.summary.flows_changed} flow{comparison.summary.flows_changed !== 1
|
||||
? 's'
|
||||
: ''}
|
||||
</span>
|
||||
{/if}
|
||||
{#if comparison.summary.apps_changed > 0}
|
||||
<span class="text-blue-700 dark:text-blue-300">
|
||||
{comparison.summary.apps_changed} app{comparison.summary.apps_changed !== 1
|
||||
? 's'
|
||||
: ''}
|
||||
</span>
|
||||
{/if}
|
||||
{#if comparison.summary.resources_changed > 0}
|
||||
<span class="text-blue-700 dark:text-blue-300">
|
||||
{comparison.summary.resources_changed} resource{comparison.summary
|
||||
.resources_changed !== 1
|
||||
? 's'
|
||||
: ''}
|
||||
</span>
|
||||
{/if}
|
||||
{#if comparison.summary.variables_changed > 0}
|
||||
<span class="text-blue-700 dark:text-blue-300">
|
||||
{comparison.summary.variables_changed} variable{comparison.summary
|
||||
.variables_changed !== 1
|
||||
? 's'
|
||||
: ''}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if comparison.summary.conflicts > 0}
|
||||
-
|
||||
<div class="flex items-center gap-1 text-orange-600 dark:text-orange-400">
|
||||
<AlertTriangle class="w-3 h-3" />
|
||||
<span
|
||||
>{comparison.summary.conflicts} conflict{comparison.summary.conflicts !== 1
|
||||
? 's'
|
||||
: ''}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if comparison.skipped_comparison}
|
||||
<span class="text-blue-600 dark:text-blue-400"> This fork was created before the addition of certain windmill features, and therefore the changes with its parent workspace cannot be displayed.</span>
|
||||
{:else}
|
||||
<span class="text-blue-600 dark:text-blue-400"> Everything is up to date </span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
{#if comparison && comparison.summary.total_diffs > 0}
|
||||
<Button size="xs" color="blue" on:click={openComparisonDrawer}>
|
||||
{#if comparison.summary.total_ahead > 0}
|
||||
Review & Deploy Changes
|
||||
{:else}
|
||||
Review & Update fork
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -2,7 +2,7 @@
|
||||
import { workspaceStore, enterpriseLicense, userStore } from '$lib/stores'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
import { Loader2, Github, RotateCw, Plus, Minus, Download } from 'lucide-svelte'
|
||||
import { Loader2, Github, RotateCw, Plus, Minus, Download, AlertTriangle } from 'lucide-svelte'
|
||||
import { onDestroy } from 'svelte'
|
||||
import {
|
||||
createGitHubAppState,
|
||||
@@ -208,7 +208,9 @@
|
||||
<select bind:value={githubState.selectedGHAppAccountId}>
|
||||
<option value="" disabled>Select GitHub Account ID</option>
|
||||
{#each githubState.workspaceGithubInstallations as installation (`select-${installation.installation_id}-${installation.workspace_id}`)}
|
||||
<option value={installation.account_id}>{installation.account_id}</option>
|
||||
<option value={installation.account_id} disabled={!!installation.error}>
|
||||
{installation.account_id}{installation.error ? ' (token error)' : ''}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
@@ -292,13 +294,26 @@
|
||||
<tbody>
|
||||
{#each githubState.workspaceGithubInstallations as installation (`current-${installation.installation_id}-${installation.workspace_id}`)}
|
||||
<tr class="border-t border-gray-200 dark:border-gray-700">
|
||||
<td class="py-2">{installation.account_id}</td>
|
||||
<td class="py-2">
|
||||
<div class="flex items-center gap-1">
|
||||
{#if installation.error}
|
||||
<span title={installation.error}>
|
||||
<AlertTriangle class="w-4 h-4 text-yellow-500" />
|
||||
</span>
|
||||
{/if}
|
||||
{installation.account_id}
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2">
|
||||
<span class="text-xs text-primary">{installation.workspace_id}</span
|
||||
>
|
||||
</td>
|
||||
<td class="py-2 text-primary">
|
||||
{installation.repositories.length} repos
|
||||
{#if installation.error}
|
||||
<span class="text-yellow-600 dark:text-yellow-400 text-xs" title={installation.error}>Token error</span>
|
||||
{:else}
|
||||
{installation.repositories.length} repos
|
||||
{/if}
|
||||
</td>
|
||||
<td class="py-2 text-right">
|
||||
<div class="flex justify-end gap-1">
|
||||
@@ -350,13 +365,26 @@
|
||||
<tbody>
|
||||
{#each githubInstallationsNotInWorkspace as installation (`other-${installation.installation_id}-${installation.workspace_id}`)}
|
||||
<tr class="border-t border-gray-200 dark:border-gray-700">
|
||||
<td class="py-2">{installation.account_id}</td>
|
||||
<td class="py-2">
|
||||
<div class="flex items-center gap-1">
|
||||
{#if installation.error}
|
||||
<span title={installation.error}>
|
||||
<AlertTriangle class="w-4 h-4 text-yellow-500" />
|
||||
</span>
|
||||
{/if}
|
||||
{installation.account_id}
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2">
|
||||
<span class="text-xs text-primary">{installation.workspace_id}</span
|
||||
>
|
||||
</td>
|
||||
<td class="py-2 text-primary">
|
||||
{installation.repositories.length} repos
|
||||
{#if installation.error}
|
||||
<span class="text-yellow-600 dark:text-yellow-400 text-xs" title={installation.error}>Token error</span>
|
||||
{:else}
|
||||
{installation.repositories.length} repos
|
||||
{/if}
|
||||
</td>
|
||||
<td class="pl-8 py-2 text-right">
|
||||
<Button
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
import TextInput from './text_input/TextInput.svelte'
|
||||
import { Trash } from 'lucide-svelte'
|
||||
import PermissionHistory from './PermissionHistory.svelte'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
@@ -81,6 +82,7 @@
|
||||
}
|
||||
})
|
||||
summary = group.summary ?? ''
|
||||
reloadHistory++
|
||||
} catch (e) {
|
||||
can_write = false
|
||||
members = []
|
||||
@@ -110,6 +112,7 @@
|
||||
})
|
||||
}
|
||||
})
|
||||
let reloadHistory = $state(0)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
@@ -311,4 +314,20 @@
|
||||
</div>
|
||||
{/if}
|
||||
</Label>
|
||||
|
||||
{#if reloadHistory > 0}
|
||||
{#key reloadHistory}
|
||||
<PermissionHistory
|
||||
{name}
|
||||
fetchHistory={async (workspace, groupName, page, perPage) => {
|
||||
return await GroupService.getGroupPermissionHistory({
|
||||
workspace,
|
||||
name: groupName,
|
||||
page,
|
||||
perPage
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
122
frontend/src/lib/components/PermissionHistory.svelte
Normal file
122
frontend/src/lib/components/PermissionHistory.svelte
Normal file
@@ -0,0 +1,122 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { untrack } from 'svelte'
|
||||
import TableCustom from './TableCustom.svelte'
|
||||
import Skeleton from './common/skeleton/Skeleton.svelte'
|
||||
import Label from './Label.svelte'
|
||||
|
||||
interface PermissionChange {
|
||||
id?: number
|
||||
changed_by?: string
|
||||
changed_at?: string
|
||||
change_type?: string
|
||||
affected?: string | null
|
||||
member_affected?: string | null
|
||||
}
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
fetchHistory: (
|
||||
workspace: string,
|
||||
name: string,
|
||||
page: number,
|
||||
perPage: number
|
||||
) => Promise<PermissionChange[]>
|
||||
}
|
||||
|
||||
let { name, fetchHistory }: Props = $props()
|
||||
let history: PermissionChange[] | undefined = $state(undefined)
|
||||
let loading = $state(false)
|
||||
let page = $state(1)
|
||||
let perPage = $state(50)
|
||||
|
||||
async function loadHistory() {
|
||||
if (!$workspaceStore) return
|
||||
loading = true
|
||||
try {
|
||||
history = await fetchHistory($workspaceStore, name, page, perPage)
|
||||
} catch (e) {
|
||||
console.error('Failed to load permission history:', e)
|
||||
history = []
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
function formatChangeType(changeType: string): string {
|
||||
return changeType
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
$effect.pre(() => {
|
||||
if ($workspaceStore && name) {
|
||||
untrack(() => {
|
||||
loadHistory()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function removeUPrefix(username: string | undefined): string | undefined {
|
||||
return username?.startsWith('u/') ? username.slice(2) : username
|
||||
}
|
||||
</script>
|
||||
|
||||
<Label label="History">
|
||||
{#if loading || history === undefined}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each new Array(3) as _}
|
||||
<Skeleton layout={[[4], 0.7]} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if history.length === 0}
|
||||
<p class="text-primary text-sm">No permission changes recorded yet</p>
|
||||
{:else}
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>Changed By</th>
|
||||
<th>Change Type</th>
|
||||
<th>Affected</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
{#snippet body()}
|
||||
<tbody>
|
||||
{#each history as change}
|
||||
<tr>
|
||||
<td>{change.changed_by ?? '-'}</td>
|
||||
<td>{change.change_type ? formatChangeType(change.change_type) : '-'}</td>
|
||||
<td>{change.affected ?? removeUPrefix(change.member_affected ?? '')}</td>
|
||||
<td class="text-xs">{change.changed_at ? formatDate(change.changed_at) : '-'}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
{/snippet}
|
||||
</TableCustom>
|
||||
<div class="flex flex-row gap-2 justify-end">
|
||||
{#if page > 1}
|
||||
<button
|
||||
class="text-primary text-xs"
|
||||
onclick={() => {
|
||||
page--
|
||||
loadHistory()
|
||||
}}>Previous</button
|
||||
>
|
||||
{/if}
|
||||
{#if history.length === perPage}
|
||||
<button
|
||||
class="text-primary text-xs"
|
||||
onclick={() => {
|
||||
page++
|
||||
loadHistory()
|
||||
}}>Next</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Label>
|
||||
@@ -274,7 +274,7 @@
|
||||
{/snippet}
|
||||
</Select>
|
||||
{#if value && hovering}
|
||||
<div class="absolute right-2 z-20">
|
||||
<div class="absolute {disabled ? 'right-2' : 'right-10'} z-20">
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs2"
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { getDbClockNow } from '$lib/forLater'
|
||||
import { displayDate } from '$lib/utils'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { onDestroy, onMount, untrack } from 'svelte'
|
||||
|
||||
export let date: string
|
||||
export let agoOnlyIfRecent: boolean = false
|
||||
export let noDate = false
|
||||
export let isRecent: boolean = true
|
||||
interface Props {
|
||||
date: string
|
||||
agoOnlyIfRecent?: boolean
|
||||
noDate?: boolean
|
||||
isRecent?: boolean
|
||||
}
|
||||
|
||||
let computedTimeAgo: string | undefined = undefined
|
||||
let {
|
||||
date,
|
||||
agoOnlyIfRecent = false,
|
||||
noDate = false,
|
||||
isRecent = $bindable(true)
|
||||
}: Props = $props()
|
||||
|
||||
let computedTimeAgo: string | undefined = $state(undefined)
|
||||
|
||||
let interval
|
||||
|
||||
$: date && computeDate()
|
||||
|
||||
onMount(() => {
|
||||
interval = setInterval(() => {
|
||||
computeDate()
|
||||
@@ -73,6 +80,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
$effect(() => {
|
||||
date && untrack(() => computeDate())
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if computedTimeAgo && (!agoOnlyIfRecent || isRecent)}
|
||||
|
||||
@@ -524,7 +524,7 @@
|
||||
const requestBody: ExecuteComponentData['requestBody'] = {
|
||||
args: nonStaticRunnableInputs,
|
||||
component: id,
|
||||
force_viewer_static_fields: !isEditor ? undefined : undefinedIfEmpty(staticRunnableInputs),
|
||||
force_viewer_static_fields: !isEditor ? undefined : staticRunnableInputs,
|
||||
force_viewer_one_of_fields: !isEditor ? undefined : undefinedIfEmpty(oneOfRunnableInputs),
|
||||
force_viewer_allow_user_resources: !isEditor
|
||||
? undefined
|
||||
|
||||
@@ -12,74 +12,91 @@
|
||||
interface Props {
|
||||
marked: string | undefined
|
||||
starred: boolean
|
||||
selected?: boolean
|
||||
disabled?: boolean
|
||||
canFavorite?: boolean
|
||||
isSelectable?: boolean
|
||||
alignWithSelectable?: boolean
|
||||
errorHandlerMuted?: boolean
|
||||
aiId?: string | undefined
|
||||
aiDescription?: string | undefined
|
||||
kind?: 'script' | 'flow' | 'app' | 'raw_app'
|
||||
kind?: 'script' | 'flow' | 'app' | 'raw_app' | 'resource' | 'variable'
|
||||
summary?: string | undefined
|
||||
path: string
|
||||
href: string
|
||||
href?: string
|
||||
workspaceId: string
|
||||
depth?: number
|
||||
badges?: import('svelte').Snippet
|
||||
actions?: import('svelte').Snippet
|
||||
customSummary?: import('svelte').Snippet
|
||||
onSelect?: (
|
||||
e: Event & {
|
||||
currentTarget: EventTarget & HTMLInputElement
|
||||
}
|
||||
) => void
|
||||
}
|
||||
|
||||
let {
|
||||
marked,
|
||||
starred,
|
||||
selected = false,
|
||||
disabled = false,
|
||||
canFavorite = true,
|
||||
isSelectable = false,
|
||||
alignWithSelectable = false,
|
||||
errorHandlerMuted = false,
|
||||
aiId = undefined,
|
||||
aiDescription = undefined,
|
||||
kind = 'script',
|
||||
summary = undefined,
|
||||
path,
|
||||
href,
|
||||
href = undefined,
|
||||
workspaceId,
|
||||
depth = 0,
|
||||
badges,
|
||||
actions
|
||||
actions,
|
||||
customSummary,
|
||||
onSelect = () => {}
|
||||
}: Props = $props()
|
||||
|
||||
let displayPath: string = (depth === 0 ? path : path?.split('/')?.slice(-1)?.[0]) ?? ''
|
||||
</script>
|
||||
|
||||
<div
|
||||
style="display: none"
|
||||
use:triggerableByAI={{
|
||||
id: aiId,
|
||||
description: aiDescription,
|
||||
callback: () => {
|
||||
goto(href)
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
{#if href}
|
||||
<div
|
||||
style="display: none"
|
||||
use:triggerableByAI={{
|
||||
id: aiId,
|
||||
description: aiDescription,
|
||||
callback: () => {
|
||||
goto(href)
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
{/if}
|
||||
<div
|
||||
class={twMerge(
|
||||
'hover:bg-surface-hover w-full inline-flex items-center gap-4 first-of-type:!border-t-0 first-of-type:rounded-t-md last-of-type:rounded-b-md [*:not(:last-child)]:border-b px-4 py-3 border-b last:border-b-0',
|
||||
depth > 0 ? '!rounded-none' : ''
|
||||
'w-full inline-flex items-center gap-4 first-of-type:!border-t-0 first-of-type:rounded-t-md last-of-type:rounded-b-md [*:not(:last-child)]:border-b px-4 py-3 border-b last:border-b-0',
|
||||
depth > 0 ? '!rounded-none' : '',
|
||||
disabled ? 'opacity-25' : 'hover:bg-surface-hover',
|
||||
selected ? 'bg-surface-accent-selected' : ''
|
||||
|
||||
)}
|
||||
style={depth > 0 ? `padding-left: ${depth * 32}px;` : ''}
|
||||
>
|
||||
<a {href} class="min-w-0 grow hover:underline decoration-gray-400 inline-flex items-center gap-4">
|
||||
<div class="shrink">
|
||||
<RowIcon {href} {kind} />
|
||||
</div>
|
||||
<div class="grow">
|
||||
<div class="text-emphasis flex-wrap text-left text-xs font-semibold">
|
||||
{#if marked}
|
||||
{@html marked}
|
||||
{:else}
|
||||
{!summary || summary.length == 0 ? displayPath : summary}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-hint text-3xs truncate text-left font-normal">
|
||||
{path}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{#if isSelectable}
|
||||
<input type="checkbox" checked={selected} onchange={onSelect} class="rounded max-w-4 w-full" />
|
||||
{:else if alignWithSelectable}
|
||||
<div class="rounded max-w-4 w-full"></div>
|
||||
{/if}
|
||||
|
||||
{#if href}
|
||||
<a {href} class="min-w-0 grow hover:underline decoration-gray-400 inline-flex items-center gap-4">
|
||||
{@render rowContent()}
|
||||
</a>
|
||||
{:else}
|
||||
{@render rowContent()}
|
||||
{/if}
|
||||
|
||||
{#if errorHandlerMuted}
|
||||
<BellOff class="w-8 opacity-60" size={12} fill="currentcolor" />
|
||||
@@ -91,8 +108,8 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if canFavorite}
|
||||
<div class="center-center h-full text-xs font-semibold text-secondary">
|
||||
{#if canFavorite && kind !== 'resource' && kind !== 'variable'}
|
||||
<div class="center-center h-full text-xs font-semibold text-secondary w-9">
|
||||
<Star
|
||||
{kind}
|
||||
{path}
|
||||
@@ -111,3 +128,25 @@
|
||||
{@render actions?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#snippet rowContent()}
|
||||
<div class="shrink">
|
||||
<RowIcon {kind} />
|
||||
</div>
|
||||
<div class="grow">
|
||||
<div class="text-emphasis flex-wrap text-left text-xs font-semibold">
|
||||
{#if customSummary}
|
||||
{@render customSummary?.()}
|
||||
{:else}
|
||||
{#if marked}
|
||||
{@html marked}
|
||||
{:else}
|
||||
{!summary || summary.length == 0 ? displayPath : summary}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-hint text-3xs truncate text-left font-normal">
|
||||
{path}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
<script lang="ts">
|
||||
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
|
||||
import { Code2, LayoutDashboard } from 'lucide-svelte'
|
||||
import { Boxes, Code2, DollarSign, LayoutDashboard } from 'lucide-svelte'
|
||||
|
||||
export let kind: 'script' | 'flow' | 'app' | 'raw_app'
|
||||
export let href: string = '#'
|
||||
export let kind: 'script' | 'flow' | 'app' | 'raw_app' | 'resource' | 'variable'
|
||||
</script>
|
||||
|
||||
<a {href} class="flex justify-center items-center">
|
||||
<div class="flex justify-center items-center">
|
||||
{#if kind === 'flow'}
|
||||
<BarsStaggered size={16} class="text-teal-500" />
|
||||
{:else if kind === 'app' || kind === 'raw_app'}
|
||||
<LayoutDashboard size={16} class="text-orange-500" />
|
||||
{:else if kind === 'script'}
|
||||
<Code2 size={16} class="text-blue-500" />
|
||||
{:else if kind === 'variable'}
|
||||
<DollarSign size={16} class="text-gray-400" />
|
||||
{:else if kind === 'resource'}
|
||||
<Boxes size={16} class="text-gray-400" />
|
||||
{:else}
|
||||
<div class="w-[16px]"></div>
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Button } from '$lib/components/common'
|
||||
import { WandSparkles } from 'lucide-svelte'
|
||||
import { aiChatManager } from './chat/AIChatManager.svelte'
|
||||
import { flowAIBtnClasses } from './chat/flow/FlowAIButton.svelte'
|
||||
import { AIBtnClasses } from './chat/AIButtonStyle'
|
||||
interface Props {
|
||||
label?: string
|
||||
initialInput?: string
|
||||
@@ -29,7 +29,7 @@
|
||||
icon: WandSparkles
|
||||
}}
|
||||
unifiedSize="md"
|
||||
btnClasses={flowAIBtnClasses('default')}
|
||||
btnClasses={AIBtnClasses('default')}
|
||||
on:click={onClick}
|
||||
>
|
||||
{label}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
inputBorderClass,
|
||||
inputSizeClasses
|
||||
} from '../text_input/TextInput.svelte'
|
||||
import { flowAIBtnClasses } from './chat/flow/FlowAIButton.svelte'
|
||||
import { AIBtnClasses } from './chat/AIButtonStyle'
|
||||
|
||||
type PromptConfig = {
|
||||
system: string
|
||||
@@ -294,7 +294,7 @@ Generate a tool name for the script below:
|
||||
<span
|
||||
class={twMerge(
|
||||
'rounded-md px-1',
|
||||
flowAIBtnClasses(!loading && generatedContent.length > 0 ? 'green' : 'selected')
|
||||
AIBtnClasses(!loading && generatedContent.length > 0 ? 'green' : 'selected')
|
||||
)}
|
||||
>
|
||||
<span class="px-0.5 py-0.5 rounded-md text-2xs text-bold flex flex-row items-center gap-1">
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import FlowCopilotInputsModal from './FlowCopilotInputsModal.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { copilotInfo } from '$lib/aiStore'
|
||||
import { flowAIBtnClasses } from './chat/flow/FlowAIButton.svelte'
|
||||
import { AIBtnClasses } from './chat/AIButtonStyle'
|
||||
|
||||
let generatedContent = $state('')
|
||||
let loading = $state(false)
|
||||
@@ -232,7 +232,7 @@ Only return the expression without any wrapper.`
|
||||
size="xs"
|
||||
variant="default"
|
||||
btnClasses={twMerge(
|
||||
flowAIBtnClasses(!loading && generatedContent.length > 0 ? 'green' : 'default'),
|
||||
AIBtnClasses(!loading && generatedContent.length > 0 ? 'green' : 'default'),
|
||||
btnClass
|
||||
)}
|
||||
on:click={() => {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import FlowCopilotInputsModal from './FlowCopilotInputsModal.svelte'
|
||||
import type { Flow } from '$lib/gen'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { flowAIBtnClasses } from './chat/flow/FlowAIButton.svelte'
|
||||
import { AIBtnClasses } from './chat/AIButtonStyle'
|
||||
|
||||
let loading = $state(false)
|
||||
interface Props {
|
||||
@@ -189,7 +189,7 @@ input_name2: expression2
|
||||
variant="default"
|
||||
btnClasses={twMerge(
|
||||
!disabled &&
|
||||
flowAIBtnClasses(
|
||||
AIBtnClasses(
|
||||
!loading && Object.keys($generatedExprs || {}).length > 0 ? 'green' : 'default'
|
||||
)
|
||||
)}
|
||||
@@ -234,7 +234,7 @@ input_name2: expression2
|
||||
<Button
|
||||
size="xs"
|
||||
variant="default"
|
||||
btnClasses={flowAIBtnClasses('default')}
|
||||
btnClasses={AIBtnClasses('default')}
|
||||
nonCaptureEvent
|
||||
startIcon={{
|
||||
icon: Wand2
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
<script module lang="ts">
|
||||
export function flowAIBtnClasses(state: 'default' | 'selected' | 'green' = 'default') {
|
||||
return twMerge(
|
||||
['selected', 'default'].includes(state) ? 'text-ai !border-ai/20 hover:bg-ai/15' : '',
|
||||
{
|
||||
default: '',
|
||||
selected: 'bg-ai/10',
|
||||
green:
|
||||
'bg-green-50 hover:bg-green-50 dark:bg-green-400/15 dark:hover:bg-green-400/15 text-green-800 border-green-200 dark:border-green-300/60 dark:text-green-400'
|
||||
}[state]
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { base } from '$lib/base'
|
||||
import { copilotInfo } from '$lib/aiStore'
|
||||
@@ -20,14 +6,13 @@
|
||||
import { ExternalLink, WandSparkles } from 'lucide-svelte'
|
||||
import { getModifierKey } from '$lib/utils'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
let {
|
||||
togglePanel,
|
||||
selected = false
|
||||
btnClasses
|
||||
}: {
|
||||
togglePanel: () => void
|
||||
selected?: boolean
|
||||
btnClasses?: string
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
@@ -72,7 +57,7 @@
|
||||
onClick={onPress}
|
||||
startIcon={{ icon: WandSparkles }}
|
||||
iconOnly
|
||||
btnClasses={flowAIBtnClasses(selected ? 'selected' : 'default')}
|
||||
{btnClasses}
|
||||
>
|
||||
AI Panel
|
||||
</Button>
|
||||
13
frontend/src/lib/components/copilot/chat/AIButtonStyle.ts
Normal file
13
frontend/src/lib/components/copilot/chat/AIButtonStyle.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export function AIBtnClasses(state: 'default' | 'selected' | 'green' = 'default') {
|
||||
return twMerge(
|
||||
['selected', 'default'].includes(state) ? 'text-ai !border-ai/20 hover:bg-ai/15' : '',
|
||||
{
|
||||
default: '',
|
||||
selected: 'bg-ai/10',
|
||||
green:
|
||||
'bg-green-50 hover:bg-green-50 dark:bg-green-400/15 dark:hover:bg-green-400/15 text-green-800 border-green-200 dark:border-green-300/60 dark:text-green-400'
|
||||
}[state]
|
||||
)
|
||||
}
|
||||
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -26,7 +26,7 @@ import type {
|
||||
ChatCompletionUserMessageParam
|
||||
} from 'openai/resources/chat/completions.mjs'
|
||||
import {
|
||||
INLINE_CHAT_SYSTEM_PROMPT,
|
||||
prepareInlineChatSystemPrompt,
|
||||
prepareScriptSystemMessage,
|
||||
prepareScriptTools
|
||||
} from './script/core'
|
||||
@@ -67,10 +67,6 @@ export enum AIMode {
|
||||
}
|
||||
|
||||
class AIChatManager {
|
||||
NAVIGATION_SYSTEM_PROMPT = `
|
||||
CONSIDERATIONS:
|
||||
- You are provided with a tool to switch to navigation mode, only use it when you are sure that the user is asking you to navigate the application, help them find something or fetch data from the API. Do not use it otherwise.
|
||||
`
|
||||
contextManager = new ContextManager()
|
||||
historyManager = new HistoryManager()
|
||||
abortController: AbortController | undefined = undefined
|
||||
@@ -221,10 +217,10 @@ class AIChatManager {
|
||||
if (mode === AIMode.SCRIPT) {
|
||||
const customPrompt = getCombinedCustomPrompt(mode)
|
||||
const currentModel = getCurrentModel()
|
||||
this.systemMessage = prepareScriptSystemMessage(currentModel, customPrompt)
|
||||
this.systemMessage.content = this.NAVIGATION_SYSTEM_PROMPT + this.systemMessage.content
|
||||
const context = this.contextManager.getSelectedContext()
|
||||
const lang = this.scriptEditorOptions?.lang ?? 'bun'
|
||||
const context = this.contextManager.getSelectedContext()
|
||||
this.systemMessage = prepareScriptSystemMessage(currentModel, lang, {}, customPrompt)
|
||||
this.systemMessage.content = this.systemMessage.content
|
||||
this.tools = [...prepareScriptTools(currentModel, lang, context)]
|
||||
this.helpers = {
|
||||
getScriptOptions: () => {
|
||||
@@ -248,7 +244,7 @@ class AIChatManager {
|
||||
} else if (mode === AIMode.FLOW) {
|
||||
const customPrompt = getCombinedCustomPrompt(mode)
|
||||
this.systemMessage = prepareFlowSystemMessage(customPrompt)
|
||||
this.systemMessage.content = this.NAVIGATION_SYSTEM_PROMPT + this.systemMessage.content
|
||||
this.systemMessage.content = this.systemMessage.content
|
||||
this.tools = [...flowTools]
|
||||
this.helpers = this.flowAiChatHelpers
|
||||
} else if (mode === AIMode.NAVIGATOR) {
|
||||
@@ -408,7 +404,6 @@ class AIChatManager {
|
||||
if (this.mode === AIMode.SCRIPT) {
|
||||
pendingUserMessage = prepareScriptUserMessage(
|
||||
pendingPrompt,
|
||||
this.scriptEditorOptions?.lang as ScriptLang | 'bunnative',
|
||||
this.contextManager.getSelectedContext()
|
||||
)
|
||||
} else if (this.mode === AIMode.FLOW) {
|
||||
@@ -508,15 +503,13 @@ class AIChatManager {
|
||||
|
||||
const systemMessage: ChatCompletionSystemMessageParam = {
|
||||
role: 'system',
|
||||
content: INLINE_CHAT_SYSTEM_PROMPT
|
||||
content: prepareInlineChatSystemPrompt(lang)
|
||||
}
|
||||
|
||||
let reply = ''
|
||||
|
||||
try {
|
||||
const userMessage = prepareScriptUserMessage(instructions, lang, selectedContext, {
|
||||
isPreprocessor: false
|
||||
})
|
||||
const userMessage = prepareScriptUserMessage(instructions, selectedContext)
|
||||
const messages = [userMessage]
|
||||
|
||||
const params = {
|
||||
@@ -606,10 +599,15 @@ class AIChatManager {
|
||||
throw new Error('No flow helpers found')
|
||||
}
|
||||
|
||||
let snapshot: ExtendedOpenFlow | undefined = undefined
|
||||
let snapshot:
|
||||
| { type: 'flow'; value: ExtendedOpenFlow }
|
||||
| { type: 'app'; value: number }
|
||||
| undefined = undefined
|
||||
if (this.mode === AIMode.FLOW) {
|
||||
snapshot = this.flowAiChatHelpers!.getFlowAndSelectedId().flow
|
||||
this.flowAiChatHelpers!.setSnapshot(snapshot)
|
||||
snapshot = { type: 'flow', value: this.flowAiChatHelpers!.getFlowAndSelectedId().flow }
|
||||
this.flowAiChatHelpers!.setSnapshot(snapshot.value)
|
||||
} else if (this.mode === AIMode.APP) {
|
||||
snapshot = { type: 'app', value: this.appAiChatHelpers!.snapshot() }
|
||||
}
|
||||
|
||||
this.displayMessages = [
|
||||
@@ -632,10 +630,6 @@ class AIChatManager {
|
||||
throw new Error('No script options passed')
|
||||
}
|
||||
|
||||
const lang = this.scriptEditorOptions?.lang ?? options.lang ?? 'bun'
|
||||
const isPreprocessor =
|
||||
this.scriptEditorOptions?.path === 'preprocessor' || options.isPreprocessor
|
||||
|
||||
let userMessage: ChatCompletionMessageParam = {
|
||||
role: 'user',
|
||||
content: ''
|
||||
@@ -655,9 +649,7 @@ class AIChatManager {
|
||||
userMessage = prepareAskUserMessage(oldInstructions)
|
||||
break
|
||||
case AIMode.SCRIPT:
|
||||
userMessage = prepareScriptUserMessage(oldInstructions, lang, oldSelectedContext, {
|
||||
isPreprocessor
|
||||
})
|
||||
userMessage = prepareScriptUserMessage(oldInstructions, oldSelectedContext)
|
||||
break
|
||||
case AIMode.API:
|
||||
userMessage = prepareApiUserMessage(oldInstructions)
|
||||
@@ -665,7 +657,6 @@ class AIChatManager {
|
||||
case AIMode.APP:
|
||||
userMessage = prepareAppUserMessage(
|
||||
oldInstructions,
|
||||
this.appAiChatHelpers?.getFiles(),
|
||||
this.appAiChatHelpers?.getSelectedContext()
|
||||
)
|
||||
break
|
||||
|
||||
@@ -89,13 +89,17 @@
|
||||
{/if}
|
||||
{#if message.role === 'user' && message.snapshot}
|
||||
<div class="mx-2 text-sm text-primary flex flex-row items-center justify-between gap-2 mt-2">
|
||||
Saved a flow snapshot
|
||||
Saved {message.snapshot.type === 'flow' ? 'a flow' : 'an app'} snapshot
|
||||
<Button
|
||||
size="xs2"
|
||||
variant="default"
|
||||
on:click={() => {
|
||||
if (message.snapshot) {
|
||||
aiChatManager.flowAiChatHelpers?.revertToSnapshot(message.snapshot)
|
||||
if (message.snapshot.type === 'flow') {
|
||||
aiChatManager.flowAiChatHelpers?.revertToSnapshot(message.snapshot.value)
|
||||
} else if (message.snapshot.type === 'app') {
|
||||
aiChatManager.appAiChatHelpers?.revertToSnapshot(message.snapshot.value)
|
||||
}
|
||||
}
|
||||
}}
|
||||
title="Revert to snapshot"
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { runVariantComparison, writeAppComparisonResults } from './appEvalRunner'
|
||||
import { BASELINE_VARIANT, STREAMLINED_VARIANT } from './variants'
|
||||
import { loadAppFixtureForEval } from './appFixtureLoader'
|
||||
// @ts-ignore - Node.js path
|
||||
import { dirname, join } from 'path'
|
||||
// @ts-ignore - Node.js url
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
// Get API key from environment - tests will be skipped if not set
|
||||
// @ts-ignore
|
||||
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY
|
||||
|
||||
// Skip all tests if no API key is provided
|
||||
const describeWithApiKey = OPENROUTER_API_KEY ? describe : describe.skip
|
||||
|
||||
// Get __dirname equivalent for ES modules
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
const MODELS = ['google/gemini-2.5-flash', 'anthropic/claude-haiku-4.5', 'openai/gpt-4o']
|
||||
const VARIANTS = [
|
||||
...MODELS.map((model) => ({
|
||||
...BASELINE_VARIANT,
|
||||
model,
|
||||
name: `baseline-${model.replace('/', '-')}`
|
||||
})),
|
||||
...MODELS.map((model) => ({
|
||||
...STREAMLINED_VARIANT,
|
||||
model,
|
||||
name: `streamlined-${model.replace('/', '-')}`
|
||||
}))
|
||||
]
|
||||
|
||||
describeWithApiKey('App Chat LLM Evaluation', () => {
|
||||
const TEST_TIMEOUT = 120_000
|
||||
if (!OPENROUTER_API_KEY) {
|
||||
console.warn('OPENROUTER_API_KEY is not set, skipping tests')
|
||||
}
|
||||
|
||||
it(
|
||||
'test1: creates a simple counter app',
|
||||
async () => {
|
||||
const USER_PROMPT = `Create a counter app with increment/decrement buttons`
|
||||
const results = await runVariantComparison(USER_PROMPT, VARIANTS, OPENROUTER_API_KEY!)
|
||||
// Write results to files
|
||||
const { summaryPath, appPaths } = await writeAppComparisonResults(USER_PROMPT, results)
|
||||
console.log(`\nResults written to: ${summaryPath}`)
|
||||
console.log(`App files: ${appPaths.join(', ')}`)
|
||||
|
||||
expect(true).toBe(true)
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
)
|
||||
|
||||
it(
|
||||
'test2: modifies existing counter app to add reset button',
|
||||
async () => {
|
||||
// Load initial app from fixture folder
|
||||
const { initialFrontend, initialBackend } = await loadAppFixtureForEval(
|
||||
join(__dirname, 'initial', 'test1_counter_app')
|
||||
)
|
||||
|
||||
const USER_PROMPT = `Add a reset button that sets the counter back to 0`
|
||||
const results = await runVariantComparison(USER_PROMPT, VARIANTS, OPENROUTER_API_KEY!, {
|
||||
initialFrontend,
|
||||
initialBackend
|
||||
})
|
||||
// Write results to files
|
||||
const { summaryPath, appPaths } = await writeAppComparisonResults(USER_PROMPT, results)
|
||||
console.log(`\nResults written to: ${summaryPath}`)
|
||||
console.log(`App files: ${appPaths.join(', ')}`)
|
||||
|
||||
expect(true).toBe(true)
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
)
|
||||
|
||||
// ==================== Shopping Cart Tests ====================
|
||||
|
||||
it(
|
||||
'test3: shopping cart - add quantity selector',
|
||||
async () => {
|
||||
const { initialFrontend, initialBackend } = await loadAppFixtureForEval(
|
||||
join(__dirname, 'initial', 'shopping_cart')
|
||||
)
|
||||
|
||||
const USER_PROMPT = `Add a quantity selector (+ and - buttons) to each cart item so users can adjust quantities without removing and re-adding items`
|
||||
const results = await runVariantComparison(USER_PROMPT, VARIANTS, OPENROUTER_API_KEY!, {
|
||||
initialFrontend,
|
||||
initialBackend
|
||||
})
|
||||
|
||||
const { summaryPath, appPaths } = await writeAppComparisonResults(USER_PROMPT, results)
|
||||
console.log(`\nResults written to: ${summaryPath}`)
|
||||
console.log(`App files: ${appPaths.join(', ')}`)
|
||||
|
||||
expect(true).toBe(true)
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
)
|
||||
|
||||
it(
|
||||
'test4: shopping cart - add discount code',
|
||||
async () => {
|
||||
const { initialFrontend, initialBackend } = await loadAppFixtureForEval(
|
||||
join(__dirname, 'initial', 'shopping_cart')
|
||||
)
|
||||
|
||||
const USER_PROMPT = `Add a discount code input field in the cart. When the code "SAVE10" is entered, apply a 10% discount to the total`
|
||||
const results = await runVariantComparison(USER_PROMPT, VARIANTS, OPENROUTER_API_KEY!, {
|
||||
initialFrontend,
|
||||
initialBackend
|
||||
})
|
||||
|
||||
const { summaryPath, appPaths } = await writeAppComparisonResults(USER_PROMPT, results)
|
||||
console.log(`\nResults written to: ${summaryPath}`)
|
||||
console.log(`App files: ${appPaths.join(', ')}`)
|
||||
|
||||
expect(true).toBe(true)
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
)
|
||||
|
||||
// ==================== File Manager Tests ====================
|
||||
|
||||
it(
|
||||
'test5: file manager - add search bar',
|
||||
async () => {
|
||||
const { initialFrontend, initialBackend } = await loadAppFixtureForEval(
|
||||
join(__dirname, 'initial', 'file_manager')
|
||||
)
|
||||
|
||||
const USER_PROMPT = `Add a search bar in the toolbar that filters files and folders by name as the user types`
|
||||
const results = await runVariantComparison(USER_PROMPT, VARIANTS, OPENROUTER_API_KEY!, {
|
||||
initialFrontend,
|
||||
initialBackend
|
||||
})
|
||||
|
||||
const { summaryPath, appPaths } = await writeAppComparisonResults(USER_PROMPT, results)
|
||||
console.log(`\nResults written to: ${summaryPath}`)
|
||||
console.log(`App files: ${appPaths.join(', ')}`)
|
||||
|
||||
expect(true).toBe(true)
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
)
|
||||
|
||||
it(
|
||||
'test6: file manager - show file details',
|
||||
async () => {
|
||||
const { initialFrontend, initialBackend } = await loadAppFixtureForEval(
|
||||
join(__dirname, 'initial', 'file_manager')
|
||||
)
|
||||
|
||||
const USER_PROMPT = `Show file size (formatted as KB/MB) and modified date in the file list for each item`
|
||||
const results = await runVariantComparison(USER_PROMPT, VARIANTS, OPENROUTER_API_KEY!, {
|
||||
initialFrontend,
|
||||
initialBackend
|
||||
})
|
||||
|
||||
const { summaryPath, appPaths } = await writeAppComparisonResults(USER_PROMPT, results)
|
||||
console.log(`\nResults written to: ${summaryPath}`)
|
||||
console.log(`App files: ${appPaths.join(', ')}`)
|
||||
|
||||
expect(true).toBe(true)
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
)
|
||||
|
||||
it(
|
||||
'test7: file manager - add select all checkbox',
|
||||
async () => {
|
||||
const { initialFrontend, initialBackend } = await loadAppFixtureForEval(
|
||||
join(__dirname, 'initial', 'file_manager')
|
||||
)
|
||||
|
||||
const USER_PROMPT = `Add a "Select All" checkbox in the file list header and individual checkboxes for each file. Add a "Delete Selected" button that appears when items are selected`
|
||||
const results = await runVariantComparison(USER_PROMPT, VARIANTS, OPENROUTER_API_KEY!, {
|
||||
initialFrontend,
|
||||
initialBackend
|
||||
})
|
||||
|
||||
const { summaryPath, appPaths } = await writeAppComparisonResults(USER_PROMPT, results)
|
||||
console.log(`\nResults written to: ${summaryPath}`)
|
||||
console.log(`App files: ${appPaths.join(', ')}`)
|
||||
|
||||
expect(true).toBe(true)
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
)
|
||||
|
||||
// ==================== From-Scratch Creation Tests ====================
|
||||
|
||||
it(
|
||||
'test8: create quiz app from scratch',
|
||||
async () => {
|
||||
const USER_PROMPT = `Create a multiple choice quiz app with 5 questions about general knowledge. Show one question at a time with 4 answer options. Track the score and show results at the end with percentage correct.`
|
||||
const results = await runVariantComparison(USER_PROMPT, VARIANTS, OPENROUTER_API_KEY!)
|
||||
|
||||
const { summaryPath, appPaths } = await writeAppComparisonResults(USER_PROMPT, results)
|
||||
console.log(`\nResults written to: ${summaryPath}`)
|
||||
console.log(`App files: ${appPaths.join(', ')}`)
|
||||
|
||||
expect(true).toBe(true)
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
)
|
||||
|
||||
it(
|
||||
'test9: create recipe book from scratch',
|
||||
async () => {
|
||||
const USER_PROMPT = `Create a recipe book app where users can add recipes with a name, ingredients list, and instructions. Include a search bar to filter recipes by name and the ability to delete recipes.`
|
||||
const results = await runVariantComparison(USER_PROMPT, VARIANTS, OPENROUTER_API_KEY!)
|
||||
|
||||
const { summaryPath, appPaths } = await writeAppComparisonResults(USER_PROMPT, results)
|
||||
console.log(`\nResults written to: ${summaryPath}`)
|
||||
console.log(`App files: ${appPaths.join(', ')}`)
|
||||
|
||||
expect(true).toBe(true)
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,166 @@
|
||||
import OpenAI from 'openai'
|
||||
import type { AppFiles, BackendRunnable } from '../../app/core'
|
||||
import { BASE_EVALUATOR_RESPONSE_FORMAT } from '../shared'
|
||||
import type { EvaluationResult } from '../shared'
|
||||
|
||||
/**
|
||||
* Expected app structure for evaluation.
|
||||
*/
|
||||
export interface ExpectedApp {
|
||||
frontend: Record<string, string>
|
||||
backend: Record<string, BackendRunnable>
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial app state for evaluation context.
|
||||
*/
|
||||
export interface InitialApp {
|
||||
frontend: Record<string, string>
|
||||
backend: Record<string, BackendRunnable>
|
||||
}
|
||||
|
||||
/**
|
||||
* System prompt for evaluating app generation without a reference expected app.
|
||||
* Evaluates based on user request fulfillment and appropriate modifications to initial state.
|
||||
*/
|
||||
const APP_GENERATION_EVALUATOR_SYSTEM_PROMPT = `You are an expert evaluator for Windmill Raw App definitions. Your task is to evaluate a generated app based on:
|
||||
1. The original user request/prompt
|
||||
2. The initial app state (if any) - this is what the app looked like before the AI made changes
|
||||
|
||||
## Windmill Raw App Context
|
||||
- Raw Apps consist of frontend files and backend runnables
|
||||
- Frontend files are TypeScript/JavaScript files bundled with esbuild (entrypoint: index.tsx)
|
||||
- Backend runnables can be: inline scripts (TypeScript/Python), workspace scripts, workspace flows, or hub scripts
|
||||
- Frontend calls backend using \`await backend.<runnable_key>(args...)\`
|
||||
- Each backend runnable has a key (identifier), name (description), type, and configuration
|
||||
|
||||
## Backend Runnable Types
|
||||
- **inline**: Custom code with \`inlineScript.language\` and \`inlineScript.content\`
|
||||
- **script**: Workspace script reference with \`path\`
|
||||
- **flow**: Workspace flow reference with \`path\`
|
||||
- **hubscript**: Hub script reference with \`path\`
|
||||
|
||||
## Evaluation Criteria
|
||||
1. **User Request Fulfillment**: Does the generated app address ALL requirements from the user's original prompt?
|
||||
- Are all requested features implemented?
|
||||
- Does the frontend UI match the requirements?
|
||||
- Are the correct backend runnables created?
|
||||
2. **Appropriate Modifications** (if initial app was provided):
|
||||
- Were the changes made relevant to the user's request?
|
||||
- Was existing functionality preserved where appropriate?
|
||||
- Were only necessary changes made (no unnecessary removals or additions)?
|
||||
3. **Frontend Structure**: Are the frontend files correctly organized and implemented?
|
||||
- Is the code valid TypeScript/JavaScript?
|
||||
- Are components properly structured?
|
||||
- Are backend calls correctly made?
|
||||
4. **Backend Structure**: Are the backend runnables correctly configured?
|
||||
- Do inline scripts have proper main functions?
|
||||
- Are types and paths correct for non-inline runnables?
|
||||
5. **Integration**: Does the frontend correctly call the backend?
|
||||
- Are the runnable keys correctly referenced?
|
||||
- Are arguments passed correctly?
|
||||
6. **Code Quality**: Is the code functionally correct and well-structured?
|
||||
|
||||
## Important Notes
|
||||
- Focus on whether the user's request was fulfilled, not on stylistic preferences
|
||||
- If an initial app was provided, evaluate the appropriateness of the changes made
|
||||
- For new apps (no initial state), evaluate completeness and correctness
|
||||
- Extra helper functions or slightly different approaches can still score high if they accomplish the goal
|
||||
|
||||
${BASE_EVALUATOR_RESPONSE_FORMAT}`
|
||||
|
||||
/**
|
||||
* Evaluates how well a generated app fulfills the user's request, considering any initial app state.
|
||||
* This evaluator does not require an expected reference app - it evaluates based on the request alone.
|
||||
*
|
||||
* @param userPrompt The original user request
|
||||
* @param generatedApp The app generated by the AI
|
||||
* @param initialApp Optional initial app state (what the app looked like before AI changes)
|
||||
* @returns Evaluation result with score, statement, and missing requirements
|
||||
*/
|
||||
export async function evaluateAppGeneration(
|
||||
userPrompt: string,
|
||||
generatedApp: AppFiles,
|
||||
initialApp?: InitialApp
|
||||
): Promise<EvaluationResult> {
|
||||
// @ts-ignore
|
||||
const apiKey = process.env.OPENROUTER_API_KEY
|
||||
|
||||
const client = new OpenAI({ baseURL: 'https://openrouter.ai/api/v1', apiKey })
|
||||
|
||||
let userMessage = `## User's Original Request
|
||||
${userPrompt}
|
||||
|
||||
`
|
||||
|
||||
if (initialApp) {
|
||||
userMessage += `## Initial App State (before AI modifications)
|
||||
\`\`\`json
|
||||
${JSON.stringify(initialApp, null, 2)}
|
||||
\`\`\`
|
||||
|
||||
`
|
||||
} else {
|
||||
userMessage += `## Initial App State
|
||||
No initial app was provided - this is a new app created from scratch.
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
userMessage += `## Generated App
|
||||
\`\`\`json
|
||||
${JSON.stringify(generatedApp, null, 2)}
|
||||
\`\`\`
|
||||
|
||||
Please evaluate how well the generated app:
|
||||
1. Fulfills ALL requirements from the user's original request
|
||||
2. ${initialApp ? 'Makes appropriate modifications to the initial app state' : 'Implements a complete and correct new app'}`
|
||||
|
||||
try {
|
||||
const response = await client.chat.completions.create({
|
||||
model: 'anthropic/claude-sonnet-4.5',
|
||||
messages: [
|
||||
{ role: 'system', content: APP_GENERATION_EVALUATOR_SYSTEM_PROMPT },
|
||||
{ role: 'user', content: userMessage }
|
||||
],
|
||||
temperature: 0
|
||||
})
|
||||
|
||||
const content = response.choices[0]?.message?.content
|
||||
if (!content) {
|
||||
return {
|
||||
success: false,
|
||||
resemblanceScore: 0,
|
||||
statement: 'No response from evaluator',
|
||||
error: 'Empty response from LLM'
|
||||
}
|
||||
}
|
||||
|
||||
// Parse JSON response - handle potential markdown code blocks
|
||||
let jsonContent = content.trim()
|
||||
if (jsonContent.startsWith('```')) {
|
||||
jsonContent = jsonContent.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '')
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonContent) as {
|
||||
resemblanceScore: number
|
||||
statement: string
|
||||
missingRequirements?: string[]
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
resemblanceScore: Math.max(0, Math.min(100, Math.round(parsed.resemblanceScore))),
|
||||
statement: parsed.statement,
|
||||
missingRequirements: parsed.missingRequirements ?? []
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err)
|
||||
return {
|
||||
success: false,
|
||||
resemblanceScore: 0,
|
||||
statement: 'Evaluation failed',
|
||||
error: errorMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import type {
|
||||
AppAIChatHelpers,
|
||||
AppFiles,
|
||||
BackendRunnable,
|
||||
LintResult,
|
||||
SelectedContext
|
||||
} from '../../app/core'
|
||||
|
||||
/**
|
||||
* Creates an empty lint result (no errors or warnings).
|
||||
*/
|
||||
function createEmptyLintResult(): LintResult {
|
||||
return {
|
||||
errorCount: 0,
|
||||
warningCount: 0,
|
||||
errors: { frontend: {}, backend: {} },
|
||||
warnings: { frontend: {}, backend: {} }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates mock AppAIChatHelpers for eval testing.
|
||||
* Tracks app files state in memory and allows tool functions to modify it.
|
||||
*/
|
||||
export function createAppEvalHelpers(
|
||||
initialFrontend: Record<string, string> = {},
|
||||
initialBackend: Record<string, BackendRunnable> = {}
|
||||
) {
|
||||
// In-memory state
|
||||
let frontend: Record<string, string> = { ...initialFrontend }
|
||||
let backend: Record<string, BackendRunnable> = { ...initialBackend }
|
||||
let snapshotId = 0
|
||||
const snapshots: Map<number, { frontend: Record<string, string>; backend: Record<string, BackendRunnable> }> = new Map()
|
||||
|
||||
const helpers: AppAIChatHelpers = {
|
||||
// Frontend file operations
|
||||
listFrontendFiles: () => Object.keys(frontend),
|
||||
|
||||
getFrontendFile: (path: string) => frontend[path],
|
||||
|
||||
getFrontendFiles: () => ({ ...frontend }),
|
||||
|
||||
setFrontendFile: (path: string, content: string) => {
|
||||
frontend[path] = content
|
||||
// Return mock lint result - in real usage this would validate the file
|
||||
return createEmptyLintResult()
|
||||
},
|
||||
|
||||
deleteFrontendFile: (path: string) => {
|
||||
delete frontend[path]
|
||||
},
|
||||
|
||||
// Backend runnable operations
|
||||
listBackendRunnables: () => {
|
||||
return Object.entries(backend).map(([key, runnable]) => ({
|
||||
key,
|
||||
name: runnable.name
|
||||
}))
|
||||
},
|
||||
|
||||
getBackendRunnable: (key: string) => backend[key],
|
||||
|
||||
getBackendRunnables: () => ({ ...backend }),
|
||||
|
||||
setBackendRunnable: async (key: string, runnable: BackendRunnable) => {
|
||||
backend[key] = runnable
|
||||
// Return mock lint result - in real usage this would validate the runnable
|
||||
return createEmptyLintResult()
|
||||
},
|
||||
|
||||
deleteBackendRunnable: (key: string) => {
|
||||
delete backend[key]
|
||||
},
|
||||
|
||||
// Combined view
|
||||
getFiles: (): AppFiles => ({
|
||||
frontend: { ...frontend },
|
||||
backend: { ...backend }
|
||||
}),
|
||||
|
||||
getSelectedContext: (): SelectedContext => ({
|
||||
type: 'none'
|
||||
}),
|
||||
|
||||
// Snapshot management
|
||||
snapshot: () => {
|
||||
const id = ++snapshotId
|
||||
snapshots.set(id, {
|
||||
frontend: { ...frontend },
|
||||
backend: { ...backend }
|
||||
})
|
||||
return id
|
||||
},
|
||||
|
||||
revertToSnapshot: (id: number) => {
|
||||
const snap = snapshots.get(id)
|
||||
if (snap) {
|
||||
frontend = { ...snap.frontend }
|
||||
backend = { ...snap.backend }
|
||||
}
|
||||
},
|
||||
|
||||
// Linting
|
||||
lint: () => {
|
||||
// Return mock lint result - no actual linting in eval
|
||||
return createEmptyLintResult()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
helpers,
|
||||
getFiles: (): AppFiles => ({
|
||||
frontend: { ...frontend },
|
||||
backend: { ...backend }
|
||||
}),
|
||||
getFrontend: () => ({ ...frontend }),
|
||||
getBackend: () => ({ ...backend })
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user