From 04c631a11aa189246c00f236abc89b9b734ae81e Mon Sep 17 00:00:00 2001 From: Alexander Petric Date: Fri, 15 Nov 2024 03:48:48 -0500 Subject: [PATCH] feat(monitoring): workspace critical alerts (#4684) * critical alert ui * updating ui, backend logic * revert * type check fix npm * checking out cli files from main * moving alert icon * adding sqlx mock data * more sqlx changes * feat(frontend): nodes from flow can be connected directly in expr input through a plug icon (#4652) * Add flow prop picker # Conflicts: # frontend/src/lib/components/propertyPicker/PropPicker.svelte * fix unwanted copy * cleaning * Fix unset context * move button and always display input * fix unwanted proppicker display * update * update * clean all * clean all --------- Co-authored-by: Ruben Fiszel * replace hide/show with toggle * adding mutable setting and navigation to settings to configure channels * merge fix * ee non ee changees * auto-acknowledge when muted * pr comments * fix bad log * user inner modal component * update unaknowledge alerts after acknowledging from modal * aknowledge -> acknowledge * format * adding backend support for workspace critical alerts * immediately check for alerts * immediately check for alerts * adding openapi changes * simplify loading of superadmin/ee * update modal logic * frontend logic update * sqlx prepare * adding mute functionality for workspace critical alerts + show alerts button for instance and workspace settings * npm / rust warnings & errors * reverting non-mac cargo toml * reverting non-mac cargo toml * mute toggles in critical alert ui * ui polish * adding acknowledged_workspace column * sqlx prepare * make sure we wait for all stores to be loaded * adding workspace mute logic to report_critical alert * auto ack recovered alerts * toggle workspace as superadmin and critical alerts in logs menu * adding critical alert button if width < 786px * refresh on change * Move notification to logs (#4698) * Move notification to logs * removing unused button --------- Co-authored-by: Alexander Petric * tailwind typo * auto ack on mute * don't change deref * sqlx prep * avoid renaming db column, keep acknowledged instead of acknowledged_global * hide critical alert menu when not ee * if workspace muted, also acknowledge global when workspace set * removing save button for mute setting and improve ux/responsiveness * fix: deployment callbacks have a concurrency limit of 1 on same path * sqlx prep * ee-repo ref * z-[9999] for modal --------- Co-authored-by: Guilhem Co-authored-by: Ruben Fiszel Co-authored-by: Ruben Fiszel --- ...acb0f4c244c60477427505bf5bd1b104d3bf9.json | 14 ++ ...35e239cf7567d18b8b3fa6608ea3a9d206ca7.json | 18 ++ ...f29876724922f148dad7af35b36efbf70207a.json | 14 -- ...c61296a3ff7489ae12f52a19f9543173ac597.json | 6 + ...c2da733e71f8a253b0122c17b9e021919809e.json | 18 ++ ...5ab9fdff1a7252198e4ae290d8c8f937ff177.json | 54 +++++ ...48cd9ff63e5fdfa1d93d8fbec32a257b6b05e.json | 15 -- ...ec72884f2a1eacf0ab0e0c6ccbf56c654c14a.json | 14 ++ ...fa1848ed3c1336fa4790f45b8189c4ac97d91.json | 15 -- ...5816583f0bae78a0dd3270dedb377dfa944a.json} | 11 +- ...dcc40f463cbc52d94ed9315cf9a547d4c89f2.json | 6 + ...c068b72294cc99ff44ed0a13179df508ebc6a.json | 15 ++ ...43c8ead47a6a531d4b031a1342c9b2f16506.json} | 11 +- ...36caae3fa256fc0803749ec5107632669adb3.json | 14 ++ ...9714c20b767508e27368be6de38d7b18a3a4d.json | 55 +++++ ...3d2b652618ac454f139db738f4499cb4eb079.json | 12 ++ ...46976ed7af2e90270902f001115e023cb947d.json | 12 -- ...25f862dcf039bbb3d75ced9d54c86f53d2580.json | 14 -- ...872c721169d418586be2d15cee6b929049098.json | 22 ++ ...1e906a7868f34c062f154b2ee4f24c57aa5c3.json | 15 ++ backend/ee-repo-ref.txt | 2 +- ...1108201445_alert_add_workspace_id.down.sql | 4 + ...241108201445_alert_add_workspace_id.up.sql | 4 + backend/src/monitor.rs | 41 ++-- backend/windmill-api/openapi.yaml | 112 ++++++++++ backend/windmill-api/src/settings.rs | 77 +------ backend/windmill-api/src/utils.rs | 162 ++++++++++++++ .../windmill-api/src/websocket_triggers.rs | 8 +- backend/windmill-api/src/workspaces.rs | 97 ++++++++- backend/windmill-common/src/utils.rs | 120 +++++++++-- backend/windmill-queue/src/jobs.rs | 8 +- .../lib/components/InstanceSettings.svelte | 23 +- .../lib/components/SuperadminSettings.svelte | 2 +- .../lib/components/common/modal/Modal.svelte | 2 +- .../src/lib/components/instanceSettings.ts | 2 +- .../sidebar/CriticalAlertButton.svelte | 64 ------ .../sidebar/CriticalAlertModal.svelte | 73 ++++++- .../sidebar/CriticalAlertModalInner.svelte | 201 ++++++++++++++---- .../lib/components/sidebar/MenuButton.svelte | 15 +- .../sidebar/SideBarNotification.svelte | 14 ++ .../components/sidebar/SidebarContent.svelte | 81 +++++-- frontend/src/lib/stores.ts | 4 +- .../src/routes/(root)/(logged)/+layout.svelte | 74 ++++--- .../(logged)/workspace_settings/+page.svelte | 66 +++++- 44 files changed, 1250 insertions(+), 361 deletions(-) create mode 100644 backend/.sqlx/query-00588a40dde5189ac1c61505f17acb0f4c244c60477427505bf5bd1b104d3bf9.json create mode 100644 backend/.sqlx/query-044e2b428ee6e2dd4543c87ad8835e239cf7567d18b8b3fa6608ea3a9d206ca7.json delete mode 100644 backend/.sqlx/query-0ee63ef2dd5c88edba2a1f56d31f29876724922f148dad7af35b36efbf70207a.json create mode 100644 backend/.sqlx/query-28987898c7e4b172d466bf08f33c2da733e71f8a253b0122c17b9e021919809e.json create mode 100644 backend/.sqlx/query-344b3a5d9683273a956b5156fed5ab9fdff1a7252198e4ae290d8c8f937ff177.json delete mode 100644 backend/.sqlx/query-3a94ad52c6b7cde844fa868167248cd9ff63e5fdfa1d93d8fbec32a257b6b05e.json create mode 100644 backend/.sqlx/query-4a3a8207627418ba7b7eabaccd7ec72884f2a1eacf0ab0e0c6ccbf56c654c14a.json delete mode 100644 backend/.sqlx/query-4d22084a5d9860832f30e8f08cbfa1848ed3c1336fa4790f45b8189c4ac97d91.json rename backend/.sqlx/{query-0b955f2cff82a2d4ba3840588143e08952f029480d4a42503ecc3c5e70437995.json => query-4d30c5a2894d655741d167f5589f5816583f0bae78a0dd3270dedb377dfa944a.json} (63%) create mode 100644 backend/.sqlx/query-65da41c7ded54cdee8d33211561c068b72294cc99ff44ed0a13179df508ebc6a.json rename backend/.sqlx/{query-cc5ab80241b88c5befea279f16c4ec68cec17b31dcd277b321f652917346496b.json => query-777e7084edf9a5d14f299ff479ae43c8ead47a6a531d4b031a1342c9b2f16506.json} (60%) create mode 100644 backend/.sqlx/query-7c32176755c6ea2b6ae531860d436caae3fa256fc0803749ec5107632669adb3.json create mode 100644 backend/.sqlx/query-7c5a29de07cbe42326a15d4d7fd9714c20b767508e27368be6de38d7b18a3a4d.json create mode 100644 backend/.sqlx/query-a59fae29ebcc9aa53308b777ead3d2b652618ac454f139db738f4499cb4eb079.json delete mode 100644 backend/.sqlx/query-a7c5008aa7ea43d0afac7d9f19846976ed7af2e90270902f001115e023cb947d.json delete mode 100644 backend/.sqlx/query-be3ae231557e794172336bc27d725f862dcf039bbb3d75ced9d54c86f53d2580.json create mode 100644 backend/.sqlx/query-e05dbe046e846c092a96b6b0a9d872c721169d418586be2d15cee6b929049098.json create mode 100644 backend/.sqlx/query-f22168826350797e88153b65a5b1e906a7868f34c062f154b2ee4f24c57aa5c3.json create mode 100644 backend/migrations/20241108201445_alert_add_workspace_id.down.sql create mode 100644 backend/migrations/20241108201445_alert_add_workspace_id.up.sql delete mode 100644 frontend/src/lib/components/sidebar/CriticalAlertButton.svelte create mode 100644 frontend/src/lib/components/sidebar/SideBarNotification.svelte diff --git a/backend/.sqlx/query-00588a40dde5189ac1c61505f17acb0f4c244c60477427505bf5bd1b104d3bf9.json b/backend/.sqlx/query-00588a40dde5189ac1c61505f17acb0f4c244c60477427505bf5bd1b104d3bf9.json new file mode 100644 index 0000000000..c54a4c6bea --- /dev/null +++ b/backend/.sqlx/query-00588a40dde5189ac1c61505f17acb0f4c244c60477427505bf5bd1b104d3bf9.json @@ -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" +} diff --git a/backend/.sqlx/query-044e2b428ee6e2dd4543c87ad8835e239cf7567d18b8b3fa6608ea3a9d206ca7.json b/backend/.sqlx/query-044e2b428ee6e2dd4543c87ad8835e239cf7567d18b8b3fa6608ea3a9d206ca7.json new file mode 100644 index 0000000000..32d381e921 --- /dev/null +++ b/backend/.sqlx/query-044e2b428ee6e2dd4543c87ad8835e239cf7567d18b8b3fa6608ea3a9d206ca7.json @@ -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" +} diff --git a/backend/.sqlx/query-0ee63ef2dd5c88edba2a1f56d31f29876724922f148dad7af35b36efbf70207a.json b/backend/.sqlx/query-0ee63ef2dd5c88edba2a1f56d31f29876724922f148dad7af35b36efbf70207a.json deleted file mode 100644 index 5cb52b6ead..0000000000 --- a/backend/.sqlx/query-0ee63ef2dd5c88edba2a1f56d31f29876724922f148dad7af35b36efbf70207a.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM healthchecks WHERE check_type = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [] - }, - "hash": "0ee63ef2dd5c88edba2a1f56d31f29876724922f148dad7af35b36efbf70207a" -} diff --git a/backend/.sqlx/query-1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597.json b/backend/.sqlx/query-1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597.json index 6189a1fdab..686158d9a1 100644 --- a/backend/.sqlx/query-1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597.json +++ b/backend/.sqlx/query-1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597.json @@ -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 ] }, diff --git a/backend/.sqlx/query-28987898c7e4b172d466bf08f33c2da733e71f8a253b0122c17b9e021919809e.json b/backend/.sqlx/query-28987898c7e4b172d466bf08f33c2da733e71f8a253b0122c17b9e021919809e.json new file mode 100644 index 0000000000..63a46f634c --- /dev/null +++ b/backend/.sqlx/query-28987898c7e4b172d466bf08f33c2da733e71f8a253b0122c17b9e021919809e.json @@ -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" +} diff --git a/backend/.sqlx/query-344b3a5d9683273a956b5156fed5ab9fdff1a7252198e4ae290d8c8f937ff177.json b/backend/.sqlx/query-344b3a5d9683273a956b5156fed5ab9fdff1a7252198e4ae290d8c8f937ff177.json new file mode 100644 index 0000000000..7b47efe873 --- /dev/null +++ b/backend/.sqlx/query-344b3a5d9683273a956b5156fed5ab9fdff1a7252198e4ae290d8c8f937ff177.json @@ -0,0 +1,54 @@ +{ + "db_name": "PostgreSQL", + "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": [ + { + "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": [ + "Bool", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + null, + true + ] + }, + "hash": "344b3a5d9683273a956b5156fed5ab9fdff1a7252198e4ae290d8c8f937ff177" +} diff --git a/backend/.sqlx/query-3a94ad52c6b7cde844fa868167248cd9ff63e5fdfa1d93d8fbec32a257b6b05e.json b/backend/.sqlx/query-3a94ad52c6b7cde844fa868167248cd9ff63e5fdfa1d93d8fbec32a257b6b05e.json deleted file mode 100644 index 5a79a3d793..0000000000 --- a/backend/.sqlx/query-3a94ad52c6b7cde844fa868167248cd9ff63e5fdfa1d93d8fbec32a257b6b05e.json +++ /dev/null @@ -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" -} diff --git a/backend/.sqlx/query-4a3a8207627418ba7b7eabaccd7ec72884f2a1eacf0ab0e0c6ccbf56c654c14a.json b/backend/.sqlx/query-4a3a8207627418ba7b7eabaccd7ec72884f2a1eacf0ab0e0c6ccbf56c654c14a.json new file mode 100644 index 0000000000..4d3b8ab93a --- /dev/null +++ b/backend/.sqlx/query-4a3a8207627418ba7b7eabaccd7ec72884f2a1eacf0ab0e0c6ccbf56c654c14a.json @@ -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" +} diff --git a/backend/.sqlx/query-4d22084a5d9860832f30e8f08cbfa1848ed3c1336fa4790f45b8189c4ac97d91.json b/backend/.sqlx/query-4d22084a5d9860832f30e8f08cbfa1848ed3c1336fa4790f45b8189c4ac97d91.json deleted file mode 100644 index b57d834f87..0000000000 --- a/backend/.sqlx/query-4d22084a5d9860832f30e8f08cbfa1848ed3c1336fa4790f45b8189c4ac97d91.json +++ /dev/null @@ -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" -} diff --git a/backend/.sqlx/query-0b955f2cff82a2d4ba3840588143e08952f029480d4a42503ecc3c5e70437995.json b/backend/.sqlx/query-4d30c5a2894d655741d167f5589f5816583f0bae78a0dd3270dedb377dfa944a.json similarity index 63% rename from backend/.sqlx/query-0b955f2cff82a2d4ba3840588143e08952f029480d4a42503ecc3c5e70437995.json rename to backend/.sqlx/query-4d30c5a2894d655741d167f5589f5816583f0bae78a0dd3270dedb377dfa944a.json index 820e4f45ab..a8c371718b 100644 --- a/backend/.sqlx/query-0b955f2cff82a2d4ba3840588143e08952f029480d4a42503ecc3c5e70437995.json +++ b/backend/.sqlx/query-4d30c5a2894d655741d167f5589f5816583f0bae78a0dd3270dedb377dfa944a.json @@ -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 ORDER BY created_at DESC\n LIMIT $1 OFFSET $2", "describe": { "columns": [ { @@ -27,11 +27,15 @@ "ordinal": 4, "name": "acknowledged", "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "workspace_id", + "type_info": "Text" } ], "parameters": { "Left": [ - "Bool", "Int8", "Int8" ] @@ -41,8 +45,9 @@ false, false, false, + null, true ] }, - "hash": "0b955f2cff82a2d4ba3840588143e08952f029480d4a42503ecc3c5e70437995" + "hash": "4d30c5a2894d655741d167f5589f5816583f0bae78a0dd3270dedb377dfa944a" } diff --git a/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json b/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json index cfef50c160..6aab7e7796 100644 --- a/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json +++ b/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json @@ -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 ] }, diff --git a/backend/.sqlx/query-65da41c7ded54cdee8d33211561c068b72294cc99ff44ed0a13179df508ebc6a.json b/backend/.sqlx/query-65da41c7ded54cdee8d33211561c068b72294cc99ff44ed0a13179df508ebc6a.json new file mode 100644 index 0000000000..e183d3b95c --- /dev/null +++ b/backend/.sqlx/query-65da41c7ded54cdee8d33211561c068b72294cc99ff44ed0a13179df508ebc6a.json @@ -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" +} diff --git a/backend/.sqlx/query-cc5ab80241b88c5befea279f16c4ec68cec17b31dcd277b321f652917346496b.json b/backend/.sqlx/query-777e7084edf9a5d14f299ff479ae43c8ead47a6a531d4b031a1342c9b2f16506.json similarity index 60% rename from backend/.sqlx/query-cc5ab80241b88c5befea279f16c4ec68cec17b31dcd277b321f652917346496b.json rename to backend/.sqlx/query-777e7084edf9a5d14f299ff479ae43c8ead47a6a531d4b031a1342c9b2f16506.json index 18bc159e6c..f25bba999a 100644 --- a/backend/.sqlx/query-cc5ab80241b88c5befea279f16c4ec68cec17b31dcd277b321f652917346496b.json +++ b/backend/.sqlx/query-777e7084edf9a5d14f299ff479ae43c8ead47a6a531d4b031a1342c9b2f16506.json @@ -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_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": [ { @@ -27,10 +27,16 @@ "ordinal": 4, "name": "acknowledged", "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "workspace_id", + "type_info": "Text" } ], "parameters": { "Left": [ + "Text", "Int8", "Int8" ] @@ -40,8 +46,9 @@ false, false, false, + null, true ] }, - "hash": "cc5ab80241b88c5befea279f16c4ec68cec17b31dcd277b321f652917346496b" + "hash": "777e7084edf9a5d14f299ff479ae43c8ead47a6a531d4b031a1342c9b2f16506" } diff --git a/backend/.sqlx/query-7c32176755c6ea2b6ae531860d436caae3fa256fc0803749ec5107632669adb3.json b/backend/.sqlx/query-7c32176755c6ea2b6ae531860d436caae3fa256fc0803749ec5107632669adb3.json new file mode 100644 index 0000000000..cf976de33f --- /dev/null +++ b/backend/.sqlx/query-7c32176755c6ea2b6ae531860d436caae3fa256fc0803749ec5107632669adb3.json @@ -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" +} diff --git a/backend/.sqlx/query-7c5a29de07cbe42326a15d4d7fd9714c20b767508e27368be6de38d7b18a3a4d.json b/backend/.sqlx/query-7c5a29de07cbe42326a15d4d7fd9714c20b767508e27368be6de38d7b18a3a4d.json new file mode 100644 index 0000000000..7145fb2df9 --- /dev/null +++ b/backend/.sqlx/query-7c5a29de07cbe42326a15d4d7fd9714c20b767508e27368be6de38d7b18a3a4d.json @@ -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" +} diff --git a/backend/.sqlx/query-a59fae29ebcc9aa53308b777ead3d2b652618ac454f139db738f4499cb4eb079.json b/backend/.sqlx/query-a59fae29ebcc9aa53308b777ead3d2b652618ac454f139db738f4499cb4eb079.json new file mode 100644 index 0000000000..f12fe52c2b --- /dev/null +++ b/backend/.sqlx/query-a59fae29ebcc9aa53308b777ead3d2b652618ac454f139db738f4499cb4eb079.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE alerts SET acknowledged = true", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "a59fae29ebcc9aa53308b777ead3d2b652618ac454f139db738f4499cb4eb079" +} diff --git a/backend/.sqlx/query-a7c5008aa7ea43d0afac7d9f19846976ed7af2e90270902f001115e023cb947d.json b/backend/.sqlx/query-a7c5008aa7ea43d0afac7d9f19846976ed7af2e90270902f001115e023cb947d.json deleted file mode 100644 index 53ef15ffa5..0000000000 --- a/backend/.sqlx/query-a7c5008aa7ea43d0afac7d9f19846976ed7af2e90270902f001115e023cb947d.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE alerts SET acknowledged = true WHERE acknowledged = false", - "describe": { - "columns": [], - "parameters": { - "Left": [] - }, - "nullable": [] - }, - "hash": "a7c5008aa7ea43d0afac7d9f19846976ed7af2e90270902f001115e023cb947d" -} diff --git a/backend/.sqlx/query-be3ae231557e794172336bc27d725f862dcf039bbb3d75ced9d54c86f53d2580.json b/backend/.sqlx/query-be3ae231557e794172336bc27d725f862dcf039bbb3d75ced9d54c86f53d2580.json deleted file mode 100644 index e726f11fe2..0000000000 --- a/backend/.sqlx/query-be3ae231557e794172336bc27d725f862dcf039bbb3d75ced9d54c86f53d2580.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE alerts SET acknowledged = true WHERE id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [] - }, - "hash": "be3ae231557e794172336bc27d725f862dcf039bbb3d75ced9d54c86f53d2580" -} diff --git a/backend/.sqlx/query-e05dbe046e846c092a96b6b0a9d872c721169d418586be2d15cee6b929049098.json b/backend/.sqlx/query-e05dbe046e846c092a96b6b0a9d872c721169d418586be2d15cee6b929049098.json new file mode 100644 index 0000000000..c0875942d3 --- /dev/null +++ b/backend/.sqlx/query-e05dbe046e846c092a96b6b0a9d872c721169d418586be2d15cee6b929049098.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT mute_critical_alerts FROM workspace_settings WHERE workspace_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mute_critical_alerts", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + true + ] + }, + "hash": "e05dbe046e846c092a96b6b0a9d872c721169d418586be2d15cee6b929049098" +} diff --git a/backend/.sqlx/query-f22168826350797e88153b65a5b1e906a7868f34c062f154b2ee4f24c57aa5c3.json b/backend/.sqlx/query-f22168826350797e88153b65a5b1e906a7868f34c062f154b2ee4f24c57aa5c3.json new file mode 100644 index 0000000000..17b42cb469 --- /dev/null +++ b/backend/.sqlx/query-f22168826350797e88153b65a5b1e906a7868f34c062f154b2ee4f24c57aa5c3.json @@ -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" +} diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 55cf6b995c..caed423d9a 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -177895136549cdb7c2f2a26ba3b654d76e60d845 \ No newline at end of file +51dcbf93b0d127af9f33fa346cc63fcd2475d4fa diff --git a/backend/migrations/20241108201445_alert_add_workspace_id.down.sql b/backend/migrations/20241108201445_alert_add_workspace_id.down.sql new file mode 100644 index 0000000000..ff3d6821b5 --- /dev/null +++ b/backend/migrations/20241108201445_alert_add_workspace_id.down.sql @@ -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; diff --git a/backend/migrations/20241108201445_alert_add_workspace_id.up.sql b/backend/migrations/20241108201445_alert_add_workspace_id.up.sql new file mode 100644 index 0000000000..17ad1c4fe1 --- /dev/null +++ b/backend/migrations/20241108201445_alert_add_workspace_id.up.sql @@ -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; \ No newline at end of file diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index b40c89c766..43c5867dce 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -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 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?; } } diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 9d99cf960d..fb0e977a80 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1701,6 +1701,8 @@ paths: type: string default_scripts: $ref: "#/components/schemas/WorkspaceDefaultScripts" + mute_critical_alerts: + type: boolean required: - code_completion_enabled - automatic_billing @@ -2717,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: [] @@ -12945,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 diff --git a/backend/windmill-api/src/settings.rs b/backend/windmill-api/src/settings.rs index 5dc46e4382..673acb132c 100644 --- a/backend/windmill-api/src/settings.rs +++ b/backend/windmill-api/src/settings.rs @@ -434,67 +434,15 @@ pub async fn test_critical_channels() -> Result { 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, - acknowledged: Option, -} - -#[cfg(feature = "enterprise")] -#[derive(Deserialize)] -pub struct AlertQueryParams { - pub page: Option, - pub page_size: Option, - pub acknowledged: Option, -} - #[cfg(feature = "enterprise")] pub async fn get_critical_alerts( Extension(db): Extension, authed: ApiAuthed, - Query(params): Query, -) -> JsonResult> { + Query(params): Query, +) -> JsonResult> { 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 { 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 { 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"))] diff --git a/backend/windmill-api/src/utils.rs b/backend/windmill-api/src/utils.rs index ea4fccc160..4302189c4e 100644 --- a/backend/windmill-api/src/utils.rs +++ b/backend/windmill-api/src/utils.rs @@ -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, @@ -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, + acknowledged: Option, + workspace_id: Option, +} + +#[cfg(feature = "enterprise")] +#[derive(Deserialize, Debug)] +pub struct AlertQueryParams { + pub page: Option, + pub page_size: Option, + pub acknowledged: Option, +} + +#[cfg(feature = "enterprise")] +pub async fn get_critical_alerts( + db: DB, + params: AlertQueryParams, + workspace_id: Option, +) -> JsonResult> { + 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, + id: i32, +) -> error::Result { + 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, +) -> error::Result { + 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()) +} diff --git a/backend/windmill-api/src/websocket_triggers.rs b/backend/windmill-api/src/websocket_triggers.rs index e10cd94c78..ecdc990061 100644 --- a/backend/windmill-api/src/websocket_triggers.rs +++ b/backend/windmill-api/src/websocket_triggers.rs @@ -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; }; } }, diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index ddd227ecbb..36bef54655 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -119,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")] { @@ -180,6 +184,7 @@ pub struct WorkspaceSettings { pub default_app: Option, pub automatic_billing: bool, pub default_scripts: Option, + pub mute_critical_alerts: Option, } #[derive(FromRow, Serialize, Debug)] @@ -3075,3 +3080,93 @@ async fn get_usage(Extension(db): Extension, Path(w_id): Path) -> Re .unwrap_or(0); Ok(usage.to_string()) } + +#[cfg(feature = "enterprise")] +pub async fn get_critical_alerts( + Extension(db): Extension, + Path(w_id): Path, + authed: ApiAuthed, + Query(params): Query, +) -> JsonResult> { + 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, + Path((w_id, id)): Path<(String, i32)>, + authed: ApiAuthed, +) -> Result { + 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, + Path(w_id): Path, + authed: ApiAuthed, +) -> Result { + 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, +} + +#[cfg(feature = "enterprise")] +async fn mute_critical_alerts( + Extension(db): Extension, + Path(w_id): Path, + ApiAuthed { is_admin, username, .. }: ApiAuthed, + Json(m_r): Json, +) -> Result { + 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()) +} \ No newline at end of file diff --git a/backend/windmill-common/src/utils.rs b/backend/windmill-common/src/utils.rs index 3223ffb75f..1454e8d9ed 100644 --- a/backend/windmill-common/src/utils.rs +++ b/backend/windmill-common/src/utils.rs @@ -29,8 +29,11 @@ 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; + +#[cfg(feature = "enterprise")] +use crate::worker::CLOUD_HOSTED; lazy_static::lazy_static! { pub static ref HTTP_CLIENT: Client = reqwest::ClientBuilder::new() @@ -249,15 +252,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; 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 +294,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 { + 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)) + } + } } diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index dd91226c7e..20122fd878 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -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; } } diff --git a/frontend/src/lib/components/InstanceSettings.svelte b/frontend/src/lib/components/InstanceSettings.svelte index f9b2c90151..213318e6fe 100644 --- a/frontend/src/lib/components/InstanceSettings.svelte +++ b/frontend/src/lib/components/InstanceSettings.svelte @@ -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 = {} let initialOauths: Record = {} @@ -814,6 +815,26 @@ placeholder={setting.placeholder} bind:value={values[setting.key]} /> + {:else if setting.key == 'critical_alert_mute_ui'} +
+ +
+ +
+
{:else if setting.fieldType == 'critical_error_channels'}
diff --git a/frontend/src/lib/components/SuperadminSettings.svelte b/frontend/src/lib/components/SuperadminSettings.svelte index e47466a02a..f5e88116f6 100644 --- a/frontend/src/lib/components/SuperadminSettings.svelte +++ b/frontend/src/lib/components/SuperadminSettings.svelte @@ -325,7 +325,7 @@
- + diff --git a/frontend/src/lib/components/common/modal/Modal.svelte b/frontend/src/lib/components/common/modal/Modal.svelte index 718a52000a..300499d71a 100644 --- a/frontend/src/lib/components/common/modal/Modal.svelte +++ b/frontend/src/lib/components/common/modal/Modal.svelte @@ -44,7 +44,7 @@
(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" >
= { { 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. Learn more', + '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. Learn more', key: 'critical_error_channels', fieldType: 'critical_error_channels', storage: 'setting', diff --git a/frontend/src/lib/components/sidebar/CriticalAlertButton.svelte b/frontend/src/lib/components/sidebar/CriticalAlertButton.svelte deleted file mode 100644 index 9c6d3db68d..0000000000 --- a/frontend/src/lib/components/sidebar/CriticalAlertButton.svelte +++ /dev/null @@ -1,64 +0,0 @@ - - -{#if !disabled} - -
- -
- - {#if label} - {label} - {/if} - -
-{/if} diff --git a/frontend/src/lib/components/sidebar/CriticalAlertModal.svelte b/frontend/src/lib/components/sidebar/CriticalAlertModal.svelte index 68d7bfbffb..249deecccf 100644 --- a/frontend/src/lib/components/sidebar/CriticalAlertModal.svelte +++ b/frontend/src/lib/components/sidebar/CriticalAlertModal.svelte @@ -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 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() } - + diff --git a/frontend/src/lib/components/sidebar/CriticalAlertModalInner.svelte b/frontend/src/lib/components/sidebar/CriticalAlertModalInner.svelte index 34ff0ffa35..5b9a53992c 100644 --- a/frontend/src/lib/components/sidebar/CriticalAlertModalInner.svelte +++ b/frontend/src/lib/components/sidebar/CriticalAlertModalInner.svelte @@ -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) + }
- {#if !hasCriticalAlertChannels} -
- -

- No critical alert channels are set up. Go to the - Instance Settings - page to configure critical alert channels. -

+
+
+ {#if !hasCriticalAlertChannels && $superadmin} +
+ +

+ No critical alert channels are set up. Go to the + Instance Settings + page to configure critical alert channels. +

+
+ {/if}
- {/if} - -
-
- +
+ +
+ + {#if $superadmin} +
+ +
+ {/if} + +
-
+ {#if $superadmin} +
+ +
+ {/if} - -
+
+ +
+ - -
-
- - Page {page} - -
-
- +
+
+ +
+ +
@@ -153,11 +261,14 @@ Type Message Created At + {#if $superadmin} + Workspace + {/if} Acknowledge - {#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} @@ -174,6 +285,9 @@ {message} {formatDate(created_at)} + {#if $superadmin} + {workspace_id ? workspace_id : 'global'} + {/if}
{#if !acknowledged} @@ -196,6 +310,11 @@
+
+ + Page {page} + +
{#if alerts.length === 0}

No critical alerts available.

diff --git a/frontend/src/lib/components/sidebar/MenuButton.svelte b/frontend/src/lib/components/sidebar/MenuButton.svelte index 35cd762332..5b3c048d0e 100644 --- a/frontend/src/lib/components/sidebar/MenuButton.svelte +++ b/frontend/src/lib/components/sidebar/MenuButton.svelte @@ -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() @@ -60,6 +61,16 @@ {/if} + + {#if isCollapsed && notificationsCount > 0} +
+ +
+ {:else if notificationsCount > 0} +
+ +
+ {/if} {#if label} diff --git a/frontend/src/lib/components/sidebar/SideBarNotification.svelte b/frontend/src/lib/components/sidebar/SideBarNotification.svelte new file mode 100644 index 0000000000..d935769972 --- /dev/null +++ b/frontend/src/lib/components/sidebar/SideBarNotification.svelte @@ -0,0 +1,14 @@ + + +{#if !small} +
+ {notificationCount > 9 ? '9+' : notificationCount} +
+{:else} +
+{/if} diff --git a/frontend/src/lib/components/sidebar/SidebarContent.svelte b/frontend/src/lib/components/sidebar/SidebarContent.svelte index 3d45f895ff..4e34071256 100644 --- a/frontend/src/lib/components/sidebar/SidebarContent.svelte +++ b/frontend/src/lib/components/sidebar/SidebarContent.svelte @@ -1,6 +1,13 @@
@@ -435,17 +448,6 @@ {/if}
- {#if $superadmin && $enterpriseLicense} - openCriticalAlertsModal()} - {numUnacknowledgedCriticalAlerts} - {isCollapsed} - label="Critical Alerts" - class="!text-xs" - disabled={numUnacknowledgedCriticalAlerts === 0} - /> - {/if}
@@ -460,7 +462,12 @@ />
- +
- +
diff --git a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte index 71d868779e..6e9ba1d116 100644 --- a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte @@ -15,7 +15,13 @@ import Tooltip from '$lib/components/Tooltip.svelte' import WorkspaceUserSettings from '$lib/components/settings/WorkspaceUserSettings.svelte' import { WORKSPACE_SHOW_SLACK_CMD, WORKSPACE_SHOW_WEBHOOK_CLI_SYNC } from '$lib/consts' - import { OauthService, WorkspaceService, JobService, ResourceService } from '$lib/gen' + import { + OauthService, + WorkspaceService, + JobService, + ResourceService, + SettingService + } from '$lib/gen' import { enterpriseLicense, copilotInfo, @@ -23,7 +29,8 @@ userStore, usersWorkspaceStore, workspaceStore, - hubBaseUrlStore + hubBaseUrlStore, + isCriticalAlertsUIOpen } from '$lib/stores' import { sendUserToast } from '$lib/toast' import { emptyString, tryEvery } from '$lib/utils' @@ -102,6 +109,8 @@ let errorHandlerItemKind: 'flow' | 'script' = 'script' let errorHandlerExtraArgs: Record = {} let errorHandlerMutedOnCancel: boolean | undefined = undefined + let criticalAlertUIMuted: boolean | undefined = undefined + let initialCriticalAlertUIMuted: boolean | undefined = undefined let aiResourceInitialPath: string | undefined = undefined let aiResourceInitialProvider: string | undefined = undefined @@ -391,6 +400,8 @@ errorHandlerScriptPath = (settings.error_handler ?? '').split('/').slice(1).join('/') errorHandlerInitialScriptPath = errorHandlerScriptPath errorHandlerMutedOnCancel = settings.error_handler_muted_on_cancel + criticalAlertUIMuted = settings.mute_critical_alerts + initialCriticalAlertUIMuted = settings.mute_critical_alerts if (emptyString($enterpriseLicense)) { errorHandlerSelected = 'custom' } else { @@ -587,6 +598,22 @@ timeout: 5000 }) } + + async function editCriticalAlertMuteSetting() { + await SettingService.workspaceMuteCriticalAlertsUi({ + workspace: $workspaceStore!, + requestBody: { + mute_critical_alerts: criticalAlertUIMuted + } + }) + sendUserToast( + `Critical alert UI mute setting for workspace is set to ${criticalAlertUIMuted}\nreloading page...` + ) + // reload page after change of setting + setTimeout(() => { + window.location.reload() + }, 3000) + } @@ -972,6 +999,41 @@ Save +
+
+
Workspace Critical Alerts
+
+ Critical alerts within the scope of a workspace are sent to the workspace admins through + a UI notification. + Learn more. +
+
+ + + +
+
+
{:else if tab == 'ai'}