diff --git a/packages/manager/.changeset/pr-10430-tech-stories-1714594879053.md b/packages/manager/.changeset/pr-10430-tech-stories-1714594879053.md new file mode 100644 index 00000000000..564952e1256 --- /dev/null +++ b/packages/manager/.changeset/pr-10430-tech-stories-1714594879053.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Clean up Main Content Banner ([#10430](https://github.com/linode/manager/pull/10430)) diff --git a/packages/manager/src/MainContent.test.ts b/packages/manager/src/MainContent.test.ts deleted file mode 100644 index 676fc7a38d6..00000000000 --- a/packages/manager/src/MainContent.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - checkFlagsForMainContentBanner, - checkPreferencesForBannerDismissal, -} from './MainContent'; - -const mainContentBanner = { - key: 'Test Text Key', - link: { - text: 'Test anchor text', - url: 'https://linode.com', - }, - text: 'Test Text', -}; - -describe('checkFlagsForMainContentBanner', () => { - it('returns `true` if a valid banner is present in the flag set', () => { - expect(checkFlagsForMainContentBanner({ mainContentBanner })).toBe(true); - expect(checkFlagsForMainContentBanner({})).toBe(false); - expect( - checkFlagsForMainContentBanner({ mainContentBanner: {} as any }) - ).toBe(false); - }); -}); - -describe('checkPreferencesForBannerDismissal', () => { - it('returns `true if the specified key is preset in preferences banner dismissals', () => { - expect( - checkPreferencesForBannerDismissal( - { - main_content_banner_dismissal: { key1: true }, - }, - 'key1' - ) - ).toBe(true); - expect( - checkPreferencesForBannerDismissal( - { - main_content_banner_dismissal: { key1: true }, - }, - 'another-key' - ) - ).toBe(false); - expect(checkPreferencesForBannerDismissal({}, 'key1')).toBe(false); - }); -}); diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index c42231b0688..1c997953f47 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -1,6 +1,5 @@ import { Theme } from '@mui/material/styles'; import Grid from '@mui/material/Unstable_Grid2'; -import { isEmpty } from 'ramda'; import * as React from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; import { makeStyles } from 'tss-react/mui'; @@ -25,13 +24,11 @@ import { useAccountManagement } from 'src/hooks/useAccountManagement'; import { useFlags } from 'src/hooks/useFlags'; import { useDatabaseEnginesQuery } from 'src/queries/databases'; import { useMutatePreferences, usePreferences } from 'src/queries/preferences'; -import { ManagerPreferences } from 'src/types/ManagerPreferences'; 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 { useIsPlacementGroupsEnabled } from './features/PlacementGroups/utils'; import { useGlobalErrors } from './hooks/useGlobalErrors'; @@ -210,8 +207,6 @@ export const MainContent = () => { const username = profile?.username || ''; - const [bannerDismissed, setBannerDismissed] = React.useState(false); - const checkRestrictedUser = !Boolean(flags.databases) && !!accountError; const { error: enginesError, @@ -231,14 +226,6 @@ export const MainContent = () => { const defaultRoot = _isManagedAccount ? '/managed' : '/linodes'; - const shouldDisplayMainContentBanner = - !bannerDismissed && - checkFlagsForMainContentBanner(flags) && - !checkPreferencesForBannerDismissal( - preferences ?? {}, - flags?.mainContentBanner?.key - ); - /** * this is the case where the user has successfully completed signup * but needs a manual review from Customer Support. In this case, @@ -292,108 +279,92 @@ export const MainContent = () => { }); }; - /** - * otherwise just show the rest of the app. - */ return (
- <> - {shouldDisplayMainContentBanner ? ( - setBannerDismissed(true)} - url={flags.mainContentBanner?.link?.url ?? ''} - /> - ) : null} - toggleMenu(false)} - collapse={desktopMenuIsOpen || false} - open={menuIsOpen} + toggleMenu(false)} + collapse={desktopMenuIsOpen || false} + open={menuIsOpen} + /> +
+ + toggleMenu(true)} + username={username} /> -
- toggleMenu(true)} - username={username} - /> -
- - - - }> - - - {isPlacementGroupsEnabled && ( - - )} - - - {isACLBEnabled && ( - - )} + + + + }> + + + {isPlacementGroupsEnabled && ( - - - - + )} + + + {isACLBEnabled && ( - - - - - - - - - {showDatabases && ( - - )} - {flags.selfServeBetas && ( - - )} - - - {/** We don't want to break any bookmarks. This can probably be removed eventually. */} - - - - - + )} + + + + + + + + + + + + + + + {showDatabases && ( + + )} + {flags.selfServeBetas && ( + + )} + + + {/** We don't want to break any bookmarks. This can probably be removed eventually. */} + + + + -
-
- + + +
@@ -401,21 +372,3 @@ export const MainContent = () => {
); }; - -// ============================================================================= -// Utilities -// ============================================================================= -export const checkFlagsForMainContentBanner = (flags: FlagSet) => { - return Boolean( - flags.mainContentBanner && - !isEmpty(flags.mainContentBanner) && - flags.mainContentBanner.key - ); -}; - -export const checkPreferencesForBannerDismissal = ( - preferences: ManagerPreferences, - key = 'defaultKey' -) => { - return Boolean(preferences?.main_content_banner_dismissal?.[key]); -}; diff --git a/packages/manager/src/components/MainContentBanner.test.tsx b/packages/manager/src/components/MainContentBanner.test.tsx new file mode 100644 index 00000000000..bd068c15b1b --- /dev/null +++ b/packages/manager/src/components/MainContentBanner.test.tsx @@ -0,0 +1,82 @@ +import { waitFor } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import React from 'react'; + +import { HttpResponse, http, server } from 'src/mocks/testServer'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { MainContentBanner } from './MainContentBanner'; + +import type { ManagerPreferences } from 'src/types/ManagerPreferences'; + +describe('MainContentBanner', () => { + const mainContentBanner = { + key: 'test-banner-1', + link: { + text: 'Learn more.', + url: 'https://akamai.com', + }, + text: 'Linode is now Akamai 🤯', + }; + + it('should render a banner from feature flags', () => { + const { getByText } = renderWithTheme(, { + flags: { mainContentBanner }, + }); + + expect(getByText('Linode is now Akamai 🤯')).toBeVisible(); + }); + + it('should render a link from feature flags', () => { + const { getByText } = renderWithTheme(, { + flags: { mainContentBanner }, + }); + + const link = getByText('Learn more.'); + + expect(link).toBeVisible(); + expect(link).toBeEnabled(); + expect(link).toHaveRole('link'); + expect(link).toHaveAttribute('href', 'https://akamai.com'); + }); + + it('should be dismissable', async () => { + const { container, getByLabelText } = renderWithTheme( + , + { + flags: { mainContentBanner }, + } + ); + + const closeButton = getByLabelText('Close'); + + expect(closeButton).toBeVisible(); + expect(closeButton).toBeEnabled(); + + await userEvent.click(closeButton); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should not render if the user dismissed the banner', async () => { + const preferences: ManagerPreferences = { + main_content_banner_dismissal: { + 'test-banner-1': true, + }, + }; + + server.use( + http.get('*/v4/profile/preferences', () => { + return HttpResponse.json(preferences); + }) + ); + + const { container } = renderWithTheme(, { + flags: { mainContentBanner }, + }); + + await waitFor(() => { + expect(container).toBeEmptyDOMElement(); + }); + }); +}); diff --git a/packages/manager/src/components/MainContentBanner.tsx b/packages/manager/src/components/MainContentBanner.tsx index 62cd07364b5..e639ce257b6 100644 --- a/packages/manager/src/components/MainContentBanner.tsx +++ b/packages/manager/src/components/MainContentBanner.tsx @@ -1,97 +1,90 @@ import Close from '@mui/icons-material/Close'; -import Grid from '@mui/material/Unstable_Grid2'; -import { Theme } from '@mui/material/styles'; -import { makeStyles } from 'tss-react/mui'; +import { IconButton } from '@mui/material'; import * as React from 'react'; import { Link } from 'src/components/Link'; import { Typography } from 'src/components/Typography'; +import { useFlags } from 'src/hooks/useFlags'; import { useMutatePreferences, usePreferences } from 'src/queries/preferences'; -const useStyles = makeStyles()((theme: Theme) => ({ - bannerOuter: { - alignItems: 'center', - backgroundColor: theme.bg.mainContentBanner, - display: 'flex', - justifyContent: 'center', - padding: theme.spacing(2), - position: 'sticky', - top: 0, - zIndex: 1110, - }, - closeIcon: { - backgroundColor: 'transparent', - border: 'none', - color: theme.palette.text.primary, - cursor: 'pointer', - position: 'absolute', - right: 4, - }, - header: { - color: '#fff', - textAlign: 'center', - [theme.breakpoints.down('md')]: { - width: '90%', - }, - [theme.breakpoints.only('xs')]: { - paddingRight: 30, - }, - width: '65%', - }, - link: { - color: '#74AAE6', - }, -})); +import { Box } from './Box'; -interface Props { - bannerKey: string; - bannerText: string; - linkText: string; - onClose: () => void; - url: string; -} +export const MainContentBanner = React.memo(() => { + // Uncomment this to test this banner: + // + // const flags = { + // mainContentBanner: { + // key: 'banner-test-1', + // link: { + // text: 'Learn more.', + // url: 'https://akamai.com', + // }, + // text: + // 'Linode is now Akamai. This is longer test for testing the pull request. Hopefully it looks nice on all viewports.', + // }, + // }; -export const MainContentBanner = React.memo((props: Props) => { - const { bannerKey, bannerText, linkText, onClose, url } = props; + const flags = useFlags(); - const { refetch: refetchPrefrences } = usePreferences(); + const { data: preferences } = usePreferences(); const { mutateAsync: updatePreferences } = useMutatePreferences(); - const { classes } = useStyles(); + const handleDismiss = (key: string) => { + const existingMainContentBannerDismissal = + preferences?.main_content_banner_dismissal ?? {}; - const dismiss = () => { - onClose(); - refetchPrefrences() - .then(({ data: preferences }) => preferences ?? Promise.reject()) - .then((preferences) => { - return updatePreferences({ - main_content_banner_dismissal: { - ...preferences.main_content_banner_dismissal, - [bannerKey]: true, - }, - }); - }) - // It's OK if this fails (the banner is still dismissed in the UI due to local state). - .catch(); + updatePreferences({ + main_content_banner_dismissal: { + ...existingMainContentBannerDismissal, + [key]: true, + }, + }); }; + const hasDismissedBanner = + flags.mainContentBanner?.key !== undefined && + preferences?.main_content_banner_dismissal?.[flags.mainContentBanner.key]; + + if ( + !flags.mainContentBanner || + Object.keys(flags.mainContentBanner).length === 0 || + hasDismissedBanner + ) { + return null; + } + + const url = flags.mainContentBanner.link.url; + const linkText = flags.mainContentBanner.link.text; + const text = flags.mainContentBanner.text; + const key = flags.mainContentBanner.key; + return ( - - - {bannerText}  - {linkText && url && ( - - {linkText} - - )} - - - + + ); });