Admin panel init (#8742)
WIP Related issues - #7090 #8547 Master issue - #4499 --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import { useCreateAppRouter } from '@/app/hooks/useCreateAppRouter';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
@ -16,6 +17,10 @@ export const AppRouter = () => {
|
||||
const isBillingPageEnabled =
|
||||
billing?.isBillingEnabled && !isFreeAccessEnabled;
|
||||
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
|
||||
const isAdminPageEnabled = currentUser?.canImpersonate;
|
||||
|
||||
return (
|
||||
<RouterProvider
|
||||
router={useCreateAppRouter(
|
||||
@ -23,6 +28,7 @@ export const AppRouter = () => {
|
||||
isCRMMigrationEnabled,
|
||||
isServerlessFunctionSettingsEnabled,
|
||||
isSSOEnabled,
|
||||
isAdminPageEnabled,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -242,11 +242,26 @@ const SettingsSecuritySSOIdentifyProvider = lazy(() =>
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsAdmin = lazy(() =>
|
||||
import('~/pages/settings/admin-panel/SettingsAdmin').then((module) => ({
|
||||
default: module.SettingsAdmin,
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsAdminFeatureFlags = lazy(() =>
|
||||
import('~/pages/settings/admin-panel/SettingsAdminFeatureFlags').then(
|
||||
(module) => ({
|
||||
default: module.SettingsAdminFeatureFlags,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
type SettingsRoutesProps = {
|
||||
isBillingEnabled?: boolean;
|
||||
isCRMMigrationEnabled?: boolean;
|
||||
isServerlessFunctionSettingsEnabled?: boolean;
|
||||
isSSOEnabled?: boolean;
|
||||
isAdminPageEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const SettingsRoutes = ({
|
||||
@ -254,6 +269,7 @@ export const SettingsRoutes = ({
|
||||
isCRMMigrationEnabled,
|
||||
isServerlessFunctionSettingsEnabled,
|
||||
isSSOEnabled,
|
||||
isAdminPageEnabled,
|
||||
}: SettingsRoutesProps) => (
|
||||
<Suspense fallback={<SettingsSkeletonLoader />}>
|
||||
<Routes>
|
||||
@ -375,6 +391,15 @@ export const SettingsRoutes = ({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{isAdminPageEnabled && (
|
||||
<>
|
||||
<Route path={SettingsPath.AdminPanel} element={<SettingsAdmin />} />
|
||||
<Route
|
||||
path={SettingsPath.FeatureFlags}
|
||||
element={<SettingsAdminFeatureFlags />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@ -14,7 +14,6 @@ import { Authorize } from '~/pages/auth/Authorize';
|
||||
import { Invite } from '~/pages/auth/Invite';
|
||||
import { PasswordReset } from '~/pages/auth/PasswordReset';
|
||||
import { SignInUp } from '~/pages/auth/SignInUp';
|
||||
import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect';
|
||||
import { NotFound } from '~/pages/not-found/NotFound';
|
||||
import { RecordIndexPage } from '~/pages/object-record/RecordIndexPage';
|
||||
import { RecordShowPage } from '~/pages/object-record/RecordShowPage';
|
||||
@ -30,6 +29,7 @@ export const useCreateAppRouter = (
|
||||
isCRMMigrationEnabled?: boolean,
|
||||
isServerlessFunctionSettingsEnabled?: boolean,
|
||||
isSSOEnabled?: boolean,
|
||||
isAdminPageEnabled?: boolean,
|
||||
) =>
|
||||
createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
@ -54,7 +54,6 @@ export const useCreateAppRouter = (
|
||||
element={<PaymentSuccess />}
|
||||
/>
|
||||
<Route path={indexAppPath.getIndexAppPath()} element={<></>} />
|
||||
<Route path={AppPath.Impersonate} element={<ImpersonateEffect />} />
|
||||
<Route path={AppPath.RecordIndexPage} element={<RecordIndexPage />} />
|
||||
<Route path={AppPath.RecordShowPage} element={<RecordShowPage />} />
|
||||
<Route
|
||||
@ -67,6 +66,7 @@ export const useCreateAppRouter = (
|
||||
isServerlessFunctionSettingsEnabled
|
||||
}
|
||||
isSSOEnabled={isSSOEnabled}
|
||||
isAdminPageEnabled={isAdminPageEnabled}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -69,6 +69,49 @@ export const useAuth = () => {
|
||||
|
||||
const setDateTimeFormat = useSetRecoilState(dateTimeFormatState);
|
||||
|
||||
const clearSession = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async () => {
|
||||
const emptySnapshot = snapshot_UNSTABLE();
|
||||
const iconsValue = snapshot.getLoadable(iconsState).getValue();
|
||||
const authProvidersValue = snapshot
|
||||
.getLoadable(authProvidersState)
|
||||
.getValue();
|
||||
const billing = snapshot.getLoadable(billingState).getValue();
|
||||
const isSignInPrefilled = snapshot
|
||||
.getLoadable(isSignInPrefilledState)
|
||||
.getValue();
|
||||
const supportChat = snapshot.getLoadable(supportChatState).getValue();
|
||||
const isDebugMode = snapshot.getLoadable(isDebugModeState).getValue();
|
||||
const captchaProvider = snapshot
|
||||
.getLoadable(captchaProviderState)
|
||||
.getValue();
|
||||
const clientConfigApiStatus = snapshot
|
||||
.getLoadable(clientConfigApiStatusState)
|
||||
.getValue();
|
||||
const isCurrentUserLoaded = snapshot
|
||||
.getLoadable(isCurrentUserLoadedState)
|
||||
.getValue();
|
||||
const initialSnapshot = emptySnapshot.map(({ set }) => {
|
||||
set(iconsState, iconsValue);
|
||||
set(authProvidersState, authProvidersValue);
|
||||
set(billingState, billing);
|
||||
set(isSignInPrefilledState, isSignInPrefilled);
|
||||
set(supportChatState, supportChat);
|
||||
set(isDebugModeState, isDebugMode);
|
||||
set(captchaProviderState, captchaProvider);
|
||||
set(clientConfigApiStatusState, clientConfigApiStatus);
|
||||
set(isCurrentUserLoadedState, isCurrentUserLoaded);
|
||||
return undefined;
|
||||
});
|
||||
goToRecoilSnapshot(initialSnapshot);
|
||||
await client.clearStore();
|
||||
sessionStorage.clear();
|
||||
localStorage.clear();
|
||||
},
|
||||
[client, goToRecoilSnapshot],
|
||||
);
|
||||
|
||||
const handleChallenge = useCallback(
|
||||
async (email: string, password: string, captchaToken?: string) => {
|
||||
const challengeResult = await challenge({
|
||||
@ -212,51 +255,9 @@ export const useAuth = () => {
|
||||
[handleChallenge, handleVerify, setIsVerifyPendingState],
|
||||
);
|
||||
|
||||
const handleSignOut = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async () => {
|
||||
const emptySnapshot = snapshot_UNSTABLE();
|
||||
const iconsValue = snapshot.getLoadable(iconsState).getValue();
|
||||
const authProvidersValue = snapshot
|
||||
.getLoadable(authProvidersState)
|
||||
.getValue();
|
||||
const billing = snapshot.getLoadable(billingState).getValue();
|
||||
const isSignInPrefilled = snapshot
|
||||
.getLoadable(isSignInPrefilledState)
|
||||
.getValue();
|
||||
const supportChat = snapshot.getLoadable(supportChatState).getValue();
|
||||
const isDebugMode = snapshot.getLoadable(isDebugModeState).getValue();
|
||||
const captchaProvider = snapshot
|
||||
.getLoadable(captchaProviderState)
|
||||
.getValue();
|
||||
const clientConfigApiStatus = snapshot
|
||||
.getLoadable(clientConfigApiStatusState)
|
||||
.getValue();
|
||||
const isCurrentUserLoaded = snapshot
|
||||
.getLoadable(isCurrentUserLoadedState)
|
||||
.getValue();
|
||||
|
||||
const initialSnapshot = emptySnapshot.map(({ set }) => {
|
||||
set(iconsState, iconsValue);
|
||||
set(authProvidersState, authProvidersValue);
|
||||
set(billingState, billing);
|
||||
set(isSignInPrefilledState, isSignInPrefilled);
|
||||
set(supportChatState, supportChat);
|
||||
set(isDebugModeState, isDebugMode);
|
||||
set(captchaProviderState, captchaProvider);
|
||||
set(clientConfigApiStatusState, clientConfigApiStatus);
|
||||
set(isCurrentUserLoadedState, isCurrentUserLoaded);
|
||||
return undefined;
|
||||
});
|
||||
|
||||
goToRecoilSnapshot(initialSnapshot);
|
||||
|
||||
await client.clearStore();
|
||||
sessionStorage.clear();
|
||||
localStorage.clear();
|
||||
},
|
||||
[client, goToRecoilSnapshot],
|
||||
);
|
||||
const handleSignOut = useCallback(async () => {
|
||||
await clearSession();
|
||||
}, [clearSession]);
|
||||
|
||||
const handleCredentialsSignUp = useCallback(
|
||||
async (
|
||||
@ -340,7 +341,7 @@ export const useAuth = () => {
|
||||
verify: handleVerify,
|
||||
|
||||
checkUserExists: { checkUserExistsData, checkUserExistsQuery },
|
||||
|
||||
clearSession,
|
||||
signOut: handleSignOut,
|
||||
signUpWithCredentials: handleCredentialsSignUp,
|
||||
signInWithCredentials: handleCrendentialsSignIn,
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { Button, H2Title, IconUser, Section } from 'twenty-ui';
|
||||
|
||||
const StyledLinkContainer = styled.div`
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const StyledErrorSection = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.danger};
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const SettingsAdminImpersonateUsers = () => {
|
||||
const [userId, setUserId] = useState('');
|
||||
const { handleImpersonate, isLoading, error, canImpersonate } =
|
||||
useImpersonate();
|
||||
|
||||
if (!canImpersonate) {
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Impersonate"
|
||||
description="You don't have permission to impersonate other users. Please contact your administrator if you need this access."
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title title="Impersonate" description="Impersonate a user." />
|
||||
<StyledContainer>
|
||||
<StyledLinkContainer>
|
||||
<TextInput
|
||||
value={userId}
|
||||
onChange={setUserId}
|
||||
placeholder="Enter user ID or email address"
|
||||
fullWidth
|
||||
disabled={isLoading}
|
||||
dataTestId="impersonate-input"
|
||||
onInputEnter={() => handleImpersonate(userId)}
|
||||
/>
|
||||
</StyledLinkContainer>
|
||||
<Button
|
||||
Icon={IconUser}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
title={'Impersonate'}
|
||||
onClick={() => handleImpersonate(userId)}
|
||||
disabled={!userId.trim() || isLoading}
|
||||
dataTestId="impersonate-button"
|
||||
/>
|
||||
</StyledContainer>
|
||||
{error && <StyledErrorSection>{error}</StyledErrorSection>}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
export const SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID =
|
||||
'settings-admin-feature-flags-tab-id';
|
||||
@ -0,0 +1,15 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPDATE_WORKSPACE_FEATURE_FLAG = gql`
|
||||
mutation UpdateWorkspaceFeatureFlag(
|
||||
$workspaceId: String!
|
||||
$featureFlag: String!
|
||||
$value: Boolean!
|
||||
) {
|
||||
updateWorkspaceFeatureFlag(
|
||||
workspaceId: $workspaceId
|
||||
featureFlag: $featureFlag
|
||||
value: $value
|
||||
)
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,30 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const USER_LOOKUP_ADMIN_PANEL = gql`
|
||||
mutation UserLookupAdminPanel($userIdentifier: String!) {
|
||||
userLookupAdminPanel(userIdentifier: $userIdentifier) {
|
||||
user {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
workspaces {
|
||||
id
|
||||
name
|
||||
logo
|
||||
totalUsers
|
||||
users {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
featureFlags {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,91 @@
|
||||
import { UserLookup } from '@/settings/admin-panel/types/UserLookup';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import {
|
||||
useUpdateWorkspaceFeatureFlagMutation,
|
||||
useUserLookupAdminPanelMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export const useFeatureFlagsManagement = () => {
|
||||
const [userLookupResult, setUserLookupResult] = useState<UserLookup | null>(
|
||||
null,
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [userLookup] = useUserLookupAdminPanelMutation({
|
||||
onCompleted: (data) => {
|
||||
setIsLoading(false);
|
||||
if (isDefined(data?.userLookupAdminPanel)) {
|
||||
setUserLookupResult(data.userLookupAdminPanel);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
setIsLoading(false);
|
||||
setError(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
const [updateFeatureFlag] = useUpdateWorkspaceFeatureFlagMutation();
|
||||
|
||||
const handleUserLookup = async (userIdentifier: string) => {
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
setUserLookupResult(null);
|
||||
|
||||
const response = await userLookup({
|
||||
variables: { userIdentifier },
|
||||
});
|
||||
|
||||
return response.data?.userLookupAdminPanel;
|
||||
};
|
||||
|
||||
const handleFeatureFlagUpdate = async (
|
||||
workspaceId: string,
|
||||
featureFlag: string,
|
||||
value: boolean,
|
||||
) => {
|
||||
setError(null);
|
||||
const previousState = userLookupResult;
|
||||
|
||||
if (isDefined(userLookupResult)) {
|
||||
setUserLookupResult({
|
||||
...userLookupResult,
|
||||
workspaces: userLookupResult.workspaces.map((workspace) =>
|
||||
workspace.id === workspaceId
|
||||
? {
|
||||
...workspace,
|
||||
featureFlags: workspace.featureFlags.map((flag) =>
|
||||
flag.key === featureFlag ? { ...flag, value } : flag,
|
||||
),
|
||||
}
|
||||
: workspace,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const response = await updateFeatureFlag({
|
||||
variables: {
|
||||
workspaceId,
|
||||
featureFlag,
|
||||
value,
|
||||
},
|
||||
onError: (error) => {
|
||||
if (isDefined(previousState)) {
|
||||
setUserLookupResult(previousState);
|
||||
}
|
||||
setError(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
return !!response.data;
|
||||
};
|
||||
|
||||
return {
|
||||
userLookupResult,
|
||||
handleUserLookup,
|
||||
handleFeatureFlagUpdate,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,60 @@
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useImpersonateMutation } from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
export const useImpersonate = () => {
|
||||
const { clearSession } = useAuth();
|
||||
const [currentUser, setCurrentUser] = useRecoilState(currentUserState);
|
||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||
const [impersonate] = useImpersonateMutation();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleImpersonate = async (userId: string) => {
|
||||
if (!userId.trim()) {
|
||||
setError('Please enter a user ID');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const impersonateResult = await impersonate({
|
||||
variables: { userId },
|
||||
});
|
||||
|
||||
if (isDefined(impersonateResult.errors)) {
|
||||
throw impersonateResult.errors;
|
||||
}
|
||||
|
||||
if (!impersonateResult.data?.impersonate) {
|
||||
throw new Error('No impersonate result');
|
||||
}
|
||||
|
||||
const { user, tokens } = impersonateResult.data.impersonate;
|
||||
await clearSession();
|
||||
setCurrentUser(user);
|
||||
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;
|
||||
} catch (error) {
|
||||
setError('Failed to impersonate user. Please try again.');
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleImpersonate,
|
||||
isLoading,
|
||||
error,
|
||||
canImpersonate: currentUser?.canImpersonate,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,4 @@
|
||||
export type FeatureFlag = {
|
||||
key: string;
|
||||
value: boolean;
|
||||
};
|
||||
@ -0,0 +1,11 @@
|
||||
import { WorkspaceInfo } from '@/settings/admin-panel/types/WorkspaceInfo';
|
||||
|
||||
export type UserLookup = {
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
};
|
||||
workspaces: WorkspaceInfo[];
|
||||
};
|
||||
@ -0,0 +1,15 @@
|
||||
import { FeatureFlag } from '@/settings/admin-panel/types/FeatureFlag';
|
||||
|
||||
export type WorkspaceInfo = {
|
||||
id: string;
|
||||
name: string;
|
||||
logo?: string | null;
|
||||
totalUsers: number;
|
||||
users: {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
}[];
|
||||
featureFlags: FeatureFlag[];
|
||||
};
|
||||
@ -13,6 +13,7 @@ import {
|
||||
IconKey,
|
||||
IconMail,
|
||||
IconRocket,
|
||||
IconServer,
|
||||
IconSettings,
|
||||
IconTool,
|
||||
IconUserCircle,
|
||||
@ -21,6 +22,7 @@ import {
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { SettingsNavigationDrawerItem } from '@/settings/components/SettingsNavigationDrawerItem';
|
||||
import { useExpandedAnimation } from '@/settings/hooks/useExpandedAnimation';
|
||||
@ -84,6 +86,8 @@ export const SettingsNavigationDrawerItems = () => {
|
||||
const isBillingPageEnabled =
|
||||
billing?.isBillingEnabled && !isFreeAccessEnabled;
|
||||
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
const isAdminPageEnabled = currentUser?.canImpersonate;
|
||||
// TODO: Refactor this part to only have arrays of navigation items
|
||||
const currentPathName = useLocation().pathname;
|
||||
|
||||
@ -230,6 +234,13 @@ export const SettingsNavigationDrawerItems = () => {
|
||||
</AnimatePresence>
|
||||
<NavigationDrawerSection>
|
||||
<NavigationDrawerSectionTitle label="Other" />
|
||||
{isAdminPageEnabled && (
|
||||
<SettingsNavigationDrawerItem
|
||||
label="Server Admin Panel"
|
||||
path={SettingsPath.AdminPanel}
|
||||
Icon={IconServer}
|
||||
/>
|
||||
)}
|
||||
<SettingsNavigationDrawerItem
|
||||
label="Releases"
|
||||
path={SettingsPath.Releases}
|
||||
|
||||
@ -26,9 +26,6 @@ export enum AppPath {
|
||||
Developers = `developers`,
|
||||
DevelopersCatchAll = `/${Developers}/*`,
|
||||
|
||||
// Impersonate
|
||||
Impersonate = '/impersonate/:userId',
|
||||
|
||||
Authorize = '/authorize',
|
||||
|
||||
// 404 page not found
|
||||
|
||||
@ -35,4 +35,6 @@ export enum SettingsPath {
|
||||
DevelopersNewWebhook = 'webhooks/new',
|
||||
DevelopersNewWebhookDetail = 'webhooks/:webhookId',
|
||||
Releases = 'releases',
|
||||
AdminPanel = 'admin-panel',
|
||||
FeatureFlags = 'admin-panel/feature-flags',
|
||||
}
|
||||
|
||||
@ -243,17 +243,6 @@ const testCases = [
|
||||
{ loc: AppPath.DevelopersCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
|
||||
{ loc: AppPath.DevelopersCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
|
||||
|
||||
{ loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
|
||||
{ loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
|
||||
{ loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
|
||||
{ loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false },
|
||||
{ loc: AppPath.Impersonate, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true },
|
||||
{ loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true },
|
||||
{ loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true },
|
||||
{ loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true },
|
||||
{ loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true },
|
||||
{ loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false },
|
||||
|
||||
{ loc: AppPath.Authorize, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true },
|
||||
{ loc: AppPath.Authorize, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false },
|
||||
{ loc: AppPath.Authorize, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false },
|
||||
|
||||
@ -15,6 +15,7 @@ type TabProps = {
|
||||
disabled?: boolean;
|
||||
pill?: string | ReactElement;
|
||||
to?: string;
|
||||
logo?: string;
|
||||
};
|
||||
|
||||
const StyledTab = styled('button', {
|
||||
@ -61,6 +62,10 @@ const StyledHover = styled.span`
|
||||
background: ${({ theme }) => theme.background.quaternary};
|
||||
}
|
||||
`;
|
||||
const StyledLogo = styled.img`
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
`;
|
||||
|
||||
export const Tab = ({
|
||||
id,
|
||||
@ -72,6 +77,7 @@ export const Tab = ({
|
||||
disabled,
|
||||
pill,
|
||||
to,
|
||||
logo,
|
||||
}: TabProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
@ -85,6 +91,7 @@ export const Tab = ({
|
||||
to={to}
|
||||
>
|
||||
<StyledHover>
|
||||
{logo && <StyledLogo src={logo} alt={`${title} logo`} />}
|
||||
{Icon && <Icon size={theme.icon.size.md} />}
|
||||
{title}
|
||||
{pill && typeof pill === 'string' ? <Pill label={pill} /> : pill}
|
||||
|
||||
@ -19,6 +19,7 @@ export type SingleTabProps = {
|
||||
disabled?: boolean;
|
||||
pill?: string | React.ReactElement;
|
||||
cards?: LayoutCard[];
|
||||
logo?: string;
|
||||
};
|
||||
|
||||
type TabListProps = {
|
||||
@ -71,6 +72,7 @@ export const TabList = ({
|
||||
key={tab.id}
|
||||
title={tab.title}
|
||||
Icon={tab.Icon}
|
||||
logo={tab.logo}
|
||||
active={tab.id === activeTabId}
|
||||
disabled={tab.disabled ?? loading}
|
||||
pill={tab.pill}
|
||||
|
||||
@ -23,7 +23,7 @@ export const useWorkspaceSwitching = () => {
|
||||
availableSSOIdentityProvidersState,
|
||||
);
|
||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||
const { signOut } = useAuth();
|
||||
const { clearSession } = useAuth();
|
||||
|
||||
const switchWorkspace = async (workspaceId: string) => {
|
||||
if (currentWorkspace?.id === workspaceId) return;
|
||||
@ -50,7 +50,7 @@ export const useWorkspaceSwitching = () => {
|
||||
}
|
||||
|
||||
if (jwt.data.generateJWT.availableSSOIDPs.length > 1) {
|
||||
await signOut();
|
||||
await clearSession();
|
||||
setAvailableWorkspacesForSSOState(
|
||||
jwt.data.generateJWT.availableSSOIDPs,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user