feat(*): allow to select auth providers + add multiworkspace with subdomain management (#8656)

## Summary
Add support for multi-workspace feature and adjust configurations and
states accordingly.
- Introduced new state isMultiWorkspaceEnabledState.
- Updated ClientConfigProviderEffect component to handle
multi-workspace.
- Modified GraphQL schema and queries to include multi-workspace related
configurations.
- Adjusted server environment variables and their respective
documentation to support multi-workspace toggle.
- Updated server-side logic to handle new multi-workspace configurations
and conditions.
This commit is contained in:
Antoine Moreaux
2024-12-03 19:06:28 +01:00
committed by GitHub
parent 9a65e80566
commit 7943141d03
167 changed files with 5180 additions and 1901 deletions

View File

@ -15,6 +15,13 @@ import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { IconChevronDown, MenuItemSelectAvatar } from 'twenty-ui';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { Link } from 'react-router-dom';
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
const StyledLink = styled(Link)`
text-decoration: none;
width: 100%;
`;
const StyledLogo = styled.div<{ logo: string }>`
background: url(${({ logo }) => logo});
@ -72,6 +79,7 @@ export const MultiWorkspaceDropdownButton = ({
useState(false);
const { switchWorkspace } = useWorkspaceSwitching();
const { buildWorkspaceUrl } = useUrlManager();
const { closeDropdown } = useDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
@ -96,13 +104,9 @@ export const MultiWorkspaceDropdownButton = ({
isNavigationDrawerExpanded={isNavigationDrawerExpanded}
>
<StyledLogo
logo={
getImageAbsoluteURI(
currentWorkspace?.logo === null
? DEFAULT_WORKSPACE_LOGO
: currentWorkspace?.logo,
) ?? ''
}
logo={getImageAbsoluteURI(
currentWorkspace?.logo ?? DEFAULT_WORKSPACE_LOGO,
)}
/>
<NavigationDrawerAnimatedCollapseWrapper>
<StyledLabel>{currentWorkspace?.displayName ?? ''}</StyledLabel>
@ -118,23 +122,26 @@ export const MultiWorkspaceDropdownButton = ({
dropdownComponents={
<DropdownMenuItemsContainer>
{workspaces.map((workspace) => (
<MenuItemSelectAvatar
<StyledLink
key={workspace.id}
text={workspace.displayName ?? ''}
avatar={
<StyledLogo
logo={
getImageAbsoluteURI(
workspace.logo === null
? DEFAULT_WORKSPACE_LOGO
: workspace.logo,
) ?? ''
}
/>
}
selected={currentWorkspace?.id === workspace.id}
onClick={() => handleChange(workspace.id)}
/>
to={buildWorkspaceUrl(workspace.subdomain)}
>
<MenuItemSelectAvatar
text={workspace.displayName ?? ''}
avatar={
<StyledLogo
logo={getImageAbsoluteURI(
workspace.logo ?? DEFAULT_WORKSPACE_LOGO,
)}
/>
}
selected={currentWorkspace?.id === workspace.id}
onClick={(event) => {
event?.preventDefault();
handleChange(workspace.id);
}}
/>
</StyledLink>
))}
</DropdownMenuItemsContainer>
}

View File

@ -11,6 +11,7 @@ import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigat
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { isNonEmptyString } from '@sniptt/guards';
import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
const StyledContainer = styled.div`
align-items: center;
@ -60,14 +61,17 @@ export const NavigationDrawerHeader = ({
}: NavigationDrawerHeaderProps) => {
const isMobile = useIsMobile();
const workspaces = useRecoilValue(workspacesState);
const isMultiWorkspace = workspaces !== null && workspaces.length > 1;
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const isNavigationDrawerExpanded = useRecoilValue(
isNavigationDrawerExpandedState,
);
return (
<StyledContainer>
{isMultiWorkspace ? (
{isMultiWorkspaceEnabled &&
workspaces !== null &&
workspaces.length > 1 ? (
<MultiWorkspaceDropdownButton workspaces={workspaces} />
) : (
<StyledSingleWorkspaceContainer>

View File

@ -1,74 +1,44 @@
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
import { useAuth } from '@/auth/hooks/useAuth';
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
import { availableSSOIdentityProvidersState } from '@/auth/states/availableWorkspacesForSSO';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { AppPath } from '@/types/AppPath';
import { useGenerateJwtMutation } from '~/generated/graphql';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSwitchWorkspaceMutation } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { sleep } from '~/utils/sleep';
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
export const useWorkspaceSwitching = () => {
const setTokenPair = useSetRecoilState(tokenPairState);
const [generateJWT] = useGenerateJwtMutation();
const { redirectToSSOLoginPage } = useSSO();
const [switchWorkspaceMutation] = useSwitchWorkspaceMutation();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const setAvailableWorkspacesForSSOState = useSetRecoilState(
availableSSOIdentityProvidersState,
);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const { clearSession } = useAuth();
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const { enqueueSnackBar } = useSnackBar();
const { redirectToHome, redirectToWorkspace } = useUrlManager();
const switchWorkspace = async (workspaceId: string) => {
if (currentWorkspace?.id === workspaceId) return;
const jwt = await generateJWT({
if (!isMultiWorkspaceEnabled) {
return enqueueSnackBar(
'Switching workspace is not available in single workspace mode',
{
variant: SnackBarVariant.Error,
},
);
}
const { data, errors } = await switchWorkspaceMutation({
variables: {
workspaceId,
},
});
if (isDefined(jwt.errors)) {
throw jwt.errors;
if (isDefined(errors) || !isDefined(data?.switchWorkspace.subdomain)) {
return redirectToHome();
}
if (!isDefined(jwt.data?.generateJWT)) {
throw new Error('could not create token');
}
if (
jwt.data.generateJWT.reason === 'WORKSPACE_USE_SSO_AUTH' &&
'availableSSOIDPs' in jwt.data.generateJWT
) {
if (jwt.data.generateJWT.availableSSOIDPs.length === 1) {
redirectToSSOLoginPage(jwt.data.generateJWT.availableSSOIDPs[0].id);
}
if (jwt.data.generateJWT.availableSSOIDPs.length > 1) {
await clearSession();
setAvailableWorkspacesForSSOState(
jwt.data.generateJWT.availableSSOIDPs,
);
setSignInUpStep(SignInUpStep.SSOWorkspaceSelection);
}
return;
}
if (
jwt.data.generateJWT.reason !== 'WORKSPACE_USE_SSO_AUTH' &&
'authTokens' in jwt.data.generateJWT
) {
const { tokens } = jwt.data.generateJWT.authTokens;
setTokenPair(tokens);
await sleep(0); // This hacky workaround is necessary to ensure the tokens stored in the cookie are updated correctly.
window.location.href = AppPath.Index;
}
redirectToWorkspace(data.switchWorkspace.subdomain);
};
return { switchWorkspace };