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: '',
sent_by_id: '',
sent_by_name: '',
permission: ShareRole.VIEWER,
permission: ShareRole.CAN_VIEW,
sent_by_avatar_url: null,
draft_session_id: null,
dashboards: [],

View File

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

View File

@ -5,6 +5,7 @@ import { Paragraph, Text } from '@/components/ui/typography';
import { useMemoizedFn } from '@/hooks';
import { Dropdown } from '@/components/ui/dropdown';
import { ChevronDown } from '@/components/ui/icons/NucleoIconFilled';
import { canEdit } from '@/lib/share';
type DropdownValue = ShareRole | 'remove' | 'notShared';
@ -21,7 +22,7 @@ export const AccessDropdown: React.FC<{
className = '',
onChangeShareLevel
}) => {
const disabled = useMemo(() => shareLevel === ShareRole.OWNER, [shareLevel]);
const disabled = useMemo(() => canEdit(shareLevel), [shareLevel]);
const items = useMemo(() => {
const baseItems: DropdownItem<DropdownValue>[] = [...standardItems];
@ -47,12 +48,28 @@ export const AccessDropdown: React.FC<{
const selectedLabel = useMemo(() => {
const selectedItem = items.find((item) => item.selected);
if (!selectedItem) return 'No shared';
if (selectedItem.value === ShareRole.OWNER) return 'Full access';
if (selectedItem.value === ShareRole.EDITOR) return 'Can edit';
if (selectedItem.value === ShareRole.VIEWER) return 'Can view';
if (selectedItem.value === 'remove') return 'Remove';
if (selectedItem.value === 'notShared') return 'Not shared';
return selectedItem.label;
const { value } = selectedItem;
// Using a type-safe switch to handle all ShareRole values
switch (value) {
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]);
const onSelectMenuItem = useMemoizedFn((value: string) => {
@ -86,19 +103,24 @@ export const AccessDropdown: React.FC<{
const standardItems: DropdownItem<ShareRole>[] = [
{
value: ShareRole.OWNER,
value: ShareRole.FULL_ACCESS,
label: 'Full access',
secondaryLabel: 'Can edit and share with others.'
},
{
value: ShareRole.EDITOR,
value: ShareRole.CAN_EDIT,
label: 'Can edit',
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',
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 { BusterShare, ShareAssetType } from '@/api/asset_interfaces';
import { ShareMenuContent } from './ShareMenuContent';
import { isShareMenuVisible } from './helpers';
import { canShare } from '@/lib/share';
export const ShareMenu: React.FC<
PropsWithChildren<{
@ -21,7 +21,7 @@ export const ShareMenu: React.FC<
setIsOpen(v);
});
const showShareMenu = shareAssetConfig && isShareMenuVisible(shareAssetConfig);
const showShareMenu = canShare(shareAssetConfig?.permission);
if (!showShareMenu) {
return null;

View File

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

View File

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

View File

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

View File

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

View File

@ -5,11 +5,12 @@ import type {
ChatEvent_GeneratingResponseMessage,
ChatEvent_GeneratingReasoningMessage
} from '@/api/buster_socket/chats';
import type {
BusterChatResponseMessage_text,
BusterChatMessageReasoning_text,
BusterChatMessageReasoning_files,
BusterChatMessageReasoning_file
import {
type BusterChatResponseMessage_text,
type BusterChatMessageReasoning_text,
type BusterChatMessageReasoning_files,
type BusterChatMessageReasoning_file,
ShareRole
} from '@/api/asset_interfaces';
const createInitialMessage = (messageId: string): IBusterChatMessage => ({
@ -26,7 +27,17 @@ const createInitialMessage = (messageId: string): IBusterChatMessage => ({
response_messages: {},
reasoning_messages: {},
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 = (

View File

@ -12,6 +12,7 @@ import { useMemoizedFn } from '@/hooks';
import { type BreadcrumbItem, Breadcrumb } from '@/components/ui/breadcrumb';
import { Dots, Pencil, Plus, ShareAllRight, ShareRight, Trash } from '@/components/ui/icons';
import { useDeleteCollection, useUpdateCollection } from '@/api/buster_rest/collections';
import { canEdit } from '@/lib/share';
export const CollectionsIndividualHeader: React.FC<{
openAddTypeModal: boolean;
@ -47,7 +48,7 @@ export const CollectionsIndividualHeader: React.FC<{
)}
</div>
{collection && canEditCollection(collection) && (
{collection && canEdit(collection.permission) && (
<ContentRight
collection={collection}
openAddTypeModal={openAddTypeModal}
@ -146,7 +147,3 @@ const CollectionBreadcrumb: React.FC<{
return <Breadcrumb items={items} />;
});
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 { cn } from '@/lib/classMerge';
import { ShareRole } from '@/api/asset_interfaces';
import { canEdit } from '@/lib/share';
export const MetricViewChart: React.FC<{
metricId: string;
@ -24,9 +25,7 @@ export const MetricViewChart: React.FC<{
const { title, description, time_frame, evaluation_score, evaluation_summary } = metric;
const isTable = metric.chart_config.selectedChartType === ChartType.Table;
const readOnly =
readOnlyProp ||
!(metric.permission === ShareRole.OWNER || metric.permission === ShareRole.EDITOR);
const readOnly = readOnlyProp || !canEdit(metric.permission);
const loadingData = !isFetchedMetricData;
const errorData = !!metricDataError;

View File

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

View File

@ -45,6 +45,7 @@ import { METRIC_CHART_CONTAINER_ID } from '@/controllers/MetricController/Metric
import { timeout } from '@/lib';
import { METRIC_CHART_TITLE_INPUT_ID } from '@/controllers/MetricController/MetricViewChart/MetricViewChartHeader';
import { ShareMenuContent } from '@/components/features/ShareMenu/ShareMenuContent';
import { isEffectiveOwner } from '@/lib/share';
export const ThreeDotMenuButton = React.memo(({ metricId }: { metricId: string }) => {
const { openSuccessMessage } = useBusterNotifications();
@ -433,7 +434,7 @@ const useRenameMetricSelectMenu = ({ metricId }: { metricId: string }) => {
export const useShareMenuSelectMenu = ({ metricId }: { metricId: string }) => {
const { data: metric } = useGetMetric(metricId);
const isOwner = metric?.permission === ShareRole.OWNER;
const isOwner = isEffectiveOwner(metric?.permission);
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 {
BusterChat,
BusterChatMessage,
BusterChatMessageReasoning,
BusterChatMessageReasoning_file,
BusterChatMessageResponse
import {
ShareRole,
type BusterChat,
type BusterChatMessage,
type BusterChatMessageReasoning,
type BusterChatMessageReasoning_file,
type BusterChatMessageResponse
} from '@/api/asset_interfaces';
import { faker } from '@faker-js/faker';
@ -14,7 +15,8 @@ const MOCK_MESSAGE_RESPONSE = (typeProp?: 'text' | 'file'): BusterChatMessageRes
return {
id: faker.string.uuid(),
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: {
rows
},
sharingKey: 'mock-sharing-key',
publicly_accessible: false,
public_password: null,
public_expiry_date: null,
public_enabled_by: null,
password_secret_id: null,
versions: []
};
const response: BusterDashboardResponse = {
access: ShareRole.EDITOR,
access: ShareRole.CAN_EDIT,
metrics: metrics.reduce((acc, metric) => ({ ...acc, [metric.id]: metric }), {}),
dashboard,
permission: ShareRole.EDITOR,
permission: ShareRole.CAN_EDIT,
public_password: null,
sharingKey: 'mock-sharing-key',
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 = {
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
};