From 2371994bc1dd3837d495003b33f2f074fc85c85d Mon Sep 17 00:00:00 2001 From: Alexander Petric Date: Wed, 8 Jan 2025 16:45:51 -0500 Subject: [PATCH 1/4] feat(frontend): allow workspace admin to set workspace color --- .../20250107212922_workspace_color.down.sql | 1 + .../20250107212922_workspace_color.up.sql | 1 + backend/windmill-api/openapi.yaml | 33 +++++++ backend/windmill-api/src/workspaces.rs | 63 ++++++++++-- .../settings/ChangeWorkspaceColor.svelte | 96 +++++++++++++++++++ .../lib/components/sidebar/MenuButton.svelte | 8 +- .../components/sidebar/WorkspaceMenu.svelte | 62 +++++++----- frontend/src/lib/stores.ts | 4 +- .../user/(user)/create_workspace/+page.svelte | 20 ++++ .../user/(user)/workspaces/+page.svelte | 13 ++- .../(logged)/workspace_settings/+page.svelte | 2 + 11 files changed, 265 insertions(+), 38 deletions(-) create mode 100644 backend/migrations/20250107212922_workspace_color.down.sql create mode 100644 backend/migrations/20250107212922_workspace_color.up.sql create mode 100644 frontend/src/lib/components/settings/ChangeWorkspaceColor.svelte 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..11ea3b2f55836 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-2' : '', 'transition-all', $$props.class )} + style={color ? `border-color: ${color}; padding: 0 calc(0.5rem - 2px);` : ''} title={label} > {#if icon} @@ -50,7 +52,7 @@
- + w.id === $workspaceStore)?.color} + />
@@ -63,17 +69,29 @@ {#each $userWorkspaces as workspace} @@ -93,19 +111,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}