diff --git a/backend/.sqlx/query-1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597.json b/backend/.sqlx/query-1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597.json index 686158d9a1d34..64d273ed70528 100644 --- a/backend/.sqlx/query-1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597.json +++ b/backend/.sqlx/query-1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597.json @@ -122,6 +122,11 @@ "ordinal": 23, "name": "mute_critical_alerts", "type_info": "Bool" + }, + { + "ordinal": 24, + "name": "color", + "type_info": "Varchar" } ], "parameters": { @@ -153,6 +158,7 @@ false, true, true, + true, true ] }, diff --git a/backend/.sqlx/query-3651ed42be75d41ab0387f1551012d72c90235bb40d2f66e6fb235c990d78352.json b/backend/.sqlx/query-3651ed42be75d41ab0387f1551012d72c90235bb40d2f66e6fb235c990d78352.json new file mode 100644 index 0000000000000..87e705630b285 --- /dev/null +++ b/backend/.sqlx/query-3651ed42be75d41ab0387f1551012d72c90235bb40d2f66e6fb235c990d78352.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT workspace.id, workspace.name, workspace.owner, workspace.deleted, workspace.premium, workspace_settings.color\n FROM workspace\n LEFT JOIN workspace_settings ON workspace.id = workspace_settings.workspace_id\n JOIN usr ON usr.workspace_id = workspace.id\n WHERE usr.email = $1 AND workspace.deleted = false", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "owner", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "deleted", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "premium", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "color", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true + ] + }, + "hash": "3651ed42be75d41ab0387f1551012d72c90235bb40d2f66e6fb235c990d78352" +} diff --git a/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json b/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json index 6aab7e7796a9a..6092b0551a48c 100644 --- a/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json +++ b/backend/.sqlx/query-55cb03040bc2a8c53dd7fbb42bbdcc40f463cbc52d94ed9315cf9a547d4c89f2.json @@ -122,6 +122,11 @@ "ordinal": 23, "name": "mute_critical_alerts", "type_info": "Bool" + }, + { + "ordinal": 24, + "name": "color", + "type_info": "Varchar" } ], "parameters": { @@ -153,6 +158,7 @@ false, true, true, + true, true ] }, diff --git a/backend/.sqlx/query-6c845f168b6265e6cf92e4d33c5409edfdc1847d0348df2ea55504ddaaa67736.json b/backend/.sqlx/query-6c845f168b6265e6cf92e4d33c5409edfdc1847d0348df2ea55504ddaaa67736.json new file mode 100644 index 0000000000000..b4199d4ca1a90 --- /dev/null +++ b/backend/.sqlx/query-6c845f168b6265e6cf92e4d33c5409edfdc1847d0348df2ea55504ddaaa67736.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color\n FROM workspace\n JOIN usr ON usr.workspace_id = workspace.id\n JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id\n WHERE usr.email = $1 AND workspace.deleted = false", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "username", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "color", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + true + ] + }, + "hash": "6c845f168b6265e6cf92e4d33c5409edfdc1847d0348df2ea55504ddaaa67736" +} diff --git a/backend/.sqlx/query-d2668c2d4ece82a3617f098fe993d3e218fa224bd0bb311e7db776c7fd69cf0b.json b/backend/.sqlx/query-d2668c2d4ece82a3617f098fe993d3e218fa224bd0bb311e7db776c7fd69cf0b.json new file mode 100644 index 0000000000000..e3bebd610a9d2 --- /dev/null +++ b/backend/.sqlx/query-d2668c2d4ece82a3617f098fe993d3e218fa224bd0bb311e7db776c7fd69cf0b.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE workspace_settings SET color = $1 WHERE workspace_id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Text" + ] + }, + "nullable": [] + }, + "hash": "d2668c2d4ece82a3617f098fe993d3e218fa224bd0bb311e7db776c7fd69cf0b" +} diff --git a/backend/.sqlx/query-e54eb583f011cea6e4d533f4d2014cbf509b5afcc2647048c9bfc839d8f290ad.json b/backend/.sqlx/query-e54eb583f011cea6e4d533f4d2014cbf509b5afcc2647048c9bfc839d8f290ad.json new file mode 100644 index 0000000000000..b94339710d90b --- /dev/null +++ b/backend/.sqlx/query-e54eb583f011cea6e4d533f4d2014cbf509b5afcc2647048c9bfc839d8f290ad.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO workspace_settings\n (workspace_id, color)\n VALUES ($1, $2)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "e54eb583f011cea6e4d533f4d2014cbf509b5afcc2647048c9bfc839d8f290ad" +} diff --git a/backend/.sqlx/query-eed16e356f3f36183c3db13fcc1950295e0d0fbdabb38434534fb3430eeddc25.json b/backend/.sqlx/query-eed16e356f3f36183c3db13fcc1950295e0d0fbdabb38434534fb3430eeddc25.json new file mode 100644 index 0000000000000..5e0817ae84196 --- /dev/null +++ b/backend/.sqlx/query-eed16e356f3f36183c3db13fcc1950295e0d0fbdabb38434534fb3430eeddc25.json @@ -0,0 +1,53 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT workspace.id, workspace.name, workspace.owner, workspace.deleted, workspace.premium, workspace_settings.color\n FROM workspace\n LEFT JOIN workspace_settings ON workspace.id = workspace_settings.workspace_id\n LIMIT $1 OFFSET $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "owner", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "deleted", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "premium", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "color", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true + ] + }, + "hash": "eed16e356f3f36183c3db13fcc1950295e0d0fbdabb38434534fb3430eeddc25" +} diff --git a/backend/migrations/20250107212922_workspace_color.down.sql b/backend/migrations/20250107212922_workspace_color.down.sql new file mode 100644 index 0000000000000..80c87a3cd448c --- /dev/null +++ b/backend/migrations/20250107212922_workspace_color.down.sql @@ -0,0 +1 @@ +ALTER TABLE workspace_settings DROP COLUMN color; diff --git a/backend/migrations/20250107212922_workspace_color.up.sql b/backend/migrations/20250107212922_workspace_color.up.sql new file mode 100644 index 0000000000000..c98aa3d4c2ece --- /dev/null +++ b/backend/migrations/20250107212922_workspace_color.up.sql @@ -0,0 +1 @@ +ALTER TABLE workspace_settings ADD COLUMN color VARCHAR(7) DEFAULT NULL; diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index ab1a252b2fc00..4443894e5e30e 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1576,6 +1576,30 @@ paths: schema: type: string + /w/{workspace}/workspaces/change_workspace_color: + post: + summary: change workspace id + operationId: changeWorkspaceColor + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + content: + application/json: + schema: + type: object + properties: + color: + type: string + responses: + "200": + description: status + content: + text/plain: + schema: + type: string + /w/{workspace}/users/whois/{username}: get: summary: whois @@ -1722,6 +1746,8 @@ paths: $ref: "#/components/schemas/WorkspaceDefaultScripts" mute_critical_alerts: type: boolean + color: + type: string required: - code_completion_enabled - automatic_billing @@ -12765,10 +12791,13 @@ components: type: string username: type: string + color: + type: string required: - id - name - username + - color required: - email - workspaces @@ -12782,6 +12811,8 @@ components: type: string username: type: string + color: + type: string required: - id - name @@ -12797,6 +12828,8 @@ components: type: string domain: type: string + color: + type: string required: - id - name diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index 433450b6e80e7..ea07df6171c66 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -106,6 +106,7 @@ pub fn workspaced_service() -> Router { .route("/leave", post(leave_workspace)) .route("/get_workspace_name", get(get_workspace_name)) .route("/change_workspace_name", post(change_workspace_name)) + .route("/change_workspace_color", post(change_workspace_color)) .route( "/change_workspace_id", post(crate::workspaces_extra::change_workspace_id), @@ -158,6 +159,7 @@ struct Workspace { owner: String, deleted: bool, premium: bool, + color: Option, } #[derive(FromRow, Serialize, Debug)] @@ -186,6 +188,7 @@ pub struct WorkspaceSettings { pub automatic_billing: bool, pub default_scripts: Option, pub mute_critical_alerts: Option, + pub color: Option, } #[derive(FromRow, Serialize, Debug)] @@ -263,6 +266,7 @@ struct CreateWorkspace { id: String, name: String, username: Option, + color: Option, } #[derive(Deserialize)] @@ -282,6 +286,7 @@ struct UserWorkspace { pub id: String, pub name: String, pub username: String, + pub color: Option, } #[derive(Deserialize)] @@ -376,8 +381,11 @@ async fn list_workspaces( let mut tx = user_db.begin(&authed).await?; let workspaces = sqlx::query_as!( Workspace, - "SELECT workspace.* FROM workspace, usr WHERE usr.workspace_id = workspace.id AND \ - usr.email = $1 AND deleted = false", + "SELECT workspace.id, workspace.name, workspace.owner, workspace.deleted, workspace.premium, workspace_settings.color + FROM workspace + LEFT JOIN workspace_settings ON workspace.id = workspace_settings.workspace_id + JOIN usr ON usr.workspace_id = workspace.id + WHERE usr.email = $1 AND workspace.deleted = false", authed.email ) .fetch_all(&mut *tx) @@ -1331,7 +1339,10 @@ async fn list_workspaces_as_super_admin( let mut tx = user_db.begin(&authed).await?; let workspaces = sqlx::query_as!( Workspace, - "SELECT * FROM workspace LIMIT $1 OFFSET $2", + "SELECT workspace.id, workspace.name, workspace.owner, workspace.deleted, workspace.premium, workspace_settings.color + FROM workspace + LEFT JOIN workspace_settings ON workspace.id = workspace_settings.workspace_id + LIMIT $1 OFFSET $2", per_page as i32, offset as i32 ) @@ -1348,9 +1359,11 @@ async fn user_workspaces( let mut tx = db.begin().await?; let workspaces = sqlx::query_as!( UserWorkspace, - "SELECT workspace.id, workspace.name, usr.username - FROM workspace, usr WHERE usr.workspace_id = workspace.id AND usr.email = $1 AND deleted = \ - false", + "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color + FROM workspace + JOIN usr ON usr.workspace_id = workspace.id + JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id + WHERE usr.email = $1 AND workspace.deleted = false", email ) .fetch_all(&mut *tx) @@ -1430,9 +1443,10 @@ async fn create_workspace( .await?; sqlx::query!( "INSERT INTO workspace_settings - (workspace_id) - VALUES ($1)", - nw.id + (workspace_id, color) + VALUES ($1, $2)", + nw.id, + nw.color, ) .execute(&mut *tx) .await?; @@ -1943,6 +1957,11 @@ struct ChangeWorkspaceName { new_name: String, } +#[derive(Deserialize)] +struct ChangeWorkspaceColor { + color: Option, +} + async fn change_workspace_name( authed: ApiAuthed, Path(w_id): Path, @@ -1977,6 +1996,32 @@ async fn change_workspace_name( Ok(format!("updated workspace name to {}", &rw.new_name)) } +async fn change_workspace_color( + authed: ApiAuthed, + Path(w_id): Path, + Extension(db): Extension, + Json(rw): Json, +) -> Result { + require_admin(authed.is_admin, &authed.username)?; + + let mut tx = db.begin().await?; + + sqlx::query!( + "UPDATE workspace_settings SET color = $1 WHERE workspace_id = $2", + rw.color, + &w_id + ) + .execute(&mut *tx) + .await?; + + tx.commit().await?; + + Ok(format!( + "updated workspace color to {}", + rw.color.as_deref().unwrap_or("no color") + )) +} + async fn get_usage(Extension(db): Extension, Path(w_id): Path) -> Result { let usage = sqlx::query_scalar!( " diff --git a/frontend/src/lib/components/settings/ChangeWorkspaceColor.svelte b/frontend/src/lib/components/settings/ChangeWorkspaceColor.svelte new file mode 100644 index 0000000000000..dd325f1354d41 --- /dev/null +++ b/frontend/src/lib/components/settings/ChangeWorkspaceColor.svelte @@ -0,0 +1,96 @@ + + +
+

Workspace Color

+
+ {#if workspaceColor} +
+ {:else} + No color set + {/if} +
+
+ + +
+ +
+ + + + +
diff --git a/frontend/src/lib/components/sidebar/MenuButton.svelte b/frontend/src/lib/components/sidebar/MenuButton.svelte index 5b3c048d0e5bc..ab7a32274fa4c 100644 --- a/frontend/src/lib/components/sidebar/MenuButton.svelte +++ b/frontend/src/lib/components/sidebar/MenuButton.svelte @@ -11,7 +11,7 @@ export let stopPropagationOnClick: boolean = false export let shortcut: string = '' export let notificationsCount: number = 0 - + export let color: string | null = null let dispatch = createEventDispatcher() @@ -26,10 +26,12 @@ 'group flex items-center px-2 py-2 font-light rounded-md h-8 gap-3 w-full', lightMode ? 'text-primary hover:bg-surface-hover ' - : ' hover:bg-[#2A3648] text-primary-inverse dark:text-primary', + : 'hover:bg-[#2A3648] text-primary-inverse dark:text-primary', + color ? 'border-4' : '', 'transition-all', $$props.class )} + style={color ? `border-color: ${color}; padding: 0 calc(0.5rem - 4px);` : ''} title={label} > {#if icon} @@ -50,7 +52,7 @@
- + w.id === $workspaceStore)?.color} + />
@@ -63,17 +70,29 @@ {#each $userWorkspaces as workspace} @@ -93,19 +112,19 @@
{/if} {#if !strictWorkspaceSelect} - + {/if} {#if ($userStore?.is_admin || $superadmin) && !strictWorkspaceSelect}
diff --git a/frontend/src/lib/stores.ts b/frontend/src/lib/stores.ts index 8af1ddf5ac267..65ba9f44ef79d 100644 --- a/frontend/src/lib/stores.ts +++ b/frontend/src/lib/stores.ts @@ -55,6 +55,7 @@ export const userWorkspaces: Readable< id: string name: string username: string + color: string | null }> > = derived([usersWorkspaceStore, superadmin], ([store, superadmin]) => { const originalWorkspaces = store?.workspaces ?? [] @@ -64,7 +65,8 @@ export const userWorkspaces: Readable< { id: 'admins', name: 'Admins', - username: 'superadmin' + username: 'superadmin', + color: null } ] } else { diff --git a/frontend/src/routes/(root)/(logged)/user/(user)/create_workspace/+page.svelte b/frontend/src/routes/(root)/(logged)/user/(user)/create_workspace/+page.svelte index fd47e0f0113a3..b57b5d2fb8cbd 100644 --- a/frontend/src/routes/(root)/(logged)/user/(user)/create_workspace/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/user/(user)/create_workspace/+page.svelte @@ -37,10 +37,19 @@ let codeCompletionEnabled = true let checking = false + let workspaceColor: string | null = null + let colorEnabled = false + + function generateRandomColor() { + const randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0'); + workspaceColor = randomColor; + } + $: id = name.toLowerCase().replace(/\s/gi, '-') $: validateName(id) $: errorUser = validateUsername(username) + $: colorEnabled && !workspaceColor && generateRandomColor() async function validateName(id: string): Promise { checking = true @@ -60,6 +69,7 @@ requestBody: { id, name, + color: colorEnabled && workspaceColor ? workspaceColor : undefined, username: automateUsernameCreation ? undefined : username } }) @@ -186,6 +196,16 @@ {/if} + {#if !automateUsernameCreation}