change share roles

This commit is contained in:
Nate Kelley 2025-03-19 10:22:11 -06:00
parent 4a8b1e3674
commit 486d93e826
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
17 changed files with 133 additions and 67 deletions

View File

@ -160,7 +160,7 @@ export const DEFAULT_IBUSTER_METRIC: Required<IBusterMetric> = {
updated_at: '', updated_at: '',
sent_by_id: '', sent_by_id: '',
sent_by_name: '', sent_by_name: '',
permission: ShareRole.VIEWER, permission: ShareRole.CAN_VIEW,
sent_by_avatar_url: null, sent_by_avatar_url: null,
draft_session_id: null, draft_session_id: null,
dashboards: [], dashboards: [],

View File

@ -1,7 +1,9 @@
export enum ShareRole { export enum ShareRole {
OWNER = 'owner', OWNER = 'owner', //owner of the asset
EDITOR = 'editor', FULL_ACCESS = 'fullAccess', //same as owner, can share with others
VIEWER = 'viewer' CAN_EDIT = 'canEdit', //can edit, cannot share
CAN_FILTER = 'canFilter', //can filter dashboard
CAN_VIEW = 'canView' //can view asset
} }
export enum ShareAssetType { export enum ShareAssetType {

View File

@ -5,6 +5,7 @@ import { Paragraph, Text } from '@/components/ui/typography';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { Dropdown } from '@/components/ui/dropdown'; import { Dropdown } from '@/components/ui/dropdown';
import { ChevronDown } from '@/components/ui/icons/NucleoIconFilled'; import { ChevronDown } from '@/components/ui/icons/NucleoIconFilled';
import { canEdit } from '@/lib/share';
type DropdownValue = ShareRole | 'remove' | 'notShared'; type DropdownValue = ShareRole | 'remove' | 'notShared';
@ -21,7 +22,7 @@ export const AccessDropdown: React.FC<{
className = '', className = '',
onChangeShareLevel onChangeShareLevel
}) => { }) => {
const disabled = useMemo(() => shareLevel === ShareRole.OWNER, [shareLevel]); const disabled = useMemo(() => canEdit(shareLevel), [shareLevel]);
const items = useMemo(() => { const items = useMemo(() => {
const baseItems: DropdownItem<DropdownValue>[] = [...standardItems]; const baseItems: DropdownItem<DropdownValue>[] = [...standardItems];
@ -47,12 +48,28 @@ export const AccessDropdown: React.FC<{
const selectedLabel = useMemo(() => { const selectedLabel = useMemo(() => {
const selectedItem = items.find((item) => item.selected); const selectedItem = items.find((item) => item.selected);
if (!selectedItem) return 'No shared'; if (!selectedItem) return 'No shared';
if (selectedItem.value === ShareRole.OWNER) return 'Full access';
if (selectedItem.value === ShareRole.EDITOR) return 'Can edit'; const { value } = selectedItem;
if (selectedItem.value === ShareRole.VIEWER) return 'Can view';
if (selectedItem.value === 'remove') return 'Remove'; // Using a type-safe switch to handle all ShareRole values
if (selectedItem.value === 'notShared') return 'Not shared'; switch (value) {
return selectedItem.label; case ShareRole.FULL_ACCESS:
return 'Full access';
case ShareRole.CAN_EDIT:
return 'Can edit';
case ShareRole.CAN_FILTER:
return 'Can filter';
case ShareRole.CAN_VIEW:
return 'Can view';
case ShareRole.OWNER:
return 'Full access';
case 'remove':
return 'Remove';
case 'notShared':
return 'Not shared';
default:
return typeof selectedItem.label === 'string' ? selectedItem.label : 'Selected';
}
}, [items]); }, [items]);
const onSelectMenuItem = useMemoizedFn((value: string) => { const onSelectMenuItem = useMemoizedFn((value: string) => {
@ -86,19 +103,24 @@ export const AccessDropdown: React.FC<{
const standardItems: DropdownItem<ShareRole>[] = [ const standardItems: DropdownItem<ShareRole>[] = [
{ {
value: ShareRole.OWNER, value: ShareRole.FULL_ACCESS,
label: 'Full access', label: 'Full access',
secondaryLabel: 'Can edit and share with others.' secondaryLabel: 'Can edit and share with others.'
}, },
{ {
value: ShareRole.EDITOR, value: ShareRole.CAN_EDIT,
label: 'Can edit', label: 'Can edit',
secondaryLabel: 'Can edit but not share with others.' secondaryLabel: 'Can edit but not share with others.'
}, },
{ {
value: ShareRole.VIEWER, value: ShareRole.CAN_FILTER,
label: 'Can filter',
secondaryLabel: 'Can filter dashboards but not edit.'
},
{
value: ShareRole.CAN_VIEW,
label: 'Can view', label: 'Can view',
secondaryLabel: 'Can view but not edit.' secondaryLabel: 'Can view asset but not edit.'
} }
]; ];

View File

@ -6,7 +6,7 @@ import { AppTooltip } from '@/components/ui/tooltip';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { BusterShare, ShareAssetType } from '@/api/asset_interfaces'; import { BusterShare, ShareAssetType } from '@/api/asset_interfaces';
import { ShareMenuContent } from './ShareMenuContent'; import { ShareMenuContent } from './ShareMenuContent';
import { isShareMenuVisible } from './helpers'; import { canShare } from '@/lib/share';
export const ShareMenu: React.FC< export const ShareMenu: React.FC<
PropsWithChildren<{ PropsWithChildren<{
@ -21,7 +21,7 @@ export const ShareMenu: React.FC<
setIsOpen(v); setIsOpen(v);
}); });
const showShareMenu = shareAssetConfig && isShareMenuVisible(shareAssetConfig); const showShareMenu = canShare(shareAssetConfig?.permission);
if (!showShareMenu) { if (!showShareMenu) {
return null; return null;

View File

@ -20,13 +20,13 @@ const mockShareConfig: BusterShare = {
{ {
id: '1', id: '1',
email: 'test_with_a_long_name_like_super_long_name@test.com', email: 'test_with_a_long_name_like_super_long_name@test.com',
role: ShareRole.VIEWER, role: ShareRole.CAN_VIEW,
name: 'Test User' name: 'Test User'
}, },
{ {
id: '2', id: '2',
email: 'test2@test.com', email: 'test2@test.com',
role: ShareRole.VIEWER, role: ShareRole.FULL_ACCESS,
name: 'Test User 2 with a long name like super long name' name: 'Test User 2 with a long name like super long name'
} }
], ],
@ -81,7 +81,7 @@ export const ViewerPermission: Story = {
assetType: ShareAssetType.METRIC, assetType: ShareAssetType.METRIC,
shareAssetConfig: { shareAssetConfig: {
...mockShareConfig, ...mockShareConfig,
permission: ShareRole.VIEWER permission: ShareRole.CAN_VIEW
} }
} }
}; };

View File

@ -6,6 +6,7 @@ import { useMemoizedFn } from '@/hooks';
import { BusterRoutes, createBusterRoute } from '@/routes'; import { BusterRoutes, createBusterRoute } from '@/routes';
import { useBusterNotifications } from '@/context/BusterNotifications'; import { useBusterNotifications } from '@/context/BusterNotifications';
import { ShareMenuContentEmbedFooter } from './ShareMenuContentEmbed'; import { ShareMenuContentEmbedFooter } from './ShareMenuContentEmbed';
import { isEffectiveOwner } from '@/lib/share';
export const ShareMenuContent: React.FC<{ export const ShareMenuContent: React.FC<{
shareAssetConfig: BusterShare; shareAssetConfig: BusterShare;
@ -20,7 +21,7 @@ export const ShareMenuContent: React.FC<{
const permission = shareAssetConfig?.permission; const permission = shareAssetConfig?.permission;
const publicly_accessible = shareAssetConfig?.publicly_accessible; const publicly_accessible = shareAssetConfig?.publicly_accessible;
const isOwner = permission === ShareRole.OWNER; const isOwner = isEffectiveOwner(permission);
const onCopyLink = useMemoizedFn(() => { const onCopyLink = useMemoizedFn(() => {
let url = ''; let url = '';

View File

@ -87,7 +87,7 @@ const ShareMenuContentShare: React.FC<{
const [inputValue, setInputValue] = React.useState<string>(''); const [inputValue, setInputValue] = React.useState<string>('');
const [isInviting, setIsInviting] = React.useState<boolean>(false); const [isInviting, setIsInviting] = React.useState<boolean>(false);
const [defaultPermissionLevel, setDefaultPermissionLevel] = React.useState<ShareRole>( const [defaultPermissionLevel, setDefaultPermissionLevel] = React.useState<ShareRole>(
ShareRole.VIEWER ShareRole.CAN_VIEW
); );
const disableSubmit = !inputHasText(inputValue) || !validate(inputValue); const disableSubmit = !inputHasText(inputValue) || !validate(inputValue);
const hasUserTeams = userTeams?.length > 0; const hasUserTeams = userTeams?.length > 0;

View File

@ -2,19 +2,9 @@ import {
BusterCollection, BusterCollection,
BusterDashboardResponse, BusterDashboardResponse,
BusterShare, BusterShare,
IBusterChatMessage, IBusterMetric
IBusterMetric,
ShareRole
} from '@/api/asset_interfaces'; } from '@/api/asset_interfaces';
export const isShareMenuVisible = (shareAssetConfig: BusterShare | null) => {
return (
!!shareAssetConfig &&
(shareAssetConfig.permission === ShareRole.OWNER ||
shareAssetConfig.permission === ShareRole.EDITOR)
);
};
export const getShareAssetConfig = ( export const getShareAssetConfig = (
message: IBusterMetric | BusterDashboardResponse | BusterCollection | null message: IBusterMetric | BusterDashboardResponse | BusterCollection | null
): BusterShare | null => { ): BusterShare | null => {

View File

@ -5,11 +5,12 @@ import type {
ChatEvent_GeneratingResponseMessage, ChatEvent_GeneratingResponseMessage,
ChatEvent_GeneratingReasoningMessage ChatEvent_GeneratingReasoningMessage
} from '@/api/buster_socket/chats'; } from '@/api/buster_socket/chats';
import type { import {
BusterChatResponseMessage_text, type BusterChatResponseMessage_text,
BusterChatMessageReasoning_text, type BusterChatMessageReasoning_text,
BusterChatMessageReasoning_files, type BusterChatMessageReasoning_files,
BusterChatMessageReasoning_file type BusterChatMessageReasoning_file,
ShareRole
} from '@/api/asset_interfaces'; } from '@/api/asset_interfaces';
const createInitialMessage = (messageId: string): IBusterChatMessage => ({ const createInitialMessage = (messageId: string): IBusterChatMessage => ({
@ -26,7 +27,17 @@ const createInitialMessage = (messageId: string): IBusterChatMessage => ({
response_messages: {}, response_messages: {},
reasoning_messages: {}, reasoning_messages: {},
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
final_reasoning_message: null final_reasoning_message: null,
sharingKey: '',
individual_permissions: [],
team_permissions: [],
organization_permissions: [],
permission: ShareRole.CAN_VIEW,
public_expiry_date: null,
public_enabled_by: null,
publicly_accessible: false,
public_password: null,
password_secret_id: null
}); });
export const initializeOrUpdateMessage = ( export const initializeOrUpdateMessage = (

View File

@ -12,6 +12,7 @@ import { useMemoizedFn } from '@/hooks';
import { type BreadcrumbItem, Breadcrumb } from '@/components/ui/breadcrumb'; import { type BreadcrumbItem, Breadcrumb } from '@/components/ui/breadcrumb';
import { Dots, Pencil, Plus, ShareAllRight, ShareRight, Trash } from '@/components/ui/icons'; import { Dots, Pencil, Plus, ShareAllRight, ShareRight, Trash } from '@/components/ui/icons';
import { useDeleteCollection, useUpdateCollection } from '@/api/buster_rest/collections'; import { useDeleteCollection, useUpdateCollection } from '@/api/buster_rest/collections';
import { canEdit } from '@/lib/share';
export const CollectionsIndividualHeader: React.FC<{ export const CollectionsIndividualHeader: React.FC<{
openAddTypeModal: boolean; openAddTypeModal: boolean;
@ -47,7 +48,7 @@ export const CollectionsIndividualHeader: React.FC<{
)} )}
</div> </div>
{collection && canEditCollection(collection) && ( {collection && canEdit(collection.permission) && (
<ContentRight <ContentRight
collection={collection} collection={collection}
openAddTypeModal={openAddTypeModal} openAddTypeModal={openAddTypeModal}
@ -146,7 +147,3 @@ const CollectionBreadcrumb: React.FC<{
return <Breadcrumb items={items} />; return <Breadcrumb items={items} />;
}); });
CollectionBreadcrumb.displayName = 'CollectionBreadcrumb'; CollectionBreadcrumb.displayName = 'CollectionBreadcrumb';
const canEditCollection = (collection: BusterCollection) => {
return collection.permission === 'owner' || collection.permission === 'editor';
};

View File

@ -9,6 +9,7 @@ import { ChartType } from '@/api/asset_interfaces/metric/charts/enum';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { cn } from '@/lib/classMerge'; import { cn } from '@/lib/classMerge';
import { ShareRole } from '@/api/asset_interfaces'; import { ShareRole } from '@/api/asset_interfaces';
import { canEdit } from '@/lib/share';
export const MetricViewChart: React.FC<{ export const MetricViewChart: React.FC<{
metricId: string; metricId: string;
@ -24,9 +25,7 @@ export const MetricViewChart: React.FC<{
const { title, description, time_frame, evaluation_score, evaluation_summary } = metric; const { title, description, time_frame, evaluation_score, evaluation_summary } = metric;
const isTable = metric.chart_config.selectedChartType === ChartType.Table; const isTable = metric.chart_config.selectedChartType === ChartType.Table;
const readOnly = const readOnly = readOnlyProp || !canEdit(metric.permission);
readOnlyProp ||
!(metric.permission === ShareRole.OWNER || metric.permission === ShareRole.EDITOR);
const loadingData = !isFetchedMetricData; const loadingData = !isFetchedMetricData;
const errorData = !!metricDataError; const errorData = !!metricDataError;

View File

@ -31,6 +31,7 @@ import { useFavoriteStar } from '@/components/features/list/FavoriteStar';
import { timeout } from '@/lib'; import { timeout } from '@/lib';
import { ShareMenuContent } from '@/components/features/ShareMenu/ShareMenuContent'; import { ShareMenuContent } from '@/components/features/ShareMenu/ShareMenuContent';
import { DASHBOARD_TITLE_INPUT_ID } from '@/controllers/DashboardController/DashboardViewDashboardController/DashboardEditTitle'; import { DASHBOARD_TITLE_INPUT_ID } from '@/controllers/DashboardController/DashboardViewDashboardController/DashboardEditTitle';
import { isEffectiveOwner } from '@/lib/share';
export const DashboardThreeDotMenu = React.memo(({ dashboardId }: { dashboardId: string }) => { export const DashboardThreeDotMenu = React.memo(({ dashboardId }: { dashboardId: string }) => {
const versionHistoryItems = useVersionHistorySelectMenu({ dashboardId }); const versionHistoryItems = useVersionHistorySelectMenu({ dashboardId });
@ -211,7 +212,7 @@ const useRenameDashboardSelectMenu = ({ dashboardId }: { dashboardId: string })
export const useShareMenuSelectMenu = ({ dashboardId }: { dashboardId: string }) => { export const useShareMenuSelectMenu = ({ dashboardId }: { dashboardId: string }) => {
const { data: dashboard } = useGetDashboard(dashboardId); const { data: dashboard } = useGetDashboard(dashboardId);
const isOwner = dashboard?.permission === ShareRole.OWNER; const isOwner = isEffectiveOwner(dashboard?.permission);
return useMemo( return useMemo(
() => ({ () => ({

View File

@ -45,6 +45,7 @@ import { METRIC_CHART_CONTAINER_ID } from '@/controllers/MetricController/Metric
import { timeout } from '@/lib'; import { timeout } from '@/lib';
import { METRIC_CHART_TITLE_INPUT_ID } from '@/controllers/MetricController/MetricViewChart/MetricViewChartHeader'; import { METRIC_CHART_TITLE_INPUT_ID } from '@/controllers/MetricController/MetricViewChart/MetricViewChartHeader';
import { ShareMenuContent } from '@/components/features/ShareMenu/ShareMenuContent'; import { ShareMenuContent } from '@/components/features/ShareMenu/ShareMenuContent';
import { isEffectiveOwner } from '@/lib/share';
export const ThreeDotMenuButton = React.memo(({ metricId }: { metricId: string }) => { export const ThreeDotMenuButton = React.memo(({ metricId }: { metricId: string }) => {
const { openSuccessMessage } = useBusterNotifications(); const { openSuccessMessage } = useBusterNotifications();
@ -433,7 +434,7 @@ const useRenameMetricSelectMenu = ({ metricId }: { metricId: string }) => {
export const useShareMenuSelectMenu = ({ metricId }: { metricId: string }) => { export const useShareMenuSelectMenu = ({ metricId }: { metricId: string }) => {
const { data: metric } = useGetMetric(metricId); const { data: metric } = useGetMetric(metricId);
const isOwner = metric?.permission === ShareRole.OWNER; const isOwner = isEffectiveOwner(metric?.permission);
return useMemo( return useMemo(
() => ({ () => ({

26
web/src/lib/share.ts Normal file
View File

@ -0,0 +1,26 @@
import { ShareRole } from '@/api/asset_interfaces';
export const isOwner = (role: ShareRole | null | undefined) => {
return role === ShareRole.OWNER;
};
export const isEffectiveOwner = (role: ShareRole | null | undefined) => {
return role === ShareRole.FULL_ACCESS || role === ShareRole.OWNER;
};
export const canEdit = (role: ShareRole | null | undefined) => {
return role === ShareRole.CAN_EDIT || role === ShareRole.FULL_ACCESS || role === ShareRole.OWNER;
};
export const canShare = (role: ShareRole | null | undefined) => {
return role === ShareRole.FULL_ACCESS || role === ShareRole.OWNER;
};
export const canFilter = (role: ShareRole) => {
return (
role === ShareRole.CAN_FILTER ||
role === ShareRole.FULL_ACCESS ||
role === ShareRole.OWNER ||
role === ShareRole.CAN_EDIT
);
};

View File

@ -1,9 +1,10 @@
import type { import {
BusterChat, ShareRole,
BusterChatMessage, type BusterChat,
BusterChatMessageReasoning, type BusterChatMessage,
BusterChatMessageReasoning_file, type BusterChatMessageReasoning,
BusterChatMessageResponse type BusterChatMessageReasoning_file,
type BusterChatMessageResponse
} from '@/api/asset_interfaces'; } from '@/api/asset_interfaces';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
@ -14,7 +15,8 @@ const MOCK_MESSAGE_RESPONSE = (typeProp?: 'text' | 'file'): BusterChatMessageRes
return { return {
id: faker.string.uuid(), id: faker.string.uuid(),
type, type,
message: faker.lorem.sentence() message: faker.lorem.sentence(),
is_final_message: false
}; };
} }
@ -141,7 +143,17 @@ const MOCK_MESSAGE = (): BusterChatMessage => {
}, },
{} {}
), ),
reasoning_message_ids: reasoningMessage.map((m) => m.id) reasoning_message_ids: reasoningMessage.map((m) => m.id),
sharingKey: '',
individual_permissions: [],
team_permissions: [],
organization_permissions: [],
permission: ShareRole.CAN_VIEW,
public_expiry_date: null,
public_enabled_by: null,
publicly_accessible: false,
public_password: null,
password_secret_id: null
}; };
}; };

View File

@ -102,20 +102,14 @@ refresh_interval: 300`,
config: { config: {
rows rows
}, },
sharingKey: 'mock-sharing-key',
publicly_accessible: false,
public_password: null,
public_expiry_date: null,
public_enabled_by: null,
password_secret_id: null,
versions: [] versions: []
}; };
const response: BusterDashboardResponse = { const response: BusterDashboardResponse = {
access: ShareRole.EDITOR, access: ShareRole.CAN_EDIT,
metrics: metrics.reduce((acc, metric) => ({ ...acc, [metric.id]: metric }), {}), metrics: metrics.reduce((acc, metric) => ({ ...acc, [metric.id]: metric }), {}),
dashboard, dashboard,
permission: ShareRole.EDITOR, permission: ShareRole.CAN_EDIT,
public_password: null, public_password: null,
sharingKey: 'mock-sharing-key', sharingKey: 'mock-sharing-key',
individual_permissions: null, individual_permissions: null,

View File

@ -1,4 +1,4 @@
import type { IBusterChatMessage } from '@/api/asset_interfaces'; import { ShareRole, type IBusterChatMessage } from '@/api/asset_interfaces';
export const mockBusterChatMessage: IBusterChatMessage = { export const mockBusterChatMessage: IBusterChatMessage = {
id: 'message-1', id: 'message-1',
@ -121,5 +121,15 @@ hunchback_of_notre_dame:
} }
} }
} }
} },
sharingKey: '',
individual_permissions: [],
team_permissions: [],
organization_permissions: [],
permission: ShareRole.CAN_VIEW,
public_expiry_date: null,
public_enabled_by: null,
publicly_accessible: false,
public_password: null,
password_secret_id: null
}; };