Add impersonate banner (#14734)
Related to https://github.com/twentyhq/twenty/pull/14360 <img width="1511" height="468" alt="Screenshot 2025-09-26 at 12 16 46" src="https://github.com/user-attachments/assets/33e0e0b5-1226-455d-90f2-e4fc3deef7a8" /> @Bonapara, let you tell me if wording and banner color are ok. This banner is displayed when a user is impersonating an other user
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { selector } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { tokenPairState } from './tokenPairState';
|
||||
|
||||
export const isImpersonatingState = selector<boolean>({
|
||||
key: 'isImpersonatingState',
|
||||
get: ({ get }) => {
|
||||
const tokenPair = get(tokenPairState);
|
||||
|
||||
if (!isDefined(tokenPair?.accessOrWorkspaceAgnosticToken?.token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const decodedToken = jwtDecode<{ isImpersonating: boolean }>(
|
||||
tokenPair.accessOrWorkspaceAgnosticToken.token,
|
||||
);
|
||||
return decodedToken?.isImpersonating ?? false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
+5
@@ -1,13 +1,16 @@
|
||||
import { isImpersonatingState } from '@/auth/states/isImpersonatingState';
|
||||
import { InformationBannerBillingSubscriptionPaused } from '@/information-banner/components/billing/InformationBannerBillingSubscriptionPaused';
|
||||
import { InformationBannerEndTrialPeriod } from '@/information-banner/components/billing/InformationBannerEndTrialPeriod';
|
||||
import { InformationBannerFailPaymentInfo } from '@/information-banner/components/billing/InformationBannerFailPaymentInfo';
|
||||
import { InformationBannerNoBillingSubscription } from '@/information-banner/components/billing/InformationBannerNoBillingSubscription';
|
||||
import { InformationBannerIsImpersonating } from '@/information-banner/components/impersonate/InformationBannerIsImpersonating';
|
||||
import { InformationBannerReconnectAccountEmailAliases } from '@/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases';
|
||||
import { InformationBannerReconnectAccountInsufficientPermissions } from '@/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions';
|
||||
import { useIsSomeMeteredProductCapReached } from '@/workspace/hooks/useIsSomeMeteredProductCapReached';
|
||||
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
||||
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { SubscriptionStatus } from '~/generated-metadata/graphql';
|
||||
@@ -27,6 +30,7 @@ export const InformationBannerWrapper = () => {
|
||||
WorkspaceActivationStatus.SUSPENDED,
|
||||
);
|
||||
const isSomeMeteredProductCapReached = useIsSomeMeteredProductCapReached();
|
||||
const isImpersonating = useRecoilValue(isImpersonatingState);
|
||||
|
||||
const displayBillingSubscriptionPausedBanner =
|
||||
isWorkspaceSuspended && subscriptionStatus === SubscriptionStatus.Paused;
|
||||
@@ -54,6 +58,7 @@ export const InformationBannerWrapper = () => {
|
||||
)}
|
||||
{displayFailPaymentInfoBanner && <InformationBannerFailPaymentInfo />}
|
||||
{displayEndTrialPeriodBanner && <InformationBannerEndTrialPeriod />}
|
||||
{isImpersonating && <InformationBannerIsImpersonating />}
|
||||
</StyledInformationBannerWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { InformationBanner } from '@/information-banner/components/InformationBanner';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconLogout } from 'twenty-ui/display';
|
||||
|
||||
export const InformationBannerIsImpersonating = () => {
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
const { signOut } = useAuth();
|
||||
|
||||
if (!isDefined(currentWorkspaceMember)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const impersonatedUser = `${currentWorkspaceMember.name.firstName} ${currentWorkspaceMember.name.lastName} (${currentWorkspaceMember.userEmail})`;
|
||||
|
||||
return (
|
||||
<InformationBanner
|
||||
message={t`Logged in as ${impersonatedUser}`}
|
||||
buttonTitle={t`Stop impersonating`}
|
||||
buttonIcon={IconLogout}
|
||||
buttonOnClick={signOut}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isImpersonatingState } from '@/auth/states/isImpersonatingState';
|
||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
@@ -5,6 +6,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconDotsVertical, IconSpy, IconTrash } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
@@ -24,7 +26,9 @@ export const ManageMembersDropdownMenu = ({
|
||||
onImpersonate,
|
||||
}: ManageMembersDropdownMenuProps) => {
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
const canImpersonate = useHasPermissionFlag(PermissionFlagType.IMPERSONATE);
|
||||
const isImpersonating = useRecoilValue(isImpersonatingState);
|
||||
const canImpersonate =
|
||||
useHasPermissionFlag(PermissionFlagType.IMPERSONATE) && !isImpersonating;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
|
||||
Reference in New Issue
Block a user