Skip to content

Commit

Permalink
upcoming: [M3-7716] - Add session expiry confirmation dialog for prox…
Browse files Browse the repository at this point in the history
…y users (#10152)


Co-authored-by: Jaalah Ramos <jaalah.ramos@gmail.com>
Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com>
  • Loading branch information
3 people authored Feb 13, 2024
1 parent 66fe7fa commit 22eb025
Show file tree
Hide file tree
Showing 16 changed files with 434 additions and 120 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add session expiry confirmation dialog for proxy to parent user account switching ([#10152](https://github.com/linode/manager/pull/10152))
189 changes: 100 additions & 89 deletions packages/manager/src/MainContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { isFeatureEnabled } from 'src/utilities/accountCapabilities';

import { ENABLE_MAINTENANCE_MODE } from './constants';
import { complianceUpdateContext } from './context/complianceUpdateContext';
import { switchAccountSessionContext } from './context/switchAccountSessionContext';
import { FlagSet } from './featureFlags';
import { useIsACLBEnabled } from './features/LoadBalancers/utils';
import { useGlobalErrors } from './hooks/useGlobalErrors';
Expand Down Expand Up @@ -192,6 +193,11 @@ export const MainContent = () => {
const ComplianceUpdateProvider = complianceUpdateContext.Provider;
const complianceUpdateContextValue = useDialogContext();

const SwitchAccountSessionProvider = switchAccountSessionContext.Provider;
const switchAccountSessionContextValue = useDialogContext({
isOpen: false,
});

const [menuIsOpen, toggleMenu] = React.useState<boolean>(false);
const {
_isManagedAccount,
Expand Down Expand Up @@ -294,100 +300,105 @@ export const MainContent = () => {
*/
return (
<div className={classes.appFrame}>
<ComplianceUpdateProvider value={complianceUpdateContextValue}>
<NotificationProvider value={contextValue}>
<>
{shouldDisplayMainContentBanner ? (
<MainContentBanner
bannerKey={flags.mainContentBanner?.key ?? ''}
bannerText={flags.mainContentBanner?.text ?? ''}
linkText={flags.mainContentBanner?.link?.text ?? ''}
onClose={() => setBannerDismissed(true)}
url={flags.mainContentBanner?.link?.url ?? ''}
/>
) : null}
<SideMenu
closeMenu={() => toggleMenu(false)}
collapse={desktopMenuIsOpen || false}
open={menuIsOpen}
/>
<div
className={cx(classes.content, {
[classes.fullWidthContent]:
desktopMenuIsOpen ||
(desktopMenuIsOpen && desktopMenuIsOpen === true),
})}
>
<TopMenu
desktopMenuToggle={desktopMenuToggle}
isSideMenuOpen={!desktopMenuIsOpen}
openSideMenu={() => toggleMenu(true)}
username={username}
<SwitchAccountSessionProvider value={switchAccountSessionContextValue}>
<ComplianceUpdateProvider value={complianceUpdateContextValue}>
<NotificationProvider value={contextValue}>
<>
{shouldDisplayMainContentBanner ? (
<MainContentBanner
bannerKey={flags.mainContentBanner?.key ?? ''}
bannerText={flags.mainContentBanner?.text ?? ''}
linkText={flags.mainContentBanner?.link?.text ?? ''}
onClose={() => setBannerDismissed(true)}
url={flags.mainContentBanner?.link?.url ?? ''}
/>
) : null}
<SideMenu
closeMenu={() => toggleMenu(false)}
collapse={desktopMenuIsOpen || false}
open={menuIsOpen}
/>
<main
className={classes.cmrWrapper}
id="main-content"
role="main"
<div
className={cx(classes.content, {
[classes.fullWidthContent]:
desktopMenuIsOpen ||
(desktopMenuIsOpen && desktopMenuIsOpen === true),
})}
>
<Grid className={classes.grid} container spacing={0}>
<Grid className={cx(classes.switchWrapper, 'p0')}>
<GlobalNotifications />
<React.Suspense fallback={<SuspenseLoader />}>
<Switch>
<Route component={LinodesRoutes} path="/linodes" />
<Route
component={PlacementGroups}
path="/placement-groups"
/>
<Route component={Volumes} path="/volumes" />
<Redirect path="/volumes*" to="/volumes" />
{isACLBEnabled && (
<TopMenu
desktopMenuToggle={desktopMenuToggle}
isSideMenuOpen={!desktopMenuIsOpen}
openSideMenu={() => toggleMenu(true)}
username={username}
/>
<main
className={classes.cmrWrapper}
id="main-content"
role="main"
>
<Grid className={classes.grid} container spacing={0}>
<Grid className={cx(classes.switchWrapper, 'p0')}>
<GlobalNotifications />
<React.Suspense fallback={<SuspenseLoader />}>
<Switch>
<Route component={LinodesRoutes} path="/linodes" />
<Route
component={PlacementGroups}
path="/placement-groups"
/>
<Route component={Volumes} path="/volumes" />
<Redirect path="/volumes*" to="/volumes" />
{isACLBEnabled && (
<Route
component={LoadBalancers}
path="/loadbalancer*"
/>
)}
<Route
component={NodeBalancers}
path="/nodebalancers"
/>
<Route component={Domains} path="/domains" />
<Route component={Managed} path="/managed" />
<Route component={Longview} path="/longview" />
<Route component={Images} path="/images" />
<Route
component={StackScripts}
path="/stackscripts"
/>
<Route
component={LoadBalancers}
path="/loadbalancer*"
component={ObjectStorage}
path="/object-storage"
/>
)}
<Route
component={NodeBalancers}
path="/nodebalancers"
/>
<Route component={Domains} path="/domains" />
<Route component={Managed} path="/managed" />
<Route component={Longview} path="/longview" />
<Route component={Images} path="/images" />
<Route component={StackScripts} path="/stackscripts" />
<Route
component={ObjectStorage}
path="/object-storage"
/>
<Route component={Kubernetes} path="/kubernetes" />
<Route component={Account} path="/account" />
<Route component={Profile} path="/profile" />
<Route component={Help} path="/support" />
<Route component={SearchLanding} path="/search" />
<Route component={EventsLanding} path="/events" />
<Route component={Firewalls} path="/firewalls" />
{showDatabases && (
<Route component={Databases} path="/databases" />
)}
{flags.selfServeBetas && (
<Route component={BetaRoutes} path="/betas" />
)}
{showVPCs && <Route component={VPC} path="/vpcs" />}
<Redirect exact from="/" to={defaultRoot} />
{/** We don't want to break any bookmarks. This can probably be removed eventually. */}
<Redirect from="/dashboard" to={defaultRoot} />
<Route component={NotFound} />
</Switch>
</React.Suspense>
<Route component={Kubernetes} path="/kubernetes" />
<Route component={Account} path="/account" />
<Route component={Profile} path="/profile" />
<Route component={Help} path="/support" />
<Route component={SearchLanding} path="/search" />
<Route component={EventsLanding} path="/events" />
<Route component={Firewalls} path="/firewalls" />
{showDatabases && (
<Route component={Databases} path="/databases" />
)}
{flags.selfServeBetas && (
<Route component={BetaRoutes} path="/betas" />
)}
{showVPCs && <Route component={VPC} path="/vpcs" />}
<Redirect exact from="/" to={defaultRoot} />
{/** We don't want to break any bookmarks. This can probably be removed eventually. */}
<Redirect from="/dashboard" to={defaultRoot} />
<Route component={NotFound} />
</Switch>
</React.Suspense>
</Grid>
</Grid>
</Grid>
</main>
</div>
</>
</NotificationProvider>
<Footer desktopMenuIsOpen={desktopMenuIsOpen} />
</ComplianceUpdateProvider>
</main>
</div>
</>
</NotificationProvider>
<Footer desktopMenuIsOpen={desktopMenuIsOpen} />
</ComplianceUpdateProvider>
</SwitchAccountSessionProvider>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,6 @@ export const LandingHeader = ({

const Actions = styled('div')(() => ({
display: 'flex',
gap: '24px',
justifyContent: 'flex-end',
}));
7 changes: 7 additions & 0 deletions packages/manager/src/context/switchAccountSessionContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as React from 'react';

import { DialogContextProps, defaultContext } from './useDialogContext';

export const switchAccountSessionContext = React.createContext<DialogContextProps>(
defaultContext
);
41 changes: 33 additions & 8 deletions packages/manager/src/context/useDialogContext.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,51 @@
import { createContext, useCallback, useState } from 'react';
import { useCallback, useState } from 'react';

export interface DialogContextProps {
[key: string]:
| (() => void)
| ((newState: UseDialogContextOptions) => void)
| boolean;
close: () => void;
isOpen: boolean;
open: () => void;
updateState: (newState: UseDialogContextOptions) => void;
}

export const defaultContext = {
export type UseDialogContextOptions = {
[key: string]: boolean;
};

export const defaultContext: DialogContextProps = {
close: () => void 0,
isOpen: false,
open: () => void 0,
updateState: () => void 0,
};

export const dialogContext = createContext<DialogContextProps>(defaultContext);
export const useDialogContext = (
initialState: UseDialogContextOptions = {}
): DialogContextProps => {
const [state, setState] = useState({ ...defaultContext, ...initialState });

// TODO: We no longer need the open and close functions after we update other references
const open = useCallback(
() => setState((prevState) => ({ ...prevState, isOpen: true })),
[]
);
const close = useCallback(
() => setState((prevState) => ({ ...prevState, isOpen: false })),
[]
);
const updateState = useCallback(
(newState: UseDialogContextOptions) =>
setState((prevState) => ({ ...prevState, ...newState })),
[]
);

export const useDialogContext = (): DialogContextProps => {
const [isOpen, setIsOpen] = useState(false);
const open = useCallback(() => setIsOpen(true), [setIsOpen]);
const close = useCallback(() => setIsOpen(false), [setIsOpen]);
return {
...state,
close,
isOpen,
open,
updateState,
};
};
17 changes: 16 additions & 1 deletion packages/manager/src/features/Account/AccountLanding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel';
import { TabLinkList } from 'src/components/Tabs/TabLinkList';
import { TabPanels } from 'src/components/Tabs/TabPanels';
import { Tabs } from 'src/components/Tabs/Tabs';
import { switchAccountSessionContext } from 'src/context/switchAccountSessionContext';
import { useParentTokenManagement } from 'src/features/Account/SwitchAccounts/useParentTokenManagement';
import { useFlags } from 'src/hooks/useFlags';
import { useAccount } from 'src/queries/account';
import { useGrants, useProfile } from 'src/queries/profile';
Expand Down Expand Up @@ -50,13 +52,16 @@ const AccountLanding = () => {

const flags = useFlags();
const [isDrawerOpen, setIsDrawerOpen] = React.useState<boolean>(false);
const sessionContext = React.useContext(switchAccountSessionContext);

const accountAccessGrant = grants?.global?.account_access;
const readOnlyAccountAccess = accountAccessGrant === 'read_only';
const isAkamaiAccount = account?.billing_source === 'akamai';
const isProxyUser = profile?.user_type === 'proxy';
const isParentUser = profile?.user_type === 'parent';

const { isParentTokenExpired } = useParentTokenManagement({ isProxyUser });

const tabs = [
{
routeName: '/account/billing',
Expand Down Expand Up @@ -90,6 +95,16 @@ const AccountLanding = () => {
'/account/billing/edit',
];

const handleAccountSwitch = () => {
if (isParentTokenExpired) {
return sessionContext.updateState({
isOpen: true,
});
}

setIsDrawerOpen(true);
};

const getDefaultTabIndex = () => {
const tabChoice = tabs.findIndex((tab) =>
Boolean(matchPath(tab.routeName, { path: location.pathname }))
Expand Down Expand Up @@ -137,7 +152,7 @@ const AccountLanding = () => {
}
landingHeaderProps.disabledCreateButton = readOnlyAccountAccess;
landingHeaderProps.extraActions = canSwitchBetweenParentOrProxyAccount ? (
<SwitchAccountButton onClick={() => setIsDrawerOpen(true)} />
<SwitchAccountButton onClick={handleAccountSwitch} />
) : undefined;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/manager/src/features/Account/SwitchAccountDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { StyledLinkButton } from 'src/components/Button/StyledLinkButton';
import { Drawer } from 'src/components/Drawer';
import { Notice } from 'src/components/Notice/Notice';
import { Typography } from 'src/components/Typography';
import { PARENT_SESSION_EXPIRED } from 'src/features/Account/constants';
import {
isParentTokenValid,
setTokenInLocalStorage,
Expand Down Expand Up @@ -153,11 +154,10 @@ export const SwitchAccountDrawer = (props: Props) => {
);

const handleSwitchToParentAccount = React.useCallback(() => {
if (!isParentTokenValid({ isProxyUser })) {
if (!isParentTokenValid()) {
const expiredTokenError: APIError = {
field: 'token',
reason:
'The reseller account token has expired. You must log back into the account manually.',
reason: PARENT_SESSION_EXPIRED,
};

setIsParentTokenError([expiredTokenError]);
Expand Down
Loading

0 comments on commit 22eb025

Please sign in to comment.