mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat: instance groups workspace (#6380)
* feat: instancd groups mapping to workspace * npm run check * nits * improve apis * sqlx prepare and ee-repo ref * adding workspace assignment in groups page * nits * correct rollback * sqlx * ee repo ref * revert accidental ee-repo-ref commit to main * ee repo ref * revert accidental ee-repo-ref commit to main
This commit is contained in:
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n workspace_settings\n SET\n error_handler = NULL,\n error_handler_extra_args = NULL,\n error_handler_muted_on_cancel = NULL\n WHERE \n workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "04f8d738b1073b8c58db0965e8fdcdb872dce2c1872359c1bebb553a29ba1637"
|
||||
}
|
||||
+12
@@ -147,6 +147,16 @@
|
||||
"ordinal": 28,
|
||||
"name": "ducklake",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"name": "auto_add_instance_groups",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 30,
|
||||
"name": "auto_add_instance_groups_roles",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -183,6 +193,8 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow\n (workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at) \n SELECT workspace_id, REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1'), summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at\n FROM flow \n WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0a56301b5aaf57339cb2904c8f617366b74e891034d32f2867ccb019da869fc8"
|
||||
}
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO usr (workspace_id, username, email, is_admin, operator) VALUES ($1, $2, $3, false, $4) ON CONFLICT DO NOTHING",
|
||||
"query": "INSERT INTO usr (workspace_id, username, email, is_admin, operator, added_via) VALUES ($1, $2, $3, false, $4, $5) ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -8,10 +8,11 @@
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
"Bool",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e822d186203fe809b764007ad7c02870a3b7d93ae43b40ac2cd3181dffab0837"
|
||||
"hash": "0d7ce0397ef15c9d6cdaeaa2730a9e27fb7387ca24980df1481e6a94622ef006"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE\n workspace_settings\n SET\n error_handler = NULL,\n error_handler_extra_args = NULL,\n error_handler_muted_on_cancel = NULL\n WHERE\n workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0fa105c49c8345916716514444bd3616ae4d114216c659233fbbc3c047e6b30a"
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT name, summary, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup GROUP BY name, summary",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "summary",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "emails",
|
||||
"type_info": "VarcharArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "10f6d3ffd7406146572b1becdce5c8da5242b58f6ce46ab10296cff9d6a3a6c4"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COALESCE(auto_add_instance_groups, '{}') FROM workspace_settings WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "coalesce",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "18e550f4ec23d465632449b88c4b25931f145f771b93828e8e6dfcc1f906443d"
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n ig.name as group_name,\n ws.workspace_id,\n w.name as workspace_name,\n ws.auto_add_instance_groups_roles->ig.name as role\n FROM instance_group ig\n INNER JOIN workspace_settings ws ON ws.auto_add_instance_groups IS NOT NULL\n AND ig.name = ANY(ws.auto_add_instance_groups)\n INNER JOIN workspace w ON w.id = ws.workspace_id AND w.deleted = false\n ORDER BY ig.name, ws.workspace_id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "group_name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "workspace_name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "role",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1e6c125c884002a1565b11a2c308ce89f9726d21bbb3cc712ad0b6450cbb44e6"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT email FROM email_to_igroup WHERE igroup = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "21099fabde943edc90d3a0125e8490691b2701d11f918da3bb5ae610d5c023a0"
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT username, added_via\n FROM usr\n WHERE workspace_id = $1 AND email = $2\n AND added_via->>'source' = 'instance_group'\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "added_via",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "2132a715995f2775917c59c01ba66f2e472a2347f25cc7109f36a805806ee6e2"
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n workspace_settings\n SET\n error_handler = $1,\n error_handler_extra_args = $2,\n error_handler_muted_on_cancel = $3\n WHERE \n workspace_id = $4\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Json",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2549197c6750bb1a20041b2d7c2654a788a32a9282ed314c2b3bcafe6550efca"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET auto_add_instance_groups = NULL, auto_add_instance_groups_roles = NULL WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "31db242523c534b2abbb5f4985f56dd4493d19611ed988d694d9f4289dc29c48"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n workspace_id,\n auto_add_instance_groups_roles\n FROM workspace_settings\n WHERE\n auto_add_instance_groups IS NOT NULL\n AND $1 = ANY(auto_add_instance_groups)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "auto_add_instance_groups_roles",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "3ebc91867be0a4830ef4dbbe833aed348f78e3cb68fb1f3c855a491bdcda5017"
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT usr.username \n FROM usr_to_group LEFT JOIN usr ON usr_to_group.usr = usr.username AND usr_to_group.workspace_id = $2\n WHERE group_ = $1 AND usr.workspace_id = $2 AND usr_to_group.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3fdfcab1a54c166b1d8d43215d61268a251160db4630f0342522091668f36af0"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET auto_add_instance_groups = $2, auto_add_instance_groups_roles = $3 WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"TextArray",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "404233e74aaafd987879c6c87d1fd80928e6fade09e60a9a2d35121c81885cca"
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT workspace_id, username, email\n FROM usr\n WHERE email = $1\n AND added_via->>'source' = 'instance_group'\n AND added_via->>'group' = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "472f224dc9e17d2c50cb9db13c34cc1fb9adb6d0ea36cf223541adb7cac17bdd"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE workspace_settings SET\n auto_add_instance_groups = array_remove(auto_add_instance_groups, $1),\n auto_add_instance_groups_roles = auto_add_instance_groups_roles - $1\n WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4a446b97cf813226d25ca40cefca2999cdcf938f817327e9168b3edb0fce7fb7"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT usage.usage FROM usage \n WHERE is_workspace = false \n AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
|
||||
"query": "\n SELECT usage.usage FROM usage\n WHERE is_workspace = false\n AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -18,5 +18,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a7b028e832396ee4d1ad6dfd44ba6134344f3eb37dbf0254154eba31b9cc2ed3"
|
||||
"hash": "51cfe6efb154934f8cfbe77a7313891ef5c46a7febe4125d164a5ae5b55af2d6"
|
||||
}
|
||||
+12
@@ -147,6 +147,16 @@
|
||||
"ordinal": 28,
|
||||
"name": "ducklake",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"name": "auto_add_instance_groups",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 30,
|
||||
"name": "auto_add_instance_groups_roles",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -183,6 +193,8 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
+6
@@ -42,6 +42,11 @@
|
||||
"ordinal": 7,
|
||||
"name": "role",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "added_via",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -57,6 +62,7 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
+6
@@ -42,6 +42,11 @@
|
||||
"ordinal": 7,
|
||||
"name": "role",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "added_via",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -58,6 +63,7 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
+7
-1
@@ -45,11 +45,16 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "added_via",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "super_admin",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
@@ -69,6 +74,7 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
]
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM group_ WHERE (group_.extra_perms ->> CONCAT('u/', $1::text))::boolean AND name = $2 AND workspace_id = $4) OR exists(\n SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f \n WHERE $2 = g.name AND $4 = g.workspace_id AND SPLIT_PART(key, '/', 1) = 'g' AND key = ANY($3::text[])\n AND value::boolean)",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM group_ WHERE (group_.extra_perms ->> CONCAT('u/', $1::text))::boolean AND name = $2 AND workspace_id = $4) OR exists(\n SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f\n WHERE $2 = g.name AND $4 = g.workspace_id AND SPLIT_PART(key, '/', 1) = 'g' AND key = ANY($3::text[])\n AND value::boolean)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -21,5 +21,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "388d6fd335a3f8a405b2d465892cf21a68d4b50ace25ef88c4cdf5b347c3d5eb"
|
||||
"hash": "6b2820973026b1628cb93fda943bc389a261a3bb975c86ac02dd1c552da92295"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT usr.username\n FROM usr_to_group LEFT JOIN usr ON usr_to_group.usr = usr.username AND usr_to_group.workspace_id = $2\n WHERE group_ = $1 AND usr.workspace_id = $2 AND usr_to_group.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6f36c05e6097066441acab227c0d5206d5a9a9828b04e4c107f1ecab4a9363d4"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace_id FROM workspace_settings WHERE auto_add_instance_groups IS NOT NULL AND $1 = ANY(auto_add_instance_groups)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "711c72a72298bb656882b35071e910dab721cda070f9c39bba3729bdc1467496"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT usr.email, usage.executions\n FROM usr\n , LATERAL (\n SELECT COALESCE(SUM(duration_ms + 1000)/1000 , 0)::BIGINT executions\n FROM v2_as_completed_job\n WHERE workspace_id = $1\n AND job_kind NOT IN ('flow', 'flowpreview', 'flownode')\n AND email = usr.email\n AND now() - '1 week'::interval < created_at \n ) usage\n WHERE workspace_id = $1\n ",
|
||||
"query": "\n SELECT usr.email, usage.executions\n FROM usr\n , LATERAL (\n SELECT COALESCE(SUM(duration_ms + 1000)/1000 , 0)::BIGINT executions\n FROM v2_as_completed_job\n WHERE workspace_id = $1\n AND job_kind NOT IN ('flow', 'flowpreview', 'flownode')\n AND email = usr.email\n AND now() - '1 week'::interval < created_at\n ) usage\n WHERE workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -24,5 +24,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "bff39cc57aba0729ddef1d53f3806c6736556f0a14b489d6708f9879393f9ea3"
|
||||
"hash": "89940a53f29b173b6a8717f057a81a4f2edad152581950fdd80d758a0d242c17"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE usr SET is_admin = true WHERE workspace_id = $1 AND email = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "94feb54cc965f19bfaf09966fd431b925c565f08c100bea812d3ac0e28664ccb"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT DISTINCT email FROM usr WHERE added_via->>'source' = 'instance_group' AND added_via->>'group' = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9530b234b2ac8f7360552be5e6bb25270e6b4185ce5c7f3c6edd9fbaabf77544"
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE\n workspace_settings\n SET\n error_handler = $1,\n error_handler_extra_args = $2,\n error_handler_muted_on_cancel = $3\n WHERE\n workspace_id = $4\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Json",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "97b6e5779661343587c297b275df9e75fd22924ad2dbe124bb361f3e7fd8bfe0"
|
||||
}
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n workspace_id,\n slack_team_id,\n teams_team_id,\n teams_team_name,\n slack_name,\n slack_command_script,\n teams_command_script,\n slack_email,\n auto_invite_domain,\n auto_invite_operator,\n auto_add,\n customer_id,\n plan,\n webhook,\n deploy_to,\n ai_config,\n error_handler,\n error_handler_extra_args,\n error_handler_muted_on_cancel,\n large_file_storage,\n ducklake,\n git_sync,\n deploy_ui,\n default_app,\n default_scripts,\n mute_critical_alerts,\n color,\n operator_settings,\n git_app_installations\n FROM \n workspace_settings\n WHERE \n workspace_id = $1\n ",
|
||||
"query": "\n SELECT\n workspace_id,\n slack_team_id,\n teams_team_id,\n teams_team_name,\n slack_name,\n slack_command_script,\n teams_command_script,\n slack_email,\n auto_invite_domain,\n auto_invite_operator,\n auto_add,\n customer_id,\n plan,\n webhook,\n deploy_to,\n ai_config,\n error_handler,\n error_handler_extra_args,\n error_handler_muted_on_cancel,\n large_file_storage,\n ducklake,\n git_sync,\n deploy_ui,\n default_app,\n default_scripts,\n mute_critical_alerts,\n color,\n operator_settings,\n git_app_installations,\n auto_add_instance_groups,\n auto_add_instance_groups_roles\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -147,6 +147,16 @@
|
||||
"ordinal": 28,
|
||||
"name": "git_app_installations",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"name": "auto_add_instance_groups",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 30,
|
||||
"name": "auto_add_instance_groups_roles",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -183,8 +193,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "8960b73f0d3cbfa0729d24ecbe3f635592feee6a5724fdff662c6ac1f3c6ddc8"
|
||||
"hash": "ac1fd12e9ec92022be38aee0e91e9002e0e810d0e76dce5ced1000a7cb514adb"
|
||||
}
|
||||
+2
-2
@@ -18,8 +18,8 @@
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow\n (workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at)\n SELECT workspace_id, REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1'), summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at\n FROM flow\n WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c269f14ae9ae4e96eff9483eb84cccbdfa316e5db051f1d52085a2d5447c81ae"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT flow.workspace_id as workspace, flow.path, summary, description, flow_version.schema \n FROM flow \n LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n WHERE flow.workspace_id = $1",
|
||||
"query": "SELECT flow.workspace_id as workspace, flow.path, summary, description, flow_version.schema\n FROM flow\n LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n WHERE flow.workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -42,5 +42,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "974c7e623f3dfa440e134eaaa8d029334c0645147200219c39b2c00b30941172"
|
||||
"hash": "ccbf71572dfc60b69a1666d4f1a883ba724d1e9c3303df32b001737b80a4b9f9"
|
||||
}
|
||||
+6
@@ -42,6 +42,11 @@
|
||||
"ordinal": 7,
|
||||
"name": "role",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "added_via",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -57,6 +62,7 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT email_to_igroup.email\n FROM email_to_igroup\n INNER JOIN instance_group ON instance_group.name = email_to_igroup.igroup\n WHERE instance_group.name = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ee182378d9a760c3593b483703ec1682488ca9e6e402ec41abaa6ae3ca263854"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE usr SET added_via = $1 WHERE workspace_id = $2 AND email = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f582cac90b4b7d732956b74eebc51323ef8acd3f627e7517451fcc72998d22bc"
|
||||
}
|
||||
+5
-5
@@ -41,11 +41,11 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1 +1 @@
|
||||
5a3f583656e7f754705ea3b5263593092cbba404
|
||||
391a67b7af2c874ee86971cc299ade066de00fc8
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Remove auto-add columns for instance groups
|
||||
ALTER TABLE workspace_settings
|
||||
DROP COLUMN auto_add_instance_groups,
|
||||
DROP COLUMN auto_add_instance_groups_roles;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Add auto-add columns for instance groups
|
||||
ALTER TABLE workspace_settings
|
||||
ADD COLUMN auto_add_instance_groups text[] DEFAULT '{}',
|
||||
ADD COLUMN auto_add_instance_groups_roles jsonb DEFAULT '{}';
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Remove added_via tracking
|
||||
DROP INDEX idx_usr_added_via;
|
||||
ALTER TABLE usr DROP COLUMN added_via;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Add added_via column to track how users were added to workspaces
|
||||
-- NULL = manual addition
|
||||
-- {"source": "domain", "domain": "company.com"} = domain auto-add
|
||||
-- {"source": "instance_group", "group": "developers", "role": "developer"} = instance group auto-add
|
||||
ALTER TABLE usr ADD COLUMN added_via jsonb DEFAULT NULL;
|
||||
|
||||
-- Add index for efficient queries on added_via
|
||||
CREATE INDEX idx_usr_added_via ON usr USING gin (added_via);
|
||||
@@ -1944,6 +1944,14 @@ paths:
|
||||
type: boolean
|
||||
auto_add:
|
||||
type: boolean
|
||||
auto_add_instance_groups:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
auto_add_instance_groups_roles:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
plan:
|
||||
type: string
|
||||
customer_id:
|
||||
@@ -2351,6 +2359,39 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/edit_instance_groups:
|
||||
post:
|
||||
summary: edit instance groups
|
||||
operationId: editInstanceGroups
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: Instance Groups Configuration
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
groups:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
roles:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/edit_webhook:
|
||||
post:
|
||||
summary: edit webhook
|
||||
@@ -10713,6 +10754,21 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/InstanceGroup"
|
||||
/groups/list_with_workspaces:
|
||||
get:
|
||||
summary: list instance groups with workspace information
|
||||
operationId: listInstanceGroupsWithWorkspaces
|
||||
tags:
|
||||
- group
|
||||
responses:
|
||||
"200":
|
||||
description: instance group list with workspaces
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/InstanceGroupWithWorkspaces"
|
||||
|
||||
/groups/get/{name}:
|
||||
get:
|
||||
@@ -14583,6 +14639,10 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
added_via:
|
||||
nullable: true
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/UserSource"
|
||||
required:
|
||||
- email
|
||||
- username
|
||||
@@ -14594,6 +14654,22 @@ components:
|
||||
- folders
|
||||
- folders_owners
|
||||
|
||||
UserSource:
|
||||
type: object
|
||||
properties:
|
||||
source:
|
||||
type: string
|
||||
enum: [domain, instance_group, manual]
|
||||
description: "How the user was added to the workspace"
|
||||
domain:
|
||||
type: string
|
||||
description: "The domain used for auto-invite (when source is 'domain')"
|
||||
group:
|
||||
type: string
|
||||
description: "The instance group name (when source is 'instance_group')"
|
||||
required:
|
||||
- source
|
||||
|
||||
UserUsage:
|
||||
type: object
|
||||
properties:
|
||||
@@ -16811,6 +16887,8 @@ components:
|
||||
|
||||
InstanceGroup:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
@@ -16820,6 +16898,34 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
InstanceGroupWithWorkspaces:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
summary:
|
||||
type: string
|
||||
emails:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
workspaces:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/WorkspaceInfo"
|
||||
|
||||
WorkspaceInfo:
|
||||
type: object
|
||||
properties:
|
||||
workspace_id:
|
||||
type: string
|
||||
workspace_name:
|
||||
type: string
|
||||
role:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ pub fn workspaced_service() -> Router {
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
.route("/list", get(list_igroups))
|
||||
.route("/list_with_workspaces", get(list_igroups_with_workspaces))
|
||||
.route("/get/:name", get(get_igroup))
|
||||
.route("/create", post(create_igroup))
|
||||
.route("/update/:name", post(update_igroup))
|
||||
@@ -187,7 +188,7 @@ pub async fn require_is_owner(
|
||||
) -> Result<()> {
|
||||
let is_owner = query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM group_ WHERE (group_.extra_perms ->> CONCAT('u/', $1::text))::boolean AND name = $2 AND workspace_id = $4) OR exists(
|
||||
SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f
|
||||
SELECT 1 FROM group_ g, jsonb_each_text(g.extra_perms) f
|
||||
WHERE $2 = g.name AND $4 = g.workspace_id AND SPLIT_PART(key, '/', 1) = 'g' AND key = ANY($3::text[])
|
||||
AND value::boolean)",
|
||||
username,
|
||||
@@ -418,7 +419,7 @@ async fn get_group(
|
||||
let group = not_found_if_none(get_group_opt(&mut tx, &w_id, &name).await?, "Group", &name)?;
|
||||
|
||||
let members = sqlx::query_scalar!(
|
||||
"SELECT usr.username
|
||||
"SELECT usr.username
|
||||
FROM usr_to_group LEFT JOIN usr ON usr_to_group.usr = usr.username AND usr_to_group.workspace_id = $2
|
||||
WHERE group_ = $1 AND usr.workspace_id = $2 AND usr_to_group.workspace_id = $2",
|
||||
name,
|
||||
@@ -638,6 +639,21 @@ struct IGroup {
|
||||
summary: Option<String>,
|
||||
emails: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct IGroupWithWorkspaces {
|
||||
name: String,
|
||||
summary: Option<String>,
|
||||
emails: Option<Vec<String>>,
|
||||
workspaces: Vec<WorkspaceInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
struct WorkspaceInfo {
|
||||
workspace_id: String,
|
||||
workspace_name: String,
|
||||
role: String,
|
||||
}
|
||||
async fn list_igroups(Extension(db): Extension<DB>) -> JsonResult<Vec<IGroup>> {
|
||||
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
|
||||
|
||||
@@ -652,6 +668,70 @@ async fn list_igroups(Extension(db): Extension<DB>) -> JsonResult<Vec<IGroup>> {
|
||||
return Ok(Json(groups));
|
||||
}
|
||||
|
||||
async fn list_igroups_with_workspaces(Extension(db): Extension<DB>) -> JsonResult<Vec<IGroupWithWorkspaces>> {
|
||||
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
|
||||
|
||||
// Get all instance groups with their emails first
|
||||
let groups = sqlx::query_as!(
|
||||
IGroup,
|
||||
"SELECT name, summary, array_remove(array_agg(email_to_igroup.email), null) as emails FROM email_to_igroup RIGHT JOIN instance_group ON instance_group.name = email_to_igroup.igroup GROUP BY name, summary"
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
// Get all workspace mappings for instance groups in a single query
|
||||
let workspace_mappings = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
ig.name as group_name,
|
||||
ws.workspace_id,
|
||||
w.name as workspace_name,
|
||||
ws.auto_add_instance_groups_roles->ig.name as role
|
||||
FROM instance_group ig
|
||||
INNER JOIN workspace_settings ws ON ws.auto_add_instance_groups IS NOT NULL
|
||||
AND ig.name = ANY(ws.auto_add_instance_groups)
|
||||
INNER JOIN workspace w ON w.id = ws.workspace_id AND w.deleted = false
|
||||
ORDER BY ig.name, ws.workspace_id
|
||||
"#
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
// Create a map of group_name -> Vec<WorkspaceInfo>
|
||||
let mut workspaces_by_group: std::collections::HashMap<String, Vec<WorkspaceInfo>> = std::collections::HashMap::new();
|
||||
for mapping in workspace_mappings {
|
||||
let role = mapping.role
|
||||
.and_then(|r| r.as_str().map(|s| s.to_string()))
|
||||
.unwrap_or_else(|| "developer".to_string());
|
||||
|
||||
let workspace_info = WorkspaceInfo {
|
||||
workspace_id: mapping.workspace_id.clone(),
|
||||
workspace_name: mapping.workspace_name,
|
||||
role,
|
||||
};
|
||||
|
||||
workspaces_by_group
|
||||
.entry(mapping.group_name)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(workspace_info);
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
for group in groups {
|
||||
let workspaces = workspaces_by_group.get(&group.name).cloned().unwrap_or_default();
|
||||
|
||||
result.push(IGroupWithWorkspaces {
|
||||
name: group.name,
|
||||
summary: group.summary,
|
||||
emails: group.emails,
|
||||
workspaces,
|
||||
});
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
return Ok(Json(result));
|
||||
}
|
||||
|
||||
async fn get_igroup(Path(name): Path<String>, Extension(db): Extension<DB>) -> JsonResult<IGroup> {
|
||||
let group = sqlx::query_as!(
|
||||
IGroup,
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use quick_cache::sync::Cache;
|
||||
use sqlx::{Postgres, Transaction};
|
||||
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
@@ -262,6 +263,8 @@ pub struct User {
|
||||
pub operator: bool,
|
||||
pub disabled: bool,
|
||||
pub role: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub added_via: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -494,7 +497,7 @@ async fn list_user_usage(
|
||||
WHERE workspace_id = $1
|
||||
AND job_kind NOT IN ('flow', 'flowpreview', 'flownode')
|
||||
AND email = usr.email
|
||||
AND now() - '1 week'::interval < created_at
|
||||
AND now() - '1 week'::interval < created_at
|
||||
) usage
|
||||
WHERE workspace_id = $1
|
||||
",
|
||||
@@ -786,8 +789,8 @@ async fn get_usage(
|
||||
) -> Result<String> {
|
||||
let usage = sqlx::query_scalar!(
|
||||
"
|
||||
SELECT usage.usage FROM usage
|
||||
WHERE is_workspace = false
|
||||
SELECT usage.usage FROM usage
|
||||
WHERE is_workspace = false
|
||||
AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)
|
||||
AND id = $1",
|
||||
email
|
||||
@@ -810,6 +813,8 @@ pub struct User2 {
|
||||
pub role: Option<String>,
|
||||
pub super_admin: bool,
|
||||
pub name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub added_via: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
async fn get_user(w_id: &str, username: &str, db: &DB) -> Result<Option<UserInfo>> {
|
||||
@@ -1315,49 +1320,15 @@ async fn update_workspace_user(
|
||||
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
if let Some(a) = eu.is_admin {
|
||||
sqlx::query_scalar!(
|
||||
"UPDATE usr SET is_admin = $1 WHERE username = $2 AND workspace_id = $3",
|
||||
a,
|
||||
&username_to_update,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(a) = eu.operator {
|
||||
sqlx::query_scalar!(
|
||||
"UPDATE usr SET operator = $1 WHERE username = $2 AND workspace_id = $3",
|
||||
a,
|
||||
&username_to_update,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(a) = eu.disabled {
|
||||
sqlx::query_scalar!(
|
||||
"UPDATE usr SET disabled = $1 WHERE username = $2 AND workspace_id = $3",
|
||||
a,
|
||||
&username_to_update,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"users.update",
|
||||
ActionKind::Update,
|
||||
update_workspace_user_internal(
|
||||
&w_id,
|
||||
Some(&username_to_update),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
&username_to_update,
|
||||
eu.is_admin,
|
||||
eu.operator,
|
||||
eu.disabled,
|
||||
&mut tx,
|
||||
Some(&authed)
|
||||
).await?;
|
||||
|
||||
let user_email = sqlx::query_scalar!(
|
||||
"SELECT email FROM usr WHERE username = $1 AND workspace_id = $2",
|
||||
@@ -1500,6 +1471,105 @@ async fn create_user(
|
||||
crate::users_oss::create_user(authed, db, webhook, argon2, nu).await
|
||||
}
|
||||
|
||||
/// Internal helper for updating workspace user permissions - used by both API and system operations
|
||||
pub async fn update_workspace_user_internal(
|
||||
w_id: &str,
|
||||
username_to_update: &str,
|
||||
is_admin: Option<bool>,
|
||||
operator: Option<bool>,
|
||||
disabled: Option<bool>,
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
authed: Option<&ApiAuthed>, // None for system operations
|
||||
) -> Result<()> {
|
||||
if let Some(a) = is_admin {
|
||||
sqlx::query_scalar!(
|
||||
"UPDATE usr SET is_admin = $1 WHERE username = $2 AND workspace_id = $3",
|
||||
a,
|
||||
username_to_update,
|
||||
w_id
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
}
|
||||
if let Some(a) = operator {
|
||||
sqlx::query_scalar!(
|
||||
"UPDATE usr SET operator = $1 WHERE username = $2 AND workspace_id = $3",
|
||||
a,
|
||||
username_to_update,
|
||||
w_id
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
}
|
||||
if let Some(a) = disabled {
|
||||
sqlx::query_scalar!(
|
||||
"UPDATE usr SET disabled = $1 WHERE username = $2 AND workspace_id = $3",
|
||||
a,
|
||||
username_to_update,
|
||||
w_id
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Only audit if we have an authenticated user (API calls)
|
||||
if let Some(auth) = authed {
|
||||
audit_log(
|
||||
&mut **tx,
|
||||
auth,
|
||||
"users.update",
|
||||
ActionKind::Update,
|
||||
w_id,
|
||||
Some(username_to_update),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Internal helper for deleting workspace users - used by both API and system operations
|
||||
pub async fn delete_workspace_user_internal(
|
||||
w_id: &str,
|
||||
username_to_delete: &str,
|
||||
email_to_delete: &str,
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
authed: Option<&ApiAuthed>, // None for system operations
|
||||
) -> Result<()> {
|
||||
sqlx::query_scalar!(
|
||||
"DELETE FROM usr WHERE email = $1 AND workspace_id = $2",
|
||||
email_to_delete,
|
||||
w_id
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM usr_to_group WHERE usr = $1 AND workspace_id = $2",
|
||||
username_to_delete,
|
||||
w_id
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
// Only audit if we have an authenticated user (API calls)
|
||||
if let Some(auth) = authed {
|
||||
audit_log(
|
||||
&mut **tx,
|
||||
auth,
|
||||
"users.delete",
|
||||
ActionKind::Delete,
|
||||
w_id,
|
||||
Some(username_to_delete),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_workspace_user(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
@@ -1519,32 +1589,7 @@ async fn delete_workspace_user(
|
||||
|
||||
let email_to_delete = not_found_if_none(email_to_delete_o, "User", &username_to_delete)?;
|
||||
|
||||
sqlx::query_scalar!(
|
||||
"DELETE FROM usr WHERE email = $1 AND workspace_id = $2",
|
||||
email_to_delete,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM usr_to_group WHERE usr = $1 AND workspace_id = $2",
|
||||
&username_to_delete,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"users.delete",
|
||||
ActionKind::Delete,
|
||||
&w_id,
|
||||
Some(&username_to_delete),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
delete_workspace_user_internal(&w_id, &username_to_delete, &email_to_delete, &mut tx, Some(&authed)).await?;
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
@@ -2096,8 +2141,8 @@ async fn get_all_runnables(
|
||||
})?;
|
||||
let mut tx = db.clone().begin(&nauthed).await?;
|
||||
let flows = sqlx::query!(
|
||||
"SELECT flow.workspace_id as workspace, flow.path, summary, description, flow_version.schema
|
||||
FROM flow
|
||||
"SELECT flow.workspace_id as workspace, flow.path, summary, description, flow_version.schema
|
||||
FROM flow
|
||||
LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
|
||||
WHERE flow.workspace_id = $1",
|
||||
workspace
|
||||
@@ -2580,9 +2625,9 @@ async fn update_username_in_workpsace<'c>(
|
||||
// ---- flows ----
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO flow
|
||||
(workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at)
|
||||
(workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at)
|
||||
SELECT workspace_id, REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\1'), summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at
|
||||
FROM flow
|
||||
FROM flow
|
||||
WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3"#,
|
||||
new_username,
|
||||
old_username,
|
||||
|
||||
@@ -101,6 +101,7 @@ pub fn workspaced_service() -> Router {
|
||||
)
|
||||
.route("/edit_webhook", post(edit_webhook))
|
||||
.route("/edit_auto_invite", post(edit_auto_invite))
|
||||
.route("/edit_instance_groups", post(edit_instance_groups))
|
||||
.route("/edit_deploy_to", post(edit_deploy_to))
|
||||
.route(
|
||||
"/get_secondary_storage_names",
|
||||
@@ -252,6 +253,10 @@ pub struct WorkspaceSettings {
|
||||
pub operator_settings: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub git_app_installations: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_add_instance_groups: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auto_add_instance_groups_roles: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::Type, Serialize, Deserialize, Debug)]
|
||||
@@ -462,7 +467,7 @@ async fn get_settings(
|
||||
let settings = sqlx::query_as!(
|
||||
WorkspaceSettings,
|
||||
r#"
|
||||
SELECT
|
||||
SELECT
|
||||
workspace_id,
|
||||
slack_team_id,
|
||||
teams_team_id,
|
||||
@@ -491,10 +496,12 @@ async fn get_settings(
|
||||
mute_critical_alerts,
|
||||
color,
|
||||
operator_settings,
|
||||
git_app_installations
|
||||
FROM
|
||||
git_app_installations,
|
||||
auto_add_instance_groups,
|
||||
auto_add_instance_groups_roles
|
||||
FROM
|
||||
workspace_settings
|
||||
WHERE
|
||||
WHERE
|
||||
workspace_id = $1
|
||||
"#,
|
||||
&w_id
|
||||
@@ -726,6 +733,28 @@ async fn edit_auto_invite(
|
||||
crate::workspaces_oss::edit_auto_invite(authed, db, w_id, ea).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "private")]
|
||||
async fn edit_instance_groups(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(config): Json<crate::workspaces_ee::EditInstanceGroups>,
|
||||
) -> Result<String> {
|
||||
crate::workspaces_ee::edit_instance_groups(authed, db, w_id, config).await
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
async fn edit_instance_groups(
|
||||
_authed: ApiAuthed,
|
||||
Extension(_db): Extension<DB>,
|
||||
Path(_w_id): Path<String>,
|
||||
Json(_config): Json<serde_json::Value>,
|
||||
) -> Result<String> {
|
||||
Err(Error::BadRequest(
|
||||
"Instance groups are only available on Windmill Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn edit_webhook(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
@@ -1716,13 +1745,13 @@ async fn edit_error_handler(
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE
|
||||
UPDATE
|
||||
workspace_settings
|
||||
SET
|
||||
error_handler = $1,
|
||||
error_handler_extra_args = $2,
|
||||
error_handler_muted_on_cancel = $3
|
||||
WHERE
|
||||
WHERE
|
||||
workspace_id = $4
|
||||
"#,
|
||||
error_handler,
|
||||
@@ -1735,13 +1764,13 @@ async fn edit_error_handler(
|
||||
} else {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE
|
||||
UPDATE
|
||||
workspace_settings
|
||||
SET
|
||||
error_handler = NULL,
|
||||
error_handler_extra_args = NULL,
|
||||
error_handler_muted_on_cancel = NULL
|
||||
WHERE
|
||||
WHERE
|
||||
workspace_id = $1
|
||||
"#,
|
||||
&w_id
|
||||
|
||||
@@ -13,10 +13,11 @@
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import type { CancelablePromise, User, UserUsage } from '$lib/gen'
|
||||
import { UserService, WorkspaceService, type WorkspaceInvite } from '$lib/gen'
|
||||
import { UserService, WorkspaceService, GroupService, type WorkspaceInvite } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { Loader2, Mails, Search } from 'lucide-svelte'
|
||||
import { Loader2, Mails, Search, Plus } from 'lucide-svelte'
|
||||
import Select from '$lib/components/select/Select.svelte'
|
||||
import SearchItems from '../SearchItems.svelte'
|
||||
import Cell from '../table/Cell.svelte'
|
||||
import Row from '../table/Row.svelte'
|
||||
@@ -34,11 +35,47 @@
|
||||
let autoAdd: boolean | undefined = $state(undefined)
|
||||
let nbDisplayed = $state(30)
|
||||
|
||||
// Instance group auto-add settings
|
||||
let instanceGroups: Array<{name: string, summary?: string, emails?: string[]}> = $state([])
|
||||
let autoAddInstanceGroups: string[] = $state([])
|
||||
let autoAddInstanceGroupsRoles: Record<string, string> = $state({})
|
||||
|
||||
// Add new instance group form state
|
||||
let selectedNewInstanceGroup: string | undefined = $state(undefined)
|
||||
let selectedNewRole: string | undefined = $state('developer')
|
||||
|
||||
// Available groups for dropdowns - filter out already configured groups
|
||||
let availableGroupItems = $derived(
|
||||
instanceGroups
|
||||
.filter(group => !autoAddInstanceGroups.includes(group.name))
|
||||
.map(group => ({
|
||||
value: group.name,
|
||||
label: group.name + (group.summary ? ` - ${group.summary}` : '')
|
||||
}))
|
||||
)
|
||||
|
||||
// Sort users so manual users come first, then instance group users
|
||||
let sortedUsers = $derived(() => {
|
||||
const userList = (filteredUsers || users || []).slice()
|
||||
return userList.sort((a: User, b: User) => {
|
||||
const aIsInstanceGroup = a.added_via?.source === 'instance_group' ? 1 : 0
|
||||
const bIsInstanceGroup = b.added_via?.source === 'instance_group' ? 1 : 0
|
||||
return aIsInstanceGroup - bIsInstanceGroup
|
||||
})
|
||||
})
|
||||
|
||||
let hasNonManualUsers = $derived(
|
||||
(filteredUsers || users || []).some((user: User) => user.added_via?.source === 'instance_group' || user.added_via?.source === 'domain')
|
||||
)
|
||||
|
||||
|
||||
async function loadSettings(): Promise<void> {
|
||||
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
|
||||
auto_invite_domain = settings.auto_invite_domain
|
||||
operatorOnly = settings.auto_invite_operator
|
||||
autoAdd = settings.auto_add
|
||||
autoAddInstanceGroups = settings.auto_add_instance_groups || []
|
||||
autoAddInstanceGroupsRoles = settings.auto_add_instance_groups_roles || {}
|
||||
}
|
||||
|
||||
let getUsagePromise: CancelablePromise<UserUsage[]> | undefined = undefined
|
||||
@@ -77,6 +114,87 @@
|
||||
allowedAutoDomain = await WorkspaceService.isDomainAllowed()
|
||||
}
|
||||
|
||||
async function loadInstanceGroups(): Promise<void> {
|
||||
try {
|
||||
instanceGroups = await GroupService.listInstanceGroups()
|
||||
} catch (e) {
|
||||
console.warn('Failed to load instance groups:', e)
|
||||
instanceGroups = []
|
||||
}
|
||||
}
|
||||
|
||||
async function saveInstanceGroupSettings(): Promise<void> {
|
||||
try {
|
||||
await WorkspaceService.editInstanceGroups({
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: {
|
||||
groups: autoAddInstanceGroups,
|
||||
roles: autoAddInstanceGroupsRoles
|
||||
}
|
||||
})
|
||||
sendUserToast('Instance group settings saved')
|
||||
// Refresh user list to show newly auto-added users
|
||||
listUsers()
|
||||
} catch (e) {
|
||||
console.error('Failed to save instance group settings:', e)
|
||||
sendUserToast('Failed to save settings', true)
|
||||
}
|
||||
}
|
||||
|
||||
async function addInstanceGroup(): Promise<void> {
|
||||
if (!selectedNewInstanceGroup || !selectedNewRole) return
|
||||
|
||||
const groupToAdd = selectedNewInstanceGroup
|
||||
const roleToAdd = selectedNewRole
|
||||
|
||||
try {
|
||||
autoAddInstanceGroups = [...autoAddInstanceGroups, groupToAdd]
|
||||
autoAddInstanceGroupsRoles[groupToAdd] = roleToAdd
|
||||
|
||||
// Reset form
|
||||
selectedNewInstanceGroup = undefined
|
||||
selectedNewRole = 'developer'
|
||||
|
||||
await saveInstanceGroupSettings()
|
||||
} catch (e) {
|
||||
// Rollback on error
|
||||
autoAddInstanceGroups = autoAddInstanceGroups.filter(g => g !== groupToAdd)
|
||||
delete autoAddInstanceGroupsRoles[groupToAdd]
|
||||
sendUserToast('Failed to add instance group', true)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeInstanceGroup(groupName: string): Promise<void> {
|
||||
const previousGroups = [...autoAddInstanceGroups]
|
||||
const previousRole = autoAddInstanceGroupsRoles[groupName]
|
||||
|
||||
try {
|
||||
autoAddInstanceGroups = autoAddInstanceGroups.filter(g => g !== groupName)
|
||||
delete autoAddInstanceGroupsRoles[groupName]
|
||||
await saveInstanceGroupSettings()
|
||||
} catch (e) {
|
||||
// Rollback on error
|
||||
autoAddInstanceGroups = previousGroups
|
||||
if (previousRole) {
|
||||
autoAddInstanceGroupsRoles[groupName] = previousRole
|
||||
}
|
||||
sendUserToast('Failed to remove instance group', true)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateGroupRole(groupName: string, role: string): Promise<void> {
|
||||
const previousRole = autoAddInstanceGroupsRoles[groupName]
|
||||
|
||||
try {
|
||||
autoAddInstanceGroupsRoles[groupName] = role
|
||||
await saveInstanceGroupSettings()
|
||||
} catch (e) {
|
||||
// Rollback on error
|
||||
autoAddInstanceGroupsRoles[groupName] = previousRole
|
||||
sendUserToast('Failed to update role', true)
|
||||
}
|
||||
}
|
||||
|
||||
let domain = $derived($userStore?.email.split('@')[1])
|
||||
|
||||
$effect(() => {
|
||||
@@ -87,6 +205,7 @@
|
||||
getUsage()
|
||||
listInvites()
|
||||
loadSettings()
|
||||
loadInstanceGroups()
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -100,6 +219,7 @@
|
||||
})
|
||||
|
||||
let deleteConfirmedCallback: (() => void) | undefined = $state(undefined)
|
||||
let removeInstanceGroupConfirmedCallback: (() => void) | undefined = $state(undefined)
|
||||
|
||||
async function removeAllInvitesFromDomain() {
|
||||
await Promise.all(
|
||||
@@ -274,7 +394,10 @@
|
||||
invite_all: !isCloudHosted(),
|
||||
auto_add: showInvites ? (autoAdd ?? false) : true
|
||||
}
|
||||
: { operator: undefined, auto_add: undefined }
|
||||
: {
|
||||
operator: undefined,
|
||||
auto_add: undefined
|
||||
}
|
||||
})
|
||||
loadSettings()
|
||||
listInvites()
|
||||
@@ -292,6 +415,179 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
|
||||
{#if instanceGroups.length > 0}
|
||||
<Popover
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
usePointerDownOutside
|
||||
floatingClass="!z-20"
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<Button
|
||||
color={autoAddInstanceGroups.length > 0 ? 'green' : 'gray'}
|
||||
variant="border"
|
||||
size="xs"
|
||||
nonCaptureEvent={true}
|
||||
startIcon={{ icon: Mails }}
|
||||
>Instance groups: {autoAddInstanceGroups.length}
|
||||
</Button>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="flex flex-col p-4 min-w-[500px]">
|
||||
<div class="flex flex-col gap-4">
|
||||
<span class="text-sm leading-6 font-semibold">
|
||||
Auto-add instance groups
|
||||
</span>
|
||||
|
||||
<!-- Add new instance group form -->
|
||||
{#if availableGroupItems.length > 0}
|
||||
<div class="flex w-full mt-1 gap-2 items-end justify-between">
|
||||
<div class="flex gap-2 items-end">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-xs text-tertiary">Instance group</span>
|
||||
<Select
|
||||
items={availableGroupItems}
|
||||
placeholder="Select group"
|
||||
bind:value={selectedNewInstanceGroup}
|
||||
class="max-w-[160px]"
|
||||
disablePortal={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-xs text-tertiary">Role</span>
|
||||
<ToggleButtonGroup
|
||||
selected={selectedNewRole}
|
||||
on:selected={(e) => {
|
||||
selectedNewRole = e.detail
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton
|
||||
value="operator"
|
||||
label="Operator"
|
||||
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="developer"
|
||||
label="Developer"
|
||||
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="admin"
|
||||
label="Admin"
|
||||
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
|
||||
{item}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
color="blue"
|
||||
size="xs"
|
||||
startIcon={{ icon: Plus }}
|
||||
disabled={!selectedNewInstanceGroup || !selectedNewRole}
|
||||
onclick={addInstanceGroup}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Configured groups table -->
|
||||
{#if autoAddInstanceGroups.length > 0}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-sm font-medium text-secondary">Configured groups:</p>
|
||||
<div class="flex flex-col gap-1">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-left text-xs text-tertiary">
|
||||
<th class="pb-2 w-1/2">Group</th>
|
||||
<th class="pb-2 w-1/4">Role</th>
|
||||
<th class="pb-2 w-1/4"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each autoAddInstanceGroups as groupName (groupName)}
|
||||
{@const group = instanceGroups.find(g => g.name === groupName)}
|
||||
<tr class="border-t border-gray-200 dark:border-gray-700">
|
||||
<td class="py-2">
|
||||
<div class="font-medium">{groupName}</div>
|
||||
{#if group?.summary}
|
||||
<div class="text-xs text-tertiary">{group.summary}</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="py-2">
|
||||
<div>
|
||||
<ToggleButtonGroup
|
||||
selected={autoAddInstanceGroupsRoles[groupName] || 'developer'}
|
||||
on:selected={async (e) => {
|
||||
autoAddInstanceGroupsRoles[groupName] = e.detail
|
||||
await updateGroupRole(groupName, e.detail)
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton
|
||||
value="operator"
|
||||
label="Operator"
|
||||
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="developer"
|
||||
label="Developer"
|
||||
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="admin"
|
||||
label="Admin"
|
||||
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
|
||||
{item}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2">
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
color="light"
|
||||
variant="contained"
|
||||
btnClasses="text-red-500"
|
||||
size="xs"
|
||||
spacingSize="xs2"
|
||||
onclick={() => {
|
||||
removeInstanceGroupConfirmedCallback = async () => {
|
||||
await removeInstanceGroup(groupName)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center text-tertiary text-sm py-4">
|
||||
No instance groups configured for auto-add
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
<AddUser
|
||||
on:new={() => {
|
||||
listUsers()
|
||||
@@ -313,6 +609,14 @@
|
||||
<tr>
|
||||
<Cell head first>Email</Cell>
|
||||
<Cell head>Username</Cell>
|
||||
{#if hasNonManualUsers}
|
||||
<Cell head>
|
||||
Added via
|
||||
<Tooltip>
|
||||
Shows how the user was added to the workspace: manually, via domain auto-invite, or through an instance group.
|
||||
</Tooltip>
|
||||
</Cell>
|
||||
{/if}
|
||||
|
||||
<Cell head>
|
||||
Executions (<abbr title="past 1w">1w</abbr>)
|
||||
@@ -330,10 +634,34 @@
|
||||
</Head>
|
||||
<tbody class="divide-y bg-surface">
|
||||
{#if filteredUsers}
|
||||
{#each filteredUsers.slice(0, nbDisplayed) as { email, username, is_admin, operator, disabled } (email)}
|
||||
{#each sortedUsers().slice(0, nbDisplayed) as { email, username, is_admin, operator, disabled, added_via }, index (email)}
|
||||
<!-- Add separator between manual users and instance group users -->
|
||||
{#if hasNonManualUsers && index > 0 && sortedUsers()[index - 1]?.added_via?.source !== 'instance_group' && added_via?.source === 'instance_group'}
|
||||
<tr class="bg-surface-secondary">
|
||||
<td colspan={hasNonManualUsers ? 8 : 7} class="px-4 py-2">
|
||||
<div class="text-xs text-tertiary font-bold">
|
||||
Instance group users
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
<tr class="!hover:bg-surface-hover">
|
||||
<Cell first><a href="mailto:{email}">{truncate(email, 20)}</a></Cell>
|
||||
<Cell>{truncate(username, 30)}</Cell>
|
||||
{#if hasNonManualUsers}
|
||||
<Cell>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if added_via?.source === 'instance_group'}
|
||||
<Badge color="blue">Group</Badge>
|
||||
<span>{truncate(added_via.group || 'Unknown', 20)}</span>
|
||||
{:else if added_via?.source === 'domain'}
|
||||
<Badge color="blue">Auto-add</Badge>
|
||||
{:else}
|
||||
<Badge color="blue">Manual</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
</Cell>
|
||||
{/if}
|
||||
<Cell
|
||||
>{#if usage?.[email] != undefined}{usage?.[email]}{:else}<Loader2
|
||||
size={14}
|
||||
@@ -351,55 +679,64 @@
|
||||
</Cell>
|
||||
<Cell>
|
||||
<div>
|
||||
<ToggleButtonGroup
|
||||
selected={is_admin ? 'admin' : operator ? 'operator' : 'developer'}
|
||||
on:selected={async (e) => {
|
||||
if (is_admin && email == $userStore?.email && e.detail != 'admin') {
|
||||
sendUserToast(
|
||||
'Admins cannot be demoted by themselves, ask another admin to demote you',
|
||||
true
|
||||
)
|
||||
e.preventDefault()
|
||||
{#if added_via?.source === 'instance_group'}
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="rounded-md text-xs px-2 py-1 bg-surface shadow-md font-bold">
|
||||
{is_admin ? 'Admin' : operator ? 'Operator' : 'Developer'}
|
||||
</span>
|
||||
<Tooltip>Role is managed through instance group configuration above.</Tooltip>
|
||||
</div>
|
||||
{:else}
|
||||
<ToggleButtonGroup
|
||||
selected={is_admin ? 'admin' : operator ? 'operator' : 'developer'}
|
||||
on:selected={async (e) => {
|
||||
if (is_admin && email == $userStore?.email && e.detail != 'admin') {
|
||||
sendUserToast(
|
||||
'Admins cannot be demoted by themselves, ask another admin to demote you',
|
||||
true
|
||||
)
|
||||
e.preventDefault()
|
||||
listUsers()
|
||||
return
|
||||
}
|
||||
const body =
|
||||
e.detail == 'admin'
|
||||
? { is_admin: true, operator: false }
|
||||
: e.detail == 'operator'
|
||||
? { is_admin: false, operator: true }
|
||||
: { is_admin: false, operator: false }
|
||||
await UserService.updateUser({
|
||||
workspace: $workspaceStore ?? '',
|
||||
username,
|
||||
requestBody: body
|
||||
})
|
||||
listUsers()
|
||||
return
|
||||
}
|
||||
const body =
|
||||
e.detail == 'admin'
|
||||
? { is_admin: true, operator: false }
|
||||
: e.detail == 'operator'
|
||||
? { is_admin: false, operator: true }
|
||||
: { is_admin: false, operator: false }
|
||||
await UserService.updateUser({
|
||||
workspace: $workspaceStore ?? '',
|
||||
username,
|
||||
requestBody: body
|
||||
})
|
||||
listUsers()
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton
|
||||
value="operator"
|
||||
label="Operator"
|
||||
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
|
||||
{item}
|
||||
/>
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton
|
||||
value="operator"
|
||||
label="Operator"
|
||||
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
|
||||
{item}
|
||||
/>
|
||||
|
||||
<ToggleButton
|
||||
value="developer"
|
||||
label="Developer"
|
||||
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="developer"
|
||||
label="Developer"
|
||||
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
|
||||
{item}
|
||||
/>
|
||||
|
||||
<ToggleButton
|
||||
value="admin"
|
||||
label="Admin"
|
||||
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
|
||||
{item}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
<ToggleButton
|
||||
value="admin"
|
||||
label="Admin"
|
||||
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
|
||||
{item}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
</div>
|
||||
</Cell>
|
||||
<Cell>
|
||||
@@ -423,25 +760,41 @@
|
||||
{disabled ? 'Enable' : 'Disable'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="light"
|
||||
variant="contained"
|
||||
btnClasses="text-red-500"
|
||||
size="xs"
|
||||
spacingSize="xs2"
|
||||
on:click={() => {
|
||||
deleteConfirmedCallback = async () => {
|
||||
await UserService.deleteUser({
|
||||
workspace: $workspaceStore ?? '',
|
||||
username
|
||||
})
|
||||
sendUserToast('User removed')
|
||||
listUsers()
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
{#if added_via?.source === 'instance_group'}
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
color="light"
|
||||
variant="contained"
|
||||
btnClasses="text-gray-400"
|
||||
size="xs"
|
||||
spacingSize="xs2"
|
||||
disabled={true}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<Tooltip>Cannot remove users synced from instance groups. Either disable the user or remove them from the SCIM group.</Tooltip>
|
||||
</div>
|
||||
{:else}
|
||||
<Button
|
||||
color="light"
|
||||
variant="contained"
|
||||
btnClasses="text-red-500"
|
||||
size="xs"
|
||||
spacingSize="xs2"
|
||||
on:click={() => {
|
||||
deleteConfirmedCallback = async () => {
|
||||
await UserService.deleteUser({
|
||||
workspace: $workspaceStore ?? '',
|
||||
username
|
||||
})
|
||||
sendUserToast('User removed')
|
||||
listUsers()
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Cell>
|
||||
</tr>
|
||||
@@ -589,3 +942,24 @@
|
||||
<span>Are you sure you want to remove ?</span>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<div class="[&>div]:!z-[5002]">
|
||||
<ConfirmationModal
|
||||
open={Boolean(removeInstanceGroupConfirmedCallback)}
|
||||
title="Remove instance group"
|
||||
confirmationText="Remove"
|
||||
on:canceled={() => {
|
||||
removeInstanceGroupConfirmedCallback = undefined
|
||||
}}
|
||||
on:confirmed={() => {
|
||||
if (removeInstanceGroupConfirmedCallback) {
|
||||
removeInstanceGroupConfirmedCallback()
|
||||
}
|
||||
removeInstanceGroupConfirmedCallback = undefined
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col w-full space-y-4">
|
||||
<span>Are you sure you want to remove this instance group from auto-add? This will not remove users already added from this group.</span>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Group } from '$lib/gen'
|
||||
import type { InstanceGroup } from '$lib/gen'
|
||||
import type { InstanceGroupWithWorkspaces } from '$lib/gen'
|
||||
import { GroupService } from '$lib/gen'
|
||||
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
let newGroupName: string = $state('')
|
||||
let groups: GroupW[] | undefined = $state(undefined)
|
||||
let instanceGroups: InstanceGroup[] | undefined = $state(undefined)
|
||||
let instanceGroups: InstanceGroupWithWorkspaces[] | undefined = $state(undefined)
|
||||
let groupDrawer: Drawer | undefined = $state()
|
||||
|
||||
async function loadGroups(): Promise<void> {
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
async function loadInstanceGroups(): Promise<void> {
|
||||
try {
|
||||
instanceGroups = await GroupService.listInstanceGroups()
|
||||
instanceGroups = await GroupService.listInstanceGroupsWithWorkspaces()
|
||||
} catch (e) {
|
||||
instanceGroups = undefined
|
||||
}
|
||||
@@ -211,22 +211,37 @@
|
||||
<tr slot="header-row">
|
||||
<th>Name</th>
|
||||
<th>Members</th>
|
||||
<th>Workspaces</th>
|
||||
</tr>
|
||||
{#snippet body()}
|
||||
<tbody>
|
||||
{#each instanceGroups ?? [] as { name, emails }}
|
||||
{#each instanceGroups ?? [] as { name, emails, workspaces }}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
href="#{name}"
|
||||
onclick={() => {
|
||||
editGroupName = name
|
||||
groupDrawer?.openDrawer()
|
||||
if (name) {
|
||||
editGroupName = name
|
||||
groupDrawer?.openDrawer()
|
||||
}
|
||||
}}
|
||||
>{name}
|
||||
</a>
|
||||
</td>
|
||||
<td>{emails?.length ?? 0} members</td>
|
||||
<td>
|
||||
{#if workspaces && workspaces.length > 0}
|
||||
{#each workspaces as workspace, index}
|
||||
{#if index > 0}${", "}{/if}<a
|
||||
href="/workspace_settings?tab=users&workspace={workspace.workspace_id}"
|
||||
class="text-blue-500 hover:underline"
|
||||
>{workspace.workspace_id}</a> ({workspace.role})
|
||||
{/each}
|
||||
{:else}
|
||||
<span class="text-tertiary text-sm">No workspaces</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user