From 4e2b6c235e832a86818585b699850e0a99932c4b Mon Sep 17 00:00:00 2001 From: dal Date: Thu, 17 Jul 2025 15:26:26 -0600 Subject: [PATCH] Refactor workspace sharing permissions across assets - Updated the `WorkspaceSharing` enum to use camelCase for serialization. - Introduced `workspace_permissions` field in update requests for chats, collections, dashboards, and metrics. - Implemented handling of workspace sharing permissions in respective update handlers, allowing for setting and removing permissions. - Adjusted frontend components and API interfaces to align with the new `workspace_sharing` naming convention. This change enhances the consistency and usability of workspace sharing across different asset types. --- apps/api/libs/database/src/enums.rs | 10 ++-- .../chats/sharing/update_sharing_handler.rs | 57 +++++++++++++++++- .../sharing/update_sharing_handler.rs | 60 ++++++++++++++++++- .../sharing/update_sharing_handler.rs | 47 ++++++++++++++- .../metrics/sharing/update_sharing_handler.rs | 47 ++++++++++++++- .../shared_interfaces/shareInterfaces.ts | 2 +- .../buster_rest/collections/queryRequests.ts | 4 +- .../buster_rest/dashboards/queryRequests.ts | 4 +- .../metrics/updateMetricQueryRequests.ts | 4 +- .../ShareMenu/ShareMenuContent.stories.tsx | 1 + .../ShareMenu/ShareMenuContentBody.tsx | 22 +++---- .../ShareMenu/WorkspaceShareSection.tsx | 6 +- .../components/features/ShareMenu/helpers.ts | 4 +- .../src/metrics/requests.types.ts | 2 +- .../src/share/share-interfaces.types.ts | 2 +- 15 files changed, 238 insertions(+), 34 deletions(-) diff --git a/apps/api/libs/database/src/enums.rs b/apps/api/libs/database/src/enums.rs index 81f0f9e70..e34b31dfa 100644 --- a/apps/api/libs/database/src/enums.rs +++ b/apps/api/libs/database/src/enums.rs @@ -742,15 +742,15 @@ impl FromSql for MessageFeedback { Serialize, )] #[diesel(sql_type = sql_types::WorkspaceSharingEnum)] -#[serde(rename_all = "snake_case")] +#[serde(rename_all = "camelCase")] pub enum WorkspaceSharing { #[serde(alias = "none")] None, - #[serde(alias = "canView")] + #[serde(alias = "can_view")] CanView, - #[serde(alias = "canEdit")] + #[serde(alias = "can_edit")] CanEdit, - #[serde(alias = "fullAccess")] + #[serde(alias = "full_access")] FullAccess, } @@ -776,4 +776,4 @@ impl FromSql for WorkspaceSharing { _ => Err("Unrecognized WorkspaceSharing variant".into()), } } -} \ No newline at end of file +} diff --git a/apps/api/libs/handlers/src/chats/sharing/update_sharing_handler.rs b/apps/api/libs/handlers/src/chats/sharing/update_sharing_handler.rs index 723434ce4..d80d0391d 100644 --- a/apps/api/libs/handlers/src/chats/sharing/update_sharing_handler.rs +++ b/apps/api/libs/handlers/src/chats/sharing/update_sharing_handler.rs @@ -2,10 +2,14 @@ use anyhow::{anyhow, Result}; use chrono::{DateTime, Utc}; use database::{ chats::fetch_chat_with_permission, - enums::{AssetPermissionRole, AssetType}, + enums::{AssetPermissionRole, AssetType, WorkspaceSharing}, + pool::get_pg_pool, + schema::chats::dsl, }; use middleware::AuthenticatedUser; use serde::{Deserialize, Serialize}; +use diesel::ExpressionMethods; +use diesel_async::RunQueryDsl as AsyncRunQueryDsl; use sharing::{check_permission_access, create_share_by_email, types::UpdateField}; use tracing::info; use uuid::Uuid; @@ -30,6 +34,9 @@ pub struct UpdateChatSharingRequest { /// Expiration date for public access #[serde(default)] pub public_expiry_date: UpdateField>, + /// Workspace sharing permissions + #[serde(rename = "workspace_sharing")] + pub workspace_permissions: Option>, } /// Updates sharing permissions for a chat @@ -115,6 +122,54 @@ pub async fn update_chat_sharing_handler( // If public sharing for chats is implemented in the future, this section will need to be updated // Following the pattern from metric_sharing_handler.rs and dashboard_sharing_handler.rs + // 4. Handle workspace_permissions if provided + if let Some(workspace_perm) = request.workspace_permissions { + let mut conn = get_pg_pool().get().await?; + + match workspace_perm { + Some(perm) => { + info!( + chat_id = %chat_id, + "Setting workspace permissions for chat to {:?}", + perm + ); + diesel::update(dsl::chats) + .filter(dsl::id.eq(chat_id)) + .set(( + dsl::workspace_sharing.eq(perm), + dsl::workspace_sharing_enabled_by.eq(if perm != WorkspaceSharing::None { + Some(user.id) + } else { + None + }), + dsl::workspace_sharing_enabled_at.eq(if perm != WorkspaceSharing::None { + Some(Utc::now()) + } else { + None + }), + )) + .execute(&mut conn) + .await?; + } + None => { + // Setting to None means removing workspace sharing + info!( + chat_id = %chat_id, + "Removing workspace permissions for chat" + ); + diesel::update(dsl::chats) + .filter(dsl::id.eq(chat_id)) + .set(( + dsl::workspace_sharing.eq(WorkspaceSharing::None), + dsl::workspace_sharing_enabled_by.eq(None::), + dsl::workspace_sharing_enabled_at.eq(None::>), + )) + .execute(&mut conn) + .await?; + } + } + } + Ok(()) } diff --git a/apps/api/libs/handlers/src/collections/sharing/update_sharing_handler.rs b/apps/api/libs/handlers/src/collections/sharing/update_sharing_handler.rs index bc79a546d..185e72581 100644 --- a/apps/api/libs/handlers/src/collections/sharing/update_sharing_handler.rs +++ b/apps/api/libs/handlers/src/collections/sharing/update_sharing_handler.rs @@ -1,11 +1,15 @@ use anyhow::{anyhow, Result}; use chrono::{DateTime, Utc}; use database::{ - enums::{AssetPermissionRole, AssetType}, + enums::{AssetPermissionRole, AssetType, WorkspaceSharing}, helpers::collections::fetch_collection_with_permission, + pool::get_pg_pool, + schema::collections::dsl, }; use middleware::AuthenticatedUser; use serde::{Deserialize, Serialize}; +use diesel::ExpressionMethods; +use diesel_async::RunQueryDsl as AsyncRunQueryDsl; use sharing::{ check_permission_access, create_asset_permission::create_share_by_email, @@ -37,6 +41,9 @@ pub struct UpdateCollectionSharingRequest { /// Note: Collections are not publicly accessible, this field is ignored #[serde(default)] pub public_expiry_date: UpdateField>, + /// Workspace sharing permissions + #[serde(rename = "workspace_sharing")] + pub workspace_permissions: Option>, } /// Update sharing permissions for a collection @@ -112,5 +119,56 @@ pub async fn update_collection_sharing_handler( // 4. Public access settings are ignored for collections // Collections are not publicly accessible, so we ignore the public_* fields + // 5. Handle workspace_permissions if provided + if let Some(workspace_perm) = request.workspace_permissions { + let mut conn = get_pg_pool().get().await?; + + // Load current collection data for updates + let collection = collection_with_permission.collection; + + match workspace_perm { + Some(perm) => { + info!( + collection_id = %collection_id, + "Setting workspace permissions for collection to {:?}", + perm + ); + diesel::update(dsl::collections) + .filter(dsl::id.eq(collection_id)) + .set(( + dsl::workspace_sharing.eq(perm), + dsl::workspace_sharing_enabled_by.eq(if perm != WorkspaceSharing::None { + Some(user.id) + } else { + None + }), + dsl::workspace_sharing_enabled_at.eq(if perm != WorkspaceSharing::None { + Some(Utc::now()) + } else { + None + }), + )) + .execute(&mut conn) + .await?; + } + None => { + // Setting to None means removing workspace sharing + info!( + collection_id = %collection_id, + "Removing workspace permissions for collection" + ); + diesel::update(dsl::collections) + .filter(dsl::id.eq(collection_id)) + .set(( + dsl::workspace_sharing.eq(WorkspaceSharing::None), + dsl::workspace_sharing_enabled_by.eq(None::), + dsl::workspace_sharing_enabled_at.eq(None::>), + )) + .execute(&mut conn) + .await?; + } + } + } + Ok(()) } \ No newline at end of file diff --git a/apps/api/libs/handlers/src/dashboards/sharing/update_sharing_handler.rs b/apps/api/libs/handlers/src/dashboards/sharing/update_sharing_handler.rs index 1447dda9e..b6a6e9585 100644 --- a/apps/api/libs/handlers/src/dashboards/sharing/update_sharing_handler.rs +++ b/apps/api/libs/handlers/src/dashboards/sharing/update_sharing_handler.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use chrono::{DateTime, Utc}; use database::{ - enums::{AssetPermissionRole, AssetType}, + enums::{AssetPermissionRole, AssetType, WorkspaceSharing}, helpers::dashboard_files::fetch_dashboard_file_with_permission, schema::dashboard_files::dsl, pool::get_pg_pool, @@ -38,6 +38,9 @@ pub struct UpdateDashboardSharingRequest { /// Expiration date for public access #[serde(default)] pub public_expiry_date: UpdateField>, + /// Workspace sharing permissions + #[serde(rename = "workspace_sharing")] + pub workspace_permissions: Option>, } /// Updates sharing permissions for a dashboard @@ -138,6 +141,9 @@ pub async fn update_dashboard_sharing_handler( let mut publicly_enabled_by = dashboard.publicly_enabled_by; let mut public_password = dashboard.public_password; let mut public_expiry_date = dashboard.public_expiry_date; + let mut workspace_sharing = dashboard.workspace_sharing; + let mut workspace_sharing_enabled_by = dashboard.workspace_sharing_enabled_by; + let mut workspace_sharing_enabled_at = dashboard.workspace_sharing_enabled_at; let mut update_needed = false; // Update publicly_accessible if provided @@ -208,6 +214,42 @@ pub async fn update_dashboard_sharing_handler( UpdateField::NoChange => {} } + // Handle workspace_permissions + if let Some(workspace_perm) = request.workspace_permissions { + match workspace_perm { + Some(perm) => { + info!( + dashboard_id = %dashboard_id, + "Setting workspace permissions for dashboard to {:?}", + perm + ); + workspace_sharing = perm; + workspace_sharing_enabled_by = if perm != WorkspaceSharing::None { + Some(user.id) + } else { + None + }; + workspace_sharing_enabled_at = if perm != WorkspaceSharing::None { + Some(Utc::now()) + } else { + None + }; + update_needed = true; + } + None => { + // Setting to None means removing workspace sharing + info!( + dashboard_id = %dashboard_id, + "Removing workspace permissions for dashboard" + ); + workspace_sharing = WorkspaceSharing::None; + workspace_sharing_enabled_by = None; + workspace_sharing_enabled_at = None; + update_needed = true; + } + } + } + // Execute the update if any changes were made if update_needed { diesel::update(dsl::dashboard_files) @@ -217,6 +259,9 @@ pub async fn update_dashboard_sharing_handler( dsl::publicly_enabled_by.eq(publicly_enabled_by), dsl::public_password.eq(public_password), dsl::public_expiry_date.eq(public_expiry_date), + dsl::workspace_sharing.eq(workspace_sharing), + dsl::workspace_sharing_enabled_by.eq(workspace_sharing_enabled_by), + dsl::workspace_sharing_enabled_at.eq(workspace_sharing_enabled_at), )) .execute(&mut conn) .await?; diff --git a/apps/api/libs/handlers/src/metrics/sharing/update_sharing_handler.rs b/apps/api/libs/handlers/src/metrics/sharing/update_sharing_handler.rs index 7760a8086..639dc0137 100644 --- a/apps/api/libs/handlers/src/metrics/sharing/update_sharing_handler.rs +++ b/apps/api/libs/handlers/src/metrics/sharing/update_sharing_handler.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use chrono::{DateTime, Utc}; use database::{ - enums::{AssetPermissionRole, AssetType}, + enums::{AssetPermissionRole, AssetType, WorkspaceSharing}, helpers::metric_files::fetch_metric_file_with_permissions, pool::get_pg_pool, schema::metric_files::dsl, @@ -38,6 +38,9 @@ pub struct UpdateMetricSharingRequest { /// Expiration date for public access #[serde(default)] pub public_expiry_date: UpdateField>, + /// Workspace sharing permissions + #[serde(rename = "workspace_sharing")] + pub workspace_permissions: Option>, } /// Handler to update sharing permissions for a metric @@ -130,6 +133,9 @@ pub async fn update_metric_sharing_handler( let mut publicly_enabled_by = metric.publicly_enabled_by; let mut public_password = metric.public_password; let mut public_expiry_date = metric.public_expiry_date; + let mut workspace_sharing = metric.workspace_sharing; + let mut workspace_sharing_enabled_by = metric.workspace_sharing_enabled_by; + let mut workspace_sharing_enabled_at = metric.workspace_sharing_enabled_at; let mut update_needed = false; // Update publicly_accessible if provided @@ -200,6 +206,42 @@ pub async fn update_metric_sharing_handler( UpdateField::NoChange => {} } + // Handle workspace_permissions + if let Some(workspace_perm) = request.workspace_permissions { + match workspace_perm { + Some(perm) => { + info!( + metric_id = %metric_id, + "Setting workspace permissions for metric to {:?}", + perm + ); + workspace_sharing = perm; + workspace_sharing_enabled_by = if perm != WorkspaceSharing::None { + Some(user.id) + } else { + None + }; + workspace_sharing_enabled_at = if perm != WorkspaceSharing::None { + Some(Utc::now()) + } else { + None + }; + update_needed = true; + } + None => { + // Setting to None means removing workspace sharing + info!( + metric_id = %metric_id, + "Removing workspace permissions for metric" + ); + workspace_sharing = WorkspaceSharing::None; + workspace_sharing_enabled_by = None; + workspace_sharing_enabled_at = None; + update_needed = true; + } + } + } + // Execute the update if any changes were made if update_needed { diesel::update(dsl::metric_files) @@ -209,6 +251,9 @@ pub async fn update_metric_sharing_handler( dsl::publicly_enabled_by.eq(publicly_enabled_by), dsl::public_password.eq(public_password), dsl::public_expiry_date.eq(public_expiry_date), + dsl::workspace_sharing.eq(workspace_sharing), + dsl::workspace_sharing_enabled_by.eq(workspace_sharing_enabled_by), + dsl::workspace_sharing_enabled_at.eq(workspace_sharing_enabled_at), )) .execute(&mut conn) .await?; diff --git a/apps/web/src/api/asset_interfaces/shared_interfaces/shareInterfaces.ts b/apps/web/src/api/asset_interfaces/shared_interfaces/shareInterfaces.ts index a86001251..c72bc13f2 100644 --- a/apps/web/src/api/asset_interfaces/shared_interfaces/shareInterfaces.ts +++ b/apps/web/src/api/asset_interfaces/shared_interfaces/shareInterfaces.ts @@ -19,7 +19,7 @@ export type ShareUpdateRequest = { email: string; role: ShareRole; }[]; - workspace_permissions?: WorkspaceShareRole | null; + workspace_sharing?: WorkspaceShareRole | null; publicly_accessible?: boolean; public_password?: string | null; public_expiry_date?: string | null; diff --git a/apps/web/src/api/buster_rest/collections/queryRequests.ts b/apps/web/src/api/buster_rest/collections/queryRequests.ts index 15fc0472d..7d9763943 100644 --- a/apps/web/src/api/buster_rest/collections/queryRequests.ts +++ b/apps/web/src/api/buster_rest/collections/queryRequests.ts @@ -246,8 +246,8 @@ export const useUpdateCollectionShare = () => { if (params.public_expiry_date !== undefined) { draft.public_expiry_date = params.public_expiry_date; } - if (params.workspace_permissions !== undefined) { - draft.workspace_permissions = params.workspace_permissions ? [params.workspace_permissions] : []; + if (params.workspace_sharing !== undefined) { + draft.workspace_sharing = params.workspace_sharing; } }); }); diff --git a/apps/web/src/api/buster_rest/dashboards/queryRequests.ts b/apps/web/src/api/buster_rest/dashboards/queryRequests.ts index 56b2971ba..dc75e3557 100644 --- a/apps/web/src/api/buster_rest/dashboards/queryRequests.ts +++ b/apps/web/src/api/buster_rest/dashboards/queryRequests.ts @@ -437,8 +437,8 @@ export const useUpdateDashboardShare = () => { if (params.public_expiry_date !== undefined) { draft.public_expiry_date = params.public_expiry_date; } - if (params.workspace_permissions !== undefined) { - draft.workspace_permissions = params.workspace_permissions ? [params.workspace_permissions] : []; + if (params.workspace_sharing !== undefined) { + draft.workspace_sharing = params.workspace_sharing; } }); }); diff --git a/apps/web/src/api/buster_rest/metrics/updateMetricQueryRequests.ts b/apps/web/src/api/buster_rest/metrics/updateMetricQueryRequests.ts index aec5fe0c0..7c774c472 100644 --- a/apps/web/src/api/buster_rest/metrics/updateMetricQueryRequests.ts +++ b/apps/web/src/api/buster_rest/metrics/updateMetricQueryRequests.ts @@ -316,8 +316,8 @@ export const useUpdateMetricShare = () => { if (variables.params.public_expiry_date !== undefined) { draft.public_expiry_date = variables.params.public_expiry_date; } - if (variables.params.workspace_permissions !== undefined) { - draft.workspace_permissions = variables.params.workspace_permissions ? [variables.params.workspace_permissions] : []; + if (variables.params.workspace_sharing !== undefined) { + draft.workspace_sharing = variables.params.workspace_sharing; } }); }); diff --git a/apps/web/src/components/features/ShareMenu/ShareMenuContent.stories.tsx b/apps/web/src/components/features/ShareMenu/ShareMenuContent.stories.tsx index 2fe4eabb0..15c72c16b 100644 --- a/apps/web/src/components/features/ShareMenu/ShareMenuContent.stories.tsx +++ b/apps/web/src/components/features/ShareMenu/ShareMenuContent.stories.tsx @@ -34,6 +34,7 @@ const mockShareConfig: ShareConfig = { publicly_accessible: false, public_password: null, permission: 'owner', + workspace_sharing: 'none' }; export const MetricShare: Story = { diff --git a/apps/web/src/components/features/ShareMenu/ShareMenuContentBody.tsx b/apps/web/src/components/features/ShareMenu/ShareMenuContentBody.tsx index 620be78ae..ef121a95b 100644 --- a/apps/web/src/components/features/ShareMenu/ShareMenuContentBody.tsx +++ b/apps/web/src/components/features/ShareMenu/ShareMenuContentBody.tsx @@ -104,7 +104,7 @@ const ShareMenuContentShare: React.FC = React.memo( const payload: Parameters[0] = { id: assetId, params: { - workspace_permissions: role + workspace_sharing: role } }; @@ -127,16 +127,6 @@ const ShareMenuContentShare: React.FC = React.memo( /> )} - {canEditPermissions && ( - - )} - {hasIndividualPermissions && (
{individual_permissions?.map((permission) => ( @@ -150,6 +140,16 @@ const ShareMenuContentShare: React.FC = React.memo( ))}
)} + + {canEditPermissions && ( + + )} ); } diff --git a/apps/web/src/components/features/ShareMenu/WorkspaceShareSection.tsx b/apps/web/src/components/features/ShareMenu/WorkspaceShareSection.tsx index dfc9b701a..b43bfa4ea 100644 --- a/apps/web/src/components/features/ShareMenu/WorkspaceShareSection.tsx +++ b/apps/web/src/components/features/ShareMenu/WorkspaceShareSection.tsx @@ -3,7 +3,7 @@ import type { ShareAssetType, ShareConfig, WorkspaceShareRole } from '@buster/se import { Dropdown } from '@/components/ui/dropdown'; import type { DropdownItem } from '@/components/ui/dropdown'; import { ChevronDown } from '@/components/ui/icons/NucleoIconFilled'; -import { Office } from '@/components/ui/icons/NucleoIconOutlined'; +import { ApartmentBuilding } from '@/components/ui/icons/NucleoIconOutlined'; import { Text } from '@/components/ui/typography'; import { useMemoizedFn } from '@/hooks'; import { cn } from '@/lib/classMerge'; @@ -44,7 +44,7 @@ export const WorkspaceShareSection: React.FC = React canEditPermissions, onUpdateWorkspacePermissions }) => { - const currentRole = shareAssetConfig.workspace_permissions?.[0] || 'none'; + const currentRole = shareAssetConfig.workspace_sharing || 'none'; const selectedLabel = React.useMemo(() => { const selectedItem = workspaceShareRoleItems.find(item => item.value === currentRole); @@ -66,7 +66,7 @@ export const WorkspaceShareSection: React.FC = React
- +
Workspace diff --git a/apps/web/src/components/features/ShareMenu/helpers.ts b/apps/web/src/components/features/ShareMenu/helpers.ts index 7517fe395..b6b92c116 100644 --- a/apps/web/src/components/features/ShareMenu/helpers.ts +++ b/apps/web/src/components/features/ShareMenu/helpers.ts @@ -17,7 +17,7 @@ export const getShareAssetConfig = ( public_enabled_by, publicly_accessible, public_password, - workspace_permissions + workspace_sharing } = message; return { @@ -27,6 +27,6 @@ export const getShareAssetConfig = ( public_enabled_by, publicly_accessible, public_password, - workspace_permissions + workspace_sharing }; }; diff --git a/packages/server-shared/src/metrics/requests.types.ts b/packages/server-shared/src/metrics/requests.types.ts index c04755ba4..f3319f005 100644 --- a/packages/server-shared/src/metrics/requests.types.ts +++ b/packages/server-shared/src/metrics/requests.types.ts @@ -76,7 +76,7 @@ export const ShareUpdateRequestSchema = z.object({ publicly_accessible: z.boolean().optional(), public_password: z.string().nullable().optional(), public_expiry_date: z.string().nullable().optional(), - workspace_permissions: WorkspaceShareRoleSchema.nullable().optional(), + workspace_sharing: WorkspaceShareRoleSchema.nullable().optional(), }); export type GetMetricDataRequest = z.infer; diff --git a/packages/server-shared/src/share/share-interfaces.types.ts b/packages/server-shared/src/share/share-interfaces.types.ts index 6d348db2f..befc5eff3 100644 --- a/packages/server-shared/src/share/share-interfaces.types.ts +++ b/packages/server-shared/src/share/share-interfaces.types.ts @@ -32,7 +32,7 @@ export const ShareConfigSchema = z.object({ publicly_accessible: z.boolean(), public_password: z.string().nullable(), permission: ShareRoleSchema, //this is the permission the user has to the metric, dashboard or collection - workspace_permissions: z.array(WorkspaceShareRoleSchema).nullable(), + workspace_sharing: WorkspaceShareRoleSchema.nullable(), }); // Export the inferred types