mirror of https://github.com/buster-so/buster.git
revise front end to use shared components
This commit is contained in:
parent
cd8a50403f
commit
0a34db8989
|
@ -2,6 +2,10 @@
|
|||
"$schema": "https://turbo.build/schema.json",
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"dev": {}
|
||||
"dev": {
|
||||
"dependsOn": ["@buster/database#start"],
|
||||
"with": [],
|
||||
"outputs": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import type { ShareAssetType, ShareRole } from '@buster/server-shared/share';
|
||||
import type { ShareAssetType, ShareRole, WorkspaceShareRole } from '@buster/server-shared/share';
|
||||
import type { DropdownItem } from '@/components/ui/dropdown';
|
||||
import { Dropdown } from '@/components/ui/dropdown';
|
||||
import { ChevronDown } from '@/components/ui/icons/NucleoIconFilled';
|
||||
|
@ -7,24 +7,45 @@ import { Paragraph, Text } from '@/components/ui/typography';
|
|||
import { useMemoizedFn } from '@/hooks';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
|
||||
type DropdownValue = ShareRole | 'remove' | 'notShared';
|
||||
type DropdownValue = ShareRole | WorkspaceShareRole | 'remove';
|
||||
|
||||
export const AccessDropdown: React.FC<{
|
||||
className?: string;
|
||||
showRemove?: boolean;
|
||||
shareLevel?: ShareRole | null;
|
||||
onChangeShareLevel?: (level: ShareRole | null) => void;
|
||||
type AccessDropdownBaseProps = {
|
||||
assetType: ShareAssetType;
|
||||
disabled: boolean;
|
||||
}> = ({
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type AccessDropdownUserProps = {
|
||||
showRemove?: boolean;
|
||||
shareLevel?: ShareRole | null;
|
||||
type: 'user';
|
||||
onChangeShareLevel?: (level: ShareRole | null) => void;
|
||||
};
|
||||
|
||||
type AccessDropdownWorkspaceProps = {
|
||||
type: 'workspace';
|
||||
shareLevel?: WorkspaceShareRole | null;
|
||||
onChangeShareLevel?: (level: WorkspaceShareRole | null) => void;
|
||||
};
|
||||
|
||||
type AccessDropdownProps = AccessDropdownBaseProps &
|
||||
(AccessDropdownUserProps | AccessDropdownWorkspaceProps);
|
||||
|
||||
export const AccessDropdown: React.FC<AccessDropdownProps> = ({
|
||||
shareLevel,
|
||||
showRemove = true,
|
||||
disabled,
|
||||
className = '',
|
||||
onChangeShareLevel,
|
||||
assetType
|
||||
assetType,
|
||||
...props
|
||||
}) => {
|
||||
const showRemove = props.type === 'user' && props.showRemove !== false;
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (props.type === 'workspace') {
|
||||
return workspaceItems;
|
||||
}
|
||||
|
||||
const baseItems: DropdownItem<DropdownValue>[] = [...(itemsRecord[assetType] || [])];
|
||||
|
||||
if (showRemove) {
|
||||
|
@ -38,7 +59,7 @@ export const AccessDropdown: React.FC<{
|
|||
...item,
|
||||
selected: item.value === shareLevel
|
||||
}));
|
||||
}, [showRemove, shareLevel, assetType]);
|
||||
}, [showRemove, shareLevel, assetType, props.type]);
|
||||
|
||||
const selectedLabel = useMemo(() => {
|
||||
const selectedItem = items.find((item) => item.selected) || OWNER_ITEM;
|
||||
|
@ -51,16 +72,17 @@ export const AccessDropdown: React.FC<{
|
|||
return 'Full access';
|
||||
case 'canEdit':
|
||||
return 'Can edit';
|
||||
case 'canFilter':
|
||||
return 'Can filter';
|
||||
case 'canView':
|
||||
return 'Can view';
|
||||
case 'owner':
|
||||
return 'Owner';
|
||||
case 'remove':
|
||||
return 'Remove';
|
||||
case 'notShared':
|
||||
case 'none':
|
||||
return 'Not shared';
|
||||
default:
|
||||
const _exhaustiveCheck: never = value;
|
||||
return value;
|
||||
}
|
||||
}, [items]);
|
||||
|
||||
|
@ -128,11 +150,6 @@ const dashboardItems: DropdownItem<ShareRole>[] = [
|
|||
label: 'Can edit',
|
||||
secondaryLabel: 'Can edit but not share with others.'
|
||||
},
|
||||
// {
|
||||
// value: 'canFilter',
|
||||
// label: 'Can filter',
|
||||
// secondaryLabel: 'Can filter dashboards but not edit.'
|
||||
// },
|
||||
{
|
||||
value: 'canView',
|
||||
label: 'Can view',
|
||||
|
@ -158,6 +175,29 @@ const collectionItems: DropdownItem<ShareRole>[] = [
|
|||
}
|
||||
];
|
||||
|
||||
const workspaceItems: DropdownItem<WorkspaceShareRole>[] = [
|
||||
{
|
||||
value: 'fullAccess',
|
||||
label: 'Full access',
|
||||
secondaryLabel: 'Can edit and share with others.'
|
||||
},
|
||||
{
|
||||
value: 'canEdit',
|
||||
label: 'Can edit',
|
||||
secondaryLabel: 'Can edit, but not share with others.'
|
||||
},
|
||||
{
|
||||
value: 'canView',
|
||||
label: 'Can view',
|
||||
secondaryLabel: 'Cannot edit or share with others.'
|
||||
},
|
||||
{
|
||||
value: 'none',
|
||||
label: 'Not shared',
|
||||
secondaryLabel: 'Does not have access.'
|
||||
}
|
||||
];
|
||||
|
||||
const itemsRecord: Record<ShareAssetType, DropdownItem<ShareRole>[]> = {
|
||||
['dashboard']: dashboardItems,
|
||||
['metric']: metricItems,
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
import React from 'react';
|
||||
import { useMemoizedFn } from '@/hooks';
|
||||
import { AccessDropdown } from './AccessDropdown';
|
||||
import type { ShareAssetType, ShareRole } from '@buster/server-shared/share';
|
||||
import { AvatarUserButton } from '../../ui/avatar/AvatarUserButton';
|
||||
|
||||
export const IndividualSharePerson: React.FC<{
|
||||
name?: string;
|
||||
email: string;
|
||||
role: ShareRole;
|
||||
avatar_url?: string | null;
|
||||
onUpdateShareRole: (email: string, role: ShareRole | null) => void;
|
||||
assetType: ShareAssetType;
|
||||
disabled: boolean;
|
||||
}> = React.memo(({ name, onUpdateShareRole, email, avatar_url, role, assetType, disabled }) => {
|
||||
const onChangeShareLevel = useMemoizedFn((v: ShareRole | null) => {
|
||||
onUpdateShareRole(email, v);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-8 items-center justify-between space-x-2 overflow-hidden"
|
||||
data-testid={`share-person-${email}`}>
|
||||
<AvatarUserButton username={name} email={email} avatarUrl={avatar_url} avatarSize={24} />
|
||||
|
||||
<AccessDropdown
|
||||
shareLevel={role}
|
||||
showRemove={true}
|
||||
disabled={disabled}
|
||||
onChangeShareLevel={onChangeShareLevel}
|
||||
assetType={assetType}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
IndividualSharePerson.displayName = 'IndividualSharePerson';
|
|
@ -1,16 +1,23 @@
|
|||
import React from 'react';
|
||||
import type { ShareAssetType, ShareConfig, ShareRole, WorkspaceShareRole } from '@buster/server-shared/share';
|
||||
import React, { useMemo } from 'react';
|
||||
import type {
|
||||
ShareAssetType,
|
||||
ShareConfig,
|
||||
ShareRole,
|
||||
WorkspaceShareRole
|
||||
} from '@buster/server-shared/share';
|
||||
import { useUnshareCollection, useUpdateCollectionShare } from '@/api/buster_rest/collections';
|
||||
import { useUnshareDashboard, useUpdateDashboardShare } from '@/api/buster_rest/dashboards';
|
||||
import { useUnshareMetric, useUpdateMetricShare } from '@/api/buster_rest/metrics';
|
||||
import { useMemoizedFn } from '@/hooks';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
import { IndividualSharePerson } from './IndividualSharePerson';
|
||||
import { ShareMenuContentEmbed } from './ShareMenuContentEmbed';
|
||||
import { ShareMenuContentPublish } from './ShareMenuContentPublish';
|
||||
import type { ShareMenuTopBarOptions } from './ShareMenuTopBar';
|
||||
import { ShareMenuInvite } from './ShareMenuInvite';
|
||||
import { WorkspaceShareSection } from './WorkspaceShareSection';
|
||||
import { ShareRowItem } from './ShareRowItem';
|
||||
import { WorkspaceAvatar } from './WorkspaceAvatar';
|
||||
import pluralize from 'pluralize';
|
||||
|
||||
export const ShareMenuContentBody: React.FC<{
|
||||
selectedOptions: ShareMenuTopBarOptions;
|
||||
|
@ -55,7 +62,14 @@ export const ShareMenuContentBody: React.FC<{
|
|||
ShareMenuContentBody.displayName = 'ShareMenuContentBody';
|
||||
|
||||
const ShareMenuContentShare: React.FC<ShareMenuContentBodyProps> = React.memo(
|
||||
({ canEditPermissions, assetType, individual_permissions, assetId, className, shareAssetConfig }) => {
|
||||
({
|
||||
canEditPermissions,
|
||||
assetType,
|
||||
individual_permissions,
|
||||
assetId,
|
||||
className,
|
||||
shareAssetConfig
|
||||
}) => {
|
||||
const { mutateAsync: onUpdateMetricShare } = useUpdateMetricShare();
|
||||
const { mutateAsync: onUpdateDashboardShare } = useUpdateDashboardShare();
|
||||
const { mutateAsync: onUpdateCollectionShare } = useUpdateCollectionShare();
|
||||
|
@ -64,6 +78,7 @@ const ShareMenuContentShare: React.FC<ShareMenuContentBodyProps> = React.memo(
|
|||
const { mutateAsync: onUnshareCollection } = useUnshareCollection();
|
||||
|
||||
const hasIndividualPermissions = !!individual_permissions?.length;
|
||||
const workspaceMemberCount = shareAssetConfig.workspace_member_count || 0;
|
||||
|
||||
const onUpdateShareRole = useMemoizedFn(async (email: string, role: ShareRole | null) => {
|
||||
if (role) {
|
||||
|
@ -130,24 +145,36 @@ const ShareMenuContentShare: React.FC<ShareMenuContentBodyProps> = React.memo(
|
|||
{hasIndividualPermissions && (
|
||||
<div className="flex flex-col space-y-2.5 overflow-hidden">
|
||||
{individual_permissions?.map((permission) => (
|
||||
<IndividualSharePerson
|
||||
key={permission.email}
|
||||
{...permission}
|
||||
onUpdateShareRole={onUpdateShareRole}
|
||||
<ShareRowItem
|
||||
primary={permission.name}
|
||||
secondary={permission.email}
|
||||
role={permission.role}
|
||||
avatar={permission.avatar_url}
|
||||
onChangeShareLevel={useMemoizedFn((role) =>
|
||||
onUpdateShareRole(permission.email, role)
|
||||
)}
|
||||
assetType={assetType}
|
||||
disabled={!canEditPermissions || permission.role === 'owner'}
|
||||
type="user"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canEditPermissions && (
|
||||
<WorkspaceShareSection
|
||||
shareAssetConfig={shareAssetConfig}
|
||||
<ShareRowItem
|
||||
primary={'Workspace'}
|
||||
secondary={`Share with ${workspaceMemberCount} ${pluralize('member', workspaceMemberCount)}`}
|
||||
role={shareAssetConfig.workspace_sharing || 'none'}
|
||||
type="workspace"
|
||||
avatar={useMemo(
|
||||
() => (
|
||||
<WorkspaceAvatar />
|
||||
),
|
||||
[]
|
||||
)}
|
||||
onChangeShareLevel={onUpdateWorkspacePermissions}
|
||||
assetType={assetType}
|
||||
assetId={assetId}
|
||||
canEditPermissions={canEditPermissions}
|
||||
onUpdateWorkspacePermissions={onUpdateWorkspacePermissions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -143,6 +143,7 @@ export const ShareMenuInvite: React.FC<ShareMenuInviteProps> = React.memo(
|
|||
onChangeShareLevel={onChangeAccessDropdown}
|
||||
assetType={assetType}
|
||||
disabled={false}
|
||||
type="user"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import type { ShareAssetType, ShareRole, WorkspaceShareRole } from '@buster/server-shared/share';
|
||||
import React from 'react';
|
||||
import { AvatarUserButton } from '../../ui/avatar/AvatarUserButton';
|
||||
import { AccessDropdown } from './AccessDropdown';
|
||||
|
||||
type ShareRowItemBaseProps = {
|
||||
primary: string | undefined;
|
||||
secondary: string | undefined;
|
||||
avatar?: string | null | React.ReactNode;
|
||||
disabled?: boolean;
|
||||
assetType: ShareAssetType;
|
||||
};
|
||||
|
||||
type ShareRowItemUserProps = {
|
||||
type: 'user';
|
||||
role: ShareRole;
|
||||
showRemove?: boolean;
|
||||
onChangeShareLevel?: (role: ShareRole | null) => void;
|
||||
};
|
||||
|
||||
type ShareRowItemWorkspaceProps = {
|
||||
type: 'workspace';
|
||||
role: WorkspaceShareRole;
|
||||
onChangeShareLevel?: (role: WorkspaceShareRole | null) => void;
|
||||
};
|
||||
|
||||
type ShareRowItemProps = ShareRowItemBaseProps &
|
||||
(ShareRowItemUserProps | ShareRowItemWorkspaceProps);
|
||||
|
||||
export const ShareRowItem: React.FC<ShareRowItemProps> = React.memo(
|
||||
({ primary, secondary, avatar, disabled = false, assetType, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
className="flex h-8 items-center justify-between space-x-2 overflow-hidden"
|
||||
data-testid={`share-row-${primary || secondary}`}>
|
||||
<AvatarUserButton username={primary} email={secondary} avatarUrl={avatar} avatarSize={24} />
|
||||
|
||||
<AccessDropdown disabled={disabled} assetType={assetType} {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import { ApartmentBuilding } from '@/components/ui/icons';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
|
||||
export const WorkspaceAvatar: React.FC<{
|
||||
className?: string;
|
||||
}> = React.memo(({ className }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'text-md flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-gray-100 text-gray-600',
|
||||
className
|
||||
)}>
|
||||
<ApartmentBuilding />
|
||||
</div>
|
||||
);
|
||||
});
|
|
@ -1,114 +0,0 @@
|
|||
import React from 'react';
|
||||
import type { ShareAssetType, ShareConfig, WorkspaceShareRole } from '@buster/server-shared/share';
|
||||
import { Dropdown } from '@/components/ui/dropdown';
|
||||
import type { DropdownItem } from '@/components/ui/dropdown';
|
||||
import { ChevronDown } from '@/components/ui/icons/NucleoIconFilled';
|
||||
import { ApartmentBuilding } from '@/components/ui/icons/NucleoIconFilled';
|
||||
import { Text } from '@/components/ui/typography';
|
||||
import { useMemoizedFn } from '@/hooks';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
|
||||
const workspaceShareRoleItems: DropdownItem<WorkspaceShareRole>[] = [
|
||||
{
|
||||
value: 'fullAccess',
|
||||
label: 'Full access',
|
||||
secondaryLabel: 'Can edit and share with others.'
|
||||
},
|
||||
{
|
||||
value: 'canEdit',
|
||||
label: 'Can edit',
|
||||
secondaryLabel: 'Can edit, but not share with others.'
|
||||
},
|
||||
{
|
||||
value: 'canView',
|
||||
label: 'Can view',
|
||||
secondaryLabel: 'Cannot edit or share with others.'
|
||||
},
|
||||
{
|
||||
value: 'none',
|
||||
label: 'Not shared',
|
||||
secondaryLabel: 'Does not have access.'
|
||||
}
|
||||
];
|
||||
|
||||
interface WorkspaceShareSectionProps {
|
||||
shareAssetConfig: ShareConfig;
|
||||
assetType: ShareAssetType;
|
||||
assetId: string;
|
||||
canEditPermissions: boolean;
|
||||
onUpdateWorkspacePermissions: (role: WorkspaceShareRole | null) => void;
|
||||
}
|
||||
|
||||
export const WorkspaceShareSection: React.FC<WorkspaceShareSectionProps> = React.memo(({
|
||||
shareAssetConfig,
|
||||
canEditPermissions,
|
||||
onUpdateWorkspacePermissions
|
||||
}) => {
|
||||
const currentRole = shareAssetConfig.workspace_sharing || 'none';
|
||||
|
||||
const selectedLabel = React.useMemo(() => {
|
||||
const selectedItem = workspaceShareRoleItems.find(item => item.value === currentRole);
|
||||
return selectedItem?.label || 'Not shared';
|
||||
}, [currentRole]);
|
||||
|
||||
const onSelectMenuItem = useMemoizedFn((value: string) => {
|
||||
onUpdateWorkspacePermissions(value as WorkspaceShareRole);
|
||||
});
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
return workspaceShareRoleItems.map(item => ({
|
||||
...item,
|
||||
selected: item.value === currentRole
|
||||
}));
|
||||
}, [currentRole]);
|
||||
|
||||
return (
|
||||
<div className="flex h-8 items-center justify-between space-x-2 overflow-hidden">
|
||||
<div className="flex w-full items-center gap-x-2 rounded-md p-1">
|
||||
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-gray-100 text-gray-600 text-md">
|
||||
<ApartmentBuilding />
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-y-0 overflow-hidden">
|
||||
<Text className="truncate">Workspace</Text>
|
||||
<Text variant="secondary" size="sm" className="truncate">
|
||||
Share with {shareAssetConfig.workspace_member_count?.toLocaleString() || 0} members
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dropdown
|
||||
items={items}
|
||||
disabled={!canEditPermissions}
|
||||
footerContent={
|
||||
<div className="bg-item-hover flex justify-center overflow-hidden rounded-b p-2 px-2.5">
|
||||
<Text variant="secondary" size="xs">
|
||||
Sharing cannot override permissions set by your account admins.
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
footerClassName="p-0!"
|
||||
onSelect={onSelectMenuItem}
|
||||
sideOffset={16}
|
||||
selectType="single"
|
||||
align="end"
|
||||
side="bottom">
|
||||
<Text
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
className={cn(
|
||||
'flex! items-center! space-x-1',
|
||||
canEditPermissions && 'cursor-pointer'
|
||||
)}>
|
||||
<span className="truncate">{selectedLabel}</span>
|
||||
{canEditPermissions && (
|
||||
<span className="text-2xs text-icon-color">
|
||||
<ChevronDown />
|
||||
</span>
|
||||
)}
|
||||
</Text>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
WorkspaceShareSection.displayName = 'WorkspaceShareSection';
|
|
@ -7,7 +7,7 @@ export const AvatarUserButton = React.forwardRef<
|
|||
HTMLDivElement,
|
||||
{
|
||||
username?: string | null;
|
||||
avatarUrl?: string | null;
|
||||
avatarUrl?: string | null | React.ReactNode;
|
||||
email?: string | null;
|
||||
className?: string;
|
||||
avatarSize?: number;
|
||||
|
@ -17,7 +17,11 @@ export const AvatarUserButton = React.forwardRef<
|
|||
|
||||
return (
|
||||
<div ref={ref} className={cn('flex w-full items-center gap-x-2 rounded-md p-1', className)}>
|
||||
<Avatar size={avatarSize} fallbackClassName="text-base" image={avatarUrl} name={username} />
|
||||
{typeof avatarUrl === 'string' || avatarUrl === null ? (
|
||||
<Avatar size={avatarSize} fallbackClassName="text-base" image={avatarUrl} name={username} />
|
||||
) : (
|
||||
avatarUrl
|
||||
)}
|
||||
<div className="flex w-full flex-col gap-y-0 overflow-hidden">
|
||||
<Text truncate className="flex-grow">
|
||||
{username}
|
||||
|
|
|
@ -88,7 +88,7 @@ const useSupabaseContextInternal = ({
|
|||
async ({ accessToken, expiresAt: _expiresAt }: { accessToken: string; expiresAt: number }) => {
|
||||
setAccessToken(accessToken);
|
||||
flushSync(() => {
|
||||
openInfoMessage('Token refreshed');
|
||||
//noop
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -4,17 +4,10 @@ export const ShareRoleSchema = z.enum([
|
|||
'owner', //owner of the asset
|
||||
'fullAccess', //same as owner, can share with others
|
||||
'canEdit', //can edit, cannot share
|
||||
'canFilter', //can filter dashboard
|
||||
'canView', //can view asset
|
||||
]);
|
||||
|
||||
export const WorkspaceShareRoleSchema = z.enum([
|
||||
'owner',
|
||||
'fullAccess',
|
||||
'canEdit',
|
||||
'canView',
|
||||
'none',
|
||||
]);
|
||||
export const WorkspaceShareRoleSchema = z.enum([...ShareRoleSchema.options, 'none']);
|
||||
|
||||
export const ShareAssetTypeSchema = z.enum(['metric', 'dashboard', 'collection', 'chat']);
|
||||
|
||||
|
|
Loading…
Reference in New Issue