fix: move alert config from config table to global_settings (#8762)

* feat: move alert config from config table to global_settings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref.txt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: rename alert setting to alert_job_queue_waiting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref.txt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add CLI unit tests for pullInstanceConfigs/pushInstanceConfigs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref.txt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref.txt to merged main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-04-08 11:54:44 -04:00
committed by GitHub
parent c69f10d20d
commit fa668707c0
11 changed files with 675 additions and 31 deletions

View File

@@ -15,7 +15,7 @@
]
},
"nullable": [
null
true
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"

View File

@@ -1 +1 @@
9ea1259fa6a95e6b24d1523b2f6ec6ae0042ecfe
9ea1259fa6a95e6b24d1523b2f6ec6ae0042ecfe

View File

@@ -0,0 +1,6 @@
-- Move alert config back from global_settings to the config table.
INSERT INTO config (name, config)
SELECT 'alert__job_queue_waiting', value FROM global_settings WHERE name = 'alert_job_queue_waiting'
ON CONFLICT (name) DO UPDATE SET config = EXCLUDED.config;
DELETE FROM global_settings WHERE name = 'alert_job_queue_waiting';

View File

@@ -0,0 +1,7 @@
-- Move alert config from the generic config table to global_settings
-- where it belongs alongside other instance-level settings.
INSERT INTO global_settings (name, value)
SELECT 'alert_job_queue_waiting', config FROM config WHERE name = 'alert__job_queue_waiting'
ON CONFLICT (name) DO UPDATE SET value = EXCLUDED.value;
DELETE FROM config WHERE name = 'alert__job_queue_waiting';

View File

@@ -1151,3 +1151,125 @@ async fn test_replace_mode_protects_jwt_secret_and_rsa_keys(db: Pool<Postgres>)
);
assert!(get_global_setting(&db, "normal_setting").await.is_none());
}
// ========================================================================
// Alert config migration tests
// ========================================================================
#[sqlx::test(fixtures("base"))]
async fn test_alert_config_in_global_settings_roundtrip(db: Pool<Postgres>) {
clear_settings_and_configs(&db).await;
let alert_value = serde_json::json!({
"alerts": [
{
"name": "Test Alert",
"tags_to_monitor": ["default", "gpu"],
"jobs_num_threshold": 5,
"alert_cooldown_seconds": 300,
"alert_time_threshold_seconds": 60
}
]
});
// Insert alert_config into global_settings
insert_global_setting(&db, "alert_job_queue_waiting", alert_value.clone()).await;
// Verify it appears in InstanceConfig global_settings (via extra)
let config = InstanceConfig::from_db(&db).await.unwrap();
assert_eq!(
config.global_settings.extra["alert_job_queue_waiting"], alert_value,
"alert_config should appear in global_settings extra"
);
// Verify it does NOT appear in worker_configs
assert!(
!config
.worker_configs
.contains_key("alert_job_queue_waiting"),
"alert_config should not appear in worker_configs"
);
// Modify the alert_config
let updated_value = serde_json::json!({
"alerts": [
{
"name": "Updated Alert",
"tags_to_monitor": ["batch"],
"jobs_num_threshold": 10,
"alert_cooldown_seconds": 600,
"alert_time_threshold_seconds": 120
}
]
});
let current = config.global_settings.to_settings_map();
let mut desired = current.clone();
desired.insert("alert_job_queue_waiting".to_string(), updated_value.clone());
let diff = diff_global_settings(&current, &desired, ApplyMode::Merge);
assert!(
diff.upserts.contains_key("alert_job_queue_waiting"),
"alert_config change should be detected in diff"
);
apply_settings_diff(&db, &diff).await.unwrap();
// Re-read and verify the update
let config2 = InstanceConfig::from_db(&db).await.unwrap();
assert_eq!(
config2.global_settings.extra["alert_job_queue_waiting"],
updated_value
);
}
#[sqlx::test(fixtures("base"))]
async fn test_alert_config_not_in_worker_configs(db: Pool<Postgres>) {
clear_settings_and_configs(&db).await;
// Insert alert_config in global_settings (the correct location)
insert_global_setting(
&db,
"alert_job_queue_waiting",
serde_json::json!({"alerts": []}),
)
.await;
// Also insert a real worker config
insert_config(
&db,
"worker__default",
serde_json::json!({"worker_tags": ["default"]}),
)
.await;
let config = InstanceConfig::from_db(&db).await.unwrap();
// alert_config should be in global_settings, not worker_configs
assert!(
config
.global_settings
.extra
.contains_key("alert_job_queue_waiting"),
"alert_config should be in global_settings.extra"
);
assert_eq!(config.worker_configs.len(), 1);
assert!(
config.worker_configs.contains_key("default"),
"only the real worker config should be in worker_configs"
);
}
#[sqlx::test(fixtures("base"))]
async fn test_no_alert_in_config_table_after_migration(db: Pool<Postgres>) {
// After the migration runs, no alert__* entries should remain in the config table
let rows: Vec<(String,)> = sqlx::query_as("SELECT name FROM config WHERE name LIKE 'alert__%'")
.fetch_all(&db)
.await
.unwrap();
assert!(
rows.is_empty(),
"No alert entries should remain in config table after migration, found: {:?}",
rows.iter().map(|(n,)| n.as_str()).collect::<Vec<_>>()
);
}

View File

@@ -0,0 +1,139 @@
use serde_json::json;
use sqlx::{Pool, Postgres};
use windmill_test_utils::*;
fn client() -> reqwest::Client {
reqwest::Client::new()
}
fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
builder.header("Authorization", "Bearer SECRET_TOKEN")
}
fn assert_2xx(status: u16, body: &str, endpoint: &str) {
assert!(
(200..300).contains(&status),
"{endpoint} returned {status}: {body}",
);
}
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_worker_group_crud(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let base = format!("http://localhost:{port}/api/configs");
// Create a worker group
let resp = authed(client().post(format!("{base}/update/worker__test_group")))
.json(&json!({"worker_tags": ["test_tag"]}))
.send()
.await?;
assert_2xx(
resp.status().as_u16(),
&resp.text().await?,
"POST /configs/update/worker__test_group",
);
// Verify it exists via get
let resp = authed(client().get(format!("{base}/get/worker__test_group")))
.send()
.await?;
let status = resp.status().as_u16();
let body = resp.text().await?;
assert_2xx(status, &body, "GET /configs/get/worker__test_group");
let config: serde_json::Value = serde_json::from_str(&body)?;
assert_eq!(config["worker_tags"], json!(["test_tag"]));
// Verify it appears in list_worker_groups with prefix stripped
let resp = authed(client().get(format!("{base}/list_worker_groups")))
.send()
.await?;
let body = resp.text().await?;
let groups: Vec<serde_json::Value> = serde_json::from_str(&body)?;
let test_group = groups.iter().find(|g| g["name"] == "test_group");
assert!(
test_group.is_some(),
"test_group should appear in list_worker_groups with prefix stripped"
);
// Delete the worker group
let resp = authed(client().delete(format!("{base}/update/worker__test_group")))
.send()
.await?;
assert_2xx(
resp.status().as_u16(),
&resp.text().await?,
"DELETE /configs/update/worker__test_group",
);
// Verify it's gone from list_worker_groups
let resp = authed(client().get(format!("{base}/list_worker_groups")))
.send()
.await?;
let body = resp.text().await?;
let groups: Vec<serde_json::Value> = serde_json::from_str(&body)?;
let test_group = groups.iter().find(|g| g["name"] == "test_group");
assert!(
test_group.is_none(),
"test_group should be gone after deletion"
);
Ok(())
}
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_list_worker_groups_excludes_non_worker_entries(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let base = format!("http://localhost:{port}/api/configs");
// Insert a non-worker entry directly into the config table
sqlx::query("INSERT INTO config (name, config) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET config = EXCLUDED.config")
.bind("test__other_entry")
.bind(json!({"some_key": "some_value"}))
.execute(&db)
.await?;
// list_worker_groups should NOT return it
let resp = authed(client().get(format!("{base}/list_worker_groups")))
.send()
.await?;
let body = resp.text().await?;
let groups: Vec<serde_json::Value> = serde_json::from_str(&body)?;
let other_entry = groups
.iter()
.find(|g| g["name"] == "test__other_entry" || g["name"] == "other_entry");
assert!(
other_entry.is_none(),
"non-worker entries should not appear in list_worker_groups"
);
// Clean up
sqlx::query("DELETE FROM config WHERE name = $1")
.bind("test__other_entry")
.execute(&db)
.await?;
Ok(())
}
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_no_alert_entries_in_config_table_after_migration(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
// After migration, no alert__* entries should remain in the config table
let rows: Vec<(String,)> = sqlx::query_as("SELECT name FROM config WHERE name LIKE 'alert__%'")
.fetch_all(&db)
.await?;
assert!(
rows.is_empty(),
"No alert entries should remain in config table after migration, found: {:?}",
rows.iter().map(|(n,)| n.as_str()).collect::<Vec<_>>()
);
Ok(())
}

View File

@@ -114,3 +114,89 @@ async fn test_settings_2xx(db: Pool<Postgres>) -> anyhow::Result<()> {
Ok(())
}
#[sqlx::test(migrations = "../migrations", fixtures("base"))]
async fn test_alert_job_queue_waiting_in_global_settings(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let settings_base = format!("http://localhost:{port}/api/settings");
let configs_base = format!("http://localhost:{port}/api/configs");
let alert_payload = json!({
"alerts": [
{
"name": "Test Alert",
"tags_to_monitor": ["default", "gpu"],
"jobs_num_threshold": 5,
"alert_cooldown_seconds": 300,
"alert_time_threshold_seconds": 60
}
]
});
// Set alert_job_queue_waiting in global_settings
let resp = authed(client().post(format!("{settings_base}/global/alert_job_queue_waiting")))
.json(&json!({"value": alert_payload}))
.send()
.await?;
assert_2xx(
resp.status().as_u16(),
&resp.text().await?,
"POST /settings/global/alert_job_queue_waiting",
);
// Read it back
let resp = authed(client().get(format!("{settings_base}/global/alert_job_queue_waiting")))
.send()
.await?;
let status = resp.status().as_u16();
let body = resp.text().await?;
assert_2xx(
status,
&body,
"GET /settings/global/alert_job_queue_waiting",
);
let value: serde_json::Value = serde_json::from_str(&body)?;
assert_eq!(value["alerts"][0]["name"], "Test Alert");
assert_eq!(value["alerts"][0]["jobs_num_threshold"], 5);
// Verify alert_job_queue_waiting does NOT appear in list_worker_groups
let resp = authed(client().get(format!("{configs_base}/list_worker_groups")))
.send()
.await?;
let body = resp.text().await?;
let groups: Vec<serde_json::Value> = serde_json::from_str(&body)?;
let alert_in_groups = groups.iter().find(|g| {
let name = g["name"].as_str().unwrap_or("");
name.contains("alert")
});
assert!(
alert_in_groups.is_none(),
"alert_job_queue_waiting should not appear in worker groups"
);
// Delete by setting to null
let resp = authed(client().post(format!("{settings_base}/global/alert_job_queue_waiting")))
.json(&json!({"value": null}))
.send()
.await?;
assert_2xx(
resp.status().as_u16(),
&resp.text().await?,
"POST /settings/global/alert_job_queue_waiting (null)",
);
// Verify it reads back as null
let resp = authed(client().get(format!("{settings_base}/global/alert_job_queue_waiting")))
.send()
.await?;
let body = resp.text().await?;
let value: serde_json::Value = serde_json::from_str(&body)?;
assert!(
value.is_null(),
"alert_job_queue_waiting should be null after deletion"
);
Ok(())
}

View File

@@ -66,6 +66,7 @@ pub const GITHUB_ENTERPRISE_APP_SETTING: &str = "github_enterprise_app";
pub const INSTANCE_EVENTS_WEBHOOK_SETTING: &str = "instance_events_webhook";
pub const WORKSPACE_REGISTRIES_SETTING: &str = "workspace_registries";
pub const RESTART_COORDINATION_SETTING: &str = "_restart_coordination";
pub const ALERT_CONFIG_SETTING: &str = "alert_job_queue_waiting";
use std::sync::Arc;
use tokio::sync::RwLock;

View File

@@ -10,7 +10,6 @@ import { AIConfig, Config, GlobalSetting } from "../../gen/types.gen.ts";
import { compareInstanceObjects, InstanceSyncOptions } from "../commands/instance/instance.ts";
import { isSuperset } from "../types.ts";
import { deepEqual } from "../utils/utils.ts";
import { removeWorkerPrefix } from "../commands/worker-groups/worker-groups.ts";
import { decrypt, encrypt } from "../utils/local_encryption.ts";
// New grouped config interfaces
@@ -625,12 +624,7 @@ export async function pullInstanceConfigs(
opts: InstanceSyncOptions,
preview = false
) {
const remoteConfigs = (await wmill.listConfigs()).map((x) => {
return {
...x,
name: removeWorkerPrefix(x.name),
};
});
const remoteConfigs = await wmill.listWorkerGroups();
if (preview) {
const localConfigs: Config[] = await readLocalConfigs(opts);
@@ -658,12 +652,7 @@ export async function pushInstanceConfigs(
opts: InstanceSyncOptions,
preview: boolean = false
) {
const remoteConfigs = (await wmill.listConfigs()).map((x) => {
return {
...x,
name: removeWorkerPrefix(x.name),
};
});
const remoteConfigs = await wmill.listWorkerGroups();
const localConfigs = await readLocalConfigs(opts);
if (preview) {
@@ -682,9 +671,7 @@ export async function pushInstanceConfigs(
}
try {
await wmill.updateConfig({
name: config.name.startsWith("worker__")
? config.name
: `worker__${config.name}`,
name: `worker__${config.name}`,
requestBody: config.config,
});
} catch (err) {
@@ -698,7 +685,7 @@ export async function pushInstanceConfigs(
if (!localMatch) {
try {
await wmill.deleteConfig({
name: removeConfig.name,
name: `worker__${removeConfig.name}`,
});
} catch (err) {
log.error(`Failed to delete config ${removeConfig.name}: ${err}`);

View File

@@ -0,0 +1,297 @@
/**
* Unit tests for pullInstanceConfigs / pushInstanceConfigs in settings.ts.
*
* Verifies that:
* - pullInstanceConfigs writes only worker group configs (no alerts)
* - pushInstanceConfigs calls updateConfig with worker__ prefix
* - pushInstanceConfigs calls deleteConfig with worker__ prefix for removed configs
* - pushInstanceConfigs skips unchanged configs
*/
import { expect, test, describe, beforeEach, afterEach, mock } from "bun:test";
import { writeFile, readFile, mkdir, rm } from "node:fs/promises";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { stringify as yamlStringify } from "yaml";
// Track calls to mocked wmill functions
let listWorkerGroupsResult: any[] = [];
let updateConfigCalls: { name: string; requestBody: any }[] = [];
let deleteConfigCalls: { name: string }[] = [];
// Mock the wmill module before importing settings.ts
mock.module("../gen/services.gen.ts", () => ({
listWorkerGroups: async () => listWorkerGroupsResult,
updateConfig: async (args: { name: string; requestBody: any }) => {
updateConfigCalls.push(args);
},
deleteConfig: async (args: { name: string }) => {
deleteConfigCalls.push(args);
},
listConfigs: async () => {
throw new Error("listConfigs should not be called");
},
}));
import {
pullInstanceConfigs,
pushInstanceConfigs,
readLocalConfigs,
} from "../src/core/settings.ts";
describe("instance configs", () => {
let tempDir: string;
beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), "wm-config-test-"));
listWorkerGroupsResult = [];
updateConfigCalls = [];
deleteConfigCalls = [];
});
afterEach(async () => {
await rm(tempDir, { recursive: true, force: true });
});
// =========================================================================
// readLocalConfigs
// =========================================================================
describe("readLocalConfigs", () => {
test("reads configs from instance_configs.yaml", async () => {
const configs = [
{ name: "default", config: { worker_tags: ["deno", "bun"] } },
{ name: "gpu", config: { dedicated_worker: "ws:f/gpu_script" } },
];
await writeFile(
join(tempDir, "instance_configs.yaml"),
yamlStringify(configs),
"utf-8"
);
const result = await readLocalConfigs({
prefix: tempDir,
folderPerInstance: true,
prefixSettings: true,
});
expect(result).toEqual(configs);
});
test("returns empty array when file does not exist", async () => {
const result = await readLocalConfigs({
prefix: tempDir,
folderPerInstance: true,
prefixSettings: true,
});
expect(result).toEqual([]);
});
});
// =========================================================================
// pullInstanceConfigs
// =========================================================================
describe("pullInstanceConfigs", () => {
test("writes worker group configs to instance_configs.yaml", async () => {
listWorkerGroupsResult = [
{ name: "default", config: { worker_tags: ["deno", "bun"] } },
{ name: "native", config: { worker_tags: ["nativets"] } },
];
// readLocalConfigs sets instanceConfigsPath when prefix is used
const opts = {
prefix: tempDir,
folderPerInstance: true,
prefixSettings: true,
};
await readLocalConfigs(opts);
await pullInstanceConfigs(opts);
const content = await readFile(
join(tempDir, "instance_configs.yaml"),
"utf-8"
);
expect(content).toContain("default");
expect(content).toContain("native");
expect(content).toContain("deno");
// Should not contain alert entries (listWorkerGroups filters them)
expect(content).not.toContain("alert");
});
test("preview mode returns change count without writing file", async () => {
listWorkerGroupsResult = [
{ name: "default", config: { worker_tags: ["deno"] } },
];
const changes = await pullInstanceConfigs(
{
prefix: tempDir,
folderPerInstance: true,
prefixSettings: true,
},
true
);
// One remote config not in local = 1 change
expect(changes).toBe(1);
});
test("preview mode returns 0 when remote matches local", async () => {
const configs = [
{ name: "default", config: { worker_tags: ["deno"] } },
];
listWorkerGroupsResult = configs;
await writeFile(
join(tempDir, "instance_configs.yaml"),
yamlStringify(configs),
"utf-8"
);
const changes = await pullInstanceConfigs(
{
prefix: tempDir,
folderPerInstance: true,
prefixSettings: true,
},
true
);
expect(changes).toBe(0);
});
});
// =========================================================================
// pushInstanceConfigs
// =========================================================================
describe("pushInstanceConfigs", () => {
test("calls updateConfig with worker__ prefix for new configs", async () => {
listWorkerGroupsResult = [];
const localConfigs = [
{ name: "mygroup", config: { worker_tags: ["python3"] } },
];
await writeFile(
join(tempDir, "instance_configs.yaml"),
yamlStringify(localConfigs),
"utf-8"
);
await pushInstanceConfigs({
prefix: tempDir,
folderPerInstance: true,
prefixSettings: true,
});
expect(updateConfigCalls).toHaveLength(1);
expect(updateConfigCalls[0].name).toBe("worker__mygroup");
expect(updateConfigCalls[0].requestBody).toEqual({
worker_tags: ["python3"],
});
});
test("calls deleteConfig with worker__ prefix for removed configs", async () => {
listWorkerGroupsResult = [
{ name: "old_group", config: { worker_tags: ["bash"] } },
];
// Empty local configs = old_group should be deleted
await writeFile(
join(tempDir, "instance_configs.yaml"),
yamlStringify([]),
"utf-8"
);
await pushInstanceConfigs({
prefix: tempDir,
folderPerInstance: true,
prefixSettings: true,
});
expect(deleteConfigCalls).toHaveLength(1);
expect(deleteConfigCalls[0].name).toBe("worker__old_group");
});
test("skips unchanged configs", async () => {
const configs = [
{ name: "default", config: { worker_tags: ["deno"] } },
];
listWorkerGroupsResult = configs;
await writeFile(
join(tempDir, "instance_configs.yaml"),
yamlStringify(configs),
"utf-8"
);
await pushInstanceConfigs({
prefix: tempDir,
folderPerInstance: true,
prefixSettings: true,
});
expect(updateConfigCalls).toHaveLength(0);
expect(deleteConfigCalls).toHaveLength(0);
});
test("updates changed configs and deletes removed ones", async () => {
listWorkerGroupsResult = [
{ name: "keep", config: { worker_tags: ["old_tag"] } },
{ name: "remove_me", config: { worker_tags: ["bash"] } },
];
const localConfigs = [
{ name: "keep", config: { worker_tags: ["new_tag"] } },
{ name: "add_me", config: { worker_tags: ["python3"] } },
];
await writeFile(
join(tempDir, "instance_configs.yaml"),
yamlStringify(localConfigs),
"utf-8"
);
await pushInstanceConfigs({
prefix: tempDir,
folderPerInstance: true,
prefixSettings: true,
});
// "keep" was changed, "add_me" is new
expect(updateConfigCalls).toHaveLength(2);
const updateNames = updateConfigCalls.map((c) => c.name).sort();
expect(updateNames).toEqual(["worker__add_me", "worker__keep"]);
// "remove_me" was deleted
expect(deleteConfigCalls).toHaveLength(1);
expect(deleteConfigCalls[0].name).toBe("worker__remove_me");
});
test("preview mode returns change count without calling API", async () => {
listWorkerGroupsResult = [
{ name: "default", config: { worker_tags: ["deno"] } },
];
const localConfigs = [
{ name: "default", config: { worker_tags: ["bun"] } },
{ name: "new_group", config: { worker_tags: ["go"] } },
];
await writeFile(
join(tempDir, "instance_configs.yaml"),
yamlStringify(localConfigs),
"utf-8"
);
const changes = await pushInstanceConfigs(
{
prefix: tempDir,
folderPerInstance: true,
prefixSettings: true,
},
true
);
// "default" changed + "new_group" added = 2 changes
expect(changes).toBe(2);
expect(updateConfigCalls).toHaveLength(0);
expect(deleteConfigCalls).toHaveLength(0);
});
});
});

View File

@@ -8,15 +8,13 @@
import { Plus, Edit3, Save, X, Trash, ExternalLink } from 'lucide-svelte'
import { sendUserToast } from '$lib/toast'
import { twMerge } from 'tailwind-merge'
import { ConfigService, type Alert } from '$lib/gen'
import { ConfigService, SettingService, type Alert } from '$lib/gen'
import Tooltip from './Tooltip.svelte'
import Badge from './common/badge/Badge.svelte'
import { enterpriseLicense } from '$lib/stores'
let queueAlertConfig = $state<Alert[]>([])
let availableTags = $state<string[]>([])
let configName = 'alert__job_queue_waiting'
let editingRowIndex = $state<number>(-1)
let editForm = $state<{
tags_to_monitor: string[]
@@ -50,8 +48,8 @@
async function fetchConfig() {
try {
const response = await ConfigService.getConfig({ name: configName })
queueAlertConfig = response?.alerts || []
const response = await SettingService.getGlobal({ key: 'alert_job_queue_waiting' })
queueAlertConfig = (response as any)?.alerts || []
expandedTagRows = []
} catch (error) {
console.error('Failed to fetch config:', error)
@@ -60,12 +58,13 @@
async function fetchWorkerTags(): Promise<string[]> {
try {
const response = await ConfigService.listConfigs()
const response = await ConfigService.listWorkerGroups()
const workerTagsSet = new Set<string>()
response.forEach((config) => {
if (config.name.startsWith('worker__') && Array.isArray(config.config?.worker_tags)) {
config?.config?.worker_tags.forEach((tag) => workerTagsSet.add(tag))
response.forEach((wg) => {
const config = wg.config as any
if (Array.isArray(config?.worker_tags)) {
config.worker_tags.forEach((tag: string) => workerTagsSet.add(tag))
}
})
@@ -190,9 +189,9 @@
}
async function saveQueueAlertConfig() {
await ConfigService.updateConfig({
name: configName,
requestBody: { alerts: queueAlertConfig }
await SettingService.setGlobal({
key: 'alert_job_queue_waiting',
requestBody: { value: { alerts: queueAlertConfig } }
})
}