create workspace settings

This commit is contained in:
Nate Kelley 2025-07-09 10:51:58 -06:00
parent 59e6aacd9d
commit 26d77fef53
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
13 changed files with 123 additions and 79 deletions

View File

@ -12,7 +12,7 @@ import {
import { BusterInfiniteList } from '@/components/ui/list/BusterInfiniteList';
import { Text } from '@/components/ui/typography';
import { BusterRoutes, createBusterRoute } from '@/routes';
import { OrganizationUserRoleText } from './config';
import { OrganizationUserRoleText } from '@/lib/organization/translations';
export const ListUsersComponent: React.FC<{
users: OrganizationUser[];

View File

@ -14,9 +14,10 @@ import type {
UpdateWorkspaceSettingsRequest
} from '@buster/server-shared/security';
import { Select, type SelectItem } from '@/components/ui/select';
import { type OrganizationRole } from '@buster/server-shared/organization';
import { OrganizationRoleEnum } from '@buster/server-shared/organization';
import { type OrganizationRole, OrganizationRoleEnum } from '@buster/server-shared/organization';
import { OrganizationUserRoleText } from '@/lib/organization/translations';
import { useGetDatasets } from '@/api/buster_rest/datasets';
import { SelectMultiple } from '@/components/ui/select/SelectMultiple';
export const WorkspaceRestrictions = React.memo(() => {
const { data: workspaceSettings } = useGetWorkspaceSettings();
@ -94,9 +95,9 @@ const DefaultRole = ({
return (
<div className="flex items-center justify-between">
<div className="flex min-w-0 flex-1 flex-col space-y-0.5">
<Text>Restrict new user invitations</Text>
<Text>Default Role</Text>
<Text variant="secondary" size={'sm'}>
{`Only allow admins to invite new members to workspace`}
{`Select which default role is assigned to new users`}
</Text>
</div>
<Select
@ -117,15 +118,42 @@ const DefaultDatasets = ({
}: Pick<GetWorkspaceSettingsResponse, 'default_datasets'> & {
updateWorkspaceSettings: (request: UpdateWorkspaceSettingsRequest) => Promise<unknown>;
}) => {
const { data: datasets, isFetched: isDatasetsFetched } = useGetDatasets();
const items: SelectItem<string>[] = useMemo(() => {
const baseItems =
datasets?.map((dataset) => ({
label: dataset.name,
value: dataset.id
})) || [];
return [{ label: 'All datasets', value: 'all' }, ...baseItems];
}, [datasets]);
const selectedItems = useMemo(() => {
return default_datasets.map((dataset) => dataset.id);
}, [default_datasets]);
return (
<div className="flex items-center justify-between">
<div className="flex flex-col space-y-0.5">
<Text>Restrict new user invitations</Text>
<Text>Default Datasets</Text>
<Text variant="secondary" size={'sm'}>
{`Only allow admins to invite new members to workspace`}
{`Select which datasets people can access by default`}
</Text>
</div>
<></>
<SelectMultiple
items={items}
value={selectedItems}
loading={!isDatasetsFetched}
placeholder="Select datasets"
className="w-40 max-w-72"
align="end"
side="left"
onChange={(v) => {
updateWorkspaceSettings({ default_datasets_ids: v });
}}
/>
</div>
);
};

View File

@ -43,6 +43,7 @@ const SelectMultipleWithHooks = () => {
onChange={handleSelect}
placeholder="Select multiple options..."
value={value}
loading={true}
/>
</div>
);

View File

@ -4,10 +4,11 @@ import type { VariantProps } from 'class-variance-authority';
import React, { useMemo } from 'react';
import { useMemoizedFn } from '@/hooks';
import { cn } from '@/lib/classMerge';
import { Dropdown, type DropdownItem } from '../dropdown/Dropdown';
import { Dropdown, type DropdownItem, type DropdownProps } from '../dropdown/Dropdown';
import { InputTag } from '../inputs/InputTag';
import type { SelectItem } from './Select';
import { selectVariants } from './SelectBase';
import { CircleSpinnerLoader } from '../loaders';
interface SelectMultipleProps extends VariantProps<typeof selectVariants> {
items: SelectItem[];
@ -17,6 +18,9 @@ interface SelectMultipleProps extends VariantProps<typeof selectVariants> {
value: string[];
disabled?: boolean;
useSearch?: boolean;
loading?: boolean;
align?: DropdownProps['align'];
side?: DropdownProps['side'];
}
export const SelectMultiple: React.FC<SelectMultipleProps> = React.memo(
@ -29,6 +33,9 @@ export const SelectMultiple: React.FC<SelectMultipleProps> = React.memo(
variant = 'default',
value,
disabled,
align = 'start',
side = 'bottom',
loading = false,
useSearch = true
}) => {
const selectedRecord = useMemo(() => {
@ -76,7 +83,8 @@ export const SelectMultiple: React.FC<SelectMultipleProps> = React.memo(
onSelect={handleSelect}
menuHeader={useSearch ? 'Search...' : undefined}
selectType="multiple"
align="start"
align={align}
side={side}
modal={false}
className="w-[var(--radix-dropdown-menu-trigger-width)] max-w-full!">
<div
@ -104,6 +112,13 @@ export const SelectMultiple: React.FC<SelectMultipleProps> = React.memo(
{selectedItems.length > 0 && (
<div className="from-background via-background/80 pointer-events-none absolute top-0 right-0 z-10 h-full w-8 bg-gradient-to-l to-transparent" />
)}
{loading && (
<div
className="absolute top-0 right-0 flex h-full w-8 items-center justify-center"
data-loading="true">
<CircleSpinnerLoader size={16} />
</div>
)}
</div>
</Dropdown>
);

View File

@ -0,0 +1 @@
export * from './translations';

View File

@ -1,4 +1,3 @@
export * from './organization.types';
export * from './roles.types';
export * from './user.types';
export * from './role.enums';

View File

@ -1,11 +0,0 @@
import type { OrganizationRole } from './roles.types';
//We need this to avoid postgres dependency in the frontend ☹️
export const OrganizationRoleEnum: Record<OrganizationRole, OrganizationRole> = {
none: 'none',
viewer: 'viewer',
workspace_admin: 'workspace_admin',
data_admin: 'data_admin',
querier: 'querier',
restricted_querier: 'restricted_querier',
};

View File

@ -1,6 +1,19 @@
import { userOrganizationRoleEnum } from '@buster/database';
import type { userOrganizationRoleEnum } from '@buster/database'; //we import as type to avoid postgres dependency in the frontend ☹️
import { z } from 'zod/v4';
export const OrganizationRoleSchema = z.enum([...userOrganizationRoleEnum.enumValues, 'none']);
type OrganizationRoleBase = (typeof userOrganizationRoleEnum.enumValues)[number] | 'none';
//We need this to avoid postgres dependency in the frontend ☹️
export const OrganizationRoleEnum: Record<OrganizationRoleBase, OrganizationRoleBase> =
Object.freeze({
none: 'none',
viewer: 'viewer',
workspace_admin: 'workspace_admin',
data_admin: 'data_admin',
querier: 'querier',
restricted_querier: 'restricted_querier',
});
export const OrganizationRoleSchema = z.enum(Object.values(OrganizationRoleEnum));
export type OrganizationRole = z.infer<typeof OrganizationRoleSchema>;

View File

@ -1,30 +1,24 @@
import { z } from "zod/v4";
import { OrganizationRoleSchema } from "../organization";
import { z } from 'zod/v4';
import { OrganizationRoleSchema } from '../organization';
export const UpdateInviteLinkRequestSchema = z.object({
enabled: z.boolean().optional(),
refresh_link: z.boolean().optional(),
});
export type UpdateInviteLinkRequest = z.infer<
typeof UpdateInviteLinkRequestSchema
>;
export type UpdateInviteLinkRequest = z.infer<typeof UpdateInviteLinkRequestSchema>;
export const AddApprovedDomainRequestSchema = z.object({
domains: z.array(z.string()),
});
export type AddApprovedDomainRequest = z.infer<
typeof AddApprovedDomainRequestSchema
>;
export type AddApprovedDomainRequest = z.infer<typeof AddApprovedDomainRequestSchema>;
export const RemoveApprovedDomainRequestSchema = z.object({
domains: z.array(z.string()),
});
export type RemoveApprovedDomainRequest = z.infer<
typeof RemoveApprovedDomainRequestSchema
>;
export type RemoveApprovedDomainRequest = z.infer<typeof RemoveApprovedDomainRequestSchema>;
export const UpdateWorkspaceSettingsRequestSchema = z.object({
restrict_new_user_invitations: z.boolean().optional(),
@ -38,12 +32,10 @@ export const UpdateWorkspaceSettingsRequestSchema = z.object({
.regex(
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/
),
z.literal("all"),
z.literal('all'),
])
)
.optional(),
});
export type UpdateWorkspaceSettingsRequest = z.infer<
typeof UpdateWorkspaceSettingsRequestSchema
>;
export type UpdateWorkspaceSettingsRequest = z.infer<typeof UpdateWorkspaceSettingsRequestSchema>;

View File

@ -1,5 +1,5 @@
import { z } from "zod/v4";
import { OrganizationRoleSchema } from "../organization";
import { z } from 'zod/v4';
import { OrganizationRoleSchema } from '../organization';
export const GetInviteLinkResponseSchema = z.object({
link: z.string(),
@ -14,12 +14,9 @@ export const GetApprovedDomainsResponseSchema = z.array(
created_at: z.string(),
})
);
export const AddApprovedDomainsResponseSchema =
GetApprovedDomainsResponseSchema;
export const UpdateApprovedDomainsResponseSchema =
GetApprovedDomainsResponseSchema;
export const RemoveApprovedDomainsResponseSchema =
GetApprovedDomainsResponseSchema;
export const AddApprovedDomainsResponseSchema = GetApprovedDomainsResponseSchema;
export const UpdateApprovedDomainsResponseSchema = GetApprovedDomainsResponseSchema;
export const RemoveApprovedDomainsResponseSchema = GetApprovedDomainsResponseSchema;
export const GetWorkspaceSettingsResponseSchema = z.object({
restrict_new_user_invitations: z.boolean(),
@ -31,31 +28,14 @@ export const GetWorkspaceSettingsResponseSchema = z.object({
})
),
});
export const UpdateWorkspaceSettingsResponseSchema =
GetWorkspaceSettingsResponseSchema;
export const UpdateWorkspaceSettingsResponseSchema = GetWorkspaceSettingsResponseSchema;
export type RefreshInviteLinkResponse = z.infer<
typeof RefreshInviteLinkResponseSchema
>;
export type UpdateInviteLinkResponse = z.infer<
typeof UpdateInviteLinkResponseSchema
>;
export type RefreshInviteLinkResponse = z.infer<typeof RefreshInviteLinkResponseSchema>;
export type UpdateInviteLinkResponse = z.infer<typeof UpdateInviteLinkResponseSchema>;
export type GetInviteLinkResponse = z.infer<typeof GetInviteLinkResponseSchema>;
export type GetApprovedDomainsResponse = z.infer<
typeof GetApprovedDomainsResponseSchema
>;
export type AddApprovedDomainsResponse = z.infer<
typeof AddApprovedDomainsResponseSchema
>;
export type UpdateApprovedDomainsResponse = z.infer<
typeof UpdateApprovedDomainsResponseSchema
>;
export type RemoveApprovedDomainsResponse = z.infer<
typeof RemoveApprovedDomainsResponseSchema
>;
export type GetWorkspaceSettingsResponse = z.infer<
typeof GetWorkspaceSettingsResponseSchema
>;
export type UpdateWorkspaceSettingsResponse = z.infer<
typeof UpdateWorkspaceSettingsResponseSchema
>;
export type GetApprovedDomainsResponse = z.infer<typeof GetApprovedDomainsResponseSchema>;
export type AddApprovedDomainsResponse = z.infer<typeof AddApprovedDomainsResponseSchema>;
export type UpdateApprovedDomainsResponse = z.infer<typeof UpdateApprovedDomainsResponseSchema>;
export type RemoveApprovedDomainsResponse = z.infer<typeof RemoveApprovedDomainsResponseSchema>;
export type GetWorkspaceSettingsResponse = z.infer<typeof GetWorkspaceSettingsResponseSchema>;
export type UpdateWorkspaceSettingsResponse = z.infer<typeof UpdateWorkspaceSettingsResponseSchema>;

View File

@ -1,8 +1,14 @@
import { teamRoleEnum } from '@buster/database';
import type { teamRoleEnum } from '@buster/database';
import { z } from 'zod/v4';
import { SharingSettingSchema } from '../user/sharing-setting.types';
export const TeamRoleSchema = z.enum([...teamRoleEnum.enumValues, 'none']);
type TeamRoleBase = (typeof teamRoleEnum.enumValues)[number] | 'none';
const TeamRoleEnums: Record<TeamRoleBase, TeamRoleBase> = Object.freeze({
none: 'none',
manager: 'manager',
member: 'member',
});
export const TeamRoleSchema = z.enum(Object.values(TeamRoleEnums));
export type TeamRole = z.infer<typeof TeamRoleSchema>;

View File

@ -1,6 +1,18 @@
import { userOrganizationRoleEnum } from '@buster/database';
import type { userOrganizationRoleEnum } from '@buster/database'; //we import as type to avoid postgres dependency in the frontend ☹️
import { z } from 'zod/v4';
export const UserOrganizationRoleSchema = z.enum([...userOrganizationRoleEnum.enumValues, 'none']);
type UserOrganizationRoleBase = (typeof userOrganizationRoleEnum.enumValues)[number] | 'none';
const UserOrganizationRoleEnums: Record<UserOrganizationRoleBase, UserOrganizationRoleBase> =
Object.freeze({
none: 'none',
viewer: 'viewer',
workspace_admin: 'workspace_admin',
data_admin: 'data_admin',
querier: 'querier',
restricted_querier: 'restricted_querier',
});
export const UserOrganizationRoleSchema = z.enum(Object.values(UserOrganizationRoleEnums));
export type UserOrganizationRole = z.infer<typeof UserOrganizationRoleSchema>;

View File

@ -1,6 +1,14 @@
import { sharingSettingEnum } from '@buster/database';
import type { sharingSettingEnum } from '@buster/database'; //we import as type to avoid postgres dependency in the frontend ☹️
import { z } from 'zod/v4';
export const SharingSettingSchema = z.enum([...sharingSettingEnum.enumValues, 'none']);
type SharingSettingBase = (typeof sharingSettingEnum.enumValues)[number] | 'none';
const SharingSettingEnums: Record<SharingSettingBase, SharingSettingBase> = Object.freeze({
none: 'none',
public: 'public',
team: 'team',
organization: 'organization',
});
export const SharingSettingSchema = z.enum(Object.values(SharingSettingEnums));
export type SharingSetting = z.infer<typeof SharingSettingSchema>;