mirror of https://github.com/buster-so/buster.git
adjust new select
This commit is contained in:
parent
f39a309a38
commit
d83193f0c4
|
@ -6,7 +6,6 @@ export * from './dataset_groups';
|
|||
export * from './datasets';
|
||||
export * from './datasources';
|
||||
export * from './metric';
|
||||
export * from './organizations';
|
||||
export * from './permission';
|
||||
export * from './permission_groups';
|
||||
export * from './search';
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export * from './interfaces';
|
|
@ -1,9 +0,0 @@
|
|||
import type { OrganizationRole } from '@buster/server-shared/organization';
|
||||
|
||||
export const BusterOrganizationRoleLabels: Record<OrganizationRole, string> = {
|
||||
workspace_admin: 'Workspace Admin',
|
||||
data_admin: 'Data Admin',
|
||||
querier: 'Querier',
|
||||
restricted_querier: 'Restricted Querier',
|
||||
viewer: 'Viewer',
|
||||
};
|
|
@ -32,7 +32,7 @@ export const ListUsersComponent: React.FC<{
|
|||
dataIndex: 'role',
|
||||
width: 165,
|
||||
render: (role: OrganizationUser['role']) => {
|
||||
return <Text variant="secondary">{OrganizationUserRoleText[role]}</Text>;
|
||||
return <Text variant="secondary">{OrganizationUserRoleText[role].title}</Text>;
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import { BusterOrganizationRoleLabels } from '@/api/asset_interfaces';
|
||||
import { useUpdateUser } from '@/api/buster_rest/users';
|
||||
import {
|
||||
Card,
|
||||
|
@ -14,6 +13,8 @@ import { Text } from '@/components/ui/typography';
|
|||
import { useMemoizedFn } from '@/hooks';
|
||||
import { User } from '@buster/server-shared/user';
|
||||
import type { OrganizationUser } from '@buster/server-shared/organization';
|
||||
import { OrganizationUserRoleText } from '@/lib/organization/translations';
|
||||
import { AccessRoleSelect } from '@/components/features/security/AccessRoleSelect';
|
||||
|
||||
export const UserDefaultAccess: React.FC<{
|
||||
user: OrganizationUser;
|
||||
|
@ -41,20 +42,6 @@ export const UserDefaultAccess: React.FC<{
|
|||
);
|
||||
};
|
||||
|
||||
const accessOptions: SelectItem<OrganizationUser['role']>[] = [
|
||||
{ label: BusterOrganizationRoleLabels.data_admin, value: 'data_admin' },
|
||||
{
|
||||
label: BusterOrganizationRoleLabels.workspace_admin,
|
||||
value: 'workspace_admin'
|
||||
},
|
||||
{ label: BusterOrganizationRoleLabels.querier, value: 'querier' },
|
||||
{
|
||||
label: BusterOrganizationRoleLabels.restricted_querier,
|
||||
value: 'restricted_querier'
|
||||
},
|
||||
{ label: BusterOrganizationRoleLabels.viewer, value: 'viewer' }
|
||||
];
|
||||
|
||||
const DefaultAccessCard = React.memo(
|
||||
({
|
||||
role,
|
||||
|
@ -91,12 +78,7 @@ const DefaultAccessCard = React.memo(
|
|||
: 'Only admins can change access'
|
||||
: undefined
|
||||
}>
|
||||
<Select
|
||||
items={accessOptions}
|
||||
value={role}
|
||||
onChange={onChange}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<AccessRoleSelect role={role} onChange={onChange} />
|
||||
</AppTooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import { useGetDatasets } from '@/api/buster_rest/datasets';
|
||||
import { Select, type SelectItem } from '@/components/ui/select/SelectOld';
|
||||
import { Select, type SelectItem } from '@/components/ui/select';
|
||||
import { SelectMultiple } from '@/components/ui/select/SelectMultiple';
|
||||
import { useMemoizedFn } from '@/hooks';
|
||||
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { AccessRoleSelect } from './AccessRoleSelect';
|
||||
import type { OrganizationRole } from '@buster/server-shared/organization';
|
||||
|
||||
const meta: Meta<typeof AccessRoleSelect> = {
|
||||
title: 'Features/Security/AccessRoleSelect',
|
||||
component: AccessRoleSelect,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'A select component for choosing organization access roles with titles and descriptions.'
|
||||
}
|
||||
}
|
||||
},
|
||||
argTypes: {
|
||||
role: {
|
||||
control: 'select',
|
||||
options: ['viewer', 'restricted_querier', 'querier', 'data_admin', 'workspace_admin'],
|
||||
description: 'The currently selected organization role'
|
||||
},
|
||||
onChange: {
|
||||
action: 'role-changed',
|
||||
description: 'Callback function called when role selection changes'
|
||||
}
|
||||
},
|
||||
tags: ['autodocs']
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AccessRoleSelect>;
|
||||
|
||||
// Default story
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
role: 'viewer',
|
||||
onChange: action('role-changed')
|
||||
}
|
||||
};
|
||||
|
||||
// Interactive story where users can change roles
|
||||
export const Interactive: Story = {
|
||||
args: {
|
||||
role: 'querier',
|
||||
onChange: action('role-changed')
|
||||
}
|
||||
};
|
||||
|
||||
// Stories for each specific role
|
||||
export const Viewer: Story = {
|
||||
args: {
|
||||
role: 'viewer',
|
||||
onChange: action('role-changed')
|
||||
}
|
||||
};
|
||||
|
||||
export const RestrictedQuerier: Story = {
|
||||
args: {
|
||||
role: 'restricted_querier',
|
||||
onChange: action('role-changed')
|
||||
}
|
||||
};
|
||||
|
||||
export const Querier: Story = {
|
||||
args: {
|
||||
role: 'querier',
|
||||
onChange: action('role-changed')
|
||||
}
|
||||
};
|
||||
|
||||
export const DataAdmin: Story = {
|
||||
args: {
|
||||
role: 'data_admin',
|
||||
onChange: action('role-changed')
|
||||
}
|
||||
};
|
||||
|
||||
export const WorkspaceAdmin: Story = {
|
||||
args: {
|
||||
role: 'workspace_admin',
|
||||
onChange: action('role-changed')
|
||||
}
|
||||
};
|
||||
|
||||
// Story without initial role (uses default)
|
||||
export const NoInitialRole: Story = {
|
||||
args: {
|
||||
onChange: action('role-changed')
|
||||
}
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import { Select, type SelectItem } from '@/components/ui/select';
|
||||
import { OrganizationRoleEnum, type OrganizationRole } from '@buster/server-shared/organization';
|
||||
import { OrganizationUserRoleText } from '@/lib/organization';
|
||||
|
||||
const items: SelectItem<OrganizationRole>[] = Object.values(OrganizationRoleEnum).map((role) => ({
|
||||
label: OrganizationUserRoleText[role as OrganizationRole].title,
|
||||
secondaryLabel: OrganizationUserRoleText[role as OrganizationRole].description,
|
||||
value: role as OrganizationRole
|
||||
}));
|
||||
|
||||
interface AccessRoleSelectProps {
|
||||
role?: OrganizationRole;
|
||||
onChange: (role: OrganizationRole) => void;
|
||||
}
|
||||
|
||||
export const AccessRoleSelect = ({ role = 'viewer', onChange }: AccessRoleSelectProps) => {
|
||||
return (
|
||||
<Select
|
||||
items={items}
|
||||
className="w-36 max-w-72"
|
||||
value={role}
|
||||
onChange={(v) => onChange(v as OrganizationRole)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -12,11 +12,12 @@ import type {
|
|||
GetWorkspaceSettingsResponse,
|
||||
UpdateWorkspaceSettingsRequest
|
||||
} from '@buster/server-shared/security';
|
||||
import { Select, type SelectItem } from '@/components/ui/select';
|
||||
import { type OrganizationRole, OrganizationRoleEnum } from '@buster/server-shared/organization';
|
||||
import { OrganizationUserRoleText } from '@/lib/organization/translations';
|
||||
import { type SelectItem } from '@/components/ui/select';
|
||||
import { type OrganizationRole } from '@buster/server-shared/organization';
|
||||
import { useGetDatasets } from '@/api/buster_rest/datasets';
|
||||
import { SelectMultiple } from '@/components/ui/select/SelectMultiple';
|
||||
import { AccessRoleSelect } from './AccessRoleSelect';
|
||||
import { useMemoizedFn } from '@/hooks';
|
||||
|
||||
export const WorkspaceRestrictions = React.memo(() => {
|
||||
const { data: workspaceSettings } = useGetWorkspaceSettings();
|
||||
|
@ -31,7 +32,7 @@ export const WorkspaceRestrictions = React.memo(() => {
|
|||
/>,
|
||||
<DefaultRole
|
||||
key="default-role"
|
||||
default_role={workspaceSettings?.default_role ?? 'viewer' as OrganizationRole}
|
||||
default_role={workspaceSettings?.default_role ?? ('viewer' as OrganizationRole)}
|
||||
updateWorkspaceSettings={updateWorkspaceSettings}
|
||||
/>,
|
||||
<DefaultDatasets
|
||||
|
@ -84,13 +85,6 @@ const DefaultRole = ({
|
|||
}: Pick<GetWorkspaceSettingsResponse, 'default_role'> & {
|
||||
updateWorkspaceSettings: (request: UpdateWorkspaceSettingsRequest) => Promise<unknown>;
|
||||
}) => {
|
||||
const items: SelectItem<OrganizationRole>[] = useMemo(() => {
|
||||
return Object.values(OrganizationRoleEnum).map((role) => ({
|
||||
label: OrganizationUserRoleText[role as OrganizationRole],
|
||||
value: role as OrganizationRole
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex min-w-0 flex-1 flex-col space-y-0.5">
|
||||
|
@ -99,13 +93,11 @@ const DefaultRole = ({
|
|||
{`Select which default role is assigned to new users`}
|
||||
</Text>
|
||||
</div>
|
||||
<Select
|
||||
items={items}
|
||||
className="w-36 max-w-72"
|
||||
value={default_role}
|
||||
onChange={(v) => {
|
||||
<AccessRoleSelect
|
||||
role={default_role}
|
||||
onChange={useMemoizedFn((v) => {
|
||||
updateWorkspaceSettings({ default_role: v });
|
||||
}}
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -27,10 +27,10 @@ export interface SelectItem<T = string> {
|
|||
|
||||
type SearchFunction<T> = (item: SelectItem<T>, searchTerm: string) => boolean;
|
||||
|
||||
export interface SelectProps<T> {
|
||||
// Base interface with common properties
|
||||
interface BaseSelectProps<T> {
|
||||
items: SelectItem<T>[] | SelectItemGroup<T>[];
|
||||
disabled?: boolean;
|
||||
onChange: (value: T | null) => void;
|
||||
placeholder?: string;
|
||||
value?: string | undefined;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
|
@ -41,10 +41,24 @@ export interface SelectProps<T> {
|
|||
dataTestId?: string;
|
||||
loading?: boolean;
|
||||
search?: boolean | SearchFunction<T>;
|
||||
clearable?: boolean;
|
||||
emptyMessage?: string;
|
||||
}
|
||||
|
||||
// Clearable version - onChange can return null
|
||||
interface ClearableSelectProps<T> extends BaseSelectProps<T> {
|
||||
clearable: true;
|
||||
onChange: (value: T | null) => void;
|
||||
}
|
||||
|
||||
// Non-clearable version - onChange cannot return null
|
||||
interface NonClearableSelectProps<T> extends BaseSelectProps<T> {
|
||||
clearable?: false;
|
||||
onChange: (value: T) => void;
|
||||
}
|
||||
|
||||
// Union type for type-safe props
|
||||
export type SelectProps<T> = ClearableSelectProps<T> | NonClearableSelectProps<T>;
|
||||
|
||||
function isGroupedItems<T>(
|
||||
items: SelectItem<T>[] | SelectItemGroup<T>[]
|
||||
): items is SelectItemGroup<T>[] {
|
||||
|
@ -83,17 +97,19 @@ const SelectItemComponent = React.memo(
|
|||
onSelect={onSelect}
|
||||
disabled={item.disabled}
|
||||
className={cn(
|
||||
'flex h-7 items-center gap-2 px-2',
|
||||
'flex min-h-7 items-center gap-2 px-2',
|
||||
item.disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
|
||||
isSelected && 'bg-item-select'
|
||||
)}>
|
||||
{item.icon}
|
||||
<span className="flex-1">
|
||||
{showIndex && `${index + 1}. `}
|
||||
{item.label}
|
||||
{item.secondaryLabel && (
|
||||
<span className="text-text-secondary ml-2 text-sm">{item.secondaryLabel}</span>
|
||||
)}
|
||||
<div className="flex flex-col space-y-0">
|
||||
<span className="text-foreground">{item.label}</span>
|
||||
{item.secondaryLabel && (
|
||||
<span className="text-text-secondary text-sm">{item.secondaryLabel}</span>
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
{isSelected && (
|
||||
<div className="text-icon-color flex h-4 w-4 items-center">
|
||||
|
@ -191,11 +207,14 @@ function SelectComponent<T = string>({
|
|||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onChange(null);
|
||||
setSearchValue('');
|
||||
handleOpenChange(false);
|
||||
// Type assertion is safe here because handleClear is only called when clearable is true
|
||||
if (clearable) {
|
||||
(onChange as (value: T | null) => void)(null);
|
||||
setSearchValue('');
|
||||
handleOpenChange(false);
|
||||
}
|
||||
},
|
||||
[onChange, handleOpenChange]
|
||||
[onChange, handleOpenChange, clearable]
|
||||
);
|
||||
|
||||
const handleInputChange = React.useCallback(
|
||||
|
|
|
@ -4,7 +4,7 @@ import { faker } from '@faker-js/faker';
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { SelectItem } from './SelectOld';
|
||||
import type { SelectItem } from './Select';
|
||||
import { SelectMultiple } from './SelectMultiple';
|
||||
|
||||
const meta = {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { useMemoizedFn } from '@/hooks';
|
|||
import { cn } from '@/lib/classMerge';
|
||||
import { Dropdown, type DropdownItem, type DropdownProps } from '../dropdown/Dropdown';
|
||||
import { InputTag } from '../inputs/InputTag';
|
||||
import type { SelectItem } from './SelectOld';
|
||||
import type { SelectItem } from './Select';
|
||||
import { selectVariants } from './SelectBase';
|
||||
import { CircleSpinnerLoader } from '../loaders';
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ export interface SelectProps<T> {
|
|||
open?: boolean;
|
||||
showIndex?: boolean;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
defaultValue?: string;
|
||||
dataTestId?: string;
|
||||
loading?: boolean;
|
||||
|
@ -50,6 +51,7 @@ export const Select = <T extends string>({
|
|||
value,
|
||||
onOpenChange,
|
||||
open,
|
||||
contentClassName,
|
||||
loading = false,
|
||||
className = '',
|
||||
defaultValue,
|
||||
|
@ -69,7 +71,7 @@ export const Select = <T extends string>({
|
|||
<SelectTrigger className={className} data-testid={dataTestId} loading={loading}>
|
||||
<SelectValue placeholder={placeholder} defaultValue={value || defaultValue} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent className={contentClassName}>
|
||||
{items.map((item, index) => (
|
||||
<SelectItemSelector
|
||||
key={index.toString()}
|
||||
|
|
|
@ -1 +1 @@
|
|||
export * from './SelectOld';
|
||||
export * from './Select';
|
||||
|
|
|
@ -5,11 +5,11 @@ import { ArrowUpRight, CircleCheck, AlertWarning } from '@/components/ui/icons';
|
|||
import { Paragraph, Text } from '@/components/ui/typography';
|
||||
import { cn } from '@/lib/classMerge';
|
||||
import type { useNewChatWarning } from './useNewChatWarning';
|
||||
import { BusterOrganizationRoleLabels } from '@/api/asset_interfaces/organizations';
|
||||
import type { OrganizationRole } from '@buster/server-shared/organization';
|
||||
import { OrganizationUserRoleText } from '@/lib/organization/translations';
|
||||
|
||||
const translateRole = (role: OrganizationRole) => {
|
||||
return BusterOrganizationRoleLabels[role];
|
||||
const translateRole = (role: OrganizationRole): string => {
|
||||
return OrganizationUserRoleText[role].title;
|
||||
};
|
||||
|
||||
export const NewChatWarning = React.memo(
|
||||
|
|
|
@ -1,9 +1,30 @@
|
|||
import type { OrganizationRole } from '@buster/server-shared/organization';
|
||||
|
||||
export const OrganizationUserRoleText: Record<OrganizationRole, string> = {
|
||||
data_admin: 'Data Admin',
|
||||
workspace_admin: 'Workspace Admin',
|
||||
querier: 'Querier',
|
||||
restricted_querier: 'Restricted Querier',
|
||||
viewer: 'Viewer',
|
||||
export const OrganizationUserRoleText: Record<
|
||||
OrganizationRole,
|
||||
{
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
> = {
|
||||
viewer: {
|
||||
title: 'Viewer',
|
||||
description: 'Can only view metrics that have been shared with them.'
|
||||
},
|
||||
restricted_querier: {
|
||||
title: 'Restricted Querier',
|
||||
description: 'Can only query datasets that have been provisioned to them.'
|
||||
},
|
||||
querier: {
|
||||
title: 'Querier',
|
||||
description: 'Can query all datasets associated with the workspace.'
|
||||
},
|
||||
data_admin: {
|
||||
title: 'Data Admin',
|
||||
description: 'Full access, except for billing.'
|
||||
},
|
||||
workspace_admin: {
|
||||
title: 'Workspace Admin',
|
||||
description: 'Full access, including billing.'
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue