mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-15 08:02:27 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b04cc4d1a | ||
|
|
d999e91583 | ||
|
|
9065716020 | ||
|
|
56f70fd7de | ||
|
|
9ad1afc8f2 | ||
|
|
c069969732 | ||
|
|
9ce1d46459 | ||
|
|
ff5fcc59d4 | ||
|
|
f7ce4d1c8b | ||
|
|
44f3dcc2b3 | ||
|
|
c32038a76d | ||
|
|
b868e446fc | ||
|
|
bb937498bb | ||
|
|
029462bc57 | ||
|
|
089826e5b5 | ||
|
|
f240d1322a | ||
|
|
47facb3826 | ||
|
|
c44fc4f5ab | ||
|
|
2a78359af7 | ||
|
|
15f1b7cf7d | ||
|
|
180bb82643 | ||
|
|
556b4a41a1 |
+1
-1
@@ -6,4 +6,4 @@ frontend/src/routes/test.svelte
|
||||
CaddyfileRemoteMalo
|
||||
*.swp
|
||||
**/.idea/
|
||||
.direnv
|
||||
.direnv
|
||||
@@ -1,5 +1,30 @@
|
||||
# Changelog
|
||||
|
||||
## [1.425.0](https://github.com/windmill-labs/windmill/compare/v1.424.0...v1.425.0) (2024-11-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Handle `pip install` by `uv` ([#4517](https://github.com/windmill-labs/windmill/issues/4517)) ([f240d13](https://github.com/windmill-labs/windmill/commit/f240d1322a3c1669dfb69ebda6d05effcca26ae3))
|
||||
* **monitoring:** workspace critical alerts ([#4684](https://github.com/windmill-labs/windmill/issues/4684)) ([c32038a](https://github.com/windmill-labs/windmill/commit/c32038a76d9d00703d8b865af72f083ac43414e3))
|
||||
|
||||
## [1.424.0](https://github.com/windmill-labs/windmill/compare/v1.423.2...v1.424.0) (2024-11-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* allow setting password and login type from superadmin UI ([2a1bff3](https://github.com/windmill-labs/windmill/commit/2a1bff3160b65eadba399229b5f53950ec2c0d48))
|
||||
* **backend:** monitor minimal version of living workers ([#4704](https://github.com/windmill-labs/windmill/issues/4704)) ([50ff183](https://github.com/windmill-labs/windmill/commit/50ff183baeaa2a39c2b0188ee8dd6172b08ae801))
|
||||
* Support mistral anthropic for ai ([#4692](https://github.com/windmill-labs/windmill/issues/4692)) ([556b4a4](https://github.com/windmill-labs/windmill/commit/556b4a41a1d010bb8a1796f485d343c1e412d278))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add countCompletedJobs api ([2a78359](https://github.com/windmill-labs/windmill/commit/2a78359af7b8e4700634c2ad2745c1d14d2da4f7))
|
||||
* add queue_couts api ([10b6b1d](https://github.com/windmill-labs/windmill/commit/10b6b1dc04cb2b2d4ec5ceb26a5dbd2ac7bd1394))
|
||||
* autoscaling when count < min worker set to min_workers ([180bb82](https://github.com/windmill-labs/windmill/commit/180bb826436f9960def8c6cf3e9692714ffdf917))
|
||||
* deployment callbacks have a concurrency limit of 1 on same path ([c44fc4f](https://github.com/windmill-labs/windmill/commit/c44fc4f5ab8bcb5dffec08d5ed4ccd61feb1cb13))
|
||||
|
||||
## [1.423.2](https://github.com/windmill-labs/windmill/compare/v1.423.1...v1.423.2) (2024-11-13)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ target/
|
||||
.env
|
||||
oauth.json
|
||||
oauth2.json
|
||||
windmill-api/openapi-deref.yaml
|
||||
tracing.folded
|
||||
heaptrack*
|
||||
index/
|
||||
windmill-api/openapi-*.*
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE alerts SET acknowledged_workspace = true, acknowledged = true WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "00588a40dde5189ac1c61505f17acb0f4c244c60477427505bf5bd1b104d3bf9"
|
||||
}
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT openai_resource_path, code_completion_enabled FROM workspace_settings WHERE workspace_id = $1",
|
||||
"query": "SELECT ai_resource, code_completion_enabled FROM workspace_settings WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "openai_resource_path",
|
||||
"type_info": "Varchar"
|
||||
"name": "ai_resource",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
@@ -24,5 +24,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ceb97024ebf1a1c00ea1ed4952f66b229ca4622c537cc252d8ed52b4d24270ee"
|
||||
"hash": "0230bd64d2b6719c3536a106e18a68c7b43b3a9a9efa92b5517132e9c0d8a25b"
|
||||
}
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n -- slack_team_id, \n -- slack_name, \n -- slack_command_script, \n -- CASE WHEN slack_email = 'missing@email.xyz' THEN NULL ELSE slack_email END AS slack_email,\n auto_invite_domain IS NOT NULL AS \"auto_invite_enabled!\",\n CASE WHEN auto_invite_operator IS TRUE THEN 'operator' ELSE 'developer' END AS \"auto_invite_as!\", \n CASE WHEN auto_add IS TRUE THEN 'add' ELSE 'invite' END AS \"auto_invite_mode!\", \n webhook, \n deploy_to, \n error_handler, \n openai_resource_path, \n code_completion_enabled, \n error_handler_extra_args, \n error_handler_muted_on_cancel, \n large_file_storage, \n git_sync,\n default_app,\n default_scripts,\n workspace.name\n FROM workspace_settings\n LEFT JOIN workspace ON workspace.id = workspace_settings.workspace_id\n WHERE workspace_id = $1",
|
||||
"query": "SELECT\n -- slack_team_id, \n -- slack_name, \n -- slack_command_script, \n -- CASE WHEN slack_email = 'missing@email.xyz' THEN NULL ELSE slack_email END AS slack_email,\n auto_invite_domain IS NOT NULL AS \"auto_invite_enabled!\",\n CASE WHEN auto_invite_operator IS TRUE THEN 'operator' ELSE 'developer' END AS \"auto_invite_as!\", \n CASE WHEN auto_add IS TRUE THEN 'add' ELSE 'invite' END AS \"auto_invite_mode!\", \n webhook, \n deploy_to, \n error_handler, \n ai_resource, \n code_completion_enabled, \n error_handler_extra_args, \n error_handler_muted_on_cancel, \n large_file_storage, \n git_sync,\n default_app,\n default_scripts,\n workspace.name\n FROM workspace_settings\n LEFT JOIN workspace ON workspace.id = workspace_settings.workspace_id\n WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -35,8 +35,8 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "openai_resource_path",
|
||||
"type_info": "Varchar"
|
||||
"name": "ai_resource",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
@@ -102,5 +102,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "188534f4b29f6461b1a6214d060f183c830b19a403ebb7b8be55a691675010c3"
|
||||
"hash": "0331a81262e2d3c1bcfaeb64617b11eb68ab4599d64e5e5af639a9ac5d791fd0"
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET openai_resource_path = $1, code_completion_enabled = $2 WHERE workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "034583442e6f8ae38d6c4e4aac26f17c8d9d0e657f28276228fc90d3e22e1304"
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO alerts (alert_type, message, acknowledged, acknowledged_workspace, workspace_id, resource)\n VALUES ('critical_error', $1, $2, $3, $4, $5)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "044e2b428ee6e2dd4543c87ad8835e239cf7567d18b8b3fa6608ea3a9d206ca7"
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM healthchecks WHERE check_type = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0ee63ef2dd5c88edba2a1f56d31f29876724922f148dad7af35b36efbf70207a"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET ai_resource = $1, code_completion_enabled = $2 WHERE workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Jsonb",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1610c79b8d238e3a35d795f34a2a0100b933b7f199d41cc2f554d57c811d55f3"
|
||||
}
|
||||
+8
-2
@@ -65,8 +65,8 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "openai_resource_path",
|
||||
"type_info": "Varchar"
|
||||
"name": "ai_resource",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
@@ -117,6 +117,11 @@
|
||||
"ordinal": 22,
|
||||
"name": "deploy_ui",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "mute_critical_alerts",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -147,6 +152,7 @@
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value\n FROM resource\n WHERE path = $1 AND workspace_id = $2",
|
||||
"query": "SELECT value\n FROM resource\n WHERE path = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -19,5 +19,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "acdaa5151f8f7f37bb8c8c5a7d146789887e47db9695fc26b1dfaedd735e1e60"
|
||||
"hash": "286fa00c088df146c08c1934556f06a4243c66787ab8759a8045e60effd2fb77"
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO alerts (alert_type, message, acknowledged, acknowledged_workspace, workspace_id, resource)\n VALUES ('recovered_critical_error', $1, $2, $3, $4, $5)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "28987898c7e4b172d466bf08f33c2da733e71f8a253b0122c17b9e021919809e"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, alert_type, message, created_at, acknowledged \n FROM alerts \n WHERE acknowledged = $1\n ORDER BY created_at DESC \n LIMIT $2 OFFSET $3",
|
||||
"query": "SELECT id, alert_type, message, created_at, COALESCE(acknowledged, false) AS acknowledged, workspace_id\n FROM alerts\n WHERE COALESCE(acknowledged, false) = $1\n ORDER BY created_at DESC\n LIMIT $2 OFFSET $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -27,6 +27,11 @@
|
||||
"ordinal": 4,
|
||||
"name": "acknowledged",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -41,8 +46,9 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "0b955f2cff82a2d4ba3840588143e08952f029480d4a42503ecc3c5e70437995"
|
||||
"hash": "344b3a5d9683273a956b5156fed5ab9fdff1a7252198e4ae290d8c8f937ff177"
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO alerts (alert_type, message, acknowledged) VALUES ('recovered_critical_error', $1, $2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3a94ad52c6b7cde844fa868167248cd9ff63e5fdfa1d93d8fbec32a257b6b05e"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE alerts SET acknowledged = true, acknowledged_workspace = true WHERE resource = $1 AND alert_type = 'critical_error'",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4a3a8207627418ba7b7eabaccd7ec72884f2a1eacf0ab0e0c6ccbf56c654c14a"
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO alerts (alert_type, message, acknowledged) VALUES ('critical_error', $1, $2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4d22084a5d9860832f30e8f08cbfa1848ed3c1336fa4790f45b8189c4ac97d91"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, alert_type, message, created_at, acknowledged \n FROM alerts \n ORDER BY created_at DESC \n LIMIT $1 OFFSET $2",
|
||||
"query": "SELECT id, alert_type, message, created_at, COALESCE(acknowledged, false) AS acknowledged, workspace_id\n FROM alerts\n ORDER BY created_at DESC\n LIMIT $1 OFFSET $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -27,6 +27,11 @@
|
||||
"ordinal": 4,
|
||||
"name": "acknowledged",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -40,8 +45,9 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "cc5ab80241b88c5befea279f16c4ec68cec17b31dcd277b321f652917346496b"
|
||||
"hash": "4d30c5a2894d655741d167f5589f5816583f0bae78a0dd3270dedb377dfa944a"
|
||||
}
|
||||
+8
-2
@@ -65,8 +65,8 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "openai_resource_path",
|
||||
"type_info": "Varchar"
|
||||
"name": "ai_resource",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
@@ -117,6 +117,11 @@
|
||||
"ordinal": 22,
|
||||
"name": "deploy_ui",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "mute_critical_alerts",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -147,6 +152,7 @@
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET openai_resource_path = NULL, code_completion_enabled = $1 WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6268eabd561502a44e273d6391102278974623f7a7bcad6891d7abadf4d3ea03"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE alerts\n SET\n acknowledged = true,\n acknowledged_workspace = CASE\n WHEN $2::text IS NOT NULL AND workspace_id = $2 THEN true\n ELSE acknowledged_workspace\n END\n WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "65da41c7ded54cdee8d33211561c068b72294cc99ff44ed0a13179df508ebc6a"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET ai_resource = NULL, code_completion_enabled = $1 WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6940ddc5ee8d2a1048213d46f52d964b199604ba66dedbe9f89cea727d726a2d"
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, alert_type, message, created_at, COALESCE(acknowledged_workspace, false) AS acknowledged, workspace_id\n FROM alerts\n WHERE workspace_id = $1\n ORDER BY created_at DESC\n LIMIT $2 OFFSET $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "alert_type",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "message",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "acknowledged",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "777e7084edf9a5d14f299ff479ae43c8ead47a6a531d4b031a1342c9b2f16506"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE alerts \n SET\n acknowledged = true,\n acknowledged_workspace = CASE\n WHEN $1::text IS NOT NULL THEN true\n ELSE acknowledged_workspace\n END\n WHERE ($1::text IS NOT NULL AND workspace_id = $1)\n OR ($1::text IS NULL)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7c32176755c6ea2b6ae531860d436caae3fa256fc0803749ec5107632669adb3"
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, alert_type, message, created_at, COALESCE(acknowledged_workspace, false) AS acknowledged, workspace_id\n FROM alerts\n WHERE workspace_id = $1 AND COALESCE(acknowledged_workspace, false) = $2\n ORDER BY created_at DESC\n LIMIT $3 OFFSET $4",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "alert_type",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "message",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "acknowledged",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bool",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "7c5a29de07cbe42326a15d4d7fd9714c20b767508e27368be6de38d7b18a3a4d"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE alerts SET acknowledged = true",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a59fae29ebcc9aa53308b777ead3d2b652618ac454f139db738f4499cb4eb079"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE alerts SET acknowledged = true WHERE acknowledged = false",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a7c5008aa7ea43d0afac7d9f19846976ed7af2e90270902f001115e023cb947d"
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE alerts SET acknowledged = true WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "be3ae231557e794172336bc27d725f862dcf039bbb3d75ced9d54c86f53d2580"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT ai_resource FROM workspace_settings WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "ai_resource",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ceda377fa534656ac12a7f41db77db1f054ec751855c8db7352b0e9c20b63ef8"
|
||||
}
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT openai_resource_path FROM workspace_settings WHERE workspace_id = $1",
|
||||
"query": "SELECT mute_critical_alerts FROM workspace_settings WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "openai_resource_path",
|
||||
"type_info": "Varchar"
|
||||
"name": "mute_critical_alerts",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -18,5 +18,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "f8b689ec3e09f14dc02ac9e9f98d252a14d3a2acfadcd9cd6ba27ca9799f4706"
|
||||
"hash": "e05dbe046e846c092a96b6b0a9d872c721169d418586be2d15cee6b929049098"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET mute_critical_alerts = $1 WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f22168826350797e88153b65a5b1e906a7868f34c062f154b2ee4f24c57aa5c3"
|
||||
}
|
||||
Generated
+34
-34
@@ -843,9 +843,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sts"
|
||||
version = "1.49.0"
|
||||
version = "1.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53dcf5e7d9bd1517b8b998e170e650047cea8a2b85fe1835abe3210713e541b7"
|
||||
checksum = "6ada54e5f26ac246dc79727def52f7f8ed38915cb47781e2a72213957dc3a7d5"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -1032,9 +1032,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae"
|
||||
checksum = "49c41b948da08fb481a94546cd874843adc1142278b0af4badf9b1b78599d68d"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
@@ -1598,9 +1598,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aeb932158bd710538c73702db6945cb68a8fb08c519e6e12706b94263b36db8"
|
||||
checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
@@ -1783,9 +1783,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "comfy-table"
|
||||
version = "7.1.2"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0d05af1e006a2407bedef5af410552494ce5be9090444dbbcb57258c1af3d56"
|
||||
checksum = "24f165e7b643266ea80cb858aed492ad9280e3e05ce24d4a99d7d7b889b6a4d9"
|
||||
dependencies = [
|
||||
"strum 0.26.3",
|
||||
"strum_macros 0.26.4",
|
||||
@@ -3355,9 +3355,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.34"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0"
|
||||
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"libz-sys",
|
||||
@@ -10610,7 +10610,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -10652,7 +10652,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -10737,7 +10737,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -10755,7 +10755,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
@@ -10768,7 +10768,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rsmq_async",
|
||||
@@ -10783,7 +10783,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -10830,7 +10830,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"rsmq_async",
|
||||
@@ -10845,7 +10845,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -10867,7 +10867,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"lazy_static",
|
||||
@@ -10879,7 +10879,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -10888,7 +10888,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -10900,7 +10900,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -10912,7 +10912,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -10924,7 +10924,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.13.0",
|
||||
@@ -10935,7 +10935,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.13.0",
|
||||
@@ -10946,7 +10946,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -10964,7 +10964,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -10981,7 +10981,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -10993,7 +10993,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -11011,7 +11011,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.15",
|
||||
@@ -11032,7 +11032,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -11042,7 +11042,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -11075,7 +11075,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -11085,7 +11085,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -29,7 +29,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.423.2"
|
||||
version = "1.425.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
0d9c8813acd28848515c736e7b684220b5a785a3
|
||||
51dcbf93b0d127af9f33fa346cc63fcd2475d4fa
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE workspace_settings
|
||||
ALTER COLUMN ai_resource TYPE text
|
||||
USING ai_resource->>'path';
|
||||
|
||||
ALTER TABLE workspace_settings
|
||||
RENAME COLUMN ai_resource to openai_resource_path;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE workspace_settings
|
||||
ALTER COLUMN openai_resource_path TYPE jsonb
|
||||
USING jsonb_build_object('provider', 'openai', 'path', openai_resource_path);
|
||||
|
||||
ALTER TABLE workspace_settings
|
||||
RENAME COLUMN openai_resource_path TO ai_resource;
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE alerts DROP COLUMN workspace_id;
|
||||
ALTER TABLE alerts DROP COLUMN acknowledged_workspace;
|
||||
ALTER TABLE alerts DROP COLUMN resource;
|
||||
ALTER TABLE workspace_settings DROP COLUMN mute_critical_alerts;
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE alerts ADD COLUMN workspace_id TEXT DEFAULT NULL;
|
||||
ALTER TABLE alerts ADD COLUMN acknowledged_workspace BOOL DEFAULT NULL;
|
||||
ALTER TABLE alerts ADD COLUMN resource TEXT DEFAULT NULL;
|
||||
ALTER TABLE workspace_settings ADD COLUMN mute_critical_alerts BOOL DEFAULT NULL;
|
||||
+2
-2
@@ -998,13 +998,13 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
|
||||
LOCK_CACHE_DIR,
|
||||
TMP_LOGS_DIR,
|
||||
UV_CACHE_DIR,
|
||||
PY311_CACHE_DIR,
|
||||
PIP_CACHE_DIR,
|
||||
TAR_PIP_CACHE_DIR,
|
||||
DENO_CACHE_DIR,
|
||||
DENO_CACHE_DIR_DEPS,
|
||||
DENO_CACHE_DIR_NPM,
|
||||
BUN_CACHE_DIR,
|
||||
PY311_CACHE_DIR,
|
||||
PIP_CACHE_DIR,
|
||||
BUN_DEPSTAR_CACHE_DIR,
|
||||
BUN_BUNDLE_CACHE_DIR,
|
||||
GO_CACHE_DIR,
|
||||
|
||||
+24
-17
@@ -34,12 +34,12 @@ use windmill_common::{
|
||||
error,
|
||||
flow_status::FlowStatusModule,
|
||||
global_settings::{
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ERROR_CHANNELS_SETTING,
|
||||
DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING,
|
||||
EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING, EXTRA_PIP_INDEX_URL_SETTING,
|
||||
HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING, JWT_SECRET_SETTING,
|
||||
KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
|
||||
PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING,
|
||||
BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING,
|
||||
CRITICAL_ERROR_CHANNELS_SETTING, DEFAULT_TAGS_PER_WORKSPACE_SETTING,
|
||||
DEFAULT_TAGS_WORKSPACES_SETTING, EXPOSE_DEBUG_METRICS_SETTING, EXPOSE_METRICS_SETTING,
|
||||
EXTRA_PIP_INDEX_URL_SETTING, HUB_BASE_URL_SETTING, JOB_DEFAULT_TIMEOUT_SECS_SETTING,
|
||||
JWT_SECRET_SETTING, KEEP_JOB_DIR_SETTING, LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING,
|
||||
OAUTH_SETTING, PIP_INDEX_URL_SETTING, REQUEST_SIZE_LIMIT_SETTING,
|
||||
REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING, RETENTION_PERIOD_SECS_SETTING,
|
||||
SAML_METADATA_SETTING, SCIM_TOKEN_SETTING, TIMEOUT_WAIT_RESULT_SETTING,
|
||||
},
|
||||
@@ -54,8 +54,8 @@ use windmill_common::{
|
||||
update_min_version, DEFAULT_TAGS_PER_WORKSPACE, DEFAULT_TAGS_WORKSPACES, SMTP_CONFIG,
|
||||
WORKER_CONFIG, WORKER_GROUP,
|
||||
},
|
||||
BASE_URL, CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL, HUB_BASE_URL, JOB_RETENTION_SECS,
|
||||
METRICS_DEBUG_ENABLED, METRICS_ENABLED, CRITICAL_ALERT_MUTE_UI_ENABLED
|
||||
BASE_URL, CRITICAL_ALERT_MUTE_UI_ENABLED, CRITICAL_ERROR_CHANNELS, DB, DEFAULT_HUB_BASE_URL,
|
||||
HUB_BASE_URL, JOB_RETENTION_SECS, METRICS_DEBUG_ENABLED, METRICS_ENABLED,
|
||||
};
|
||||
use windmill_queue::cancel_job;
|
||||
use windmill_worker::{
|
||||
@@ -231,13 +231,20 @@ pub async fn load_tag_per_workspace_workspaces(db: &DB) -> error::Result<()> {
|
||||
}
|
||||
|
||||
pub async fn reload_critical_alert_mute_ui_setting(db: &DB) -> error::Result<()> {
|
||||
let mute = load_value_from_global_settings(db, CRITICAL_ALERT_MUTE_UI_SETTING).await;
|
||||
match mute {
|
||||
Ok(Some(serde_json::Value::Bool(t))) => {
|
||||
CRITICAL_ALERT_MUTE_UI_ENABLED.store(t, Ordering::Relaxed);
|
||||
if let Ok(Some(serde_json::Value::Bool(t))) =
|
||||
load_value_from_global_settings(db, CRITICAL_ALERT_MUTE_UI_SETTING).await
|
||||
{
|
||||
CRITICAL_ALERT_MUTE_UI_ENABLED.store(t, Ordering::Relaxed);
|
||||
|
||||
if t {
|
||||
if let Err(e) = sqlx::query!("UPDATE alerts SET acknowledged = true")
|
||||
.execute(db)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error updating alerts: {}", e.to_string());
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1323,7 +1330,7 @@ async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>
|
||||
ON CONFLICT (job_id) DO UPDATE SET logs = job_logs.logs || '\nRestarted job after not receiving job''s ping for too long the ' || now() || '\n\n' WHERE job_logs.job_id = $1", r.id)
|
||||
.execute(db).await;
|
||||
tracing::error!(error_message);
|
||||
report_critical_error(error_message, db.clone()).await;
|
||||
report_critical_error(error_message, db.clone(), Some(&r.workspace_id), None).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1437,7 +1444,7 @@ async fn handle_zombie_flows(
|
||||
flow.id, flow.workspace_id
|
||||
);
|
||||
tracing::error!(error_message);
|
||||
report_critical_error(error_message, db.clone()).await;
|
||||
report_critical_error(error_message, db.clone(), Some(&flow.workspace_id), None).await;
|
||||
// if the flow hasn't started and is a zombie, we can simply restart it
|
||||
sqlx::query!(
|
||||
"UPDATE queue SET running = false, started_at = null WHERE id = $1 AND canceled = false",
|
||||
@@ -1457,7 +1464,7 @@ async fn handle_zombie_flows(
|
||||
format!("Flow {id} was cancelled because it")
|
||||
}
|
||||
);
|
||||
report_critical_error(reason.clone(), db.clone()).await;
|
||||
report_critical_error(reason.clone(), db.clone(), Some(&flow.workspace_id), None).await;
|
||||
cancel_zombie_flow_job(db, flow, &rsmq, reason).await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"version": "1.418.0",
|
||||
"version": "1.423.2",
|
||||
"title": "Windmill API",
|
||||
"contact": {
|
||||
"name": "Windmill Team",
|
||||
@@ -286,7 +286,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/w/{workspace}/users/{username}": {
|
||||
"/w/{workspace}/users/get/{username}": {
|
||||
"get": {
|
||||
"summary": "get user (require admin privilege)",
|
||||
"operationId": "getUser",
|
||||
@@ -436,6 +436,106 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/set_password_of/{user}": {
|
||||
"post": {
|
||||
"summary": "set password for a specific user (require super admin)",
|
||||
"operationId": "setPasswordForUser",
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "set password",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"password"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "password set",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/set_login_type/{user}": {
|
||||
"post": {
|
||||
"summary": "set login type for a specific user (require super admin)",
|
||||
"operationId": "setLoginTypeForUser",
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "set login type",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"login_type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"login_type"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "login type set",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/create": {
|
||||
"post": {
|
||||
"summary": "create user",
|
||||
@@ -1105,6 +1205,9 @@
|
||||
},
|
||||
"tls_implicit": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"disable_tls": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -1113,7 +1216,8 @@
|
||||
"password",
|
||||
"port",
|
||||
"from",
|
||||
"tls_implicit"
|
||||
"tls_implicit",
|
||||
"disable_tls"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -2460,8 +2564,8 @@
|
||||
"deploy_to": {
|
||||
"type": "string"
|
||||
},
|
||||
"openai_resource_path": {
|
||||
"type": "string"
|
||||
"ai_resource": {
|
||||
"$ref": "#/components/schemas/AiResource"
|
||||
},
|
||||
"code_completion_enabled": {
|
||||
"type": "boolean"
|
||||
@@ -2901,8 +3005,8 @@
|
||||
"code_completion_enabled"
|
||||
],
|
||||
"properties": {
|
||||
"openai_resource_path": {
|
||||
"type": "string"
|
||||
"ai_resource": {
|
||||
"$ref": "#/components/schemas/AiResource"
|
||||
},
|
||||
"code_completion_enabled": {
|
||||
"type": "boolean"
|
||||
@@ -2946,7 +3050,10 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"exists_openai_resource_path": {
|
||||
"ai_provider": {
|
||||
"type": "string"
|
||||
},
|
||||
"exists_ai_resource": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"code_completion_enabled": {
|
||||
@@ -2954,7 +3061,8 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"exists_openai_resource_path",
|
||||
"ai_provider",
|
||||
"exists_ai_resource",
|
||||
"code_completion_enabled"
|
||||
]
|
||||
}
|
||||
@@ -13160,6 +13268,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workers/queue_counts": {
|
||||
"get": {
|
||||
"summary": "get counts of jobs waiting for an executor per tag",
|
||||
"operationId": "getCountsOfJobsWaitingPerTag",
|
||||
"tags": [
|
||||
"worker"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "queue counts",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/configs/list_worker_groups": {
|
||||
"get": {
|
||||
"summary": "list worker groups",
|
||||
@@ -14810,6 +14942,20 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "content_type",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "content_disposition",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
@@ -16116,6 +16262,21 @@
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"AiResource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"provider"
|
||||
]
|
||||
},
|
||||
"Script": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -17226,7 +17387,7 @@
|
||||
"jobs.disapproval",
|
||||
"jobs.delete",
|
||||
"account.delete",
|
||||
"openai.request",
|
||||
"ai.request",
|
||||
"resources.create",
|
||||
"resources.update",
|
||||
"resources.delete",
|
||||
@@ -18067,6 +18228,23 @@
|
||||
"route_path": {
|
||||
"type": "string"
|
||||
},
|
||||
"static_asset_config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"s3": {
|
||||
"type": "string"
|
||||
},
|
||||
"storage": {
|
||||
"type": "string"
|
||||
},
|
||||
"filename": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"s3"
|
||||
]
|
||||
},
|
||||
"is_flow": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -18126,6 +18304,23 @@
|
||||
"route_path": {
|
||||
"type": "string"
|
||||
},
|
||||
"static_asset_config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"s3": {
|
||||
"type": "string"
|
||||
},
|
||||
"storage": {
|
||||
"type": "string"
|
||||
},
|
||||
"filename": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"s3"
|
||||
]
|
||||
},
|
||||
"is_flow": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -18168,6 +18363,23 @@
|
||||
"route_path": {
|
||||
"type": "string"
|
||||
},
|
||||
"static_asset_config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"s3": {
|
||||
"type": "string"
|
||||
},
|
||||
"storage": {
|
||||
"type": "string"
|
||||
},
|
||||
"filename": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"s3"
|
||||
]
|
||||
},
|
||||
"is_flow": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -18917,6 +19129,12 @@
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"s3_inputs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"execution_mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
+1116
-974
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.423.2
|
||||
version: 1.425.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -1681,8 +1681,8 @@ paths:
|
||||
type: string
|
||||
deploy_to:
|
||||
type: string
|
||||
openai_resource_path:
|
||||
type: string
|
||||
ai_resource:
|
||||
$ref: "#/components/schemas/AiResource"
|
||||
code_completion_enabled:
|
||||
type: boolean
|
||||
error_handler:
|
||||
@@ -1701,6 +1701,8 @@ paths:
|
||||
type: string
|
||||
default_scripts:
|
||||
$ref: "#/components/schemas/WorkspaceDefaultScripts"
|
||||
mute_critical_alerts:
|
||||
type: boolean
|
||||
required:
|
||||
- code_completion_enabled
|
||||
- automatic_billing
|
||||
@@ -1966,8 +1968,8 @@ paths:
|
||||
required:
|
||||
- code_completion_enabled
|
||||
properties:
|
||||
openai_resource_path:
|
||||
type: string
|
||||
ai_resource:
|
||||
$ref: "#/components/schemas/AiResource"
|
||||
code_completion_enabled:
|
||||
type: boolean
|
||||
responses:
|
||||
@@ -1995,12 +1997,15 @@ paths:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
exists_openai_resource_path:
|
||||
ai_provider:
|
||||
type: string
|
||||
exists_ai_resource:
|
||||
type: boolean
|
||||
code_completion_enabled:
|
||||
type: boolean
|
||||
required:
|
||||
- exists_openai_resource_path
|
||||
- ai_provider
|
||||
- exists_ai_resource
|
||||
- code_completion_enabled
|
||||
|
||||
/w/{workspace}/workspaces/edit_error_handler:
|
||||
@@ -2714,6 +2719,112 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/ContextualVariable"
|
||||
|
||||
/w/{workspace}/workspaces/critical_alerts:
|
||||
get:
|
||||
summary: Get all critical alerts for this workspace
|
||||
operationId: workspaceGetCriticalAlerts
|
||||
tags:
|
||||
- setting
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- in: query
|
||||
name: page
|
||||
schema:
|
||||
type: integer
|
||||
default: 1
|
||||
description: The page number to retrieve (minimum value is 1)
|
||||
- in: query
|
||||
name: page_size
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
description: Number of alerts per page (maximum is 100)
|
||||
- in: query
|
||||
name: acknowledged
|
||||
schema:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: Filter by acknowledgment status; true for acknowledged, false for unacknowledged, and omit for all alerts
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully retrieved all critical alerts
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/CriticalAlert'
|
||||
|
||||
/w/{workspace}/workspaces/critical_alerts/{id}/acknowledge:
|
||||
post:
|
||||
summary: Acknowledge a critical alert for this workspace
|
||||
operationId: workspaceAcknowledgeCriticalAlert
|
||||
tags:
|
||||
- setting
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
description: The ID of the critical alert to acknowledge
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully acknowledged the critical alert
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
example: "Critical alert acknowledged"
|
||||
|
||||
/w/{workspace}/workspaces/critical_alerts/acknowledge_all:
|
||||
post:
|
||||
summary: Acknowledge all unacknowledged critical alerts for this workspace
|
||||
operationId: workspaceAcknowledgeAllCriticalAlerts
|
||||
tags:
|
||||
- setting
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
responses:
|
||||
"200":
|
||||
description: Successfully acknowledged all unacknowledged critical alerts.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
example: "All unacknowledged critical alerts acknowledged"
|
||||
|
||||
/w/{workspace}/workspaces/critical_alerts/mute:
|
||||
post:
|
||||
summary: Mute critical alert UI for this workspace
|
||||
operationId: workspaceMuteCriticalAlertsUI
|
||||
tags:
|
||||
- setting
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: Boolean flag to mute critical alerts.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
mute_critical_alerts:
|
||||
type: boolean
|
||||
description: Whether critical alerts should be muted.
|
||||
example: true
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully updated mute critical alert settings.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
example: "Updated mute critical alert UI settings for workspace: workspace_id"
|
||||
|
||||
/oauth/login_callback/{client_name}:
|
||||
post:
|
||||
security: []
|
||||
@@ -6051,6 +6162,39 @@ paths:
|
||||
required:
|
||||
- database_length
|
||||
|
||||
/w/{workspace}/jobs/completed/count_jobs:
|
||||
get:
|
||||
summary: count number of completed jobs with filter
|
||||
operationId: countCompletedJobs
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: completed_after_s_ago
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
- name: success
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: tags
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: all_workspaces
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: Count of completed jobs
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: integer
|
||||
|
||||
|
||||
/w/{workspace}/jobs/queue/list_filtered_uuids:
|
||||
get:
|
||||
summary: get the ids of all jobs matching the given filters
|
||||
@@ -10194,6 +10338,16 @@ components:
|
||||
schemas:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas"
|
||||
|
||||
AiResource:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
provider:
|
||||
type: string
|
||||
required:
|
||||
- path
|
||||
- provider
|
||||
Script:
|
||||
type: object
|
||||
properties:
|
||||
@@ -11012,7 +11166,7 @@ components:
|
||||
- "jobs.disapproval"
|
||||
- "jobs.delete"
|
||||
- "account.delete"
|
||||
- "openai.request"
|
||||
- "ai.request"
|
||||
- "resources.create"
|
||||
- "resources.update"
|
||||
- "resources.delete"
|
||||
@@ -12899,3 +13053,7 @@ components:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: Acknowledgment status of the alert, can be true, false, or null if not set
|
||||
workspace_id:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Workspace id if the alert is in the scope of a workspace
|
||||
|
||||
@@ -0,0 +1,507 @@
|
||||
use crate::{
|
||||
db::{ApiAuthed, DB},
|
||||
variables::decrypt,
|
||||
};
|
||||
use anthropic::AnthropicCache;
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
extract::{Path, Query},
|
||||
response::IntoResponse,
|
||||
routing::post,
|
||||
Extension, Router,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use mistral::MistralCache;
|
||||
use openai::OpenaiCache;
|
||||
use quick_cache::sync::Cache;
|
||||
use reqwest::{Client, RequestBuilder};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use windmill_audit::audit_ee::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
use windmill_common::error::{to_anyhow, Result};
|
||||
use windmill_common::variables::build_crypt;
|
||||
|
||||
use windmill_common::error::Error;
|
||||
|
||||
use serde_json::value::{RawValue, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
|
||||
.timeout(std::time::Duration::from_secs(60 * 5))
|
||||
.user_agent("windmill/beta")
|
||||
.build().unwrap();
|
||||
}
|
||||
|
||||
trait AiRequest {
|
||||
fn prepare_request(self, path: &str, body: Bytes) -> Result<RequestBuilder>;
|
||||
}
|
||||
|
||||
mod openai {
|
||||
use super::*;
|
||||
|
||||
use super::{get_variable_or_self, KeyCache};
|
||||
|
||||
const API_VERSION: &str = "2023-05-15";
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct OpenaiResource {
|
||||
api_key: String,
|
||||
organization_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct OpenaiClientCredentialsOauthResource {
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
token_url: String,
|
||||
user: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(untagged, rename_all = "snake_case")]
|
||||
enum OpenaiConfig {
|
||||
Resource(OpenaiResource),
|
||||
ClientCredentialsOauthResource(OpenaiClientCredentialsOauthResource),
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref OPENAI_AZURE_BASE_PATH: Option<String> = std::env::var("OPENAI_AZURE_BASE_PATH").ok();
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct OpenaiCredentials {
|
||||
access_token: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct OpenaiCache {
|
||||
api_key: String,
|
||||
organization_id: Option<String>,
|
||||
azure_base_path: Option<String>,
|
||||
user: Option<String>,
|
||||
}
|
||||
|
||||
impl OpenaiCache {
|
||||
pub fn new(
|
||||
api_key: String,
|
||||
organization_id: Option<String>,
|
||||
azure_base_path: Option<String>,
|
||||
user: Option<String>,
|
||||
) -> Self {
|
||||
Self { api_key, organization_id, azure_base_path, user }
|
||||
}
|
||||
}
|
||||
|
||||
const BASE_URL: &str = "https://api.openai.com/v1";
|
||||
impl AiRequest for OpenaiCache {
|
||||
fn prepare_request(self, openai_path: &str, mut body: Bytes) -> Result<RequestBuilder> {
|
||||
let OpenaiCache { api_key, azure_base_path, organization_id, user } = self;
|
||||
if user.is_some() {
|
||||
tracing::debug!("Adding user to request body");
|
||||
let mut json_body: HashMap<String, Box<RawValue>> = serde_json::from_slice(&body)
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!("Failed to parse request body: {}", e))
|
||||
})?;
|
||||
|
||||
let user_json_string = serde_json::Value::String(user.unwrap()).to_string(); // makes sure to escape characters
|
||||
|
||||
json_body.insert(
|
||||
"user".to_string(),
|
||||
RawValue::from_string(user_json_string)
|
||||
.map_err(|e| Error::InternalErr(format!("Failed to parse user: {}", e)))?,
|
||||
);
|
||||
|
||||
body = serde_json::to_vec(&json_body)
|
||||
.map_err(|e| {
|
||||
Error::InternalErr(format!("Failed to reserialize request body: {}", e))
|
||||
})?
|
||||
.into();
|
||||
}
|
||||
|
||||
let base_url = if let Some(base_url) = azure_base_path {
|
||||
base_url
|
||||
} else {
|
||||
BASE_URL.to_string()
|
||||
};
|
||||
let url = format!("{}/{}", base_url, openai_path);
|
||||
let mut request = HTTP_CLIENT
|
||||
.post(url)
|
||||
.header("content-type", "application/json")
|
||||
.body(body);
|
||||
|
||||
if base_url != BASE_URL {
|
||||
request = request
|
||||
.header("api-key", api_key)
|
||||
.query(&[("api-version", API_VERSION)])
|
||||
} else {
|
||||
request = request.header("authorization", format!("Bearer {}", api_key))
|
||||
}
|
||||
|
||||
if let Some(org_id) = organization_id {
|
||||
request = request.header("OpenAI-Organization", org_id);
|
||||
}
|
||||
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_openai_key_using_credentials_flow(
|
||||
mut resource: OpenaiClientCredentialsOauthResource,
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
) -> Result<String> {
|
||||
resource.client_id = get_variable_or_self(resource.client_id, db, w_id).await?;
|
||||
resource.client_secret = get_variable_or_self(resource.client_secret, db, w_id).await?;
|
||||
resource.token_url = get_variable_or_self(resource.token_url, db, w_id).await?;
|
||||
let mut params = HashMap::new();
|
||||
params.insert("grant_type", "client_credentials");
|
||||
let response = HTTP_CLIENT
|
||||
.post(resource.token_url)
|
||||
.form(¶ms)
|
||||
.basic_auth(resource.client_id, Some(resource.client_secret))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
Error::InternalErr(format!(
|
||||
"Failed to get OpenAI credentials using credentials flow: {}",
|
||||
err
|
||||
))
|
||||
})?;
|
||||
let response = response.json::<OpenaiCredentials>().await.map_err(|err| {
|
||||
Error::InternalErr(format!(
|
||||
"Failed to parse OpenAI credentials from credentials flow: {}",
|
||||
err
|
||||
))
|
||||
})?;
|
||||
Ok(response.access_token)
|
||||
}
|
||||
|
||||
pub async fn get_cached_value(db: &DB, w_id: &str, resource: Value) -> Result<KeyCache> {
|
||||
let config = serde_json::from_value(resource)
|
||||
.map_err(|e| Error::InternalErr(format!("validating openai resource {e:#}")))?;
|
||||
|
||||
let mut user = None::<String>;
|
||||
let mut resource = match config {
|
||||
OpenaiConfig::Resource(resource) => {
|
||||
tracing::debug!("Getting OpenAI key from static resource");
|
||||
resource
|
||||
}
|
||||
OpenaiConfig::ClientCredentialsOauthResource(resource) => {
|
||||
tracing::debug!("Getting OpenAI key with client credentials flow");
|
||||
user = resource.user.clone();
|
||||
let token = get_openai_key_using_credentials_flow(resource, db, w_id).await?;
|
||||
OpenaiResource { api_key: token, organization_id: None }
|
||||
}
|
||||
};
|
||||
|
||||
resource.api_key = get_variable_or_self(resource.api_key, db, w_id).await?;
|
||||
|
||||
if let Some(organization_id) = resource.organization_id {
|
||||
resource.organization_id = Some(get_variable_or_self(organization_id, db, w_id).await?);
|
||||
}
|
||||
|
||||
if user.is_some() {
|
||||
user = Some(get_variable_or_self(user.unwrap(), db, w_id).await?);
|
||||
}
|
||||
|
||||
let azure_base_path = sqlx::query_scalar!(
|
||||
"SELECT value
|
||||
FROM global_settings
|
||||
WHERE name = 'openai_azure_base_path'",
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let azure_base_path = if let Some(azure_base_path) = azure_base_path {
|
||||
Some(
|
||||
serde_json::from_value::<String>(azure_base_path).map_err(|e| {
|
||||
Error::InternalErr(format!("validating openai azure base path {e:#}"))
|
||||
})?,
|
||||
)
|
||||
} else {
|
||||
OPENAI_AZURE_BASE_PATH.clone()
|
||||
};
|
||||
|
||||
let workspace_cache = OpenaiCache::new(
|
||||
resource.api_key.clone(),
|
||||
resource.organization_id.clone(),
|
||||
azure_base_path.clone(),
|
||||
user.clone(),
|
||||
);
|
||||
Ok(KeyCache::Openai(workspace_cache))
|
||||
}
|
||||
}
|
||||
|
||||
mod anthropic {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Deserialize, Debug)]
|
||||
pub struct AnthropicCache {
|
||||
#[serde(rename = "apiKey")]
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
const API_VERSION: &str = "2023-06-01";
|
||||
|
||||
impl AnthropicCache {
|
||||
pub fn new(api_key: String) -> Self {
|
||||
Self { api_key }
|
||||
}
|
||||
}
|
||||
|
||||
const BASE_URL: &str = "https://api.anthropic.com";
|
||||
impl AiRequest for AnthropicCache {
|
||||
fn prepare_request(self, anthropic_path: &str, body: Bytes) -> Result<RequestBuilder> {
|
||||
let AnthropicCache { api_key } = self;
|
||||
let url = format!("{}/{}", BASE_URL, anthropic_path);
|
||||
let request = HTTP_CLIENT
|
||||
.post(url)
|
||||
.header("x-api-key", api_key)
|
||||
.header("anthropic-version", API_VERSION)
|
||||
.header("content-type", "application/json")
|
||||
.body(body);
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_cached_value(db: &DB, w_id: &str, resource: Value) -> Result<KeyCache> {
|
||||
let mut resource: AnthropicCache = serde_json::from_value(resource)
|
||||
.map_err(|e| Error::InternalErr(format!("validating anthropic resource {e:#}")))?;
|
||||
resource.api_key = get_variable_or_self(resource.api_key, db, w_id).await?;
|
||||
let workspace_cache = AnthropicCache::new(resource.api_key);
|
||||
Ok(KeyCache::Anthropic(workspace_cache))
|
||||
}
|
||||
}
|
||||
|
||||
mod mistral {
|
||||
use super::*;
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct MistralCache {
|
||||
#[serde(rename = "apiKey")]
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
impl MistralCache {
|
||||
pub fn new(api_key: String) -> Self {
|
||||
Self { api_key }
|
||||
}
|
||||
}
|
||||
|
||||
const BASE_URL: &str = "https://api.mistral.ai";
|
||||
impl AiRequest for MistralCache {
|
||||
fn prepare_request(self, mistral_path: &str, body: Bytes) -> Result<RequestBuilder> {
|
||||
let MistralCache { api_key } = self;
|
||||
|
||||
let url = format!("{}/{}", BASE_URL, mistral_path);
|
||||
let request = HTTP_CLIENT
|
||||
.post(url)
|
||||
.header("content-type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.header("authorization", format!("Bearer {}", api_key))
|
||||
.body(body);
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_cached_value(db: &DB, w_id: &str, resource: Value) -> Result<KeyCache> {
|
||||
let mut resource: MistralCache = serde_json::from_value(resource)
|
||||
.map_err(|e| Error::InternalErr(format!("validating mistral resource {e:#}")))?;
|
||||
resource.api_key = get_variable_or_self(resource.api_key, db, w_id).await?;
|
||||
|
||||
let workspace_cache = MistralCache::new(resource.api_key);
|
||||
Ok(KeyCache::Mistral(workspace_cache))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum KeyCache {
|
||||
Openai(OpenaiCache),
|
||||
Anthropic(AnthropicCache),
|
||||
Mistral(MistralCache),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AiCache {
|
||||
pub path: String,
|
||||
pub cached_key: KeyCache,
|
||||
pub expires_at: std::time::Instant,
|
||||
}
|
||||
|
||||
impl AiCache {
|
||||
pub fn new(path: String, cached_key: KeyCache) -> Self {
|
||||
Self {
|
||||
path,
|
||||
cached_key,
|
||||
expires_at: std::time::Instant::now() + std::time::Duration::from_secs(60),
|
||||
}
|
||||
}
|
||||
fn is_expired(&self) -> bool {
|
||||
self.expires_at < std::time::Instant::now()
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref AI_KEY_CACHE: Cache<String, AiCache> = Cache::new(500);
|
||||
}
|
||||
|
||||
struct Variable {
|
||||
value: String,
|
||||
is_secret: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ProxyQueryParams {
|
||||
no_cache: Option<bool>,
|
||||
}
|
||||
|
||||
async fn get_variable_or_self(path: String, db: &DB, w_id: &str) -> Result<String> {
|
||||
if !path.starts_with("$var:") {
|
||||
return Ok(path);
|
||||
}
|
||||
let path = path.strip_prefix("$var:").unwrap().to_string();
|
||||
let mut variable = sqlx::query_as!(
|
||||
Variable,
|
||||
"SELECT value, is_secret
|
||||
FROM variable
|
||||
WHERE path = $1 AND workspace_id = $2",
|
||||
&path,
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
if variable.is_secret {
|
||||
let mc = build_crypt(db, w_id).await?;
|
||||
variable.value = decrypt(&mc, variable.value)?;
|
||||
}
|
||||
Ok(variable.value)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AiResource {
|
||||
pub path: String,
|
||||
#[serde(deserialize_with = "check_if_valid_ai_provider")]
|
||||
pub provider: String,
|
||||
}
|
||||
|
||||
fn check_if_valid_ai_provider<'de, D>(provider: D) -> std::result::Result<String, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let provider = String::deserialize(provider)?;
|
||||
match provider.as_str() {
|
||||
"anthropic" | "openai" | "mistral" => Ok(provider),
|
||||
_ => Err(serde::de::Error::custom(
|
||||
"Only the following Ai providers are supported: openai, anthropic and mistral"
|
||||
.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
let router = Router::new().route("/proxy/*ai", post(proxy));
|
||||
|
||||
router
|
||||
}
|
||||
|
||||
async fn proxy(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, ai_path)): Path<(String, String)>,
|
||||
Query(query_params): Query<ProxyQueryParams>,
|
||||
body: Bytes,
|
||||
) -> impl IntoResponse {
|
||||
let workspace_cache = AI_KEY_CACHE.get(&w_id);
|
||||
let ai_cache = match workspace_cache {
|
||||
Some(cache) if !cache.is_expired() && !query_params.no_cache.unwrap_or(false) => {
|
||||
cache.cached_key
|
||||
}
|
||||
_ => {
|
||||
let ai_resource = sqlx::query_scalar!(
|
||||
"SELECT ai_resource FROM workspace_settings WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
|
||||
if ai_resource.is_none() {
|
||||
return Err(Error::InternalErr("AI resource not configured".to_string()));
|
||||
}
|
||||
|
||||
let ai_resource = serde_json::from_value::<AiResource>(ai_resource.unwrap())
|
||||
.map_err(|e| Error::BadRequest(e.to_string()))?;
|
||||
let ai_resource_path = ai_resource.path;
|
||||
|
||||
let resource = sqlx::query_scalar!(
|
||||
"SELECT value
|
||||
FROM resource
|
||||
WHERE path = $1 AND workspace_id = $2",
|
||||
&ai_resource_path,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
Error::InternalErr(format!(
|
||||
"Could not find the {} resource at path {ai_resource_path}, update the resource path in the workspace settings", ai_resource.provider
|
||||
))
|
||||
})?;
|
||||
|
||||
if resource.is_none() {
|
||||
return Err(Error::InternalErr(format!(
|
||||
"{} resource missing value",
|
||||
ai_resource.provider
|
||||
)));
|
||||
}
|
||||
|
||||
let resource = resource.unwrap();
|
||||
|
||||
let ai_cache = match ai_resource.provider.as_str() {
|
||||
"openai" => openai::get_cached_value(&db, &w_id, resource).await,
|
||||
"anthropic" => anthropic::get_cached_value(&db, &w_id, resource).await,
|
||||
"mistral" => mistral::get_cached_value(&db, &w_id, resource).await,
|
||||
provider => {
|
||||
return Err(Error::BadRequest(format!("{} is not supported", provider)))
|
||||
}
|
||||
};
|
||||
let ai_cache = ai_cache?;
|
||||
AI_KEY_CACHE.insert(
|
||||
w_id.clone(),
|
||||
AiCache::new(ai_resource_path, ai_cache.clone()),
|
||||
);
|
||||
ai_cache
|
||||
}
|
||||
};
|
||||
let (path, request) = match ai_cache {
|
||||
KeyCache::Openai(cached) => ("openai_path", cached.prepare_request(&ai_path, body)),
|
||||
KeyCache::Anthropic(cached) => ("anthropic_path", cached.prepare_request(&ai_path, body)),
|
||||
KeyCache::Mistral(cached) => ("mistral_path", cached.prepare_request(&ai_path, body)),
|
||||
};
|
||||
|
||||
let response = request?.send().await.map_err(to_anyhow)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"ai.request",
|
||||
ActionKind::Execute,
|
||||
&w_id,
|
||||
Some(&authed.email),
|
||||
Some([(path, &format!("{:?}", ai_path)[..])].into()),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
if response.error_for_status_ref().is_err() {
|
||||
let err_msg = response.text().await.unwrap_or("".to_string());
|
||||
return Err(Error::AiError(err_msg));
|
||||
}
|
||||
|
||||
let status_code = response.status();
|
||||
let headers = response.headers().clone();
|
||||
let stream = response.bytes_stream();
|
||||
Ok((status_code, headers, axum::body::Body::from_stream(stream)))
|
||||
}
|
||||
@@ -206,6 +206,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/queue/list_filtered_uuids", get(list_filtered_uuids))
|
||||
.route("/queue/cancel_selection", post(cancel_selection))
|
||||
.route("/completed/count", get(count_completed_jobs))
|
||||
.route("/completed/count_jobs", get(count_completed_jobs_detail))
|
||||
.route(
|
||||
"/completed/list",
|
||||
get(list_completed_jobs).layer(cors.clone()),
|
||||
@@ -1592,6 +1593,50 @@ async fn count_queue_jobs(
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CountCompletedJobsQuery {
|
||||
completed_after_s_ago: Option<i64>,
|
||||
success: Option<bool>,
|
||||
tags: Option<String>,
|
||||
all_workspaces: Option<bool>,
|
||||
}
|
||||
|
||||
async fn count_completed_jobs_detail(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Query(query): Query<CountCompletedJobsQuery>,
|
||||
) -> error::JsonResult<i64> {
|
||||
let mut sqlb = SqlBuilder::select_from("completed_job");
|
||||
sqlb
|
||||
.field("COUNT(*) as count");
|
||||
|
||||
if !query.all_workspaces.unwrap_or(false) {
|
||||
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
|
||||
}
|
||||
|
||||
if let Some(after_s_ago) = query.completed_after_s_ago {
|
||||
let after = Utc::now() - chrono::Duration::seconds(after_s_ago);
|
||||
sqlb.and_where_gt("started_at + duration_ms / 1000 * interval '1 second'", "?".bind(&after.to_rfc3339()));
|
||||
}
|
||||
|
||||
if let Some(success) = query.success {
|
||||
sqlb.and_where_eq("success", "?".bind(&success));
|
||||
}
|
||||
|
||||
if let Some(tags) = query.tags {
|
||||
sqlb.and_where_in("tag", &tags.split(",").map(|t| format!("'{}'", t)).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
let sql = sqlb.sql()?;
|
||||
let stats = sqlx::query_scalar::<_, i64>(&sql)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(stats))
|
||||
}
|
||||
|
||||
|
||||
|
||||
async fn count_completed_jobs(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
|
||||
@@ -62,13 +62,14 @@ mod http_triggers;
|
||||
mod indexer_ee;
|
||||
mod inputs;
|
||||
mod integration;
|
||||
mod ai;
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
mod job_helpers_ee;
|
||||
pub mod job_metrics;
|
||||
pub mod jobs;
|
||||
pub mod oauth2_ee;
|
||||
mod oidc_ee;
|
||||
mod openai;
|
||||
mod raw_apps;
|
||||
mod resources;
|
||||
mod saml_ee;
|
||||
@@ -94,6 +95,7 @@ mod workspaces_ee;
|
||||
|
||||
pub const DEFAULT_BODY_LIMIT: usize = 2097152 * 100; // 200MB
|
||||
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
|
||||
pub static ref REQUEST_SIZE_LIMIT: Arc<RwLock<usize>> = Arc::new(RwLock::new(DEFAULT_BODY_LIMIT));
|
||||
@@ -278,7 +280,7 @@ pub async fn run_server(
|
||||
.nest("/job_helpers", job_helpers_service)
|
||||
.nest("/jobs", jobs::workspaced_service())
|
||||
.nest("/oauth", oauth2_ee::workspaced_service())
|
||||
.nest("/openai", openai::workspaced_service())
|
||||
.nest("/ai", ai::workspaced_service())
|
||||
.nest("/raw_apps", raw_apps::workspaced_service())
|
||||
.nest("/resources", resources::workspaced_service())
|
||||
.nest("/schedules", schedule::workspaced_service())
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::db::{ApiAuthed, DB};
|
||||
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
extract::{Extension, Path, Query},
|
||||
response::IntoResponse,
|
||||
routing::post,
|
||||
Router,
|
||||
};
|
||||
use quick_cache::sync::Cache;
|
||||
use reqwest::Client;
|
||||
use serde_json::value::RawValue;
|
||||
use windmill_audit::audit_ee::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
use windmill_common::{
|
||||
error::{to_anyhow, Error},
|
||||
variables::build_crypt,
|
||||
};
|
||||
|
||||
use crate::variables::decrypt;
|
||||
use serde::Deserialize;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
|
||||
.timeout(std::time::Duration::from_secs(60 * 5))
|
||||
.user_agent("windmill/beta")
|
||||
.build().unwrap();
|
||||
}
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
let router = Router::new().route("/proxy/*openai_path", post(proxy));
|
||||
|
||||
router
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenaiResource {
|
||||
api_key: String,
|
||||
organization_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenaiClientCredentialsOauthResource {
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
token_url: String,
|
||||
user: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum OpenaiConfig {
|
||||
Resource(OpenaiResource),
|
||||
ClientCredentialsOauthResource(OpenaiClientCredentialsOauthResource),
|
||||
}
|
||||
|
||||
struct Variable {
|
||||
value: String,
|
||||
is_secret: bool,
|
||||
}
|
||||
async fn get_variable_or_self(path: String, db: &DB, w_id: &String) -> Result<String, Error> {
|
||||
if !path.starts_with("$var:") {
|
||||
return Ok(path);
|
||||
}
|
||||
let path = path.strip_prefix("$var:").unwrap().to_string();
|
||||
let mut variable = sqlx::query_as!(
|
||||
Variable,
|
||||
"SELECT value, is_secret
|
||||
FROM variable
|
||||
WHERE path = $1 AND workspace_id = $2",
|
||||
&path,
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
if variable.is_secret {
|
||||
let mc = build_crypt(&db, &w_id).await?;
|
||||
variable.value = decrypt(&mc, variable.value)?;
|
||||
}
|
||||
Ok(variable.value)
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref OPENAI_AZURE_BASE_PATH: Option<String> = std::env::var("OPENAI_AZURE_BASE_PATH").ok();
|
||||
static ref OPENAI_KEY_CACHE: Cache<String, OpenaiKeyCache> = Cache::new(500);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenaiCredentials {
|
||||
access_token: String,
|
||||
}
|
||||
async fn get_openai_key_using_credentials_flow(
|
||||
mut resource: OpenaiClientCredentialsOauthResource,
|
||||
db: &DB,
|
||||
w_id: &String,
|
||||
) -> Result<String, Error> {
|
||||
resource.client_id = get_variable_or_self(resource.client_id, &db, &w_id).await?;
|
||||
resource.client_secret = get_variable_or_self(resource.client_secret, &db, &w_id).await?;
|
||||
resource.token_url = get_variable_or_self(resource.token_url, &db, &w_id).await?;
|
||||
let mut params = HashMap::new();
|
||||
params.insert("grant_type", "client_credentials");
|
||||
let response = HTTP_CLIENT
|
||||
.post(resource.token_url)
|
||||
.form(¶ms)
|
||||
.basic_auth(resource.client_id, Some(resource.client_secret))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
Error::InternalErr(format!(
|
||||
"Failed to get OpenAI credentials using credentials flow: {}",
|
||||
err
|
||||
))
|
||||
})?;
|
||||
let response = response.json::<OpenaiCredentials>().await.map_err(|err| {
|
||||
Error::InternalErr(format!(
|
||||
"Failed to parse OpenAI credentials from credentials flow: {}",
|
||||
err
|
||||
))
|
||||
})?;
|
||||
Ok(response.access_token)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct OpenaiKeyCache {
|
||||
api_key: String,
|
||||
organization_id: Option<String>,
|
||||
azure_base_path: Option<String>,
|
||||
user: Option<String>,
|
||||
expires_at: std::time::Instant,
|
||||
}
|
||||
|
||||
impl OpenaiKeyCache {
|
||||
fn new(
|
||||
api_key: String,
|
||||
organization_id: Option<String>,
|
||||
azure_base_path: Option<String>,
|
||||
expires_at: std::time::Instant,
|
||||
user: Option<String>,
|
||||
) -> Self {
|
||||
Self { api_key, organization_id, azure_base_path, expires_at, user }
|
||||
}
|
||||
fn is_expired(&self) -> bool {
|
||||
self.expires_at < std::time::Instant::now()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ProxyQueryParams {
|
||||
no_cache: Option<bool>,
|
||||
}
|
||||
async fn proxy(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, openai_path)): Path<(String, String)>,
|
||||
Query(query_params): Query<ProxyQueryParams>,
|
||||
mut body: Bytes,
|
||||
) -> impl IntoResponse {
|
||||
let workspace_cache = OPENAI_KEY_CACHE.get(&w_id);
|
||||
let (api_key, organization_id, azure_base_path, user) = if query_params
|
||||
.no_cache
|
||||
.unwrap_or(false)
|
||||
|| workspace_cache.is_none()
|
||||
|| workspace_cache.clone().unwrap().is_expired()
|
||||
{
|
||||
let openai_resource_path = sqlx::query_scalar!(
|
||||
"SELECT openai_resource_path FROM workspace_settings WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?;
|
||||
|
||||
if openai_resource_path.is_none() {
|
||||
return Err(Error::InternalErr(
|
||||
"OpenAI resource not configured".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let openai_resource_path = openai_resource_path.unwrap();
|
||||
|
||||
let resource = sqlx::query_scalar!(
|
||||
"SELECT value
|
||||
FROM resource
|
||||
WHERE path = $1 AND workspace_id = $2",
|
||||
&openai_resource_path,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
Error::InternalErr(format!(
|
||||
"Could not find the OpenAI resource at path {openai_resource_path}, update the resource path in the workspace settings"
|
||||
))
|
||||
})?;
|
||||
|
||||
if resource.is_none() {
|
||||
return Err(Error::InternalErr(
|
||||
"OpenAI resource missing value".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let config: OpenaiConfig = serde_json::from_value(resource.unwrap())
|
||||
.map_err(|e| Error::InternalErr(format!("validating openai resource {e:#}")))?;
|
||||
|
||||
let mut user = None::<String>;
|
||||
let mut resource = match config {
|
||||
OpenaiConfig::Resource(resource) => {
|
||||
tracing::debug!("Getting OpenAI key from static resource");
|
||||
resource
|
||||
}
|
||||
OpenaiConfig::ClientCredentialsOauthResource(resource) => {
|
||||
tracing::debug!("Getting OpenAI key with client credentials flow");
|
||||
user = resource.user.clone();
|
||||
let token = get_openai_key_using_credentials_flow(resource, &db, &w_id).await?;
|
||||
OpenaiResource { api_key: token, organization_id: None }
|
||||
}
|
||||
};
|
||||
|
||||
resource.api_key = get_variable_or_self(resource.api_key, &db, &w_id).await?;
|
||||
|
||||
if resource.organization_id.is_some() {
|
||||
resource.organization_id =
|
||||
Some(get_variable_or_self(resource.organization_id.unwrap(), &db, &w_id).await?);
|
||||
}
|
||||
|
||||
if user.is_some() {
|
||||
user = Some(get_variable_or_self(user.unwrap(), &db, &w_id).await?);
|
||||
}
|
||||
|
||||
let expires_at = std::time::Instant::now() + std::time::Duration::from_secs(60);
|
||||
|
||||
let azure_base_path = sqlx::query_scalar!(
|
||||
"SELECT value
|
||||
FROM global_settings
|
||||
WHERE name = 'openai_azure_base_path'",
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
|
||||
let azure_base_path = if let Some(azure_base_path) = azure_base_path {
|
||||
Some(
|
||||
serde_json::from_value::<String>(azure_base_path).map_err(|e| {
|
||||
Error::InternalErr(format!("validating openai azure base path {e:#}"))
|
||||
})?,
|
||||
)
|
||||
} else {
|
||||
OPENAI_AZURE_BASE_PATH.clone()
|
||||
};
|
||||
|
||||
let workspace_cache = OpenaiKeyCache::new(
|
||||
resource.api_key.clone(),
|
||||
resource.organization_id.clone(),
|
||||
azure_base_path.clone(),
|
||||
expires_at,
|
||||
user.clone(),
|
||||
);
|
||||
OPENAI_KEY_CACHE.insert(w_id.clone(), workspace_cache);
|
||||
(
|
||||
resource.api_key,
|
||||
resource.organization_id,
|
||||
azure_base_path,
|
||||
user,
|
||||
)
|
||||
} else {
|
||||
tracing::debug!("Using cached OpenAI key");
|
||||
let workspace_cache = workspace_cache.unwrap();
|
||||
(
|
||||
workspace_cache.api_key.clone(),
|
||||
workspace_cache.organization_id.clone(),
|
||||
workspace_cache.azure_base_path.clone(),
|
||||
workspace_cache.user.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
if user.is_some() {
|
||||
tracing::debug!("Adding user to request body");
|
||||
let mut json_body: HashMap<String, Box<RawValue>> = serde_json::from_slice(&body)
|
||||
.map_err(|e| Error::InternalErr(format!("Failed to parse request body: {}", e)))?;
|
||||
|
||||
let user_json_string = serde_json::Value::String(user.unwrap()).to_string(); // makes sure to escape characters
|
||||
|
||||
json_body.insert(
|
||||
"user".to_string(),
|
||||
RawValue::from_string(user_json_string)
|
||||
.map_err(|e| Error::InternalErr(format!("Failed to parse user: {}", e)))?,
|
||||
);
|
||||
|
||||
body = serde_json::to_vec(&json_body)
|
||||
.map_err(|e| Error::InternalErr(format!("Failed to reserialize request body: {}", e)))?
|
||||
.into();
|
||||
}
|
||||
|
||||
let base_url = if let Some(base_url) = azure_base_path {
|
||||
base_url
|
||||
} else {
|
||||
"https://api.openai.com/v1".to_string()
|
||||
};
|
||||
|
||||
let url = format!("{}/{}", base_url, openai_path);
|
||||
let mut request = HTTP_CLIENT
|
||||
.post(url)
|
||||
.header("content-type", "application/json")
|
||||
.body(body);
|
||||
|
||||
if base_url != "https://api.openai.com/v1" {
|
||||
request = request
|
||||
.header("api-key", api_key)
|
||||
.query(&[("api-version", "2023-05-15")])
|
||||
} else {
|
||||
request = request.header("authorization", format!("Bearer {}", api_key))
|
||||
}
|
||||
|
||||
if let Some(org_id) = organization_id {
|
||||
request = request.header("OpenAI-Organization", org_id);
|
||||
}
|
||||
|
||||
let response = request.send().await.map_err(to_anyhow)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"openai.request",
|
||||
ActionKind::Execute,
|
||||
&w_id,
|
||||
Some(&authed.email),
|
||||
Some([("openai_path", &format!("{:?}", openai_path)[..])].into()),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
if response.error_for_status_ref().is_err() {
|
||||
return Err(Error::OpenAIError(
|
||||
response.text().await.unwrap_or("".to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
let status_code = response.status();
|
||||
let headers = response.headers().clone();
|
||||
let stream = response.bytes_stream();
|
||||
|
||||
Ok((status_code, headers, axum::body::Body::from_stream(stream)))
|
||||
}
|
||||
@@ -300,7 +300,7 @@ async fn get_resource(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
if resource_o.is_none() {
|
||||
explain_resource_perm_error(&path, &w_id, &db).await?;
|
||||
explain_resource_perm_error(&path, &w_id, &db, &authed).await?;
|
||||
}
|
||||
let resource = not_found_if_none(resource_o, "Resource", path)?;
|
||||
Ok(Json(resource))
|
||||
@@ -343,7 +343,7 @@ async fn get_resource_value(
|
||||
|
||||
tx.commit().await?;
|
||||
if value_o.is_none() {
|
||||
explain_resource_perm_error(&path, &w_id, &db).await?;
|
||||
explain_resource_perm_error(&path, &w_id, &db, &authed).await?;
|
||||
}
|
||||
|
||||
let value = not_found_if_none(value_o, "Resource", path)?;
|
||||
@@ -354,6 +354,7 @@ async fn explain_resource_perm_error(
|
||||
path: &str,
|
||||
w_id: &str,
|
||||
db: &sqlx::Pool<Postgres>,
|
||||
authed: &ApiAuthed,
|
||||
) -> windmill_common::error::Result<()> {
|
||||
let extra_perms = sqlx::query_scalar!(
|
||||
"SELECT extra_perms from resource WHERE path = $1 AND workspace_id = $2",
|
||||
@@ -378,12 +379,12 @@ async fn explain_resource_perm_error(
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
return Err(Error::NotAuthorized(format!(
|
||||
"Resource exists but you don't have access to it:\nresource perms: {}\nfolder perms: {}",
|
||||
"Resource exists but you don't have access to it:\nresource perms: {}\nfolder perms: {}\nauthed as: {authed:?}",
|
||||
serde_json::to_string_pretty(&extra_perms).unwrap_or_default(), serde_json::to_string_pretty(&folder_extra_perms).unwrap_or_default()
|
||||
)));
|
||||
} else {
|
||||
return Err(Error::NotAuthorized(format!(
|
||||
"Resource exists but you don't have access to it:\nresource perms: {}",
|
||||
"Resource exists but you don't have access to it:\nresource perms: {}\nauthed as: {authed:?}",
|
||||
serde_json::to_string_pretty(&extra_perms).unwrap_or_default()
|
||||
)));
|
||||
}
|
||||
@@ -457,7 +458,7 @@ pub async fn get_resource_value_interpolated_internal(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
if value_o.is_none() {
|
||||
explain_resource_perm_error(path, workspace, db).await?;
|
||||
explain_resource_perm_error(path, workspace, db, &authed).await?;
|
||||
}
|
||||
|
||||
let value = not_found_if_none(value_o, "Resource", path)?;
|
||||
|
||||
@@ -434,67 +434,15 @@ pub async fn test_critical_channels() -> Result<String> {
|
||||
Ok("Critical channels require EE".to_string())
|
||||
}
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CriticalAlert {
|
||||
id: i32,
|
||||
alert_type: String,
|
||||
message: String,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
acknowledged: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[derive(Deserialize)]
|
||||
pub struct AlertQueryParams {
|
||||
pub page: Option<i32>,
|
||||
pub page_size: Option<i32>,
|
||||
pub acknowledged: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn get_critical_alerts(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
Query(params): Query<AlertQueryParams>,
|
||||
) -> JsonResult<Vec<CriticalAlert>> {
|
||||
Query(params): Query<crate::utils::AlertQueryParams>,
|
||||
) -> JsonResult<Vec<crate::utils::CriticalAlert>> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
|
||||
// Default pagination values if not provided
|
||||
let page = params.page.unwrap_or(1).max(1);
|
||||
let page_size = params.page_size.unwrap_or(10).min(100) as i64;
|
||||
let offset = ((page - 1) * page_size as i32) as i64;
|
||||
|
||||
let alerts = if let Some(acknowledged) = params.acknowledged {
|
||||
sqlx::query_as!(
|
||||
CriticalAlert,
|
||||
"SELECT id, alert_type, message, created_at, acknowledged
|
||||
FROM alerts
|
||||
WHERE acknowledged = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3",
|
||||
acknowledged,
|
||||
page_size,
|
||||
offset
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as!(
|
||||
CriticalAlert,
|
||||
"SELECT id, alert_type, message, created_at, acknowledged
|
||||
FROM alerts
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2",
|
||||
page_size,
|
||||
offset
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?
|
||||
};
|
||||
|
||||
Ok(Json(alerts))
|
||||
crate::utils::get_critical_alerts(db, params, None).await
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
@@ -510,15 +458,7 @@ pub async fn acknowledge_critical_alert(
|
||||
) -> error::Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE alerts SET acknowledged = true WHERE id = $1",
|
||||
id
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
tracing::info!("Acknowledged critical alert with id: {}", id);
|
||||
Ok("Critical alert acknowledged".to_string())
|
||||
crate::utils::acknowledge_critical_alert(db, None, id).await
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
@@ -533,14 +473,7 @@ pub async fn acknowledge_all_critical_alerts(
|
||||
) -> error::Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE alerts SET acknowledged = true WHERE acknowledged = false"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
tracing::info!("Acknowledged all unacknowledged critical alerts");
|
||||
Ok("All unacknowledged critical alerts acknowledged".to_string())
|
||||
crate::utils::acknowledge_all_critical_alerts(db, None).await
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
|
||||
@@ -16,6 +16,12 @@ use windmill_common::{
|
||||
DB,
|
||||
};
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::error::JsonResult;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use axum::Json;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct WithStarredInfoQuery {
|
||||
pub with_starred_info: Option<bool>,
|
||||
@@ -162,3 +168,159 @@ pub fn content_plain(body: Body) -> Response {
|
||||
.body(body)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CriticalAlert {
|
||||
id: i32,
|
||||
alert_type: String,
|
||||
message: String,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
acknowledged: Option<bool>,
|
||||
workspace_id: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AlertQueryParams {
|
||||
pub page: Option<i32>,
|
||||
pub page_size: Option<i32>,
|
||||
pub acknowledged: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn get_critical_alerts(
|
||||
db: DB,
|
||||
params: AlertQueryParams,
|
||||
workspace_id: Option<String>,
|
||||
) -> JsonResult<Vec<CriticalAlert>> {
|
||||
let page = params.page.unwrap_or(1).max(1);
|
||||
let page_size = params.page_size.unwrap_or(10).min(100) as i64;
|
||||
let offset = ((page - 1) * page_size as i32) as i64;
|
||||
|
||||
let alerts = if let Some(workspace_id) = workspace_id {
|
||||
// `workspace_id` is provided => workspace admin
|
||||
if params.acknowledged.is_none() {
|
||||
// Case: return all rows where `workspace_id` matches
|
||||
sqlx::query_as!(
|
||||
CriticalAlert,
|
||||
"SELECT id, alert_type, message, created_at, COALESCE(acknowledged_workspace, false) AS acknowledged, workspace_id
|
||||
FROM alerts
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3",
|
||||
workspace_id,
|
||||
page_size,
|
||||
offset
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?
|
||||
} else {
|
||||
// Case: return rows where `acknowledged_workspace` matches `params.acknowledged`
|
||||
sqlx::query_as!(
|
||||
CriticalAlert,
|
||||
"SELECT id, alert_type, message, created_at, COALESCE(acknowledged_workspace, false) AS acknowledged, workspace_id
|
||||
FROM alerts
|
||||
WHERE workspace_id = $1 AND COALESCE(acknowledged_workspace, false) = $2
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $3 OFFSET $4",
|
||||
workspace_id,
|
||||
params.acknowledged,
|
||||
page_size,
|
||||
offset
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?
|
||||
}
|
||||
} else {
|
||||
// `workspace_id` is not provided => superadmin
|
||||
if params.acknowledged.is_none() {
|
||||
// Case: Return all rows unfiltered with global acknowledged as acknowledged
|
||||
sqlx::query_as!(
|
||||
CriticalAlert,
|
||||
"SELECT id, alert_type, message, created_at, COALESCE(acknowledged, false) AS acknowledged, workspace_id
|
||||
FROM alerts
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2",
|
||||
page_size,
|
||||
offset
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?
|
||||
} else {
|
||||
// Case: Return rows where global acknowledged matches params.acknowledged
|
||||
sqlx::query_as!(
|
||||
CriticalAlert,
|
||||
"SELECT id, alert_type, message, created_at, COALESCE(acknowledged, false) AS acknowledged, workspace_id
|
||||
FROM alerts
|
||||
WHERE COALESCE(acknowledged, false) = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3",
|
||||
params.acknowledged,
|
||||
page_size,
|
||||
offset
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Json(alerts))
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn acknowledge_critical_alert(
|
||||
db: DB,
|
||||
workspace_id: Option<String>,
|
||||
id: i32,
|
||||
) -> error::Result<String> {
|
||||
sqlx::query!(
|
||||
"UPDATE alerts
|
||||
SET
|
||||
acknowledged = true,
|
||||
acknowledged_workspace = CASE
|
||||
WHEN $2::text IS NOT NULL AND workspace_id = $2 THEN true
|
||||
ELSE acknowledged_workspace
|
||||
END
|
||||
WHERE id = $1",
|
||||
id,
|
||||
workspace_id
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
tracing::info!(
|
||||
"Acknowledged critical alert with id: {}{}",
|
||||
id,
|
||||
workspace_id.map_or_else(|| "".to_string(), |w| format!(" for workspace_id: {}", w))
|
||||
);
|
||||
Ok("Critical alert acknowledged".to_string())
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn acknowledge_all_critical_alerts(
|
||||
db: DB,
|
||||
workspace_id: Option<String>,
|
||||
) -> error::Result<String> {
|
||||
sqlx::query!(
|
||||
"UPDATE alerts
|
||||
SET
|
||||
acknowledged = true,
|
||||
acknowledged_workspace = CASE
|
||||
WHEN $1::text IS NOT NULL THEN true
|
||||
ELSE acknowledged_workspace
|
||||
END
|
||||
WHERE ($1::text IS NOT NULL AND workspace_id = $1)
|
||||
OR ($1::text IS NULL)",
|
||||
workspace_id
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
tracing::info!(
|
||||
"Acknowledged all unacknowledged critical alerts{}",
|
||||
workspace_id.map_or_else(|| "".to_string(), |w| format!(" for workspace_id: {}", w))
|
||||
);
|
||||
Ok("All unacknowledged critical alerts acknowledged".to_string())
|
||||
}
|
||||
|
||||
@@ -754,12 +754,14 @@ async fn disable_with_error(db: &DB, ws_trigger: &WebsocketTrigger, error: Strin
|
||||
)
|
||||
.execute(db).await {
|
||||
Ok(_) => {
|
||||
report_critical_error(format!("Disabling websocket {} because of error: {}", ws_trigger.url, error), db.clone()).await;
|
||||
report_critical_error(format!("Disabling websocket {} because of error: {}", ws_trigger.url, error), db.clone(), Some(&ws_trigger.workspace_id), None).await;
|
||||
},
|
||||
Err(disable_err) => {
|
||||
report_critical_error(
|
||||
format!("Could not disable websocket {} with err {}, disabling because of error {}", ws_trigger.path, disable_err, error),
|
||||
db.clone()
|
||||
db.clone(),
|
||||
Some(&ws_trigger.workspace_id),
|
||||
None,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
@@ -896,7 +898,7 @@ async fn listen_to_websocket(
|
||||
}
|
||||
if should_handle {
|
||||
if let Err(err) = run_job(&db, rsmq.clone(), &ws_trigger, text).await {
|
||||
report_critical_error(format!("Failed to trigger job from websocket {}: {:?}", ws_trigger.url, err), db.clone()).await;
|
||||
report_critical_error(format!("Failed to trigger job from websocket {}: {:?}", ws_trigger.url, err), db.clone(), Some(&ws_trigger.workspace_id), None).await;
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::ai::{AiResource, AI_KEY_CACHE};
|
||||
use crate::db::ApiAuthed;
|
||||
use crate::users_ee::send_email_if_possible;
|
||||
use crate::utils::get_instance_username_or_create_pending;
|
||||
@@ -118,7 +119,11 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/change_workspace_name", post(change_workspace_name))
|
||||
.route("/change_workspace_id", post(change_workspace_id))
|
||||
.route("/usage", get(get_usage))
|
||||
.route("/used_triggers", get(get_used_triggers));
|
||||
.route("/used_triggers", get(get_used_triggers))
|
||||
.route("/critical_alerts", get(get_critical_alerts))
|
||||
.route("/critical_alerts/:id/acknowledge", post(acknowledge_critical_alert))
|
||||
.route("/critical_alerts/acknowledge_all", post(acknowledge_all_critical_alerts))
|
||||
.route("/critical_alerts/mute", post(mute_critical_alerts));
|
||||
|
||||
#[cfg(feature = "stripe")]
|
||||
{
|
||||
@@ -168,7 +173,7 @@ pub struct WorkspaceSettings {
|
||||
pub plan: Option<String>,
|
||||
pub webhook: Option<String>,
|
||||
pub deploy_to: Option<String>,
|
||||
pub openai_resource_path: Option<String>,
|
||||
pub ai_resource: Option<serde_json::Value>,
|
||||
pub code_completion_enabled: bool,
|
||||
pub error_handler: Option<String>,
|
||||
pub error_handler_extra_args: Option<serde_json::Value>,
|
||||
@@ -179,6 +184,7 @@ pub struct WorkspaceSettings {
|
||||
pub default_app: Option<String>,
|
||||
pub automatic_billing: bool,
|
||||
pub default_scripts: Option<serde_json::Value>,
|
||||
pub mute_critical_alerts: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Debug)]
|
||||
@@ -234,7 +240,7 @@ struct EditWebhook {
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EditCopilotConfig {
|
||||
openai_resource_path: Option<String>,
|
||||
ai_resource: Option<serde_json::Value>,
|
||||
code_completion_enabled: bool,
|
||||
}
|
||||
|
||||
@@ -645,23 +651,33 @@ async fn edit_copilot_config(
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
if let Some(openai_resource_path) = &eo.openai_resource_path {
|
||||
if let Some(ai_resource) = &eo.ai_resource {
|
||||
let path = serde_json::from_value::<AiResource>(ai_resource.clone())
|
||||
.map_err(|e| Error::BadRequest(e.to_string()))?
|
||||
.path;
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET openai_resource_path = $1, code_completion_enabled = $2 WHERE workspace_id = $3",
|
||||
openai_resource_path,
|
||||
"UPDATE workspace_settings SET ai_resource = $1, code_completion_enabled = $2 WHERE workspace_id = $3",
|
||||
ai_resource,
|
||||
eo.code_completion_enabled,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if let Some(cached) = AI_KEY_CACHE.get(&w_id) {
|
||||
if cached.path != path {
|
||||
AI_KEY_CACHE.remove(&w_id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET openai_resource_path = NULL, code_completion_enabled = $1 WHERE workspace_id = $2",
|
||||
"UPDATE workspace_settings SET ai_resource = NULL, code_completion_enabled = $1 WHERE workspace_id = $2",
|
||||
eo.code_completion_enabled,
|
||||
&w_id,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
AI_KEY_CACHE.remove(&w_id);
|
||||
}
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
@@ -672,10 +688,7 @@ async fn edit_copilot_config(
|
||||
Some(&authed.email),
|
||||
Some(
|
||||
[
|
||||
(
|
||||
"openai_resource_path",
|
||||
&format!("{:?}", eo.openai_resource_path)[..],
|
||||
),
|
||||
("ai_resource", &format!("{:?}", eo.ai_resource)[..]),
|
||||
(
|
||||
"code_completion_enabled",
|
||||
&format!("{:?}", eo.code_completion_enabled)[..],
|
||||
@@ -692,7 +705,8 @@ async fn edit_copilot_config(
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CopilotInfo {
|
||||
pub exists_openai_resource_path: bool,
|
||||
pub ai_provider: String,
|
||||
pub exists_ai_resource: bool,
|
||||
pub code_completion_enabled: bool,
|
||||
}
|
||||
async fn get_copilot_info(
|
||||
@@ -701,16 +715,32 @@ async fn get_copilot_info(
|
||||
) -> JsonResult<CopilotInfo> {
|
||||
let mut tx = db.begin().await?;
|
||||
let record = sqlx::query!(
|
||||
"SELECT openai_resource_path, code_completion_enabled FROM workspace_settings WHERE workspace_id = $1",
|
||||
"SELECT ai_resource, code_completion_enabled FROM workspace_settings WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| Error::InternalErr(format!("getting openai_resource_path and code_completion_enabled: {e:#}")))?;
|
||||
.map_err(|e| Error::InternalErr(format!("getting ai_resource and code_completion_enabled: {e:#}")))?;
|
||||
tx.commit().await?;
|
||||
|
||||
let (ai_provider, exists_ai_resource) = if let Some(ai_resource) = record.ai_resource {
|
||||
let ai_resource = serde_json::from_value::<AiResource>(ai_resource);
|
||||
let exist = ai_resource.is_ok();
|
||||
(
|
||||
if exist {
|
||||
ai_resource.unwrap().provider
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
exist,
|
||||
)
|
||||
} else {
|
||||
("".to_string(), false)
|
||||
};
|
||||
|
||||
Ok(Json(CopilotInfo {
|
||||
exists_openai_resource_path: record.openai_resource_path.is_some(),
|
||||
ai_provider,
|
||||
exists_ai_resource,
|
||||
code_completion_enabled: record.code_completion_enabled,
|
||||
}))
|
||||
}
|
||||
@@ -2223,7 +2253,7 @@ struct SimplifiedSettings {
|
||||
error_handler: Option<String>,
|
||||
error_handler_extra_args: Option<Value>,
|
||||
error_handler_muted_on_cancel: bool,
|
||||
openai_resource_path: Option<String>,
|
||||
ai_resource: Option<serde_json::Value>,
|
||||
code_completion_enabled: bool,
|
||||
large_file_storage: Option<Value>,
|
||||
git_sync: Option<Value>,
|
||||
@@ -2588,7 +2618,7 @@ async fn tarball_workspace(
|
||||
webhook,
|
||||
deploy_to,
|
||||
error_handler,
|
||||
openai_resource_path,
|
||||
ai_resource,
|
||||
code_completion_enabled,
|
||||
error_handler_extra_args,
|
||||
error_handler_muted_on_cancel,
|
||||
@@ -3050,3 +3080,93 @@ async fn get_usage(Extension(db): Extension<DB>, Path(w_id): Path<String>) -> Re
|
||||
.unwrap_or(0);
|
||||
Ok(usage.to_string())
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn get_critical_alerts(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
authed: ApiAuthed,
|
||||
Query(params): Query<crate::utils::AlertQueryParams>,
|
||||
) -> JsonResult<Vec<crate::utils::CriticalAlert>> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
crate::utils::get_critical_alerts(db, params, Some(w_id)).await
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
pub async fn get_critical_alerts() -> Error {
|
||||
Error::NotFound("Critical Alerts require EE".to_string())
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn acknowledge_critical_alert(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, id)): Path<(String, i32)>,
|
||||
authed: ApiAuthed,
|
||||
) -> Result<String> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
crate::utils::acknowledge_critical_alert(db, Some(w_id), id).await
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
pub async fn acknowledge_critical_alert() -> Error {
|
||||
Error::NotFound("Critical Alerts require EE".to_string())
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn acknowledge_all_critical_alerts(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
authed: ApiAuthed,
|
||||
) -> Result<String> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
crate::utils::acknowledge_all_critical_alerts(db, Some(w_id)).await
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
pub async fn acknowledge_all_critical_alerts() -> Error {
|
||||
Error::NotFound("Critical Alerts require EE".to_string())
|
||||
}
|
||||
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[derive(Deserialize)]
|
||||
pub struct MuteCriticalAlertRequest {
|
||||
pub mute_critical_alerts: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn mute_critical_alerts(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
ApiAuthed { is_admin, username, .. }: ApiAuthed,
|
||||
Json(m_r): Json<MuteCriticalAlertRequest>,
|
||||
) -> Result<String> {
|
||||
require_admin(is_admin, &username)?;
|
||||
|
||||
let mute_alerts = m_r.mute_critical_alerts.unwrap_or(false);
|
||||
|
||||
if mute_alerts {
|
||||
sqlx::query!(
|
||||
"UPDATE alerts SET acknowledged_workspace = true, acknowledged = true WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET mute_critical_alerts = $1 WHERE workspace_id = $2",
|
||||
mute_alerts,
|
||||
&w_id
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
Ok(format!("Updated mute criticital alert ui settings for workspace: {}", &w_id))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
pub async fn mute_critical_alerts() -> Error {
|
||||
Error::NotFound("Critical Alerts require EE".to_string())
|
||||
}
|
||||
@@ -59,7 +59,7 @@ pub enum Error {
|
||||
#[error("Error: {0:#?}")]
|
||||
JsonErr(serde_json::Value),
|
||||
#[error("{0}")]
|
||||
OpenAIError(String),
|
||||
AiError(String),
|
||||
#[error("{0}")]
|
||||
AlreadyCompleted(String),
|
||||
}
|
||||
@@ -90,7 +90,7 @@ impl IntoResponse for Error {
|
||||
Self::RequireAdmin(_) => axum::http::StatusCode::FORBIDDEN,
|
||||
Self::SqlErr(_)
|
||||
| Self::BadRequest(_)
|
||||
| Self::OpenAIError(_)
|
||||
| Self::AiError(_)
|
||||
| Self::QuotaExceeded(_) => axum::http::StatusCode::BAD_REQUEST,
|
||||
_ => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
@@ -29,8 +29,10 @@ pub const DEFAULT_PER_PAGE: usize = 1000;
|
||||
pub const GIT_VERSION: &str =
|
||||
git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
use crate::CRITICAL_ALERT_MUTE_UI_ENABLED;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::worker::CLOUD_HOSTED;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new()
|
||||
@@ -249,15 +251,40 @@ pub fn generate_lock_id(database_name: &str) -> i64 {
|
||||
0x3d32ad9e * (CRC_IEEE.checksum(database_name.as_bytes()) as i64)
|
||||
}
|
||||
|
||||
pub async fn report_critical_error(error_message: String, _db: DB) -> () {
|
||||
pub async fn report_critical_error(
|
||||
error_message: String,
|
||||
_db: DB,
|
||||
workspace_id: Option<&str>,
|
||||
resource: Option<&str>,
|
||||
) -> () {
|
||||
tracing::error!("CRITICAL ERROR: {error_message}");
|
||||
|
||||
let mute = CRITICAL_ALERT_MUTE_UI_ENABLED.load(Ordering::Relaxed);
|
||||
let mute_global = CRITICAL_ALERT_MUTE_UI_ENABLED.load(Ordering::Relaxed);
|
||||
let mute_workspace = if let Some(workspace_id) = workspace_id {
|
||||
match fetch_mute_workspace(&_db, workspace_id).await {
|
||||
Ok(flag) => flag,
|
||||
Err(err) => {
|
||||
tracing::error!("Error fetching mute_workspace: {}", err);
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// we ack_global if mute_global is true, or if mute_workspace is true
|
||||
// but we ignore global mute setting for ack_workspace
|
||||
let acknowledge_workspace = mute_workspace;
|
||||
let acknowledge_global = mute_global || mute_workspace || ( workspace_id.is_some() && *CLOUD_HOSTED);
|
||||
|
||||
if let Err(err) = sqlx::query!(
|
||||
"INSERT INTO alerts (alert_type, message, acknowledged) VALUES ('critical_error', $1, $2)",
|
||||
"INSERT INTO alerts (alert_type, message, acknowledged, acknowledged_workspace, workspace_id, resource)
|
||||
VALUES ('critical_error', $1, $2, $3, $4, $5)",
|
||||
error_message,
|
||||
mute
|
||||
acknowledge_global,
|
||||
acknowledge_workspace,
|
||||
workspace_id,
|
||||
resource,
|
||||
)
|
||||
.execute(&_db)
|
||||
.await
|
||||
@@ -266,30 +293,86 @@ pub async fn report_critical_error(error_message: String, _db: DB) -> () {
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
send_critical_alert(error_message, &_db, CriticalAlertKind::CriticalError, None).await;
|
||||
if *CLOUD_HOSTED && workspace_id.is_some() {
|
||||
tracing::error!(error_message)
|
||||
} else {
|
||||
send_critical_alert(error_message, &_db, CriticalAlertKind::CriticalError, None).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn report_recovered_critical_error(message: String, _db: DB) -> () {
|
||||
pub async fn report_recovered_critical_error(
|
||||
message: String,
|
||||
_db: DB,
|
||||
workspace_id: Option<&str>,
|
||||
resource: Option<&str>,
|
||||
) -> () {
|
||||
tracing::info!("RECOVERED CRITICAL ERROR: {message}");
|
||||
|
||||
let mute = CRITICAL_ALERT_MUTE_UI_ENABLED.load(Ordering::Relaxed);
|
||||
|
||||
if let Err(err) = sqlx::query!(
|
||||
"INSERT INTO alerts (alert_type, message, acknowledged) VALUES ('recovered_critical_error', $1, $2)",
|
||||
"INSERT INTO alerts (alert_type, message, acknowledged, acknowledged_workspace, workspace_id, resource)
|
||||
VALUES ('recovered_critical_error', $1, $2, $3, $4, $5)",
|
||||
message,
|
||||
mute
|
||||
true,
|
||||
true,
|
||||
workspace_id,
|
||||
resource,
|
||||
)
|
||||
.execute(&_db)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to save critical error to database: {}", err);
|
||||
tracing::error!("Failed to save recovered critical error to database: {}", err);
|
||||
}
|
||||
|
||||
// acknowledge all alerts with the same resource
|
||||
if let Some(resource) = resource {
|
||||
if let Err(err) = sqlx::query!(
|
||||
"UPDATE alerts SET acknowledged = true, acknowledged_workspace = true WHERE resource = $1 AND alert_type = 'critical_error'",
|
||||
resource,
|
||||
)
|
||||
.execute(&_db)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to acknowledge critical error alerts for resource {}: {}", resource, err);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
send_critical_alert(
|
||||
message,
|
||||
&_db,
|
||||
CriticalAlertKind::RecoveredCriticalError,
|
||||
None,
|
||||
if *CLOUD_HOSTED && workspace_id.is_some() {
|
||||
tracing::error!(message);
|
||||
} else {
|
||||
send_critical_alert(
|
||||
message,
|
||||
&_db,
|
||||
CriticalAlertKind::RecoveredCriticalError,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_mute_workspace(_db: &DB, workspace_id: &str) -> Result<bool> {
|
||||
match sqlx::query!(
|
||||
"SELECT mute_critical_alerts FROM workspace_settings WHERE workspace_id = $1",
|
||||
workspace_id
|
||||
)
|
||||
.await;
|
||||
.fetch_optional(_db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(record)) => Ok(record.mute_critical_alerts.unwrap_or(false)),
|
||||
Ok(None) => {
|
||||
tracing::warn!(
|
||||
"Workspace ID {} not found in workspace_settings table",
|
||||
workspace_id
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
"Error querying workspace_settings for workspace_id {}: {}",
|
||||
workspace_id,
|
||||
err
|
||||
);
|
||||
Err(Error::SqlErr(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -924,6 +924,8 @@ pub async fn add_completed_job<
|
||||
.unwrap_or("".to_string()),
|
||||
),
|
||||
db.clone(),
|
||||
Some(&w_id),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
} else if queued_job.email == SCHEDULE_ERROR_HANDLER_USER_EMAIL {
|
||||
@@ -992,7 +994,7 @@ pub async fn add_completed_job<
|
||||
"Could not push workspace error handler for failed job ({base_url}/run/{}?workspace={w_id}): {}",
|
||||
queued_job.id,
|
||||
err
|
||||
), db.clone())
|
||||
), db.clone(), Some(&w_id), None)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
@@ -1175,10 +1177,10 @@ pub async fn report_error_to_workspace_handler_or_critical_side_channel<
|
||||
queued_job.id,
|
||||
err
|
||||
);
|
||||
report_critical_error(error_message, db.clone()).await;
|
||||
report_critical_error(error_message, db.clone(), Some(&w_id), None).await;
|
||||
}
|
||||
} else {
|
||||
report_critical_error(error_message, db.clone()).await;
|
||||
report_critical_error(error_message, db.clone(), Some(&w_id), None).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3670,15 +3672,15 @@ pub async fn push<'c, 'd, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
}
|
||||
JobPayload::DeploymentCallback { path } => (
|
||||
None,
|
||||
Some(path),
|
||||
Some(path.clone()),
|
||||
None,
|
||||
JobKind::DeploymentCallback,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(format!("{workspace_id}:git_sync")),
|
||||
Some(1),
|
||||
Some(0),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
|
||||
@@ -41,7 +41,7 @@ use tokio::{io::AsyncWriteExt, process::Child, time::Instant};
|
||||
|
||||
use crate::{
|
||||
AuthedClient, AuthedClientBackgroundTask, JOB_DEFAULT_TIMEOUT, MAX_RESULT_SIZE,
|
||||
MAX_TIMEOUT_DURATION,
|
||||
MAX_TIMEOUT_DURATION, PATH_ENV,
|
||||
};
|
||||
|
||||
pub async fn build_args_map<'a>(
|
||||
@@ -860,11 +860,17 @@ pub async fn save_in_cache(
|
||||
}
|
||||
|
||||
fn tentatively_improve_error(err: Error, executable: &str) -> Error {
|
||||
#[cfg(unix)]
|
||||
let err_msg = "No such file or directory (os error 2)";
|
||||
|
||||
#[cfg(windows)]
|
||||
let err_msg = "program not found";
|
||||
|
||||
if err
|
||||
.to_string()
|
||||
.contains("No such file or directory (os error 2)")
|
||||
.contains(&err_msg)
|
||||
{
|
||||
return Error::InternalErr(format!("Executable {executable} not found on worker"));
|
||||
return Error::InternalErr(format!("Executable {executable} not found on worker. PATH: {}", *PATH_ENV));
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -416,24 +416,32 @@ pub async fn do_postgresql(
|
||||
}
|
||||
|
||||
fn map_as_single_type<T>(
|
||||
vec: &Vec<Value>,
|
||||
vec: Option<&Vec<Value>>,
|
||||
f: impl Fn(&Value) -> Option<T>,
|
||||
) -> anyhow::Result<Vec<Option<T>>> {
|
||||
vec.into_iter()
|
||||
.map(|v| {
|
||||
// allow nulls in arrays
|
||||
if matches!(v, Value::Null) {
|
||||
Some(None)
|
||||
} else {
|
||||
f(v).map(Some)
|
||||
}
|
||||
})
|
||||
.collect::<Option<Vec<Option<T>>>>()
|
||||
.ok_or_else(|| anyhow::anyhow!("Mixed types in array"))
|
||||
) -> anyhow::Result<Option<Vec<Option<T>>>> {
|
||||
if let Some(vec) = vec {
|
||||
Ok(Some(
|
||||
vec.into_iter()
|
||||
.map(|v| {
|
||||
// first option is if the value is of the right type (if none, will stop the collection and throw error)
|
||||
// second option is if the value is null
|
||||
// allow nulls in arrays
|
||||
if matches!(v, Value::Null) {
|
||||
Some(None)
|
||||
} else {
|
||||
f(v).map(Some)
|
||||
}
|
||||
})
|
||||
.collect::<Option<Vec<Option<T>>>>()
|
||||
.ok_or_else(|| anyhow::anyhow!("Mixed types in array"))?,
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_vec_val(
|
||||
vec: &Vec<Value>,
|
||||
vec: Option<&Vec<Value>>,
|
||||
arg_t: &String,
|
||||
) -> windmill_common::error::Result<Box<dyn ToSql + Sync + Send>> {
|
||||
match arg_t.as_str() {
|
||||
@@ -489,7 +497,9 @@ fn convert_vec_val(
|
||||
.unwrap_or_default()
|
||||
})
|
||||
})?)),
|
||||
"jsonb" | "json" => Ok(Box::new(vec.clone().into_iter().map(Some).collect_vec())),
|
||||
"jsonb" | "json" => Ok(Box::new(
|
||||
vec.map(|v| v.clone().into_iter().map(Some).collect_vec()),
|
||||
)),
|
||||
"bytea" => Ok(Box::new(map_as_single_type(vec, |v| {
|
||||
v.as_str().map(|x| {
|
||||
engine::general_purpose::STANDARD
|
||||
@@ -512,9 +522,32 @@ fn convert_val(
|
||||
match value {
|
||||
Value::Array(vec) if arg_t.ends_with("[]") => {
|
||||
let arg_t = arg_t.trim_end_matches("[]").to_string();
|
||||
convert_vec_val(vec, &arg_t)
|
||||
convert_vec_val(Some(vec), &arg_t)
|
||||
}
|
||||
Value::Null => Ok(Box::new(None::<bool>)),
|
||||
Value::Null if arg_t.ends_with("[]") => {
|
||||
let arg_t = arg_t.trim_end_matches("[]").to_string();
|
||||
convert_vec_val(None, &arg_t)
|
||||
}
|
||||
Value::Null => match arg_t.as_str() {
|
||||
"bool" | "boolean" => Ok(Box::new(None::<bool>)),
|
||||
"char" | "character" => Ok(Box::new(None::<i8>)),
|
||||
"smallint" | "smallserial" | "int2" | "serial2" => Ok(Box::new(None::<i16>)),
|
||||
"int" | "integer" | "int4" | "serial" => Ok(Box::new(None::<i32>)),
|
||||
"numeric" | "decimal" => Ok(Box::new(None::<Decimal>)),
|
||||
"oid" => Ok(Box::new(None::<u32>)),
|
||||
"bigint" | "bigserial" | "int8" | "serial8" => Ok(Box::new(None::<i64>)),
|
||||
"real" | "float4" => Ok(Box::new(None::<f32>)),
|
||||
"double" | "float8" => Ok(Box::new(None::<f64>)),
|
||||
"uuid" => Ok(Box::new(None::<Uuid>)),
|
||||
"date" => Ok(Box::new(None::<chrono::NaiveDate>)),
|
||||
"time" | "timetz" => Ok(Box::new(None::<chrono::NaiveTime>)),
|
||||
"timestamp" | "timestamptz" => Ok(Box::new(None::<chrono::NaiveDateTime>)),
|
||||
"jsonb" | "json" => Ok(Box::new(None::<Option<Value>>)),
|
||||
"bytea" => Ok(Box::new(None::<Vec<u8>>)),
|
||||
"text" | "varchar" => Ok(Box::new(None::<String>)),
|
||||
_ => Err(anyhow::anyhow!("Unsupported JSON null type"))?,
|
||||
},
|
||||
Value::Bool(s) if arg_t == "text" || arg_t == "varchar" => Ok(Box::new(s.to_string())),
|
||||
Value::Bool(b) => Ok(Box::new(b.clone())),
|
||||
Value::Number(n) if matches!(typ, Typ::Str(_)) => Ok(Box::new(n.to_string())),
|
||||
Value::Number(n) if arg_t == "char" && n.is_i64() => {
|
||||
@@ -583,6 +616,11 @@ fn convert_val(
|
||||
.unwrap_or(vec![]);
|
||||
Ok(Box::new(bytes))
|
||||
}
|
||||
Value::Object(_) if arg_t == "text" || arg_t == "varchar" => {
|
||||
Ok(Box::new(serde_json::to_string(value).map_err(|err| {
|
||||
Error::ExecutionErr(format!("Failed to convert JSON to text: {}", err))
|
||||
})?))
|
||||
}
|
||||
Value::Object(_) => Ok(Box::new(value.clone())),
|
||||
Value::String(s) => Ok(Box::new(s.clone())),
|
||||
_ => Err(Error::ExecutionErr(format!(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{collections::HashMap, process::Stdio};
|
||||
use std::{collections::HashMap, fs, process::Stdio};
|
||||
|
||||
use itertools::Itertools;
|
||||
use regex::Regex;
|
||||
@@ -320,7 +320,7 @@ pub async fn uv_pip_compile(
|
||||
.args(args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
let child_process = start_child_process(child_cmd, "/usr/local/bin/uv").await?;
|
||||
let child_process = start_child_process(child_cmd, uv_cmd).await?;
|
||||
append_logs(&job_id, &w_id, logs, db).await;
|
||||
handle_child(
|
||||
job_id,
|
||||
@@ -1153,7 +1153,11 @@ pub async fn handle_python_reqs(
|
||||
|
||||
for (req, venv_p) in req_with_penv {
|
||||
let mut logs1 = String::new();
|
||||
logs1.push_str("\n\n--- UV PIP INSTALL ---\n");
|
||||
if no_uv_install {
|
||||
logs1.push_str("\n\n--- PIP INSTALL ---\n");
|
||||
} else {
|
||||
logs1.push_str("\n\n--- UV PIP INSTALL ---\n");
|
||||
}
|
||||
logs1.push_str(&format!("\n{req} is being installed for the first time.\n It will be cached for all ulterior uses."));
|
||||
append_logs(&job_id, w_id, logs1, db).await;
|
||||
|
||||
@@ -1326,6 +1330,16 @@ pub async fn handle_python_reqs(
|
||||
"finished setting up python dependencies {}",
|
||||
job_id
|
||||
);
|
||||
if child.is_err() {
|
||||
|
||||
if let Err(e) = fs::remove_dir_all(&venv_p) {
|
||||
tracing::warn!(
|
||||
workspace_id = %w_id,
|
||||
"failed to remove cache dir: {:?}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
child?;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.423.2";
|
||||
export const VERSION = "v1.425.0";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -33,10 +33,10 @@
|
||||
},
|
||||
{
|
||||
"kind": "bigrawscript",
|
||||
"jobs": 10000
|
||||
"jobs": 4000
|
||||
},
|
||||
{
|
||||
"kind": "bigscriptinflow",
|
||||
"jobs": 10000
|
||||
"jobs": 4000
|
||||
}
|
||||
]
|
||||
+1
-1
@@ -60,7 +60,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.423.2";
|
||||
export const VERSION = "1.425.0";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
Generated
+46
-2
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.423.2",
|
||||
"version": "1.425.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.423.2",
|
||||
"version": "1.425.0",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@aws-crypto/sha256-js": "^4.0.0",
|
||||
"@codingame/monaco-vscode-configuration-service-override": "~8.0.2",
|
||||
"@codingame/monaco-vscode-files-service-override": "~8.0.2",
|
||||
@@ -21,6 +22,7 @@
|
||||
"@codingame/monaco-vscode-standalone-typescript-language-features": "~8.0.2",
|
||||
"@json2csv/plainjs": "^7.0.6",
|
||||
"@leeoniya/ufuzzy": "^1.0.8",
|
||||
"@mistralai/mistralai": "^1.3.0",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@redocly/json-to-json-schema": "^0.0.1",
|
||||
"@tanstack/svelte-table": "^8.9.9",
|
||||
@@ -175,6 +177,30 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.32.1.tgz",
|
||||
"integrity": "sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"agentkeepalive": "^4.2.1",
|
||||
"form-data-encoder": "1.7.2",
|
||||
"formdata-node": "^4.3.2",
|
||||
"node-fetch": "^2.6.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
|
||||
"version": "18.19.64",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.64.tgz",
|
||||
"integrity": "sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.6.1.tgz",
|
||||
@@ -3659,6 +3685,14 @@
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz",
|
||||
"integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA=="
|
||||
},
|
||||
"node_modules/@mistralai/mistralai": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.3.0.tgz",
|
||||
"integrity": "sha512-G5DPCSC8sEhG3LUEZDYLD7qEZWDuZXgaX3IcoC3a/ydm9jFuh2pRZtknsgMx2sU8d7kxRuxblY3fPH5C38wnhQ==",
|
||||
"peerDependencies": {
|
||||
"zod": ">= 3"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -13738,6 +13772,16 @@
|
||||
"integrity": "sha512-A7AMeJfGefk317I/3tBoUYRcDcNavKEkpiPN/nQsBz/viI2GvT7BtrqdPD6rGqBFN8Ax7v4obf+Cl32JF9DDVw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zstddec": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.1.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.423.2",
|
||||
"version": "1.425.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
@@ -82,6 +82,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@aws-crypto/sha256-js": "^4.0.0",
|
||||
"@codingame/monaco-vscode-configuration-service-override": "~8.0.2",
|
||||
"@codingame/monaco-vscode-files-service-override": "~8.0.2",
|
||||
@@ -94,6 +95,7 @@
|
||||
"@codingame/monaco-vscode-standalone-typescript-language-features": "~8.0.2",
|
||||
"@json2csv/plainjs": "^7.0.6",
|
||||
"@leeoniya/ufuzzy": "^1.0.8",
|
||||
"@mistralai/mistralai": "^1.3.0",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@redocly/json-to-json-schema": "^0.0.1",
|
||||
"@tanstack/svelte-table": "^8.9.9",
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
import { CornerDownLeft, Play } from 'lucide-svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { setLicense } from '$lib/enterpriseUtils'
|
||||
import { workspacedOpenai } from './copilot/lib'
|
||||
import type { FlowCopilotContext, FlowCopilotModule } from './copilot/flow'
|
||||
import { pickScript } from './flows/flowStateUtils'
|
||||
import {
|
||||
@@ -49,6 +48,7 @@
|
||||
} from '$lib/relative_imports'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import type { ScheduleTrigger, TriggerContext } from './triggers'
|
||||
import { initAllAiWorkspace } from './copilot/lib'
|
||||
import type { FlowPropPickerConfig, PropPickerContext } from './prop_picker'
|
||||
import type { PickableProperties } from './flows/previousResults'
|
||||
$: token = $page.url.searchParams.get('wm_token') ?? undefined
|
||||
@@ -110,12 +110,13 @@
|
||||
|
||||
async function setCopilotInfo() {
|
||||
if (workspace) {
|
||||
workspacedOpenai.init(workspace, token)
|
||||
initAllAiWorkspace(workspace)
|
||||
try {
|
||||
copilotInfo.set(await WorkspaceService.getCopilotInfo({ workspace }))
|
||||
} catch (err) {
|
||||
copilotInfo.set({
|
||||
exists_openai_resource_path: false,
|
||||
ai_provider: '',
|
||||
exists_ai_resource: false,
|
||||
code_completion_enabled: false
|
||||
})
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
cleanValueProperties,
|
||||
orderedJsonStringify,
|
||||
orderedYamlStringify,
|
||||
replaceFalseWithUndefined,
|
||||
type Value
|
||||
} from '$lib/utils'
|
||||
import type { Script } from '$lib/gen'
|
||||
@@ -54,8 +55,9 @@
|
||||
diffViewer.closeDrawer()
|
||||
}
|
||||
|
||||
|
||||
function prepareDiff(data: Value) {
|
||||
const metadata = structuredClone(cleanValueProperties(data))
|
||||
const metadata = structuredClone(cleanValueProperties(replaceFalseWithUndefined(data)))
|
||||
const content = metadata['content']
|
||||
if (metadata['content'] !== undefined) {
|
||||
metadata['content'] = 'check content diff'
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
import { initVim } from './monaco_keybindings'
|
||||
import { buildWorkerDefinition } from '$lib/monaco_workers/build_workers'
|
||||
import { parseTypescriptDeps } from '$lib/relative_imports'
|
||||
import type { AiProviderTypes } from './copilot/lib'
|
||||
|
||||
// import EditorTheme from './EditorTheme.svelte'
|
||||
|
||||
@@ -593,11 +594,13 @@
|
||||
token.onCancellationRequested(() => {
|
||||
abortController?.abort()
|
||||
})
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
const insertText = await editorCodeCompletion(
|
||||
textUntilPosition,
|
||||
textAfterPosition,
|
||||
lang,
|
||||
abortController
|
||||
abortController,
|
||||
aiProvider as AiProviderTypes
|
||||
)
|
||||
if (insertText) {
|
||||
items = [
|
||||
@@ -624,7 +627,7 @@
|
||||
)
|
||||
}
|
||||
|
||||
$: $copilotInfo.exists_openai_resource_path &&
|
||||
$: $copilotInfo.exists_ai_resource &&
|
||||
$copilotInfo.code_completion_enabled &&
|
||||
$codeCompletionSessionEnabled &&
|
||||
initialized &&
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
encodeState,
|
||||
formatCron,
|
||||
orderedJsonStringify,
|
||||
replaceFalseWithUndefined,
|
||||
sleep,
|
||||
type Value
|
||||
} from '$lib/utils'
|
||||
@@ -85,6 +86,7 @@
|
||||
import type { FlowBuilderWhitelabelCustomUi } from './custom_ui'
|
||||
import FlowYamlEditor from './flows/header/FlowYamlEditor.svelte'
|
||||
import { type TriggerContext, type ScheduleTrigger } from './triggers'
|
||||
import type { AiProviderTypes } from './copilot/lib'
|
||||
|
||||
export let initialPath: string = ''
|
||||
export let pathStoreInit: string | undefined = undefined
|
||||
@@ -293,12 +295,21 @@
|
||||
// We need it for diff
|
||||
await syncWithDeployed()
|
||||
|
||||
// Handle through confirmation modal
|
||||
confirmCallback = async () => {
|
||||
if (
|
||||
deployedValue &&
|
||||
$flowStore &&
|
||||
orderedJsonStringify(deployedValue) ===
|
||||
orderedJsonStringify(replaceFalseWithUndefined({ ...$flowStore, path: $pathStore }))
|
||||
) {
|
||||
await saveFlow(deploymentMsg)
|
||||
} else {
|
||||
// Handle through confirmation modal
|
||||
confirmCallback = async () => {
|
||||
await saveFlow(deploymentMsg)
|
||||
}
|
||||
// Open confirmation modal
|
||||
open = true
|
||||
}
|
||||
// Open confirmation modal
|
||||
open = true
|
||||
}
|
||||
}
|
||||
async function syncWithDeployed() {
|
||||
@@ -307,18 +318,12 @@
|
||||
path: initialPath,
|
||||
withStarredInfo: true
|
||||
})
|
||||
deployedValue = {
|
||||
deployedValue = replaceFalseWithUndefined({
|
||||
...flow,
|
||||
starred: undefined,
|
||||
id: undefined,
|
||||
edited_at: undefined,
|
||||
edited_by: undefined,
|
||||
workspace_id: undefined,
|
||||
archived: undefined,
|
||||
same_worker: undefined,
|
||||
visible_to_runner_only: undefined,
|
||||
ws_error_handler_muted: undefined
|
||||
}
|
||||
workspace_id: undefined
|
||||
})
|
||||
deployedBy = flow.edited_by
|
||||
}
|
||||
|
||||
@@ -789,6 +794,7 @@
|
||||
try {
|
||||
push(history, $flowStore)
|
||||
let module = stepOnly ? $copilotModulesStore[0] : $copilotModulesStore[idx]
|
||||
const aiProvider = $copilotInfo.ai_provider as AiProviderTypes
|
||||
|
||||
copilotLoading = true
|
||||
copilotStatus = "Generating code for step '" + module.id + "'..."
|
||||
@@ -918,7 +924,8 @@
|
||||
})
|
||||
: undefined,
|
||||
isFirstInLoop,
|
||||
abortController
|
||||
abortController,
|
||||
aiProvider
|
||||
)
|
||||
unsubscribe()
|
||||
}
|
||||
@@ -934,7 +941,7 @@
|
||||
pastModule.value.type === 'script')
|
||||
) {
|
||||
const stepSchema: Schema = JSON.parse(JSON.stringify($flowStateStore[module.id].schema)) // deep copy
|
||||
if (isHubStep && pastModule !== undefined && $copilotInfo.exists_openai_resource_path) {
|
||||
if (isHubStep && pastModule !== undefined && $copilotInfo.exists_ai_resource) {
|
||||
// ask AI to set step inputs
|
||||
abortController = new AbortController()
|
||||
const { inputs, allExprs } = await glueCopilot(
|
||||
@@ -944,7 +951,8 @@
|
||||
value: RawScript | PathScript
|
||||
},
|
||||
isFirstInLoop,
|
||||
abortController
|
||||
abortController,
|
||||
aiProvider
|
||||
)
|
||||
|
||||
// create flow inputs used by AI for autocompletion
|
||||
@@ -992,11 +1000,7 @@
|
||||
$shouldUpdatePropertyType[key] = 'javascript'
|
||||
})
|
||||
} else {
|
||||
if (
|
||||
isHubStep &&
|
||||
pastModule !== undefined &&
|
||||
!$copilotInfo.exists_openai_resource_path
|
||||
) {
|
||||
if (isHubStep && pastModule !== undefined && !$copilotInfo.exists_ai_resource) {
|
||||
sendUserToast(
|
||||
'For better input generation, enable Windmill AI in the workspace settings',
|
||||
true
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import Alert from './common/alert/Alert.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { capitalize, classNames, sleep } from '$lib/utils'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import { enterpriseLicense, isCriticalAlertsUIOpen } from '$lib/stores'
|
||||
import CustomOauth from './CustomOauth.svelte'
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -43,6 +43,7 @@
|
||||
export let tab: string = 'Core'
|
||||
export let hideTabs: boolean = false
|
||||
export let hideSave: boolean = false
|
||||
export let closeDrawer: (() => void) | undefined = () => {}
|
||||
|
||||
let values: Record<string, any> = {}
|
||||
let initialOauths: Record<string, any> = {}
|
||||
@@ -814,6 +815,26 @@
|
||||
placeholder={setting.placeholder}
|
||||
bind:value={values[setting.key]}
|
||||
/>
|
||||
{:else if setting.key == 'critical_alert_mute_ui'}
|
||||
<div class="flex flex-col gap-y-2 my-2 py-2">
|
||||
<Toggle
|
||||
disabled={!$enterpriseLicense}
|
||||
bind:checked={values[setting.key]}
|
||||
options={{ right: setting.description }}
|
||||
/>
|
||||
<div class="flex flex-row">
|
||||
<Button
|
||||
disabled={!$enterpriseLicense}
|
||||
size="sm"
|
||||
on:click={() => {
|
||||
isCriticalAlertsUIOpen.set(true)
|
||||
closeDrawer?.()
|
||||
}}
|
||||
>
|
||||
Show Critical Alerts
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if setting.fieldType == 'critical_error_channels'}
|
||||
<div class="w-full flex gap-x-16 flex-wrap">
|
||||
<div class="w-full max-w-lg">
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
encodeState,
|
||||
formatCron,
|
||||
orderedJsonStringify,
|
||||
replaceFalseWithUndefined,
|
||||
type Value
|
||||
} from '$lib/utils'
|
||||
import Path from './Path.svelte'
|
||||
@@ -278,17 +279,28 @@
|
||||
// Fetch entire script, since we need it to show Diff
|
||||
await syncWithDeployed()
|
||||
|
||||
// Handle through confirmation modal
|
||||
confirmCallback = async () => {
|
||||
open = false
|
||||
if (actual_parent_hash) {
|
||||
await editScript(stay, actual_parent_hash, deployMsg)
|
||||
} else {
|
||||
sendUserToast('Could not fetch latest version of the script', true)
|
||||
if (
|
||||
deployedValue &&
|
||||
script &&
|
||||
orderedJsonStringify({ ...deployedValue, hash: undefined }) ===
|
||||
orderedJsonStringify(
|
||||
replaceFalseWithUndefined({ ...script, hash: undefined, parent_hash: undefined })
|
||||
)
|
||||
) {
|
||||
await editScript(stay, actual_parent_hash, deployMsg)
|
||||
} else {
|
||||
// Handle through confirmation modal
|
||||
confirmCallback = async () => {
|
||||
open = false
|
||||
if (actual_parent_hash) {
|
||||
await editScript(stay, actual_parent_hash, deployMsg)
|
||||
} else {
|
||||
sendUserToast('Could not fetch latest version of the script', true)
|
||||
}
|
||||
}
|
||||
// Open confirmation modal
|
||||
open = true
|
||||
}
|
||||
// Open confirmation modal
|
||||
open = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,20 +311,16 @@
|
||||
withStarredInfo: true
|
||||
})
|
||||
|
||||
deployedValue = {
|
||||
deployedValue = replaceFalseWithUndefined({
|
||||
...latestScript,
|
||||
starred: undefined,
|
||||
workspace_id: undefined,
|
||||
archived: undefined,
|
||||
created_at: undefined,
|
||||
created_by: undefined,
|
||||
deleted: undefined,
|
||||
extra_perms: undefined,
|
||||
is_template: undefined,
|
||||
lock: undefined,
|
||||
lock_error_logs: undefined,
|
||||
parent_hashes: undefined
|
||||
}
|
||||
})
|
||||
|
||||
deployedBy = latestScript.created_by
|
||||
}
|
||||
@@ -691,8 +699,11 @@
|
||||
</Label>
|
||||
<Label label="Path">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/core_concepts/roles_and_permissions#path">
|
||||
The unique identifier of the script in the workspace that defines permissions
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/roles_and_permissions#path"
|
||||
>
|
||||
The unique identifier of the script in the workspace that defines
|
||||
permissions
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<Path
|
||||
|
||||
@@ -325,7 +325,7 @@
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="" values={settingsKeys}>
|
||||
<InstanceSettings bind:this={instanceSettings} hideTabs hideSave {tab} />
|
||||
<InstanceSettings bind:this={instanceSettings} hideTabs hideSave {tab} {closeDrawer}/>
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
copyToClipboard,
|
||||
truncateRev,
|
||||
orderedJsonStringify,
|
||||
type Value
|
||||
type Value,
|
||||
replaceFalseWithUndefined
|
||||
} from '../../../utils'
|
||||
import type {
|
||||
AppInput,
|
||||
@@ -422,13 +423,27 @@
|
||||
// We need it to show diff
|
||||
// Handle through confirmation modal
|
||||
await syncWithDeployed()
|
||||
|
||||
confirmCallback = async () => {
|
||||
open = false
|
||||
if (
|
||||
deployedValue &&
|
||||
savedApp &&
|
||||
$app &&
|
||||
orderedJsonStringify(deployedValue) ===
|
||||
orderedJsonStringify(replaceFalseWithUndefined({
|
||||
summary: $summary,
|
||||
value: $app,
|
||||
path: newPath || savedApp.draft?.path || savedApp.path,
|
||||
policy
|
||||
}))
|
||||
) {
|
||||
await updateApp(npath)
|
||||
} else {
|
||||
confirmCallback = async () => {
|
||||
open = false
|
||||
await updateApp(npath)
|
||||
}
|
||||
// Open confirmation modal
|
||||
open = true
|
||||
}
|
||||
// Open confirmation modal
|
||||
open = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,15 +457,14 @@
|
||||
deployedBy = deployedApp.created_by
|
||||
|
||||
// Strip off extra information
|
||||
deployedValue = {
|
||||
deployedValue = replaceFalseWithUndefined({
|
||||
...deployedApp,
|
||||
starred: undefined,
|
||||
id: undefined,
|
||||
created_at: undefined,
|
||||
created_by: undefined,
|
||||
versions: undefined,
|
||||
extra_perms: undefined //
|
||||
}
|
||||
extra_perms: undefined
|
||||
})
|
||||
}
|
||||
|
||||
async function updateApp(npath: string) {
|
||||
|
||||
@@ -221,7 +221,7 @@
|
||||
JOBS_DISAPPROVAL: 'jobs.disapproval',
|
||||
JOBS_DELETE: 'jobs.delete',
|
||||
ACCOUNT_DELETE: 'account.delete',
|
||||
OPENAI_REQUEST: 'openai.request',
|
||||
AI_REQUEST: 'ai.request',
|
||||
RESOURCES_CREATE: 'resources.create',
|
||||
RESOURCES_UPDATE: 'resources.update',
|
||||
RESOURCES_DELETE: 'resources.delete',
|
||||
|
||||
+10
-2
@@ -4,7 +4,12 @@
|
||||
import { goto as gotoUrl } from '$app/navigation'
|
||||
import Button from '../button/Button.svelte'
|
||||
import type DiffDrawer from '$lib/components/DiffDrawer.svelte'
|
||||
import { cleanValueProperties, orderedJsonStringify, type Value } from '$lib/utils'
|
||||
import {
|
||||
cleanValueProperties,
|
||||
orderedJsonStringify,
|
||||
replaceFalseWithUndefined,
|
||||
type Value
|
||||
} from '$lib/utils'
|
||||
import { tick } from 'svelte'
|
||||
import { page } from '$app/stores'
|
||||
|
||||
@@ -36,7 +41,10 @@
|
||||
path: undefined
|
||||
})
|
||||
const current = cleanValueProperties({ ...(modifiedValue ?? {}), path: undefined })
|
||||
if (orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(current)) {
|
||||
if (
|
||||
orderedJsonStringify(replaceFalseWithUndefined(draftOrDeployed)) ===
|
||||
orderedJsonStringify(replaceFalseWithUndefined(current))
|
||||
) {
|
||||
bypassBeforeNavigate = true
|
||||
gotoUrl(goingTo)
|
||||
} else {
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<div
|
||||
on:click={() => (open = false)}
|
||||
transition:fadeFast|local
|
||||
class={'absolute top-0 bottom-0 left-0 right-0 z-50'}
|
||||
class={'absolute top-0 bottom-0 left-0 right-0 z-[9999]'}
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $copilotInfo.exists_openai_resource_path && $copilotInfo.code_completion_enabled}
|
||||
{#if $copilotInfo.exists_ai_resource && $copilotInfo.code_completion_enabled}
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={$codeCompletionSessionEnabled}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { ExternalLink, Wand2 } from 'lucide-svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { getNonStreamingCompletion } from './lib'
|
||||
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
|
||||
import Popup from '../common/popup/Popup.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { copilotInfo } from '$lib/stores'
|
||||
|
||||
import { base } from '$lib/base'
|
||||
import type { ChatCompletionMessageParam } from 'openai/resources/index.mjs'
|
||||
|
||||
export let schedule: string
|
||||
|
||||
@@ -14,31 +16,35 @@
|
||||
let genLoading = false
|
||||
let abortController = new AbortController()
|
||||
$: instructionsField && setTimeout(() => instructionsField?.focus(), 100)
|
||||
|
||||
const SYSTEM =
|
||||
"You are a helpful assitant for creating CRON schedules. The structure is 'second minute hour dayOfMonth month dayOfWeek'. Weekdays are Sunday (1), Monday (2), Tuesday (3), Wednesday (4), Thursday (5), Friday (6), Saturday (7). You only return the CRON string without any wrapping characters. If it is invalid, you will return an error message preceeded by 'ERROR:'."
|
||||
const USER = 'CRON schedule instructions: {instructions}'
|
||||
async function generateCron() {
|
||||
genLoading = true
|
||||
abortController = new AbortController()
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
try {
|
||||
const messages: ChatCompletionMessageParam[] = [
|
||||
{
|
||||
role: 'system',
|
||||
content: SYSTEM
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: USER.replace('{instructions}', instructions)
|
||||
}
|
||||
]
|
||||
|
||||
const response = await getNonStreamingCompletion(
|
||||
[
|
||||
{
|
||||
role: 'system',
|
||||
content: SYSTEM
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: USER.replace('{instructions}', instructions)
|
||||
}
|
||||
],
|
||||
abortController
|
||||
messages,
|
||||
abortController,
|
||||
aiProvider as AiProviderTypes
|
||||
)
|
||||
|
||||
if (response.startsWith('ERROR:')) {
|
||||
throw response.replace('ERROR:', '').trim()
|
||||
}
|
||||
|
||||
schedule = response
|
||||
} catch (err) {
|
||||
if (!abortController.signal.aborted) {
|
||||
@@ -69,7 +75,7 @@
|
||||
on:click={genLoading ? () => abortController?.abort() : undefined}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
{#if $copilotInfo.exists_openai_resource_path}
|
||||
{#if $copilotInfo.exists_ai_resource}
|
||||
<div class="flex w-96">
|
||||
<input
|
||||
bind:this={instructionsField}
|
||||
@@ -104,7 +110,7 @@
|
||||
<div class="block text-primary">
|
||||
<p class="text-sm"
|
||||
>Enable Windmill AI in the <a
|
||||
href="{base}/workspace_settings?tab=openai"
|
||||
href="{base}/workspace_settings?tab=ai"
|
||||
target="_blank"
|
||||
class="inline-flex flex-row items-center gap-1"
|
||||
>workspace settings <ExternalLink size={16} /></a
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
$: copilotStatus && copilotPopover?.refresh()
|
||||
</script>
|
||||
|
||||
{#if $copilotInfo.exists_openai_resource_path}
|
||||
{#if $copilotInfo.exists_ai_resource}
|
||||
<ManualPopover bind:this={copilotPopover}>
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -137,7 +137,7 @@
|
||||
<div class="block text-primary">
|
||||
<p class="text-sm"
|
||||
>Enable Windmill AI in the <a
|
||||
href="{base}/workspace_settings?tab=openai"
|
||||
href="{base}/workspace_settings?tab=ai"
|
||||
target="_blank"
|
||||
class="inline-flex flex-row items-center gap-1"
|
||||
>workspace settings <ExternalLink size={16} /></a
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Check, Loader2, Wand2 } from 'lucide-svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { getNonStreamingCompletion } from './lib'
|
||||
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type { Flow, InputTransform } from '$lib/gen'
|
||||
import ManualPopover from '../ManualPopover.svelte'
|
||||
@@ -62,7 +62,7 @@ ${YAML.stringify(availableData)}</available>
|
||||
Reply with the most probable answer, do not explain or discuss.
|
||||
Use javascript object dot notation to access the properties.
|
||||
Only output the expression, do not explain or discuss.`
|
||||
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
generatedContent = await getNonStreamingCompletion(
|
||||
[
|
||||
{
|
||||
@@ -70,7 +70,8 @@ Only output the expression, do not explain or discuss.`
|
||||
content: user
|
||||
}
|
||||
],
|
||||
abortController
|
||||
abortController,
|
||||
aiProvider as AiProviderTypes
|
||||
)
|
||||
} catch (err) {
|
||||
if (!abortController.signal.aborted) {
|
||||
@@ -82,7 +83,7 @@ Only output the expression, do not explain or discuss.`
|
||||
}
|
||||
|
||||
export function onKeyUp(event: KeyboardEvent) {
|
||||
if (!$copilotInfo.exists_openai_resource_path || !$stepInputCompletionEnabled) {
|
||||
if (!$copilotInfo.exists_ai_resource || !$stepInputCompletionEnabled) {
|
||||
return
|
||||
}
|
||||
if (event.key === 'Tab') {
|
||||
@@ -117,7 +118,7 @@ Only output the expression, do not explain or discuss.`
|
||||
cancelOnOutOfFocus()
|
||||
}
|
||||
|
||||
$: if ($copilotInfo.exists_openai_resource_path && $stepInputCompletionEnabled && focused) {
|
||||
$: if ($copilotInfo.exists_ai_resource && $stepInputCompletionEnabled && focused) {
|
||||
automaticGeneration()
|
||||
}
|
||||
|
||||
@@ -131,7 +132,7 @@ Only output the expression, do not explain or discuss.`
|
||||
let out = true // hack to prevent regenerating answer when accepting the answer due to mouseenter on new icon
|
||||
</script>
|
||||
|
||||
{#if $copilotInfo.exists_openai_resource_path && $stepInputCompletionEnabled}
|
||||
{#if $copilotInfo.exists_ai_resource && $stepInputCompletionEnabled}
|
||||
<ManualPopover showTooltip={!empty && generatedContent.length > 0} placement="bottom" class="p-2">
|
||||
<Button
|
||||
size="xs"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getCompletion } from './lib'
|
||||
import { getCompletion, getResponseFromEvent, type AiProviderTypes } from './lib'
|
||||
import { isInitialCode } from '$lib/script_helpers'
|
||||
import { Check, Loader2, Wand2 } from 'lucide-svelte'
|
||||
import { copilotInfo, metadataCompletionEnabled } from '$lib/stores'
|
||||
@@ -118,11 +118,11 @@ Generate a description for the flow below:
|
||||
content: config.user.replace(`{${config.placeholderName}}`, placeholderContent)
|
||||
}
|
||||
]
|
||||
const response = await getCompletion(messages, abortController)
|
||||
const aiProvider = $copilotInfo.ai_provider as AiProviderTypes
|
||||
const response = await getCompletion(messages, abortController, aiProvider)
|
||||
generatedContent = ''
|
||||
for await (const chunk of response) {
|
||||
const toks = chunk.choices[0]?.delta?.content || ''
|
||||
generatedContent += toks
|
||||
generatedContent += getResponseFromEvent(chunk, aiProvider)
|
||||
}
|
||||
} catch (err) {
|
||||
if (!abortController.signal.aborted) {
|
||||
@@ -138,7 +138,7 @@ Generate a description for the flow below:
|
||||
}
|
||||
|
||||
if (
|
||||
$copilotInfo.exists_openai_resource_path &&
|
||||
$copilotInfo.exists_ai_resource &&
|
||||
$metadataCompletionEnabled &&
|
||||
generateOnAppear &&
|
||||
!content &&
|
||||
@@ -158,7 +158,7 @@ Generate a description for the flow below:
|
||||
}
|
||||
|
||||
$: active =
|
||||
$copilotInfo.exists_openai_resource_path &&
|
||||
$copilotInfo.exists_ai_resource &&
|
||||
$metadataCompletionEnabled &&
|
||||
!content &&
|
||||
(loading || focused || !!generatedContent) &&
|
||||
@@ -192,7 +192,7 @@ Generate a description for the flow below:
|
||||
class={twMerge('relative', $$props.class)}
|
||||
bind:clientWidth={width}
|
||||
on:keydown={(event) => {
|
||||
if (!$copilotInfo.exists_openai_resource_path || !$metadataCompletionEnabled) {
|
||||
if (!$copilotInfo.exists_ai_resource || !$metadataCompletionEnabled) {
|
||||
return
|
||||
}
|
||||
if (event.key === 'Tab') {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Wand2 } from 'lucide-svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { getNonStreamingCompletion } from './lib'
|
||||
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../flows/types'
|
||||
@@ -59,7 +59,7 @@ Here's a summary of the available data:
|
||||
${YAML.stringify(availableData)}</available>
|
||||
If the branching is made inside a for-loop, the iterator value is accessible as flow_input.iter.value
|
||||
Only return the expression without any wrapper. Do not explain or discuss.`
|
||||
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
const result = await getNonStreamingCompletion(
|
||||
[
|
||||
{
|
||||
@@ -67,7 +67,8 @@ Only return the expression without any wrapper. Do not explain or discuss.`
|
||||
content: user
|
||||
}
|
||||
],
|
||||
abortController
|
||||
abortController,
|
||||
aiProvider as AiProviderTypes
|
||||
)
|
||||
|
||||
dispatch('setExpr', result)
|
||||
@@ -82,7 +83,7 @@ Only return the expression without any wrapper. Do not explain or discuss.`
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $copilotInfo.exists_openai_resource_path && $stepInputCompletionEnabled}
|
||||
{#if $copilotInfo.exists_ai_resource && $stepInputCompletionEnabled}
|
||||
<Popup
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { base } from '$lib/base'
|
||||
import { Button } from '../common'
|
||||
|
||||
import { getNonStreamingCompletion } from './lib'
|
||||
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Popup from '../common/popup/Popup.svelte'
|
||||
import { copilotInfo } from '$lib/stores'
|
||||
@@ -25,6 +25,7 @@
|
||||
savePrompt()
|
||||
genLoading = true
|
||||
abortController = new AbortController()
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
try {
|
||||
const res = await getNonStreamingCompletion(
|
||||
[
|
||||
@@ -38,7 +39,8 @@
|
||||
content: funcDesc
|
||||
}
|
||||
],
|
||||
abortController
|
||||
abortController,
|
||||
aiProvider as AiProviderTypes
|
||||
)
|
||||
dispatch('gen', { res: res, prompt: funcDesc })
|
||||
funcDesc = ''
|
||||
@@ -114,7 +116,7 @@
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<div class="block text-primary z-[2000]">
|
||||
{#if $copilotInfo.exists_openai_resource_path}
|
||||
{#if $copilotInfo.exists_ai_resource}
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex w-96">
|
||||
<input
|
||||
@@ -167,7 +169,7 @@
|
||||
{:else}
|
||||
<p class="text-sm">
|
||||
Enable Windmill AI in the <a
|
||||
href="{base}/workspace_settings?tab=openai"
|
||||
href="{base}/workspace_settings?tab=ai"
|
||||
target="_blank"
|
||||
class="inline-flex flex-row items-center gap-1"
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { base } from '$lib/base'
|
||||
import { Button } from '../common'
|
||||
|
||||
import { SUPPORTED_LANGUAGES, copilot } from './lib'
|
||||
import { SUPPORTED_LANGUAGES, copilot, type AiProviderTypes } from './lib'
|
||||
import type { SupportedLanguage } from '$lib/common'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type Editor from '../Editor.svelte'
|
||||
@@ -40,6 +40,7 @@
|
||||
try {
|
||||
genLoading = true
|
||||
abortController = new AbortController()
|
||||
const aiProvider = $copilotInfo.ai_provider as AiProviderTypes
|
||||
await copilot(
|
||||
{
|
||||
language: lang,
|
||||
@@ -51,7 +52,8 @@
|
||||
},
|
||||
generatedCode,
|
||||
abortController,
|
||||
generatedExplanation
|
||||
aiProvider,
|
||||
generatedExplanation,
|
||||
)
|
||||
setupDiff()
|
||||
diffEditor?.setModified($generatedCode)
|
||||
@@ -186,12 +188,12 @@
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
{@const fixAction = (_) => {
|
||||
if ($copilotInfo.exists_openai_resource_path) {
|
||||
if ($copilotInfo.exists_ai_resource) {
|
||||
onFix(() => close(null))
|
||||
}
|
||||
}}
|
||||
<div use:fixAction>
|
||||
{#if $copilotInfo.exists_openai_resource_path}
|
||||
{#if $copilotInfo.exists_ai_resource}
|
||||
<div class="w-[42rem] min-h-[3rem] max-h-[34rem] overflow-y-scroll">
|
||||
{#if $generatedCode.length > 0}
|
||||
<div class="overflow-x-scroll">
|
||||
@@ -211,7 +213,7 @@
|
||||
<p class="text-sm"
|
||||
>Enable Windmill AI in the <a
|
||||
class="inline-flex flex-row items-center gap-1"
|
||||
href="{base}/workspace_settings?tab=openai"
|
||||
href="{base}/workspace_settings?tab=ai"
|
||||
target="_blank">workspace settings</a
|
||||
></p
|
||||
></div
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
import { base } from '$lib/base'
|
||||
import { Button } from '../common'
|
||||
|
||||
import { MAX_SCHEMA_LENGTH, SUPPORTED_LANGUAGES, addThousandsSeparator, copilot } from './lib'
|
||||
import {
|
||||
MAX_SCHEMA_LENGTH,
|
||||
SUPPORTED_LANGUAGES,
|
||||
addThousandsSeparator,
|
||||
copilot,
|
||||
type AiProviderTypes
|
||||
} from './lib'
|
||||
import type { SupportedLanguage } from '$lib/common'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type Editor from '../Editor.svelte'
|
||||
@@ -68,6 +74,7 @@
|
||||
return
|
||||
}
|
||||
savePrompt()
|
||||
const aiProvider = $copilotInfo.ai_provider as AiProviderTypes
|
||||
try {
|
||||
genLoading = true
|
||||
blockPopupOpen = true
|
||||
@@ -84,7 +91,8 @@
|
||||
workspace: $workspaceStore!
|
||||
},
|
||||
generatedCode,
|
||||
abortController
|
||||
abortController,
|
||||
aiProvider
|
||||
)
|
||||
} else {
|
||||
await copilot(
|
||||
@@ -97,7 +105,8 @@
|
||||
workspace: $workspaceStore!
|
||||
},
|
||||
generatedCode,
|
||||
abortController
|
||||
abortController,
|
||||
aiProvider
|
||||
)
|
||||
}
|
||||
setupDiff()
|
||||
@@ -388,7 +397,7 @@
|
||||
<LoadingIcon />
|
||||
{/if}
|
||||
</div>
|
||||
{:else if $copilotInfo.exists_openai_resource_path}
|
||||
{:else if $copilotInfo.exists_ai_resource}
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-row justify-between items-center">
|
||||
<ToggleButtonGroup class="w-auto shrink-0" bind:selected={mode}>
|
||||
@@ -397,7 +406,14 @@
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<div class="text-[0.6rem] text-secondary opacity-60 flex flex-row items-center gap-0.5">
|
||||
GPT-4o<Bot size={14} />
|
||||
{#if $copilotInfo.ai_provider === 'openai'}
|
||||
GPT-4o
|
||||
{:else if $copilotInfo.ai_provider === 'anthropic'}
|
||||
Claude-3.5
|
||||
{:else}
|
||||
Codestral
|
||||
{/if}
|
||||
<Bot size={14} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-96 items-start">
|
||||
@@ -493,7 +509,7 @@
|
||||
{:else}
|
||||
<p class="text-sm">
|
||||
Enable Windmill AI in the <a
|
||||
href="{base}/workspace_settings?tab=openai"
|
||||
href="{base}/workspace_settings?tab=ai"
|
||||
target="_blank"
|
||||
class="inline-flex flex-row items-center gap-1"
|
||||
>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
}
|
||||
|
||||
async function onGenerate() {
|
||||
if (!selectedCompletion && !$copilotInfo.exists_openai_resource_path) {
|
||||
if (!selectedCompletion && !$copilotInfo.exists_ai_resource) {
|
||||
sendUserToast(
|
||||
'Windmill AI is not enabled, you can activate it in the workspace settings',
|
||||
true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Check, Loader2, Wand2 } from 'lucide-svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { getNonStreamingCompletion } from './lib'
|
||||
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type { Flow, InputTransform } from '$lib/gen'
|
||||
import ManualPopover from '../ManualPopover.svelte'
|
||||
@@ -107,7 +107,7 @@ If none of the available results are appropriate, are already used or are more a
|
||||
Reply with the most probable answer, do not explain or discuss.
|
||||
Use javascript object dot notation to access the properties.
|
||||
Only return the expression without any wrapper.`
|
||||
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
generatedContent = await getNonStreamingCompletion(
|
||||
[
|
||||
{
|
||||
@@ -115,7 +115,8 @@ Only return the expression without any wrapper.`
|
||||
content: user
|
||||
}
|
||||
],
|
||||
abortController
|
||||
abortController,
|
||||
aiProvider as AiProviderTypes
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -138,7 +139,7 @@ Only return the expression without any wrapper.`
|
||||
}
|
||||
|
||||
export function onKeyUp(event: KeyboardEvent) {
|
||||
if (!$copilotInfo.exists_openai_resource_path || !$stepInputCompletionEnabled) {
|
||||
if (!$copilotInfo.exists_ai_resource || !$stepInputCompletionEnabled) {
|
||||
return
|
||||
}
|
||||
if (event.key === 'Tab') {
|
||||
@@ -181,7 +182,7 @@ Only return the expression without any wrapper.`
|
||||
cancelOnOutOfFocus()
|
||||
}
|
||||
|
||||
$: if ($copilotInfo.exists_openai_resource_path && $stepInputCompletionEnabled && focused) {
|
||||
$: if ($copilotInfo.exists_ai_resource && $stepInputCompletionEnabled && focused) {
|
||||
automaticGeneration()
|
||||
}
|
||||
|
||||
@@ -193,7 +194,7 @@ Only return the expression without any wrapper.`
|
||||
let openInputsModal = false
|
||||
</script>
|
||||
|
||||
{#if $copilotInfo.exists_openai_resource_path && $stepInputCompletionEnabled}
|
||||
{#if $copilotInfo.exists_ai_resource && $stepInputCompletionEnabled}
|
||||
<FlowCopilotInputsModal
|
||||
on:confirmed={async () => {
|
||||
createFlowInput()
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import type { FlowEditorContext } from '../flows/types'
|
||||
import type { PickableProperties } from '../flows/previousResults'
|
||||
import { getContext } from 'svelte'
|
||||
import { getNonStreamingCompletion } from './lib'
|
||||
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import type { FlowCopilotContext } from './flow'
|
||||
@@ -83,7 +83,7 @@ Your answer has to be in the following format (one line per input):
|
||||
input_name1: expression1
|
||||
input_name2: expression2
|
||||
...`
|
||||
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
generatedContent = await getNonStreamingCompletion(
|
||||
[
|
||||
{
|
||||
@@ -91,7 +91,8 @@ input_name2: expression2
|
||||
content: user
|
||||
}
|
||||
],
|
||||
abortController
|
||||
abortController,
|
||||
aiProvider as AiProviderTypes
|
||||
)
|
||||
|
||||
parsedInputs = generatedContent.split('\n').map((x) => x.split(': '))
|
||||
@@ -168,7 +169,7 @@ input_name2: expression2
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row justify-end">
|
||||
{#if $copilotInfo.exists_openai_resource_path && $stepInputCompletionEnabled}
|
||||
{#if $copilotInfo.exists_ai_resource && $stepInputCompletionEnabled}
|
||||
<FlowCopilotInputsModal
|
||||
on:confirmed={async () => {
|
||||
createFlowInputs()
|
||||
@@ -236,10 +237,10 @@ input_name2: expression2
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<p class="text-sm">
|
||||
{#if !$copilotInfo.exists_openai_resource_path}
|
||||
{#if !$copilotInfo.exists_ai_resource}
|
||||
Enable Windmill AI in the{' '}
|
||||
<a
|
||||
href="{base}/workspace_settings?tab=openai"
|
||||
href="{base}/workspace_settings?tab=ai"
|
||||
target="_blank"
|
||||
class="inline-flex flex-row items-center gap-1"
|
||||
>
|
||||
|
||||
+4
-2
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { testKey } from './lib'
|
||||
import { testKey, type AiProviderTypes } from './lib'
|
||||
export let disabled = false
|
||||
export let apiKey: string | undefined = undefined
|
||||
export let aiProvider: AiProviderTypes
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
@@ -28,7 +29,8 @@
|
||||
content: "this is a test, simply reply with 'ok'"
|
||||
}
|
||||
],
|
||||
abortController
|
||||
abortController,
|
||||
aiProvider
|
||||
})
|
||||
sendUserToast('Valid key')
|
||||
} catch (err) {
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ChatCompletionMessageParam } from 'openai/resources/chat/index.mjs'
|
||||
import { getNonStreamingCompletion } from './lib'
|
||||
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
|
||||
import { codeCompletionLoading } from '$lib/stores'
|
||||
|
||||
const systemPrompt = `You are a code completion assistant, return the code that should go instead of the <completion_tokens>.
|
||||
@@ -73,7 +73,8 @@ export async function editorCodeCompletion(
|
||||
before: string,
|
||||
after: string,
|
||||
lang: string,
|
||||
abortController: AbortController
|
||||
abortController: AbortController,
|
||||
aiProvider: AiProviderTypes
|
||||
) {
|
||||
codeCompletionLoading.set(true)
|
||||
const messages: ChatCompletionMessageParam[] = [
|
||||
@@ -91,7 +92,7 @@ export async function editorCodeCompletion(
|
||||
]
|
||||
|
||||
try {
|
||||
const result = await getNonStreamingCompletion(messages, abortController)
|
||||
const result = await getNonStreamingCompletion(messages, abortController, aiProvider)
|
||||
|
||||
const match = result.match(/```[a-zA-Z]+\n([\s\S]*?)\n```/)
|
||||
|
||||
|
||||
@@ -6,7 +6,12 @@ import {
|
||||
type InputTransform,
|
||||
type Script
|
||||
} from '$lib/gen'
|
||||
import { addResourceTypes, deltaCodeCompletion, getNonStreamingCompletion } from './lib'
|
||||
import {
|
||||
addResourceTypes,
|
||||
deltaCodeCompletion,
|
||||
getNonStreamingCompletion,
|
||||
type AiProviderTypes
|
||||
} from './lib'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type Editor from '../Editor.svelte'
|
||||
import type { Drawer } from '../common'
|
||||
@@ -14,7 +19,7 @@ import { scriptLangToEditorLang } from '$lib/scripts'
|
||||
|
||||
export type FlowCopilotModule = {
|
||||
id: string
|
||||
type: 'trigger' | 'script'
|
||||
type: 'trigger' | 'script'
|
||||
description: string
|
||||
code: string
|
||||
source: 'hub' | 'custom' | undefined
|
||||
@@ -106,7 +111,6 @@ To maintain state across runs, you can use get_state() and set_state(value) whic
|
||||
{additionalInformation}`
|
||||
}
|
||||
|
||||
|
||||
// const preprocessorPrompts: {
|
||||
// bun: string
|
||||
// python3: string
|
||||
@@ -128,7 +132,7 @@ To maintain state across runs, you can use get_state() and set_state(value) whic
|
||||
// headers: Record<string, string>
|
||||
// }
|
||||
// },
|
||||
// /* your other args */
|
||||
// /* your other args */
|
||||
// ) {
|
||||
// return {
|
||||
// // return the args to be passed to the flow
|
||||
@@ -168,7 +172,6 @@ To maintain state across runs, you can use get_state() and set_state(value) whic
|
||||
// {additionalInformation}`
|
||||
// }
|
||||
|
||||
|
||||
const firstActionPrompt = `I'm building a workflow which is a sequence of script steps. Write a script in {codeLang} which should {description}.
|
||||
Return the script's output.
|
||||
|
||||
@@ -255,7 +258,8 @@ export async function stepCopilot(
|
||||
})
|
||||
| undefined,
|
||||
isFirstInLoop: boolean,
|
||||
abortController: AbortController
|
||||
abortController: AbortController,
|
||||
aiProvider: AiProviderTypes
|
||||
) {
|
||||
if (module.source !== 'custom') {
|
||||
throw new Error('Not a custom module')
|
||||
@@ -265,9 +269,9 @@ export async function stepCopilot(
|
||||
let prompt =
|
||||
module.type === 'trigger'
|
||||
? triggerPrompts[lang]
|
||||
// : module.type === 'preprocessor'
|
||||
: // : module.type === 'preprocessor'
|
||||
// ? preprocessorPrompts[lang]
|
||||
: pastModule === undefined
|
||||
pastModule === undefined
|
||||
? firstActionPrompt
|
||||
: isFirstInLoop
|
||||
? loopActionPrompt
|
||||
@@ -310,7 +314,8 @@ export async function stepCopilot(
|
||||
}
|
||||
],
|
||||
deltaCodeStore,
|
||||
abortController
|
||||
abortController,
|
||||
aiProvider
|
||||
)
|
||||
return code
|
||||
}
|
||||
@@ -322,7 +327,8 @@ export async function glueCopilot(
|
||||
value: RawScript | PathScript
|
||||
},
|
||||
isFirstInLoop: boolean,
|
||||
abortController: AbortController
|
||||
abortController: AbortController,
|
||||
aiProvider: AiProviderTypes
|
||||
) {
|
||||
const { prevCode, prevLang } = await getPreviousStepContent(pastModule, workspace)
|
||||
|
||||
@@ -357,7 +363,8 @@ export async function glueCopilot(
|
||||
)
|
||||
}
|
||||
],
|
||||
abortController
|
||||
abortController,
|
||||
aiProvider
|
||||
)
|
||||
|
||||
const matches = response.matchAll(/([a-zA-Z_0-9.]+): (.+)/g)
|
||||
|
||||
@@ -1,42 +1,147 @@
|
||||
import { OpenAI } from 'openai'
|
||||
import { OpenAPI, ResourceService, type Script } from '../../gen'
|
||||
import type { Writable } from 'svelte/store'
|
||||
|
||||
import { Anthropic } from '@anthropic-ai/sdk'
|
||||
import type { DBSchema, GraphqlSchema, SQLSchema } from '$lib/stores'
|
||||
import { formatResourceTypes } from './utils'
|
||||
|
||||
import { EDIT_CONFIG, FIX_CONFIG, GEN_CONFIG } from './prompts'
|
||||
|
||||
import { Mistral } from '@mistralai/mistralai'
|
||||
import { buildClientSchema, printSchema } from 'graphql'
|
||||
import type {
|
||||
ChatCompletionCreateParamsStreaming,
|
||||
ChatCompletionMessageParam
|
||||
} from 'openai/resources/index.mjs'
|
||||
|
||||
import type { MessageCreateParams, MessageParam } from '@anthropic-ai/sdk/resources/messages.mjs'
|
||||
import type { ChatCompletionRequest } from '@mistralai/mistralai/models/components/chatcompletionrequest'
|
||||
import type {
|
||||
SystemMessage,
|
||||
UserMessage,
|
||||
AssistantMessage,
|
||||
ToolMessage,
|
||||
CompletionEvent,
|
||||
ContentChunk
|
||||
} from '@mistralai/mistralai/models/components'
|
||||
|
||||
export const SUPPORTED_LANGUAGES = new Set(Object.keys(GEN_CONFIG.prompts))
|
||||
|
||||
const openaiConfig: ChatCompletionCreateParamsStreaming = {
|
||||
temperature: 0,
|
||||
max_tokens: 16384,
|
||||
model: 'gpt-4o-2024-08-06',
|
||||
seed: 42,
|
||||
stream: true,
|
||||
messages: []
|
||||
export type AiProviderTypes = 'openai' | 'anthropic' | 'mistral'
|
||||
|
||||
interface AiProvider {
|
||||
init: (workspace: string, updateClient: boolean, token?: string) => void
|
||||
}
|
||||
|
||||
class WorkspacedOpenai {
|
||||
class WorkspacedMistral implements AiProvider {
|
||||
private client: Mistral | undefined
|
||||
|
||||
init(workspace: string, updateClient: boolean, token?: string) {
|
||||
if (!this.client || updateClient) {
|
||||
this.client = initWorkspaceAiProvider(workspace, 'mistral', token) as unknown as Mistral
|
||||
}
|
||||
}
|
||||
|
||||
getClient() {
|
||||
if (!this.client) {
|
||||
throw new Error('AnthropicAi not initialized')
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
}
|
||||
|
||||
export namespace MistralAi {
|
||||
export let workspace = new WorkspacedMistral()
|
||||
|
||||
export const mistralConfig: ChatCompletionRequest = {
|
||||
temperature: 0,
|
||||
model: null,
|
||||
maxTokens: 32000,
|
||||
messages: []
|
||||
}
|
||||
|
||||
export type MistralParamsMessage =
|
||||
| (SystemMessage & { role: 'system' })
|
||||
| (UserMessage & { role: 'user' })
|
||||
| (AssistantMessage & { role: 'assistant' })
|
||||
| (ToolMessage & { role: 'tool' })
|
||||
|
||||
export function retrieveTextValue(chunks: string | ContentChunk[] | null | undefined): string {
|
||||
let response = ''
|
||||
if (Array.isArray(chunks)) {
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.type === 'text') {
|
||||
response += chunk.text
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
return chunks as string
|
||||
}
|
||||
}
|
||||
|
||||
class WorkspacedAnthropic implements AiProvider {
|
||||
private client: Anthropic | undefined
|
||||
|
||||
init(workspace: string, updateClient: boolean, token: string | undefined = undefined) {
|
||||
if (!this.client || updateClient) {
|
||||
this.client = initWorkspaceAiProvider(workspace, 'anthropic', token) as unknown as Anthropic
|
||||
}
|
||||
}
|
||||
|
||||
getClient() {
|
||||
if (!this.client) {
|
||||
throw new Error('AnthropicAi not initialized')
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
}
|
||||
|
||||
export namespace AnthropicAi {
|
||||
export let workspace = new WorkspacedAnthropic()
|
||||
|
||||
export const config: MessageCreateParams = {
|
||||
temperature: 0,
|
||||
max_tokens: 8192,
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
messages: []
|
||||
}
|
||||
|
||||
export function getSystemPromptAndArrayMessages(
|
||||
messages: ChatCompletionMessageParam[]
|
||||
): [string, MessageParam[]] {
|
||||
let system: string | undefined = undefined
|
||||
if (messages[0].role == 'system') {
|
||||
system = messages[0].content as string
|
||||
messages.shift()
|
||||
}
|
||||
const anthropicMessages: MessageParam[] = messages.map((message) => {
|
||||
return {
|
||||
role: message.role == 'user' ? 'user' : 'assistant',
|
||||
content: message.content as string
|
||||
}
|
||||
})
|
||||
return [system as string, anthropicMessages ?? []]
|
||||
}
|
||||
|
||||
export function retrieveTextValue(part: Anthropic.Messages.RawMessageStreamEvent) {
|
||||
let response = ''
|
||||
if (part.type == 'content_block_delta') {
|
||||
if (part.delta.type == 'text_delta') {
|
||||
response = part.delta.text
|
||||
} else {
|
||||
response = part.delta.partial_json
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
class WorkspacedOpenai implements AiProvider {
|
||||
private client: OpenAI | undefined
|
||||
|
||||
init(workspace: string, token: string | undefined = undefined) {
|
||||
const baseURL = `${location.origin}${OpenAPI.BASE}/w/${workspace}/openai/proxy`
|
||||
this.client = new OpenAI({
|
||||
baseURL,
|
||||
apiKey: 'fakekey',
|
||||
defaultHeaders: {
|
||||
Authorization: token ? `Bearer ${token}` : ''
|
||||
},
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
init(workspace: string, updateClient: boolean, token: string | undefined = undefined) {
|
||||
if (!this.client || updateClient) {
|
||||
this.client = initWorkspaceAiProvider(workspace, 'openai', token) as unknown as OpenAI
|
||||
}
|
||||
}
|
||||
|
||||
getClient() {
|
||||
@@ -47,34 +152,141 @@ class WorkspacedOpenai {
|
||||
}
|
||||
}
|
||||
|
||||
export let workspacedOpenai = new WorkspacedOpenai()
|
||||
export namespace OpenAi {
|
||||
export let workspace = new WorkspacedOpenai()
|
||||
|
||||
export const openaiConfig: ChatCompletionCreateParamsStreaming = {
|
||||
temperature: 0,
|
||||
max_tokens: 16384,
|
||||
model: 'gpt-4o-2024-08-06',
|
||||
seed: 42,
|
||||
stream: true,
|
||||
messages: []
|
||||
}
|
||||
|
||||
export function retrieveTextValue(part: OpenAI.Chat.Completions.ChatCompletionChunk) {
|
||||
return part.choices[0]?.delta?.content || ''
|
||||
}
|
||||
}
|
||||
|
||||
export function initAllAiWorkspace(workspace: string, updateClient: boolean = false) {
|
||||
OpenAi.workspace.init(workspace, updateClient)
|
||||
AnthropicAi.workspace.init(workspace, updateClient)
|
||||
MistralAi.workspace.init(workspace, updateClient)
|
||||
}
|
||||
|
||||
function initWorkspaceAiProvider(
|
||||
workspace: string,
|
||||
aiProvider: AiProviderTypes,
|
||||
token: string | undefined = undefined
|
||||
): Anthropic | OpenAI | Mistral {
|
||||
const baseURL = `${location.origin}${OpenAPI.BASE}/w/${workspace}/ai/proxy`
|
||||
let client
|
||||
switch (aiProvider) {
|
||||
case 'openai': {
|
||||
client = new OpenAI({
|
||||
baseURL,
|
||||
apiKey: 'fake-key',
|
||||
defaultHeaders: {
|
||||
Authorization: token ? `Bearer ${token}` : ''
|
||||
},
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'anthropic': {
|
||||
client = new Anthropic({
|
||||
baseURL,
|
||||
apiKey: 'fake-key',
|
||||
defaultHeaders: {
|
||||
Authorization: token ? `Bearer ${token}` : ''
|
||||
},
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'mistral': {
|
||||
client = new Mistral({
|
||||
serverURL: baseURL
|
||||
})
|
||||
}
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
export async function testKey({
|
||||
apiKey,
|
||||
abortController,
|
||||
messages
|
||||
messages,
|
||||
aiProvider
|
||||
}: {
|
||||
apiKey?: string
|
||||
messages: ChatCompletionMessageParam[]
|
||||
abortController: AbortController
|
||||
aiProvider: AiProviderTypes
|
||||
}) {
|
||||
if (apiKey) {
|
||||
const openai = new OpenAI({
|
||||
apiKey,
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
await openai.chat.completions.create(
|
||||
{
|
||||
...openaiConfig,
|
||||
messages,
|
||||
stream: false
|
||||
},
|
||||
{
|
||||
signal: abortController.signal
|
||||
switch (aiProvider) {
|
||||
case 'openai': {
|
||||
const openai = new OpenAI({
|
||||
apiKey,
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
await openai.chat.completions.create(
|
||||
{
|
||||
...OpenAi.openaiConfig,
|
||||
messages,
|
||||
stream: false
|
||||
},
|
||||
{
|
||||
signal: abortController.signal
|
||||
}
|
||||
)
|
||||
break
|
||||
}
|
||||
)
|
||||
case 'anthropic': {
|
||||
const anthropic = new Anthropic({
|
||||
apiKey,
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
const [, anthropicMessages] = AnthropicAi.getSystemPromptAndArrayMessages(messages)
|
||||
await anthropic.messages.create(
|
||||
{
|
||||
...AnthropicAi.config,
|
||||
messages: anthropicMessages,
|
||||
stream: false
|
||||
},
|
||||
{
|
||||
signal: abortController.signal
|
||||
}
|
||||
)
|
||||
break
|
||||
}
|
||||
case 'mistral': {
|
||||
const mistral = new Mistral({
|
||||
apiKey
|
||||
})
|
||||
await mistral.chat.complete(
|
||||
{
|
||||
...MistralAi.mistralConfig,
|
||||
model: 'codestral-latest',
|
||||
stream: false,
|
||||
messages: messages as MistralAi.MistralParamsMessage[]
|
||||
},
|
||||
{
|
||||
fetchOptions: {
|
||||
signal: abortController.signal,
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await getNonStreamingCompletion(messages, abortController, undefined, true)
|
||||
await getNonStreamingCompletion(messages, abortController, aiProvider, undefined, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,57 +467,154 @@ const PROMPTS_CONFIGS = {
|
||||
export async function getNonStreamingCompletion(
|
||||
messages: ChatCompletionMessageParam[],
|
||||
abortController: AbortController,
|
||||
model = openaiConfig.model,
|
||||
aiProvider: AiProviderTypes,
|
||||
model = OpenAi.openaiConfig.model,
|
||||
noCache?: boolean
|
||||
) {
|
||||
const openaiClient = workspacedOpenai.getClient()
|
||||
const completion = await openaiClient.chat.completions.create(
|
||||
{
|
||||
...openaiConfig,
|
||||
messages,
|
||||
stream: false,
|
||||
model
|
||||
let response: string | undefined = ''
|
||||
const queryOptions = {
|
||||
query: {
|
||||
no_cache: noCache
|
||||
},
|
||||
{
|
||||
query: {
|
||||
no_cache: noCache
|
||||
},
|
||||
signal: abortController.signal
|
||||
signal: abortController.signal
|
||||
}
|
||||
switch (aiProvider) {
|
||||
case 'openai': {
|
||||
const openaiClient = OpenAi.workspace.getClient()
|
||||
const completion = await openaiClient.chat.completions.create(
|
||||
{
|
||||
...OpenAi.openaiConfig,
|
||||
messages,
|
||||
stream: false,
|
||||
model
|
||||
},
|
||||
queryOptions
|
||||
)
|
||||
response = completion.choices[0]?.message.content || ''
|
||||
break
|
||||
}
|
||||
)
|
||||
|
||||
// if (completion.usage) {
|
||||
// const { prompt_tokens, completion_tokens } = completion.usage
|
||||
// console.log('Cost: ', (prompt_tokens * 0.0015 + completion_tokens * 0.002) / 1000)
|
||||
// }
|
||||
|
||||
return completion.choices[0]?.message.content || ''
|
||||
case 'anthropic': {
|
||||
const anthropicClient = AnthropicAi.workspace.getClient()
|
||||
const [system, anthropicMessages] = AnthropicAi.getSystemPromptAndArrayMessages(messages)
|
||||
const message = await anthropicClient.messages.create(
|
||||
{
|
||||
...AnthropicAi.config,
|
||||
system,
|
||||
messages: anthropicMessages,
|
||||
stream: false
|
||||
},
|
||||
queryOptions
|
||||
)
|
||||
response = message.content[0].type === 'text' ? message.content[0].text : ''
|
||||
break
|
||||
}
|
||||
case 'mistral': {
|
||||
const mistralClient = MistralAi.workspace.getClient()
|
||||
const message = await mistralClient.chat.complete(
|
||||
{
|
||||
...MistralAi.mistralConfig,
|
||||
model: 'codestral-latest',
|
||||
stream: false,
|
||||
messages: messages as MistralAi.MistralParamsMessage[]
|
||||
},
|
||||
{
|
||||
fetchOptions: {
|
||||
signal: abortController.signal,
|
||||
cache: 'no-store'
|
||||
}
|
||||
}
|
||||
)
|
||||
response = MistralAi.retrieveTextValue(message.choices && message.choices[0].message.content)
|
||||
break
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
export async function getCompletion(
|
||||
messages: ChatCompletionMessageParam[],
|
||||
abortController: AbortController,
|
||||
model = openaiConfig.model
|
||||
aiProvider: AiProviderTypes,
|
||||
model = OpenAi.openaiConfig.model
|
||||
) {
|
||||
const openaiClient = workspacedOpenai.getClient()
|
||||
const completion = await openaiClient.chat.completions.create(
|
||||
{
|
||||
...openaiConfig,
|
||||
messages,
|
||||
model
|
||||
},
|
||||
{
|
||||
signal: abortController.signal
|
||||
}
|
||||
)
|
||||
switch (aiProvider) {
|
||||
case 'anthropic': {
|
||||
const anthropicClient = AnthropicAi.workspace.getClient()
|
||||
const [system, anthropicMessages] = AnthropicAi.getSystemPromptAndArrayMessages(messages)
|
||||
|
||||
return completion
|
||||
const completion = await anthropicClient.messages.create(
|
||||
{
|
||||
...AnthropicAi.config,
|
||||
system,
|
||||
messages: anthropicMessages,
|
||||
stream: true
|
||||
},
|
||||
{ signal: abortController.signal }
|
||||
)
|
||||
return completion
|
||||
}
|
||||
case 'openai': {
|
||||
const openaiClient = OpenAi.workspace.getClient()
|
||||
const completion = await openaiClient.chat.completions.create(
|
||||
{
|
||||
...OpenAi.openaiConfig,
|
||||
messages,
|
||||
model
|
||||
},
|
||||
{
|
||||
signal: abortController.signal
|
||||
}
|
||||
)
|
||||
return completion
|
||||
}
|
||||
case 'mistral': {
|
||||
const mistralClient = MistralAi.workspace.getClient()
|
||||
const message = await mistralClient.chat.stream(
|
||||
{
|
||||
...MistralAi.mistralConfig,
|
||||
model: 'codestral-latest',
|
||||
messages: messages as MistralAi.MistralParamsMessage[]
|
||||
},
|
||||
{
|
||||
fetchOptions: {
|
||||
signal: abortController.signal,
|
||||
cache: 'no-store'
|
||||
}
|
||||
}
|
||||
)
|
||||
return message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getResponseFromEvent(
|
||||
part:
|
||||
| Anthropic.Messages.RawMessageStreamEvent
|
||||
| OpenAI.Chat.Completions.ChatCompletionChunk
|
||||
| CompletionEvent,
|
||||
aiProvider: AiProviderTypes
|
||||
): string {
|
||||
switch (aiProvider) {
|
||||
case 'openai': {
|
||||
const messages = part as OpenAI.Chat.Completions.ChatCompletionChunk
|
||||
return OpenAi.retrieveTextValue(messages)
|
||||
}
|
||||
case 'anthropic': {
|
||||
const messages = part as Anthropic.Messages.RawMessageStreamEvent
|
||||
return AnthropicAi.retrieveTextValue(messages)
|
||||
}
|
||||
case 'mistral': {
|
||||
const messages = part as CompletionEvent
|
||||
return MistralAi.retrieveTextValue(messages.data.choices[0].delta.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function copilot(
|
||||
scriptOptions: CopilotOptions,
|
||||
generatedCode: Writable<string>,
|
||||
abortController: AbortController,
|
||||
aiProvider: AiProviderTypes,
|
||||
generatedExplanation?: Writable<string>
|
||||
) {
|
||||
const { prompt, systemPrompt } = await getPrompts(scriptOptions)
|
||||
@@ -321,13 +630,14 @@ export async function copilot(
|
||||
content: prompt
|
||||
}
|
||||
],
|
||||
abortController
|
||||
abortController,
|
||||
aiProvider
|
||||
)
|
||||
|
||||
let response = ''
|
||||
let code = ''
|
||||
for await (const part of completion) {
|
||||
response += part.choices[0]?.delta?.content || ''
|
||||
response += getResponseFromEvent(part, aiProvider)
|
||||
let match = response.match(/```[a-zA-Z]+\n([\s\S]*?)\n```/)
|
||||
|
||||
if (match) {
|
||||
@@ -393,15 +703,16 @@ function getStringEndDelta(prev: string, now: string) {
|
||||
export async function deltaCodeCompletion(
|
||||
messages: ChatCompletionMessageParam[],
|
||||
generatedCodeDelta: Writable<string>,
|
||||
abortController: AbortController
|
||||
abortController: AbortController,
|
||||
aiProvider: AiProviderTypes
|
||||
) {
|
||||
const completion = await getCompletion(messages, abortController)
|
||||
const completion = await getCompletion(messages, abortController, aiProvider)
|
||||
|
||||
let response = ''
|
||||
let code = ''
|
||||
let delta = ''
|
||||
for await (const part of completion) {
|
||||
response += part.choices[0]?.delta?.content || ''
|
||||
response += getResponseFromEvent(part, aiProvider)
|
||||
let match = response.match(/```[a-zA-Z]+\n([\s\S]*?)\n```/)
|
||||
|
||||
if (match) {
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
{:else if $selectedId === 'Input'}
|
||||
<FlowInput {noEditor} disabled={disabledFlowInputs} />
|
||||
{:else if $selectedId === 'Result'}
|
||||
<p class="p-4 text-secondary">Nothing to show about the result node. Happy flow building!</p>
|
||||
<p class="p-4 text-secondary">The result of the flow will be the result of the last node.</p>
|
||||
{:else if $selectedId === 'constants'}
|
||||
<FlowConstants {noEditor} />
|
||||
{:else if $selectedId === 'failure'}
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
}
|
||||
|
||||
async function onGenerate() {
|
||||
if (!selectedCompletion && !$copilotInfo.exists_openai_resource_path) {
|
||||
if (!selectedCompletion && !$copilotInfo.exists_ai_resource) {
|
||||
sendUserToast(
|
||||
'Windmill AI is not enabled, you can activate it in the workspace settings',
|
||||
true
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<Menu pointerDown noMinW placement="bottom-center" let:close bind:show={openNoCopilot}>
|
||||
<button
|
||||
title="AI Flow Builder"
|
||||
on:pointerdown={$copilotInfo.exists_openai_resource_path
|
||||
on:pointerdown={$copilotInfo.exists_ai_resource
|
||||
? (ev) => {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
@@ -33,12 +33,12 @@
|
||||
>
|
||||
<Wand2 size={12} />
|
||||
</button>
|
||||
{#if !$copilotInfo.exists_openai_resource_path}
|
||||
{#if !$copilotInfo.exists_ai_resource}
|
||||
<div class="text-primary p-4">
|
||||
<p class="text-sm w-80">
|
||||
Enable Windmill AI in the
|
||||
<a
|
||||
href="{base}/workspace_settings?tab=openai"
|
||||
href="{base}/workspace_settings?tab=ai"
|
||||
target="_blank"
|
||||
class="inline-flex flex-row items-center gap-1"
|
||||
on:click={() => {
|
||||
|
||||
@@ -91,10 +91,7 @@
|
||||
kind
|
||||
)
|
||||
} else if (kind == 'forloop') {
|
||||
;[module, state] = await createLoop(
|
||||
module.id,
|
||||
!disableAi && $copilotInfo.exists_openai_resource_path
|
||||
)
|
||||
;[module, state] = await createLoop(module.id, !disableAi && $copilotInfo.exists_ai_resource)
|
||||
} else if (kind == 'whileloop') {
|
||||
;[module, state] = await createWhileLoop(module.id)
|
||||
} else if (kind == 'branchone') {
|
||||
|
||||
@@ -140,7 +140,7 @@ export const settings: Record<string, Setting[]> = {
|
||||
{
|
||||
label: 'Critical alert channels',
|
||||
description:
|
||||
'Channels to send critical alerts to. SMTP must be configured for the email channel. A Slack workspace must be connected to the instance for the Slack channel. <a href="https://www.windmill.dev/docs/core_concepts/critical_alert_channels">Learn more</a>',
|
||||
'Channels to send critical alerts to. SMTP must be configured for the email channel. A Slack workspace must be connected to the instance for the Slack channel. <a href="https://www.windmill.dev/docs/core_concepts/critical_alerts">Learn more</a>',
|
||||
key: 'critical_error_channels',
|
||||
fieldType: 'critical_error_channels',
|
||||
storage: 'setting',
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import Popover from '../Popover.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let label: string | undefined = undefined
|
||||
export let numUnacknowledgedCriticalAlerts: number
|
||||
export let isCollapsed: boolean
|
||||
export let disabled: boolean = false
|
||||
export let lightMode: boolean = false
|
||||
export let stopPropagationOnClick: boolean = false
|
||||
export let shortcut: string = ''
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
{#if !disabled}
|
||||
<Popover appearTimeout={0} disappearTimeout={0} class="w-full" disablePopup={!isCollapsed}>
|
||||
<div class="py-1.5 px-1 border-t border-gray-700">
|
||||
<button
|
||||
on:click={(e) => {
|
||||
if (stopPropagationOnClick) e.preventDefault()
|
||||
dispatch('click')
|
||||
}}
|
||||
class={twMerge(
|
||||
'group flex items-center px-2 py-2 font-light rounded-md h-8 gap-3 w-full',
|
||||
lightMode
|
||||
? 'text-primary hover:bg-surface-hover '
|
||||
: ' hover:bg-[#2A3648] text-primary-inverse dark:text-primary',
|
||||
'transition-all',
|
||||
$$props.class
|
||||
)}
|
||||
title={label}
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-center h-6 w-6 rounded-lg bg-red-600 text-white font-bold text-xs"
|
||||
>
|
||||
{numUnacknowledgedCriticalAlerts > 9 ? '9+' : numUnacknowledgedCriticalAlerts}
|
||||
</span>
|
||||
|
||||
{#if !isCollapsed && label}
|
||||
<span
|
||||
class={twMerge(
|
||||
'whitespace-pre truncate',
|
||||
lightMode ? 'text-primary' : 'text-primary-inverse dark:text-primary',
|
||||
'transition-all',
|
||||
$$props.class
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
<span class="pl-2 text-xs dark:text-secondary light:text-secondary-inverse font-semibold">
|
||||
{shortcut}
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
<svelte:fragment slot="text">
|
||||
{#if label}
|
||||
{label}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
@@ -3,16 +3,62 @@
|
||||
import CriticalAlertModalInner from './CriticalAlertModalInner.svelte'
|
||||
import { SettingService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { superadmin, workspaceStore, isCriticalAlertsUIOpen } from '$lib/stores'
|
||||
import Modal from '../common/modal/Modal.svelte'
|
||||
|
||||
export let open: boolean = false
|
||||
export let numUnacknowledgedCriticalAlerts: number = 0
|
||||
export let muteSettings
|
||||
let workspaceContext = false
|
||||
|
||||
$: {
|
||||
setupApiFunctions(workspaceContext)
|
||||
}
|
||||
|
||||
function setupApiFunctions(_ctx?) {
|
||||
getCriticalAlerts = withSuperadminLogic(
|
||||
SettingService.getCriticalAlerts,
|
||||
SettingService.workspaceGetCriticalAlerts
|
||||
)
|
||||
|
||||
acknowledgeCriticalAlert = withSuperadminLogic(
|
||||
SettingService.acknowledgeCriticalAlert,
|
||||
SettingService.workspaceAcknowledgeCriticalAlert
|
||||
)
|
||||
|
||||
acknowledgeAllCriticalAlerts = withSuperadminLogic(
|
||||
SettingService.acknowledgeAllCriticalAlerts,
|
||||
SettingService.workspaceAcknowledgeAllCriticalAlerts
|
||||
)
|
||||
}
|
||||
|
||||
$: isCriticalAlertsUIOpen.set(open)
|
||||
$: if ($isCriticalAlertsUIOpen) open = $isCriticalAlertsUIOpen
|
||||
|
||||
let checkForNewAlertsInterval: ReturnType<typeof setInterval>
|
||||
let checkingForNewAlerts = false
|
||||
|
||||
onMount(() => {
|
||||
updateHasUnacknowledgedCriticalAlerts(true)
|
||||
const withSuperadminLogic = (superadminFunction, workspaceFunction) => {
|
||||
return async (params = {}) => {
|
||||
if (!$superadmin || workspaceContext) {
|
||||
return workspaceFunction({
|
||||
...params,
|
||||
workspace: $workspaceStore
|
||||
})
|
||||
} else {
|
||||
return superadminFunction(params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let getCriticalAlerts
|
||||
let acknowledgeCriticalAlert
|
||||
let acknowledgeAllCriticalAlerts
|
||||
|
||||
setupApiFunctions()
|
||||
|
||||
onMount(async () => {
|
||||
await updateHasUnacknowledgedCriticalAlerts(false)
|
||||
checkForNewAlertsInterval = setInterval(() => {
|
||||
updateHasUnacknowledgedCriticalAlerts(true)
|
||||
}, 15000)
|
||||
@@ -25,15 +71,18 @@
|
||||
async function updateHasUnacknowledgedCriticalAlerts(sendToast: boolean = false) {
|
||||
if (checkingForNewAlerts) return
|
||||
checkingForNewAlerts = true
|
||||
|
||||
try {
|
||||
const unacknowledged = await SettingService.getCriticalAlerts({
|
||||
const unacknowledged = await getCriticalAlerts({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
acknowledged: false
|
||||
})
|
||||
|
||||
if (numUnacknowledgedCriticalAlerts === 0 && unacknowledged.length > 0 && sendToast) {
|
||||
if (
|
||||
numUnacknowledgedCriticalAlerts === 0 &&
|
||||
unacknowledged.length > 0 &&
|
||||
sendToast &&
|
||||
(($superadmin && !muteSettings.global) || (!$superadmin && !muteSettings.workspace))
|
||||
) {
|
||||
sendUserToast(
|
||||
'Critical Alert:',
|
||||
true,
|
||||
@@ -62,11 +111,19 @@
|
||||
}
|
||||
|
||||
async function acknowledgeAlert(id: number) {
|
||||
await SettingService.acknowledgeCriticalAlert({ id })
|
||||
await acknowledgeCriticalAlert({ id })
|
||||
updateHasUnacknowledgedCriticalAlerts()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:open title="Critical Alerts" cancelText="Close" style="max-width: 66%;">
|
||||
<CriticalAlertModalInner {updateHasUnacknowledgedCriticalAlerts} />
|
||||
<CriticalAlertModalInner
|
||||
{numUnacknowledgedCriticalAlerts}
|
||||
{updateHasUnacknowledgedCriticalAlerts}
|
||||
{getCriticalAlerts}
|
||||
{acknowledgeCriticalAlert}
|
||||
{acknowledgeAllCriticalAlerts}
|
||||
{muteSettings}
|
||||
bind:workspaceContext
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
@@ -2,23 +2,77 @@
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { SettingService } from '$lib/gen'
|
||||
import { CheckCircle2, AlertCircle, RefreshCw, CheckSquare2, AlertTriangle } from 'lucide-svelte'
|
||||
import {
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
RefreshCw,
|
||||
CheckSquare2,
|
||||
AlertTriangle,
|
||||
} from 'lucide-svelte'
|
||||
import type { CriticalAlert } from '$lib/gen'
|
||||
import { onMount } from 'svelte'
|
||||
import { instanceSettingsSelectedTab } from '$lib/stores'
|
||||
import { goto } from '$app/navigation'
|
||||
import { superadmin, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
|
||||
export let updateHasUnacknowledgedCriticalAlerts: () => void = () => {}
|
||||
export let updateHasUnacknowledgedCriticalAlerts
|
||||
export let getCriticalAlerts
|
||||
export let acknowledgeCriticalAlert
|
||||
export let acknowledgeAllCriticalAlerts
|
||||
export let numUnacknowledgedCriticalAlerts
|
||||
|
||||
let alerts: CriticalAlert[] = []
|
||||
|
||||
let isRefreshing = false
|
||||
let hasCriticalAlertChannels = false
|
||||
let hasCriticalAlertChannels = true
|
||||
|
||||
export let muteSettings = {
|
||||
workspace: true,
|
||||
global: true
|
||||
}
|
||||
|
||||
$: muteSettings
|
||||
$: {
|
||||
if (initialMuteSettings.workspace !== muteSettings.workspace || initialMuteSettings.global !== muteSettings.global) {
|
||||
saveMuteSettings()
|
||||
}
|
||||
}
|
||||
|
||||
$: numUnacknowledgedCriticalAlerts >= 0 && getAlerts(true)
|
||||
|
||||
let initialMuteSettings = muteSettings
|
||||
|
||||
async function saveMuteSettings() {
|
||||
if (initialMuteSettings.workspace !== muteSettings.workspace) {
|
||||
// Workspace
|
||||
await SettingService.workspaceMuteCriticalAlertsUi({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
mute_critical_alerts: muteSettings.workspace
|
||||
}
|
||||
})
|
||||
}
|
||||
if ($superadmin && initialMuteSettings.global !== muteSettings.global) {
|
||||
// Global
|
||||
await SettingService.setGlobal({
|
||||
key: 'critical_alert_mute_ui',
|
||||
requestBody: { value: muteSettings.global }
|
||||
})
|
||||
}
|
||||
sendUserToast(
|
||||
`Critical alert UI mute settings changed.\nPlease reload page for UI changes to take effect.`
|
||||
)
|
||||
getAlerts(true)
|
||||
initialMuteSettings = { ...muteSettings }
|
||||
}
|
||||
|
||||
$: loading = isRefreshing
|
||||
|
||||
onMount(() => {
|
||||
refreshAlerts()
|
||||
initialMuteSettings = { ...muteSettings }
|
||||
})
|
||||
|
||||
// Pagination
|
||||
@@ -29,14 +83,14 @@
|
||||
let hideAcknowledged = false
|
||||
|
||||
async function acknowledgeAll() {
|
||||
await SettingService.acknowledgeAllCriticalAlerts()
|
||||
await acknowledgeAllCriticalAlerts()
|
||||
getAlerts(false)
|
||||
}
|
||||
|
||||
async function fetchAlerts(pageNumber: number) {
|
||||
isRefreshing = true
|
||||
try {
|
||||
const newAlerts = await SettingService.getCriticalAlerts({
|
||||
const newAlerts = await getCriticalAlerts({
|
||||
page: pageNumber,
|
||||
pageSize: pageSize,
|
||||
acknowledged: hideAcknowledged ? false : undefined
|
||||
@@ -55,6 +109,7 @@
|
||||
|
||||
async function getAlerts(reset?: boolean) {
|
||||
if (reset) page = 1
|
||||
updateHasUnacknowledgedCriticalAlerts()
|
||||
await fetchAlerts(page)
|
||||
}
|
||||
|
||||
@@ -64,7 +119,7 @@
|
||||
}
|
||||
|
||||
async function acknowledgeAlert(id: number) {
|
||||
await SettingService.acknowledgeCriticalAlert({ id })
|
||||
await acknowledgeCriticalAlert({ id })
|
||||
getAlerts(false)
|
||||
}
|
||||
|
||||
@@ -82,7 +137,7 @@
|
||||
}
|
||||
|
||||
async function refreshAlerts() {
|
||||
checkCriticalAlertChannels()
|
||||
if ($superadmin) checkCriticalAlertChannels()
|
||||
await getAlerts(true)
|
||||
}
|
||||
|
||||
@@ -102,46 +157,99 @@
|
||||
goto('/#superadmin-settings')
|
||||
instanceSettingsSelectedTab.set('Core')
|
||||
}
|
||||
|
||||
export let workspaceContext = false
|
||||
|
||||
$: {
|
||||
workspaceContextChanged(workspaceContext)
|
||||
}
|
||||
|
||||
async function workspaceContextChanged(_ctx) {
|
||||
await getAlerts(true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if !hasCriticalAlertChannels}
|
||||
<div class="flex flex-row pb-4">
|
||||
<AlertTriangle color="orange" class="w-6 h-6 mr-2" />
|
||||
<p>
|
||||
No critical alert channels are set up. Go to the
|
||||
<a href="/#superadmin-settings" on:click|preventDefault={goToCoreTab}>Instance Settings</a>
|
||||
page to configure critical alert channels.
|
||||
</p>
|
||||
<div class="grid grid-cols-3 gap-4 col-start-3">
|
||||
<div class="pt-1 col-span-2">
|
||||
{#if !hasCriticalAlertChannels && $superadmin}
|
||||
<div class="flex flex-row pb-4">
|
||||
<AlertTriangle color="orange" class="w-6 h-6 mr-2" />
|
||||
<p>
|
||||
No critical alert channels are set up. Go to the
|
||||
<a href="/#superadmin-settings" on:click|preventDefault={goToCoreTab}
|
||||
>Instance Settings</a
|
||||
>
|
||||
page to configure critical alert channels.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Row of action buttons above the table -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex space-x-2">
|
||||
<Button color="green" startIcon={{ icon: CheckSquare2 }} size="sm" on:click={acknowledgeAll}
|
||||
>Acknowledge All</Button
|
||||
|
||||
<div class="flex flex-col justify-between col-start-3">
|
||||
<div class="flex flex-row justify-end mt-[-38px] pb-3">
|
||||
<Button
|
||||
color="green"
|
||||
startIcon={{ icon: CheckSquare2 }}
|
||||
size="xs"
|
||||
disabled={numUnacknowledgedCriticalAlerts === 0}
|
||||
on:click={acknowledgeAll}
|
||||
>
|
||||
Acknowledge All</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if $superadmin}
|
||||
<div class="flex flex-row py-2 pb-3">
|
||||
<Toggle
|
||||
bind:checked={workspaceContext}
|
||||
options={{ right: `Workspace: '${$workspaceStore}'`, left: "Context: 'Global'" }}
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Section
|
||||
label="Mute Settings"
|
||||
collapsable={true}
|
||||
small={true}
|
||||
>
|
||||
</div>
|
||||
{#if $superadmin}
|
||||
<div class="flex flex-row pb-1">
|
||||
<Toggle
|
||||
bind:checked={muteSettings.global}
|
||||
options={{ right: 'Mute critical alerts instance wide' }}
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button class="p-2 rounded-full hover:bg-gray-200" on:click={refreshAlerts} disabled={loading}>
|
||||
<RefreshCw class={loading ? 'animate-spin ' : ''} size="20" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-row pb-1">
|
||||
<Toggle
|
||||
bind:checked={muteSettings.workspace}
|
||||
options={{ right: 'Mute critical alerts for current workspace' }}
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<!-- Pagination controls above the table -->
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="flex items-center space-x-4">
|
||||
<Button size="xs2" on:click={goToPreviousPage} disabled={page <= 1}>Previous</Button>
|
||||
<span>Page {page}</span>
|
||||
<Button size="xs2" on:click={goToNextPage} disabled={!hasMore}>Next</Button>
|
||||
</div>
|
||||
<div class="pr-3">
|
||||
<Toggle
|
||||
bind:checked={hideAcknowledged}
|
||||
on:change={refreshAlerts}
|
||||
options={{ right: 'Hide Acknowledged' }}
|
||||
size="xs"
|
||||
/>
|
||||
<div class="pt-2 flex justify-between items-center">
|
||||
<div class="pr-2">
|
||||
<Toggle
|
||||
bind:checked={hideAcknowledged}
|
||||
on:change={refreshAlerts}
|
||||
options={{ right: 'Hide Acknowledged' }}
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="mb-1 p-2 rounded-full hover:bg-gray-200"
|
||||
on:click={refreshAlerts}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw class={loading ? 'animate-spin ' : ''} size="20" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -153,11 +261,14 @@
|
||||
<th class="w-[60px] px-4 py-2 text-center">Type</th>
|
||||
<th class="px-4 py-2 text-center">Message</th>
|
||||
<th class="w-[150px] px-4 py-2 text-center">Created At</th>
|
||||
{#if $superadmin}
|
||||
<th class="w-[80px] px-4 py-2 text-center">Workspace</th>
|
||||
{/if}
|
||||
<th class="w-[180px] px-4 py-2 text-center">Acknowledge</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each alerts as { id, alert_type, message, created_at, acknowledged }}
|
||||
{#each alerts as { id, alert_type, message, created_at, acknowledged, workspace_id }}
|
||||
{#if !hideAcknowledged || !acknowledged}
|
||||
<tr class="bg-gray-100 dark:bg-gray-700 dark:text-white text-center">
|
||||
<td class="border px-4 py-2 w-[100px]">
|
||||
@@ -174,6 +285,9 @@
|
||||
<td class="border px-4 py-2">{message}</td>
|
||||
<!-- Flexible width -->
|
||||
<td class="border px-4 py-2 w-[150px]">{formatDate(created_at)}</td>
|
||||
{#if $superadmin}
|
||||
<td class="border px-4 py-2 w-[150px]">{workspace_id ? workspace_id : 'global'}</td>
|
||||
{/if}
|
||||
<td class="border px-4 py-2 w-[180px]">
|
||||
<div class="flex justify-center items-center">
|
||||
{#if !acknowledged}
|
||||
@@ -196,6 +310,11 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="flex flex-1 pt-2 gap-x-4 justify-end">
|
||||
<Button size="xs2" on:click={goToPreviousPage} disabled={page <= 1}>Previous</Button>
|
||||
<span>Page {page}</span>
|
||||
<Button size="xs2" on:click={goToNextPage} disabled={!hasMore}>Next</Button>
|
||||
</div>
|
||||
|
||||
{#if alerts.length === 0}
|
||||
<p class="text-center text-gray-500 mt-4">No critical alerts available.</p>
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import Popover from '../Popover.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import SideBarNotification from './SideBarNotification.svelte'
|
||||
export let label: string | undefined = undefined
|
||||
export let icon: any | undefined = undefined
|
||||
export let isCollapsed: boolean
|
||||
export let disabled: boolean = false
|
||||
export let lightMode: boolean = false
|
||||
export let stopPropagationOnClick: boolean = false
|
||||
export let shortcut: string = ""
|
||||
export let shortcut: string = ''
|
||||
export let notificationsCount: number = 0
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
</script>
|
||||
@@ -60,6 +61,16 @@
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if isCollapsed && notificationsCount > 0}
|
||||
<div class="absolute translate-x-1/2 translate-y-1/2 -top-2 right-1 flex h-fit w-fit">
|
||||
<SideBarNotification notificationCount={notificationsCount} small={true} />
|
||||
</div>
|
||||
{:else if notificationsCount > 0}
|
||||
<div class="ml-auto">
|
||||
<SideBarNotification notificationCount={notificationsCount} small={false} />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
<svelte:fragment slot="text">
|
||||
{#if label}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
export let notificationCount = 0
|
||||
export let small: boolean = false
|
||||
</script>
|
||||
|
||||
{#if !small}
|
||||
<div
|
||||
class="bg-red-500 text-white text-[0.6rem] rounded-md w-5 h-5 flex items-center justify-center"
|
||||
>
|
||||
{notificationCount > 9 ? '9+' : notificationCount}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="bg-red-500 rounded-md w-3 h-3 flex items-center justify-center" />
|
||||
{/if}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user