Skip to content

Commit

Permalink
Merge pull request RedHatInsights#2699 from Hyperkid123/jotai
Browse files Browse the repository at this point in the history
Use jotai for `activeModule` state managment
  • Loading branch information
Hyperkid123 authored Dec 6, 2023
2 parents 29ca371 + ed0c237 commit 7b15489
Show file tree
Hide file tree
Showing 24 changed files with 269 additions and 193 deletions.
260 changes: 152 additions & 108 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
"classnames": "^2.3.2",
"commander": "^10.0.0",
"history": "^5.3.0",
"jotai": "^2.5.1",
"js-cookie": "^3.0.1",
"js-yaml": "^4.1.0",
"localforage": "^1.10.0",
Expand Down
46 changes: 25 additions & 21 deletions src/bootstrap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { Provider, useSelector } from 'react-redux';
import { IntlProvider, ReactIntlErrorCode } from 'react-intl';
import { Provider as JotaiProvider } from 'jotai';

import { spinUpStore } from './redux/redux-config';
import RootApp from './components/RootApp';
Expand All @@ -11,6 +12,7 @@ import { ReduxState } from './redux/store';
import OIDCProvider from './auth/OIDCConnector/OIDCProvider';
import messages from './locales/data.json';
import ErrorBoundary from './components/ErrorComponents/ErrorBoundary';
import chromeStore from './state/chromeStore';

const isITLessEnv = ITLess();
const language: keyof typeof messages = 'en';
Expand Down Expand Up @@ -46,26 +48,28 @@ const entry = document.getElementById('chrome-entry');
if (entry) {
const reactRoot = createRoot(entry);
reactRoot.render(
<Provider store={spinUpStore()?.store}>
<AuthProvider>
<IntlProvider
locale={language}
messages={messages[language]}
onError={(error) => {
if (
(getEnv() === 'stage' && !window.location.origin.includes('foo')) ||
localStorage.getItem('chrome:intl:debug') === 'true' ||
!(error.code === ReactIntlErrorCode.MISSING_TRANSLATION)
) {
console.error(error);
}
}}
>
<ErrorBoundary>
<App />
</ErrorBoundary>
</IntlProvider>
</AuthProvider>
</Provider>
<JotaiProvider store={chromeStore}>
<Provider store={spinUpStore()?.store}>
<AuthProvider>
<IntlProvider
locale={language}
messages={messages[language]}
onError={(error) => {
if (
(getEnv() === 'stage' && !window.location.origin.includes('foo')) ||
localStorage.getItem('chrome:intl:debug') === 'true' ||
!(error.code === ReactIntlErrorCode.MISSING_TRANSLATION)
) {
console.error(error);
}
}}
>
<ErrorBoundary>
<App />
</ErrorBoundary>
</IntlProvider>
</AuthProvider>
</Provider>
</JotaiProvider>
);
}
9 changes: 6 additions & 3 deletions src/components/ChromeRoute/ChromeRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ScalprumComponent } from '@scalprum/react-core';
import React, { memo, useContext, useEffect } from 'react';
import LoadingFallback from '../../utils/loading-fallback';
import { batch, useDispatch, useSelector } from 'react-redux';
import { changeActiveModule, toggleGlobalFilter, updateDocumentTitle } from '../../redux/actions';
import { toggleGlobalFilter, updateDocumentTitle } from '../../redux/actions';
import ErrorComponent from '../ErrorComponents/DefaultErrorComponent';
import { getPendoConf } from '../../analytics';
import classNames from 'classnames';
Expand All @@ -12,6 +12,8 @@ import { ReduxState } from '../../redux/store';
import { DeepRequired } from 'utility-types';
import { ChromeUser } from '@redhat-cloud-services/types';
import ChromeAuthContext from '../../auth/ChromeAuthContext';
import { useAtom } from 'jotai';
import { activeModuleAtom } from '../../state/atoms';

export type ChromeRouteProps = {
scope: string;
Expand All @@ -29,9 +31,10 @@ const ChromeRoute = memo(
const { setActiveHelpTopicByName } = useContext(HelpTopicContext);
const { user } = useContext(ChromeAuthContext);
const gatewayError = useSelector(({ chrome: { gatewayError } }: ReduxState) => gatewayError);
const activeModule = useSelector(({ chrome: { activeModule } }: ReduxState) => activeModule);
const defaultTitle = useSelector(({ chrome: { modules } }: ReduxState) => modules?.[scope]?.defaultDocumentTitle || scope);

const [activeModule, setActiveModule] = useAtom(activeModuleAtom);

useEffect(() => {
batch(() => {
// Only trigger update on a first application render before any active module has been selected
Expand All @@ -42,7 +45,7 @@ const ChromeRoute = memo(
*/
dispatch(updateDocumentTitle(defaultTitle || 'Hybrid Cloud Console'));
}
dispatch(changeActiveModule(scope));
setActiveModule(scope);
});
/**
* update pendo metadata on application change
Expand Down
6 changes: 3 additions & 3 deletions src/components/ErrorComponents/DefaultErrorComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/ex
import { chunkLoadErrorRefreshKey } from '../../utils/common';
import { useIntl } from 'react-intl';
import messages from '../../locales/Messages';
import { useSelector } from 'react-redux';
import { ReduxState } from '../../redux/store';
import './ErrorComponent.scss';
import { get3scaleError } from '../../utils/responseInterceptors';
import GatewayErrorComponent from './GatewayErrorComponent';
import { getUrl } from '../../hooks/useBundle';
import { useAtomValue } from 'jotai';
import { activeModuleAtom } from '../../state/atoms';

export type DefaultErrorComponentProps = {
error?: any | Error;
Expand All @@ -30,7 +30,7 @@ const DefaultErrorComponent = (props: DefaultErrorComponentProps) => {
const intl = useIntl();
const [sentryId, setSentryId] = useState<string | undefined>();

const activeModule = useSelector(({ chrome: { activeModule } }: ReduxState) => activeModule);
const activeModule = useAtomValue(activeModuleAtom);
const exceptionMessage = (props.error as Error)?.message ? (props.error as Error).message : 'Unhandled UI runtime error';
useEffect(() => {
const sentryId =
Expand Down
6 changes: 5 additions & 1 deletion src/components/ErrorComponents/GatewayErrorComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { Text, TextContent } from '@patternfly/react-core/dist/dynamic/component
import { useIntl } from 'react-intl';
import Messages from '../../locales/Messages';
import { ThreeScaleError } from '../../utils/responseInterceptors';
import { useAtomValue } from 'jotai';
import { activeModuleAtom } from '../../state/atoms';

export type GatewayErrorComponentProps = {
error: ThreeScaleError;
Expand Down Expand Up @@ -48,8 +50,10 @@ const Description = ({ detail, complianceError }: DescriptionProps) => {
};

const GatewayErrorComponent = ({ error }: GatewayErrorComponentProps) => {
const activeModule = useAtomValue(activeModuleAtom);
const activeProduct = useSelector((state: ReduxState) => state.chrome.activeProduct);
// get active product, fallback to module name if product is not defined
const serviceName = useSelector((state: ReduxState) => state.chrome.activeProduct || state.chrome.activeModule);
const serviceName = activeProduct || activeModule;
return <NotAuthorized description={<Description complianceError={error.complianceError} detail={error.detail} />} serviceName={serviceName} />;
};

Expand Down
36 changes: 27 additions & 9 deletions src/components/Header/HeaderTests/ToolbarToggle.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { act, fireEvent, render } from '@testing-library/react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import ToolbarToggle from '../ToolbarToggle';

describe('ToolbarToggle', () => {
Expand Down Expand Up @@ -34,35 +34,53 @@ describe('ToolbarToggle', () => {
const toggleButton = container.querySelector('#foo');
expect(toggleButton).toBeTruthy();
await act(async () => {
fireEvent.click(toggleButton);
await fireEvent.click(toggleButton);
});
expect(container.querySelector('div')).toMatchSnapshot();
});

it('should open/close menu correctly', async () => {
const { container } = render(<ToolbarToggle {...toolbarToggleProps} />);
const toggleButton = container.querySelector('#foo');
expect(toggleButton).toBeTruthy();
const expectedTexts = toolbarToggleProps.dropdownItems.filter((item) => !item.isHidden);
expect(toggleButton).toBeInTheDocument();
await act(async () => {
await fireEvent.click(toggleButton);
});

// wait for async actions on toggle to complete
await act(async () => {
fireEvent.click(toggleButton);
await Promise.resolve();
});
expect(container.querySelectorAll('.pf-v5-c-menu__list-item')).toHaveLength(2);

for (const item of expectedTexts) {
expect(screen.getByText(item.title)).toBeInTheDocument();
}
// closes button
await act(async () => {
await fireEvent.click(toggleButton);
});

// wait for async actions on toggle to complete
await act(async () => {
fireEvent.click(toggleButton);
await Promise.resolve();
});
expect(container.querySelectorAll('.pf-v5-c-menu__list-item')).toHaveLength(0);
for (const item of expectedTexts) {
expect(screen.queryByText(item.title)).not.toBeInTheDocument();
}
// expect(container.querySelectorAll('.pf-v5-c-menu__list-item')).toHaveLength(0);
});

it('should call onClick menu item callback', async () => {
const { container } = render(<ToolbarToggle {...toolbarToggleProps} />);
const toggleButton = container.querySelector('#foo');
await act(async () => {
fireEvent.click(toggleButton);
await fireEvent.click(toggleButton);
});
const actionButton = container.querySelector('button.pf-v5-c-menu__item');
expect(actionButton).toBeTruthy();
await act(async () => {
fireEvent.click(actionButton);
await fireEvent.click(actionButton);
});
expect(clickSpy).toHaveBeenCalled();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ exports[`HeaderAlert should render correctly dismissable 1`] = `
class="pf-v5-c-alert__title"
>
<span
class="pf-v5-u-screen-reader"
class="pf-v5-screen-reader"
>
Info alert:
</span>
Expand Down Expand Up @@ -92,7 +92,7 @@ exports[`HeaderAlert should render correctly not dismissable 1`] = `
class="pf-v5-c-alert__title"
>
<span
class="pf-v5-u-screen-reader"
class="pf-v5-screen-reader"
>
Info alert:
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ exports[`ToolbarToggle should render correctly 1`] = `
data-popper-escaped="true"
data-popper-placement="bottom-end"
data-popper-reference-hidden="true"
style="position: absolute; left: 0px; top: 0px; z-index: 9999; min-width: 0px; transform: translate(0px, 0px);"
style="position: absolute; left: 0px; top: 0px; z-index: 9999; opacity: 1; transition: opacity 0ms cubic-bezier(.54, 1.5, .38, 1.11); min-width: 0px; transform: translate(0px, 0px);"
>
<div
class="pf-v5-c-menu__content"
Expand All @@ -24,14 +24,14 @@ exports[`ToolbarToggle should render correctly 1`] = `
data-ouia-component-type="PF5/DropdownItem"
data-ouia-safe="true"
href="url1"
rel="noopener noreferrer"
role="none"
target="_blank"
>
<a
class="pf-v5-c-menu__item"
rel="noopener noreferrer"
role="menuitem"
tabindex="0"
target="_blank"
>
<span
class="pf-v5-c-menu__item-main"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ exports[`UserToggle should render correctly with isSmall false 1`] = `
data-popper-escaped="true"
data-popper-placement="bottom-end"
data-popper-reference-hidden="true"
style="position: absolute; left: 0px; top: 0px; z-index: 9999; min-width: 0px; transform: translate(0px, 0px);"
style="position: absolute; left: 0px; top: 0px; z-index: 9999; opacity: 1; transition: opacity 0ms cubic-bezier(.54, 1.5, .38, 1.11); min-width: 0px; transform: translate(0px, 0px);"
>
<div
class="pf-v5-c-menu__content"
Expand Down Expand Up @@ -199,15 +199,15 @@ exports[`UserToggle should render correctly with isSmall false 1`] = `
data-ouia-component-id="OUIA-Generated-DropdownItem-3"
data-ouia-component-type="PF5/DropdownItem"
data-ouia-safe="true"
rel="noopener noreferrer"
role="none"
target="_blank"
>
<a
class="pf-v5-c-menu__item"
href="https://www.qa.redhat.com/wapps/ugc/protected/personalInfo.html"
rel="noopener noreferrer"
role="menuitem"
tabindex="0"
target="_blank"
>
<span
class="pf-v5-c-menu__item-main"
Expand Down
6 changes: 1 addition & 5 deletions src/components/Navigation/ChromeNavItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,7 @@ const ChromeNavItem = ({
isActive={active}
to={href}
ouiaId={title}
component={
((props: LinkWrapperProps) => (
<ChromeLink {...props} isBeta={isBetaEnv} isExternal={isExternal} appId={appId} />
)) as unknown as React.ReactNode
}
component={(props: LinkWrapperProps) => <ChromeLink {...props} isBeta={isBetaEnv} isExternal={isExternal} appId={appId} />}
>
{typeof title === 'string' && !ignoreCase ? titleCase(title) : title}{' '}
{isExternal && (
Expand Down
10 changes: 5 additions & 5 deletions src/components/Navigation/__snapshots__/Navigation.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ exports[`ChromeNavItem should render navigation loader if schema was not loaded
class="pf-v5-c-skeleton ins-c-skeleton ins-c-skeleton__lg ins-m-dark"
>
<span
class="pf-v5-u-screen-reader"
class="pf-v5-screen-reader"
/>
</div>
</section>
Expand Down Expand Up @@ -60,7 +60,7 @@ exports[`ChromeNavItem should render navigation loader if schema was not loaded
class="pf-v5-c-skeleton ins-c-skeleton ins-c-skeleton__lg ins-m-dark"
>
<span
class="pf-v5-u-screen-reader"
class="pf-v5-screen-reader"
/>
</div>
</a>
Expand All @@ -79,7 +79,7 @@ exports[`ChromeNavItem should render navigation loader if schema was not loaded
class="pf-v5-c-skeleton ins-c-skeleton ins-c-skeleton__lg ins-m-dark"
>
<span
class="pf-v5-u-screen-reader"
class="pf-v5-screen-reader"
/>
</div>
</a>
Expand All @@ -98,7 +98,7 @@ exports[`ChromeNavItem should render navigation loader if schema was not loaded
class="pf-v5-c-skeleton ins-c-skeleton ins-c-skeleton__lg ins-m-dark"
>
<span
class="pf-v5-u-screen-reader"
class="pf-v5-screen-reader"
/>
</div>
</a>
Expand All @@ -117,7 +117,7 @@ exports[`ChromeNavItem should render navigation loader if schema was not loaded
class="pf-v5-c-skeleton ins-c-skeleton ins-c-skeleton__lg ins-m-dark"
>
<span
class="pf-v5-u-screen-reader"
class="pf-v5-screen-reader"
/>
</div>
</a>
Expand Down
4 changes: 3 additions & 1 deletion src/components/RootApp/RootApp.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { Suspense, lazy, memo, useContext, useEffect } from 'react';
import { unstable_HistoryRouter as HistoryRouter, HistoryRouterProps } from 'react-router-dom';
import { HelpTopicContainer, QuickStart, QuickStartContainer, QuickStartContainerProps } from '@patternfly/quickstarts';
import { useAtomValue } from 'jotai';
import chromeHistory from '../../utils/chromeHistory';
import { FeatureFlagsProvider } from '../FeatureFlags';
import ScalprumRoot from './ScalprumRoot';
Expand All @@ -18,6 +19,7 @@ import { DeepRequired } from 'utility-types';
import ReactDOM from 'react-dom';
import { FooterProps } from '../Footer/Footer';
import ChromeAuthContext, { ChromeAuthContextValue } from '../../auth/ChromeAuthContext';
import { activeModuleAtom } from '../../state/atoms';

const NotEntitledModal = lazy(() => import('../NotEntitledModal'));
const Debugger = lazy(() => import('../Debugger'));
Expand All @@ -29,7 +31,7 @@ const RootApp = memo((props: RootAppProps) => {
const { activateQuickstart, allQuickStartStates, setAllQuickStartStates, activeQuickStartID, setActiveQuickStartID } = useQuickstartsStates();
const { helpTopics, addHelpTopics, disableTopics, enableTopics } = useHelpTopicState();
const dispatch = useDispatch();
const activeModule = useSelector(({ chrome: { activeModule } }: ReduxState) => activeModule);
const activeModule = useAtomValue(activeModuleAtom);
const quickStarts = useSelector(
({
chrome: {
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/useDisablePendoOnLanding.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { ReduxState } from '../redux/store';
import { isITLessEnv } from '../utils/consts';
import { useAtomValue } from 'jotai';
import { activeModuleAtom } from '../state/atoms';

// interval timing is short because we want to catch the bubble before ASAP so it does not cover the VA button
const RETRY_ATTEMPS = 2000;
Expand All @@ -23,7 +23,7 @@ function retry(fn: () => void, retriesLeft = 50, interval = 100) {
}

const useDisablePendoOnLanding = () => {
const activeModule = useSelector((state: ReduxState) => state.chrome.activeModule);
const activeModule = useAtomValue(activeModuleAtom);

const toggleGuides = () => {
// push the call to the end of the event loop to make sure the pendo script is loaded and initialized
Expand Down
Loading

0 comments on commit 7b15489

Please sign in to comment.