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,
)]
#[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,
}

View File

@ -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<DateTime<Utc>>,
/// Workspace sharing permissions
#[serde(rename = "workspace_sharing")]
pub workspace_permissions: Option<Option<WorkspaceSharing>>,
}
/// 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::<Uuid>),
dsl::workspace_sharing_enabled_at.eq(None::<DateTime<Utc>>),
))
.execute(&mut conn)
.await?;
}
}
}
Ok(())
}

View File

@ -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<DateTime<Utc>>,
/// Workspace sharing permissions
#[serde(rename = "workspace_sharing")]
pub workspace_permissions: Option<Option<WorkspaceSharing>>,
}
/// 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::<Uuid>),
dsl::workspace_sharing_enabled_at.eq(None::<DateTime<Utc>>),
))
.execute(&mut conn)
.await?;
}
}
}
Ok(())
}

View File

@ -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<DateTime<Utc>>,
/// Workspace sharing permissions
#[serde(rename = "workspace_sharing")]
pub workspace_permissions: Option<Option<WorkspaceSharing>>,
}
/// 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?;

View File

@ -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<DateTime<Utc>>,
/// Workspace sharing permissions
#[serde(rename = "workspace_sharing")]
pub workspace_permissions: Option<Option<WorkspaceSharing>>,
}
/// 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?;

View File

@ -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;

View File

@ -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;
}
});
});

View File

@ -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;
}
});
});

View File

@ -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;
}
});
});

View File

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

View File

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

View File

@ -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<WorkspaceShareSectionProps> = 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<WorkspaceShareSectionProps> = React
<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 h-6 w-6 items-center justify-center rounded bg-gray-100 text-gray-600">
<Office />
<ApartmentBuilding />
</div>
<div className="flex flex-col overflow-hidden">
<Text className="truncate font-medium">Workspace</Text>

View File

@ -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
};
};

View File

@ -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<typeof GetMetricDataRequestSchema>;

View File

@ -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