diff --git a/apps/web/src/api/buster_rest/security/queryRequests.ts b/apps/web/src/api/buster_rest/security/queryRequests.ts index 1b93a6337..34d681a7c 100644 --- a/apps/web/src/api/buster_rest/security/queryRequests.ts +++ b/apps/web/src/api/buster_rest/security/queryRequests.ts @@ -10,12 +10,20 @@ import { addApprovedDomain, removeApprovedDomain } from './requests'; -import type { GetApprovedDomainsResponse } from '@buster/server-shared/security'; +import type { + GetApprovedDomainsResponse, + GetWorkspaceSettingsResponse +} from '@buster/server-shared/security'; export const useGetWorkspaceSettings = () => { return useQuery({ ...securityQueryKeys.securityGetWorkspaceSettings, - queryFn: getWorkspaceSettings + queryFn: getWorkspaceSettings, + initialData: { + restrict_new_user_invitations: false, + default_role: 'viewer', + default_datasets: [] + } satisfies GetWorkspaceSettingsResponse }); }; @@ -29,7 +37,8 @@ export const useGetInviteLink = () => { export const useGetApprovedDomains = () => { return useQuery({ ...securityQueryKeys.securityApprovedDomains, - queryFn: getApprovedDomains + queryFn: getApprovedDomains, + initialData: [] }); }; diff --git a/apps/web/src/app/app/(settings_layout)/settings/(permissions)/users/config.ts b/apps/web/src/app/app/(settings_layout)/settings/(permissions)/users/config.ts index 021e6f0a4..98a64fe6c 100644 --- a/apps/web/src/app/app/(settings_layout)/settings/(permissions)/users/config.ts +++ b/apps/web/src/app/app/(settings_layout)/settings/(permissions)/users/config.ts @@ -4,12 +4,3 @@ export const OrganizationUserStatusText: Record = { - data_admin: 'Data Admin', - workspace_admin: 'Workspace Admin', - querier: 'Querier', - restricted_querier: 'Restricted Querier', - viewer: 'Viewer', - none: 'None' -}; diff --git a/apps/web/src/app/app/(settings_layout)/settings/(restricted-width)/(admin-only)/security/page.tsx b/apps/web/src/app/app/(settings_layout)/settings/(restricted-width)/(admin-only)/security/page.tsx index 034d04800..c88316992 100644 --- a/apps/web/src/app/app/(settings_layout)/settings/(restricted-width)/(admin-only)/security/page.tsx +++ b/apps/web/src/app/app/(settings_layout)/settings/(restricted-width)/(admin-only)/security/page.tsx @@ -1,6 +1,7 @@ import { InviteLinks } from '@/components/features/security/InviteLinks'; import { SettingsPageHeader } from '../../../_components/SettingsPageHeader'; import { ApprovedEmailDomains } from '@/components/features/security/ApprovedEmailDomains'; +import { WorkspaceRestrictions } from '@/components/features/security/WorkspaceRestrictions'; export default function Page() { return ( @@ -14,6 +15,7 @@ export default function Page() {
+
diff --git a/apps/web/src/components/features/security/ApprovedEmailDomains.tsx b/apps/web/src/components/features/security/ApprovedEmailDomains.tsx index cdb2671c5..2911c95d4 100644 --- a/apps/web/src/components/features/security/ApprovedEmailDomains.tsx +++ b/apps/web/src/components/features/security/ApprovedEmailDomains.tsx @@ -5,7 +5,7 @@ import { SecurityCards } from './SecurityCards'; import { Input } from '@/components/ui/inputs'; import { Button } from '@/components/ui/buttons'; import { Text } from '@/components/ui/typography'; -import { Plus, Dots } from '@/components/ui/icons'; +import { Plus, Dots, Trash } from '@/components/ui/icons'; import { useBusterNotifications } from '@/context/BusterNotifications'; import { Dropdown } from '@/components/ui/dropdown'; import { @@ -15,6 +15,7 @@ import { } from '@/api/buster_rest/security/queryRequests'; import pluralize from 'pluralize'; import { useMemoizedFn } from '@/hooks'; +import { formatDate } from '@/lib/date'; interface ApprovedDomain { domain: string; @@ -24,12 +25,12 @@ interface ApprovedDomain { export const ApprovedEmailDomains = React.memo(() => { const { data: approvedDomains = [] } = useGetApprovedDomains(); const { mutateAsync: removeDomain } = useRemoveApprovedDomain(); + const domainCount = approvedDomains.length; - const [isAddingDomain, setIsAddingDomain] = useState(false); + const [isEnabledAddDomain, setIsEnabledAddDomain] = useState(false); const { openInfoMessage, openErrorMessage } = useBusterNotifications(); - const domainCount = approvedDomains.length; const countText = pluralize('approved email domain', domainCount, true); const handleRemoveDomain = useMemoizedFn(async (domain: string) => { @@ -47,13 +48,15 @@ export const ApprovedEmailDomains = React.memo(() => { // Header section with count and add button
{countText} -
, // Add domain input section (when adding) - isAddingDomain && , + isEnabledAddDomain && ( + + ), // Domain list sections ...approvedDomains.map((domainData) => ( @@ -64,7 +67,7 @@ export const ApprovedEmailDomains = React.memo(() => { /> )) ].filter(Boolean), - [countText, isAddingDomain, approvedDomains, handleRemoveDomain] + [countText, isEnabledAddDomain, approvedDomains, handleRemoveDomain] ); return ( @@ -79,7 +82,7 @@ export const ApprovedEmailDomains = React.memo(() => { ApprovedEmailDomains.displayName = 'ApprovedEmailDomains'; const AddDomainInput = React.memo( - ({ setIsAddingDomain }: { setIsAddingDomain: (isAddingDomain: boolean) => void }) => { + ({ setIsEnabledAddDomain }: { setIsEnabledAddDomain: (isEnabledAddDomain: boolean) => void }) => { const { mutateAsync: addDomain } = useAddApprovedDomain(); const { openInfoMessage, openErrorMessage } = useBusterNotifications(); @@ -91,7 +94,7 @@ const AddDomainInput = React.memo( try { await addDomain({ domains: [newDomain.trim()] }); setNewDomain(''); - setIsAddingDomain(false); + setIsEnabledAddDomain(false); openInfoMessage('Domain added successfully'); } catch (error) { openErrorMessage('Failed to add domain'); @@ -109,7 +112,7 @@ const AddDomainInput = React.memo( if (e.key === 'Enter') { handleAddDomain(); } else if (e.key === 'Escape') { - setIsAddingDomain(false); + setIsEnabledAddDomain(false); setNewDomain(''); } }} @@ -122,7 +125,7 @@ const AddDomainInput = React.memo( size="tall" variant={'ghost'} onClick={() => { - setIsAddingDomain(false); + setIsEnabledAddDomain(false); setNewDomain(''); }}> Cancel @@ -134,37 +137,34 @@ const AddDomainInput = React.memo( AddDomainInput.displayName = 'AddDomainInput'; -interface DomainListItemProps { +const DomainListItem = React.memo<{ domainData: ApprovedDomain; onRemove: (domain: string) => Promise; -} - -const DomainListItem = React.memo(({ domainData, onRemove }) => { - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - }); - }; +}>(({ domainData, onRemove }) => { + const items = useMemo( + () => [ + { + label: 'Remove domain', + value: 'remove', + icon: , + onClick: () => onRemove(domainData.domain) + } + ], + [domainData.domain, onRemove] + ); return (
-
- {domainData.domain} +
+ + {domainData.domain} + - Added {formatDate(domainData.created_at)} + Added {formatDate({ date: domainData.created_at, format: 'LL' })}
- onRemove(domainData.domain) - } - ]}> -
); diff --git a/apps/web/src/components/features/security/WorkspaceRestrictions.tsx b/apps/web/src/components/features/security/WorkspaceRestrictions.tsx new file mode 100644 index 000000000..29b48279f --- /dev/null +++ b/apps/web/src/components/features/security/WorkspaceRestrictions.tsx @@ -0,0 +1,131 @@ +'use client'; + +import React, { useMemo, type ReactNode } from 'react'; +import { SecurityCards } from './SecurityCards'; +import { Text } from '@/components/ui/typography'; +import { Button } from '@/components/ui/buttons'; +import { Switch } from '@/components/ui/switch'; +import { + useGetWorkspaceSettings, + useUpdateWorkspaceSettings +} from '@/api/buster_rest/security/queryRequests'; +import type { + GetWorkspaceSettingsResponse, + 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 { OrganizationUserRoleText } from '@/lib/organization/translations'; + +export const WorkspaceRestrictions = React.memo(() => { + const { data: workspaceSettings } = useGetWorkspaceSettings(); + const { mutateAsync: updateWorkspaceSettings } = useUpdateWorkspaceSettings(); + + const sections: ReactNode[] = useMemo( + () => [ + , + , + + ], + [workspaceSettings?.restrict_new_user_invitations] + ); + + return ( + + ); +}); + +WorkspaceRestrictions.displayName = 'WorkspaceRestrictions'; + +const EnableRestrictions = ({ + restrict_new_user_invitations = false, + updateWorkspaceSettings +}: Pick & { + updateWorkspaceSettings: (request: UpdateWorkspaceSettingsRequest) => Promise; +}) => { + return ( +
+
+ Restrict new user invitations + + {`Only allow admins to invite new members to workspace`} + +
+ { + updateWorkspaceSettings({ restrict_new_user_invitations: v }); + }} + /> +
+ ); +}; + +const DefaultRole = ({ + default_role = 'viewer', + updateWorkspaceSettings +}: Pick & { + updateWorkspaceSettings: (request: UpdateWorkspaceSettingsRequest) => Promise; +}) => { + const items: SelectItem[] = useMemo(() => { + return Object.values(OrganizationRoleEnum).map((role) => ({ + label: OrganizationUserRoleText[role as OrganizationRole], + value: role as OrganizationRole + })); + }, []); + + return ( +
+
+ Restrict new user invitations + + {`Only allow admins to invite new members to workspace`} + +
+