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.
This commit is contained in:
dal 2025-07-17 15:26:26 -06:00
parent d9f9182ab2
commit 4e2b6c235e
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
15 changed files with 238 additions and 34 deletions

View File

@ -742,15 +742,15 @@ impl FromSql<sql_types::MessageFeedbackEnum, Pg> for MessageFeedback {
Serialize, Serialize,
)] )]
#[diesel(sql_type = sql_types::WorkspaceSharingEnum)] #[diesel(sql_type = sql_types::WorkspaceSharingEnum)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "camelCase")]
pub enum WorkspaceSharing { pub enum WorkspaceSharing {
#[serde(alias = "none")] #[serde(alias = "none")]
None, None,
#[serde(alias = "canView")] #[serde(alias = "can_view")]
CanView, CanView,
#[serde(alias = "canEdit")] #[serde(alias = "can_edit")]
CanEdit, CanEdit,
#[serde(alias = "fullAccess")] #[serde(alias = "full_access")]
FullAccess, FullAccess,
} }

View File

@ -2,10 +2,14 @@ use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use database::{ use database::{
chats::fetch_chat_with_permission, chats::fetch_chat_with_permission,
enums::{AssetPermissionRole, AssetType}, enums::{AssetPermissionRole, AssetType, WorkspaceSharing},
pool::get_pg_pool,
schema::chats::dsl,
}; };
use middleware::AuthenticatedUser; use middleware::AuthenticatedUser;
use serde::{Deserialize, Serialize}; 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 sharing::{check_permission_access, create_share_by_email, types::UpdateField};
use tracing::info; use tracing::info;
use uuid::Uuid; use uuid::Uuid;
@ -30,6 +34,9 @@ pub struct UpdateChatSharingRequest {
/// Expiration date for public access /// Expiration date for public access
#[serde(default)] #[serde(default)]
pub public_expiry_date: UpdateField<DateTime<Utc>>, pub public_expiry_date: UpdateField<DateTime<Utc>>,
/// Workspace sharing permissions
#[serde(rename = "workspace_sharing")]
pub workspace_permissions: Option<Option<WorkspaceSharing>>,
} }
/// Updates sharing permissions for a chat /// 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 // 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 // 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::<Uuid>),
dsl::workspace_sharing_enabled_at.eq(None::<DateTime<Utc>>),
))
.execute(&mut conn)
.await?;
}
}
}
Ok(()) Ok(())
} }

View File

@ -1,11 +1,15 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use database::{ use database::{
enums::{AssetPermissionRole, AssetType}, enums::{AssetPermissionRole, AssetType, WorkspaceSharing},
helpers::collections::fetch_collection_with_permission, helpers::collections::fetch_collection_with_permission,
pool::get_pg_pool,
schema::collections::dsl,
}; };
use middleware::AuthenticatedUser; use middleware::AuthenticatedUser;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use diesel::ExpressionMethods;
use diesel_async::RunQueryDsl as AsyncRunQueryDsl;
use sharing::{ use sharing::{
check_permission_access, check_permission_access,
create_asset_permission::create_share_by_email, create_asset_permission::create_share_by_email,
@ -37,6 +41,9 @@ pub struct UpdateCollectionSharingRequest {
/// Note: Collections are not publicly accessible, this field is ignored /// Note: Collections are not publicly accessible, this field is ignored
#[serde(default)] #[serde(default)]
pub public_expiry_date: UpdateField<DateTime<Utc>>, pub public_expiry_date: UpdateField<DateTime<Utc>>,
/// Workspace sharing permissions
#[serde(rename = "workspace_sharing")]
pub workspace_permissions: Option<Option<WorkspaceSharing>>,
} }
/// Update sharing permissions for a collection /// 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 // 4. Public access settings are ignored for collections
// Collections are not publicly accessible, so we ignore the public_* fields // 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::<Uuid>),
dsl::workspace_sharing_enabled_at.eq(None::<DateTime<Utc>>),
))
.execute(&mut conn)
.await?;
}
}
}
Ok(()) Ok(())
} }

View File

@ -1,7 +1,7 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use database::{ use database::{
enums::{AssetPermissionRole, AssetType}, enums::{AssetPermissionRole, AssetType, WorkspaceSharing},
helpers::dashboard_files::fetch_dashboard_file_with_permission, helpers::dashboard_files::fetch_dashboard_file_with_permission,
schema::dashboard_files::dsl, schema::dashboard_files::dsl,
pool::get_pg_pool, pool::get_pg_pool,
@ -38,6 +38,9 @@ pub struct UpdateDashboardSharingRequest {
/// Expiration date for public access /// Expiration date for public access
#[serde(default)] #[serde(default)]
pub public_expiry_date: UpdateField<DateTime<Utc>>, pub public_expiry_date: UpdateField<DateTime<Utc>>,
/// Workspace sharing permissions
#[serde(rename = "workspace_sharing")]
pub workspace_permissions: Option<Option<WorkspaceSharing>>,
} }
/// Updates sharing permissions for a dashboard /// 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 publicly_enabled_by = dashboard.publicly_enabled_by;
let mut public_password = dashboard.public_password; let mut public_password = dashboard.public_password;
let mut public_expiry_date = dashboard.public_expiry_date; 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; let mut update_needed = false;
// Update publicly_accessible if provided // Update publicly_accessible if provided
@ -208,6 +214,42 @@ pub async fn update_dashboard_sharing_handler(
UpdateField::NoChange => {} 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 // Execute the update if any changes were made
if update_needed { if update_needed {
diesel::update(dsl::dashboard_files) 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::publicly_enabled_by.eq(publicly_enabled_by),
dsl::public_password.eq(public_password), dsl::public_password.eq(public_password),
dsl::public_expiry_date.eq(public_expiry_date), 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) .execute(&mut conn)
.await?; .await?;

View File

@ -1,7 +1,7 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use database::{ use database::{
enums::{AssetPermissionRole, AssetType}, enums::{AssetPermissionRole, AssetType, WorkspaceSharing},
helpers::metric_files::fetch_metric_file_with_permissions, helpers::metric_files::fetch_metric_file_with_permissions,
pool::get_pg_pool, pool::get_pg_pool,
schema::metric_files::dsl, schema::metric_files::dsl,
@ -38,6 +38,9 @@ pub struct UpdateMetricSharingRequest {
/// Expiration date for public access /// Expiration date for public access
#[serde(default)] #[serde(default)]
pub public_expiry_date: UpdateField<DateTime<Utc>>, pub public_expiry_date: UpdateField<DateTime<Utc>>,
/// Workspace sharing permissions
#[serde(rename = "workspace_sharing")]
pub workspace_permissions: Option<Option<WorkspaceSharing>>,
} }
/// Handler to update sharing permissions for a metric /// 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 publicly_enabled_by = metric.publicly_enabled_by;
let mut public_password = metric.public_password; let mut public_password = metric.public_password;
let mut public_expiry_date = metric.public_expiry_date; 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; let mut update_needed = false;
// Update publicly_accessible if provided // Update publicly_accessible if provided
@ -200,6 +206,42 @@ pub async fn update_metric_sharing_handler(
UpdateField::NoChange => {} 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 // Execute the update if any changes were made
if update_needed { if update_needed {
diesel::update(dsl::metric_files) 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::publicly_enabled_by.eq(publicly_enabled_by),
dsl::public_password.eq(public_password), dsl::public_password.eq(public_password),
dsl::public_expiry_date.eq(public_expiry_date), 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) .execute(&mut conn)
.await?; .await?;

View File

@ -19,7 +19,7 @@ export type ShareUpdateRequest = {
email: string; email: string;
role: ShareRole; role: ShareRole;
}[]; }[];
workspace_permissions?: WorkspaceShareRole | null; workspace_sharing?: WorkspaceShareRole | null;
publicly_accessible?: boolean; publicly_accessible?: boolean;
public_password?: string | null; public_password?: string | null;
public_expiry_date?: string | null; public_expiry_date?: string | null;

View File

@ -246,8 +246,8 @@ export const useUpdateCollectionShare = () => {
if (params.public_expiry_date !== undefined) { if (params.public_expiry_date !== undefined) {
draft.public_expiry_date = params.public_expiry_date; draft.public_expiry_date = params.public_expiry_date;
} }
if (params.workspace_permissions !== undefined) { if (params.workspace_sharing !== undefined) {
draft.workspace_permissions = params.workspace_permissions ? [params.workspace_permissions] : []; draft.workspace_sharing = params.workspace_sharing;
} }
}); });
}); });

View File

@ -437,8 +437,8 @@ export const useUpdateDashboardShare = () => {
if (params.public_expiry_date !== undefined) { if (params.public_expiry_date !== undefined) {
draft.public_expiry_date = params.public_expiry_date; draft.public_expiry_date = params.public_expiry_date;
} }
if (params.workspace_permissions !== undefined) { if (params.workspace_sharing !== undefined) {
draft.workspace_permissions = params.workspace_permissions ? [params.workspace_permissions] : []; draft.workspace_sharing = params.workspace_sharing;
} }
}); });
}); });

View File

@ -316,8 +316,8 @@ export const useUpdateMetricShare = () => {
if (variables.params.public_expiry_date !== undefined) { if (variables.params.public_expiry_date !== undefined) {
draft.public_expiry_date = variables.params.public_expiry_date; draft.public_expiry_date = variables.params.public_expiry_date;
} }
if (variables.params.workspace_permissions !== undefined) { if (variables.params.workspace_sharing !== undefined) {
draft.workspace_permissions = variables.params.workspace_permissions ? [variables.params.workspace_permissions] : []; draft.workspace_sharing = variables.params.workspace_sharing;
} }
}); });
}); });

View File

@ -34,6 +34,7 @@ const mockShareConfig: ShareConfig = {
publicly_accessible: false, publicly_accessible: false,
public_password: null, public_password: null,
permission: 'owner', permission: 'owner',
workspace_sharing: 'none'
}; };
export const MetricShare: Story = { export const MetricShare: Story = {

View File

@ -104,7 +104,7 @@ const ShareMenuContentShare: React.FC<ShareMenuContentBodyProps> = React.memo(
const payload: Parameters<typeof onUpdateMetricShare>[0] = { const payload: Parameters<typeof onUpdateMetricShare>[0] = {
id: assetId, id: assetId,
params: { params: {
workspace_permissions: role workspace_sharing: role
} }
}; };
@ -127,16 +127,6 @@ const ShareMenuContentShare: React.FC<ShareMenuContentBodyProps> = React.memo(
/> />
)} )}
{canEditPermissions && (
<WorkspaceShareSection
shareAssetConfig={shareAssetConfig}
assetType={assetType}
assetId={assetId}
canEditPermissions={canEditPermissions}
onUpdateWorkspacePermissions={onUpdateWorkspacePermissions}
/>
)}
{hasIndividualPermissions && ( {hasIndividualPermissions && (
<div className="flex flex-col space-y-2 overflow-hidden"> <div className="flex flex-col space-y-2 overflow-hidden">
{individual_permissions?.map((permission) => ( {individual_permissions?.map((permission) => (
@ -150,6 +140,16 @@ const ShareMenuContentShare: React.FC<ShareMenuContentBodyProps> = React.memo(
))} ))}
</div> </div>
)} )}
{canEditPermissions && (
<WorkspaceShareSection
shareAssetConfig={shareAssetConfig}
assetType={assetType}
assetId={assetId}
canEditPermissions={canEditPermissions}
onUpdateWorkspacePermissions={onUpdateWorkspacePermissions}
/>
)}
</div> </div>
); );
} }

View File

@ -3,7 +3,7 @@ import type { ShareAssetType, ShareConfig, WorkspaceShareRole } from '@buster/se
import { Dropdown } from '@/components/ui/dropdown'; import { Dropdown } from '@/components/ui/dropdown';
import type { DropdownItem } from '@/components/ui/dropdown'; import type { DropdownItem } from '@/components/ui/dropdown';
import { ChevronDown } from '@/components/ui/icons/NucleoIconFilled'; 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 { Text } from '@/components/ui/typography';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { cn } from '@/lib/classMerge'; import { cn } from '@/lib/classMerge';
@ -44,7 +44,7 @@ export const WorkspaceShareSection: React.FC<WorkspaceShareSectionProps> = React
canEditPermissions, canEditPermissions,
onUpdateWorkspacePermissions onUpdateWorkspacePermissions
}) => { }) => {
const currentRole = shareAssetConfig.workspace_permissions?.[0] || 'none'; const currentRole = shareAssetConfig.workspace_sharing || 'none';
const selectedLabel = React.useMemo(() => { const selectedLabel = React.useMemo(() => {
const selectedItem = workspaceShareRoleItems.find(item => item.value === currentRole); const selectedItem = workspaceShareRoleItems.find(item => item.value === currentRole);
@ -66,7 +66,7 @@ export const WorkspaceShareSection: React.FC<WorkspaceShareSectionProps> = React
<div className="flex h-8 items-center justify-between space-x-2 overflow-hidden"> <div className="flex h-8 items-center justify-between space-x-2 overflow-hidden">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="flex h-6 w-6 items-center justify-center rounded bg-gray-100 text-gray-600"> <div className="flex h-6 w-6 items-center justify-center rounded bg-gray-100 text-gray-600">
<Office /> <ApartmentBuilding />
</div> </div>
<div className="flex flex-col overflow-hidden"> <div className="flex flex-col overflow-hidden">
<Text className="truncate font-medium">Workspace</Text> <Text className="truncate font-medium">Workspace</Text>

View File

@ -17,7 +17,7 @@ export const getShareAssetConfig = (
public_enabled_by, public_enabled_by,
publicly_accessible, publicly_accessible,
public_password, public_password,
workspace_permissions workspace_sharing
} = message; } = message;
return { return {
@ -27,6 +27,6 @@ export const getShareAssetConfig = (
public_enabled_by, public_enabled_by,
publicly_accessible, publicly_accessible,
public_password, public_password,
workspace_permissions workspace_sharing
}; };
}; };

View File

@ -76,7 +76,7 @@ export const ShareUpdateRequestSchema = z.object({
publicly_accessible: z.boolean().optional(), publicly_accessible: z.boolean().optional(),
public_password: z.string().nullable().optional(), public_password: z.string().nullable().optional(),
public_expiry_date: 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<typeof GetMetricDataRequestSchema>; export type GetMetricDataRequest = z.infer<typeof GetMetricDataRequestSchema>;

View File

@ -32,7 +32,7 @@ export const ShareConfigSchema = z.object({
publicly_accessible: z.boolean(), publicly_accessible: z.boolean(),
public_password: z.string().nullable(), public_password: z.string().nullable(),
permission: ShareRoleSchema, //this is the permission the user has to the metric, dashboard or collection 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 // Export the inferred types