From 4f12d89650664c665b1a752ba202c7f2442825be Mon Sep 17 00:00:00 2001 From: CD Cabrera Date: Mon, 6 Feb 2023 18:06:09 -0500 Subject: [PATCH] refactor(router): sw-625 move to router v6 --- config/build.plugins.js | 5 +- package.json | 5 +- public/locales/en-US.json | 6 + src/AppEntry.js | 15 +- src/app.js | 13 +- src/common/helpers.js | 30 ++ .../authentication/authenticationContext.js | 17 +- src/components/i18n/i18n.js | 47 +-- src/components/productView/productView.js | 113 +++++++- .../productView/productViewMissing.js | 64 ++-- src/components/router/router.js | 91 ++---- src/components/router/routerContext.js | 274 ++++++++++++------ src/components/router/routerHelpers.js | 79 ++--- src/config/product.rhacs.js | 2 +- src/config/product.rhel.js | 2 +- src/config/product.rhods.js | 2 +- src/config/product.rhosak.js | 2 +- src/config/products.js | 46 ++- src/config/routes.js | 34 +-- src/index.appEntry.js | 18 ++ src/index.bootstrap.js | 7 + src/index.js | 2 +- src/redux/reducers/viewReducer.js | 15 +- src/redux/types/appTypes.js | 6 +- yarn.lock | 124 ++------ 25 files changed, 577 insertions(+), 442 deletions(-) create mode 100644 src/index.appEntry.js create mode 100644 src/index.bootstrap.js diff --git a/config/build.plugins.js b/config/build.plugins.js index c19413022..263eff5a6 100644 --- a/config/build.plugins.js +++ b/config/build.plugins.js @@ -33,7 +33,10 @@ const setCommonPlugins = () => { }), fedModulePlugin({ root: RELATIVE_DIRNAME, - shared: [{ 'react-redux': { requiredVersion: dependencies['react-redux'] } }] + shared: [ + { 'react-router-dom': { singleton: true, requiredVersion: '*' } }, + { 'react-redux': { requiredVersion: dependencies['react-redux'] } } + ] }) ]; diff --git a/package.json b/package.json index 41d86b616..4feeb74d7 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "axios": "^0.27.2", "classnames": "^2.3.2", "crypto-js": "^4.1.1", + "fastest-levenshtein": "^1.0.16", "i18next": "^22.0.6", "i18next-http-backend": "^2.0.2", "iso-639-1": "^2.1.15", @@ -97,8 +98,8 @@ "react-dom": "^17.0.2", "react-i18next": "^12.0.0", "react-redux": "^8.0.5", - "react-router": "5.3.3", - "react-router-dom": "5.3.0", + "react-router": "6.8.1", + "react-router-dom": "6.8.1", "react-use": "^17.4.0", "redux": "^4.2.0", "redux-logger": "^3.0.6", diff --git a/public/locales/en-US.json b/public/locales/en-US.json index a7b9f3015..347c4876f 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -368,6 +368,12 @@ "title_OpenShift Container Platform": "OpenShift Container Platform", "subtitle_OpenShift Container Platform": "Monitor your OpenShift Container Platform usage for both Annual and On-Demand subscriptions. <0>Learn more about {{appName}} reporting", "description_OpenShift Container Platform": "Monitor your OpenShift Container Platform usage for both Annual and On-Demand subscriptions.", + "title_openshift-container": "$t(curiosity-view.title_OpenShift Container Platform)", + "subtitle_openshift-container": "$t(curiosity-view.subtitle_OpenShift Container Platform)", + "description_openshift-container": "$t(curiosity-view.description_OpenShift Container Platform)", + "title_OpenShift-metrics": "OpenShift Container Platform On-Demand", + "subtitle_OpenShift-metrics": "Monitor your OpenShift Container Platform usage for On-Demand subscriptions.", + "description_OpenShift-metrics": "Monitor your OpenShift Container Platform usage for On-Demand subscriptions.", "title_OpenShift-dedicated-metrics": "OpenShift Dedicated", "subtitle_OpenShift-dedicated-metrics": "Monitor your OpenShift Dedicated usage for On-Demand subscriptions. <0>Learn more about {{appName}} reporting", "description_OpenShift-dedicated-metrics": "Monitor your OpenShift Dedicated usage for On-Demand subscriptions.", diff --git a/src/AppEntry.js b/src/AppEntry.js index 5263c7141..aeff9c274 100644 --- a/src/AppEntry.js +++ b/src/AppEntry.js @@ -1,18 +1,17 @@ import React from 'react'; import { Provider } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; -import { routerHelpers } from './components/router'; import { store } from './redux'; import App from './app'; import './styles/index.scss'; import '@patternfly/react-styles/css/components/Select/select.css'; -const AppEntry = () => ( - - +const AppEntry = () => { + console.log('>>> APP ENTRY LOAD >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>'); + return ( + - - -); + + ); +}; export { AppEntry as default, AppEntry }; diff --git a/src/app.js b/src/app.js index 96764bac2..9ddd37786 100644 --- a/src/app.js +++ b/src/app.js @@ -25,9 +25,12 @@ const App = ({ getLocale, useDispatch: useAliasDispatch, useSelector: useAliasSe const dispatch = useAliasDispatch(); const { value: locale } = useAliasSelector(({ user }) => user?.locale?.data, {}); let platformNotifications = null; - + console.log('>>> APP load', locale); useMount(() => { - dispatch(getLocale()); + console.log('>>> APP mount comp', locale); + if (!locale) { + dispatch(getLocale()); + } }); if (!helpers.UI_DISABLED_NOTIFICATIONS) { @@ -35,10 +38,10 @@ const App = ({ getLocale, useDispatch: useAliasDispatch, useSelector: useAliasSe } return ( - + {platformNotifications} - - + + ); diff --git a/src/common/helpers.js b/src/common/helpers.js index 2e00ae2db..74585df71 100644 --- a/src/common/helpers.js +++ b/src/common/helpers.js @@ -134,6 +134,35 @@ const numberDisplay = value => { return numbro(value); }; +/** + * Recursive object and props freeze/immutable. + * Used from deep-freeze-strict, an older npm package, license - public domain + * https://bit.ly/3HR4XWP and https://bit.ly/3Ye4S6B + * + * @param {object} obj + * @returns {*} + */ +const objFreeze = obj => { + Object.freeze(obj); + + const oIsFunction = typeof obj === 'function'; + const hasOwnProp = Object.prototype.hasOwnProperty; + + Object.getOwnPropertyNames(obj).forEach(prop => { + if ( + hasOwnProp.call(obj, prop) && + (oIsFunction ? prop !== 'caller' && prop !== 'callee' && prop !== 'arguments' : true) && + obj[prop] !== null && + (typeof obj[prop] === 'object' || typeof obj[prop] === 'function') && + !Object.isFrozen(obj[prop]) + ) { + objFreeze(obj[prop]); + } + }); + + return obj; +}; + /** * Is dev mode active. * Associated with using the NPM script "start". See dotenv config files for activation. @@ -370,6 +399,7 @@ const helpers = { noopPromise, noopTranslate, numberDisplay, + objFreeze, DEV_MODE, PROD_MODE, REVIEW_MODE, diff --git a/src/components/authentication/authenticationContext.js b/src/components/authentication/authenticationContext.js index b9b40a337..c516e4277 100644 --- a/src/components/authentication/authenticationContext.js +++ b/src/components/authentication/authenticationContext.js @@ -1,8 +1,8 @@ -import React, { useContext, useState } from 'react'; -import { useMount, useUnmount } from 'react-use'; +import React, { useContext } from 'react'; +import { useMount } from 'react-use'; import { reduxActions, storeHooks } from '../../redux'; import { helpers } from '../../common'; -import { routerContext, routerHelpers } from '../router'; +import { routerHelpers } from '../router'; /** * Base context. @@ -27,10 +27,8 @@ const useAuthContext = () => useContext(AuthenticationContext); * @param {string} options.appName * @param {Function} options.authorizeUser * @param {Function} options.hideGlobalFilter - * @param {Function} options.onNavigation * @param {Function} options.setAppName * @param {Function} options.useDispatch - * @param {Function} options.useHistory * @param {Function} options.useSelectorsResponse * @returns {{data: {errorCodes, errorStatus: *, locale}, pending: boolean, fulfilled: boolean, error: boolean}} */ @@ -38,14 +36,10 @@ const useGetAuthorization = ({ appName = routerHelpers.appName, authorizeUser = reduxActions.platform.authorizeUser, hideGlobalFilter = reduxActions.platform.hideGlobalFilter, - onNavigation = reduxActions.platform.onNavigation, setAppName = reduxActions.platform.setAppName, useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch, - useHistory: useAliasHistory = routerContext.useHistory, useSelectorsResponse: useAliasSelectorsResponse = storeHooks.reactRedux.useSelectorsResponse } = {}) => { - const [unregister, setUnregister] = useState(() => helpers.noop); - const history = useAliasHistory(); const dispatch = useAliasDispatch(); const { data, error, fulfilled, pending, responses } = useAliasSelectorsResponse([ { id: 'auth', selector: ({ user }) => user?.auth }, @@ -59,11 +53,6 @@ const useGetAuthorization = ({ useMount(async () => { await dispatch(authorizeUser()); dispatch([setAppName(appName), hideGlobalFilter()]); - setUnregister(() => dispatch(onNavigation(event => history.push(event.navId)))); - }); - - useUnmount(() => { - unregister(); }); const [user = {}, app = {}] = (Array.isArray(data.auth) && data.auth) || []; diff --git a/src/components/i18n/i18n.js b/src/components/i18n/i18n.js index 2bad0450d..9c52e6802 100644 --- a/src/components/i18n/i18n.js +++ b/src/components/i18n/i18n.js @@ -24,35 +24,38 @@ const I18n = ({ children, fallbackLng, loadPath, locale }) => { * Initialize i18next */ useMount(async () => { - try { - await i18next - .use(XHR) - .use(initReactI18next) - .init({ - backend: { - loadPath - }, - fallbackLng, - lng: undefined, - debug: !helpers.PROD_MODE, - ns: ['default'], - defaultNS: 'default', - react: { - useSuspense: false - } - }); - } catch (e) { - // - } + console.log('>>> mount i18n'); + if (!initialized) { + try { + await i18next + .use(XHR) + .use(initReactI18next) + .init({ + backend: { + loadPath + }, + fallbackLng, + lng: undefined, + debug: !helpers.PROD_MODE, + ns: ['default'], + defaultNS: 'default', + react: { + useSuspense: false + } + }); + } catch (e) { + // + } - setInitialized(true); + setInitialized(true); + } }); /** * Update locale. */ useEffect(() => { - if (initialized) { + if (initialized && locale) { try { i18next.changeLanguage(locale); } catch (e) { diff --git a/src/components/productView/productView.js b/src/components/productView/productView.js index d448b13e3..73fa4a4fc 100644 --- a/src/components/productView/productView.js +++ b/src/components/productView/productView.js @@ -1,6 +1,8 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; +import { useMount } from 'react-use'; import { routerContext } from '../router'; +// import { PageLayout, PageHeader, PageColumns } from '../pageLayout/pageLayout'; import { ProductViewContext } from './productViewContext'; import { PageLayout, PageHeader, PageSection, PageToolbar, PageMessages, PageColumns } from '../pageLayout/pageLayout'; import { GraphCard } from '../graphCard/graphCard'; @@ -13,6 +15,7 @@ import InventoryTabs, { InventoryTab } from '../inventoryTabs/inventoryTabs'; import { InventoryCardSubscriptions } from '../inventoryCardSubscriptions/inventoryCardSubscriptions'; import { RHSM_INTERNAL_PRODUCT_DISPLAY_TYPES as DISPLAY_TYPES } from '../../services/rhsm/rhsmConstants'; import { translate } from '../i18n/i18n'; +import { storeHooks } from '../../redux'; /** * Display product columns. @@ -20,12 +23,28 @@ import { translate } from '../i18n/i18n'; * @param {object} props * @param {Function} props.t * @param {Function} props.useRouteDetail + * @param props.useSelector * @returns {Node} */ -const ProductView = ({ t, useRouteDetail: useAliasRouteDetail }) => { - const { productParameter: routeProductLabel, productConfig } = useAliasRouteDetail(); - const updatedRouteProductLabel = (Array.isArray(routeProductLabel) && routeProductLabel?.[0]) || routeProductLabel; +const ProductView = ({ t, useRouteDetail: useAliasRouteDetail, useSelector: useAliasSelector }) => { + const { productGroup, productConfig } = useAliasRouteDetail(); + // const [] = useState(); + // useEffect(() => {}, []); + // const { productGroup, productConfig } = detail || {}; + // useAliasRouteDetail(); + // const { productGroup, productConfig } = useAliasSelector(({ view }) => view?.product?.config, {}); + useMount(() => { + console.log('>>>> PRODUCT VIEW MOUNTED', useAliasSelector, productGroup); + }); + + /** + * Render a product with a context provider + * + * @param {object} config + * @returns {React.ReactNode|null} + */ + /* const renderProduct = config => { const { initialInventoryFilters, initialSubscriptionsInventoryFilters, productDisplay, productId, viewId } = config; @@ -84,14 +103,82 @@ const ProductView = ({ t, useRouteDetail: useAliasRouteDetail }) => { ); }; + */ + const renderProduct = useCallback(() => { + const updated = config => { + console.log('>>>> PRODUCT VIEW', config); + const { initialInventoryFilters, initialSubscriptionsInventoryFilters, productDisplay, productId, viewId } = + config; + + if (!productId || !viewId) { + return null; + } + + return ( + + {productDisplay !== DISPLAY_TYPES.HOURLY && } + + + + + + + + + {!helpers.UI_DISABLED_TABLE_HOSTS && + productDisplay !== DISPLAY_TYPES.HOURLY && + productDisplay !== DISPLAY_TYPES.CAPACITY && + initialInventoryFilters && ( + + + + )} + {!helpers.UI_DISABLED_TABLE_INSTANCES && + productDisplay !== DISPLAY_TYPES.DUAL_AXES && + productDisplay !== DISPLAY_TYPES.LEGACY && + productDisplay !== DISPLAY_TYPES.PARTIAL && + initialInventoryFilters && ( + + + + )} + {!helpers.UI_DISABLED_TABLE_SUBSCRIPTIONS && initialSubscriptionsInventoryFilters && ( + + + + )} + + + + ); + }; + + return productConfig?.map(config => updated(config)); + }, [productConfig, t]); return ( - - - {t(`curiosity-view.title`, { appName: helpers.UI_DISPLAY_NAME, context: updatedRouteProductLabel })} - - {productConfig.map(config => renderProduct(config))} - + (productGroup && ( + + + {t(`curiosity-view.title`, { appName: helpers.UI_DISPLAY_NAME, context: productGroup })} + + {renderProduct()} + + )) || + null ); }; @@ -102,7 +189,8 @@ const ProductView = ({ t, useRouteDetail: useAliasRouteDetail }) => { */ ProductView.propTypes = { t: PropTypes.func, - useRouteDetail: PropTypes.func + useRouteDetail: PropTypes.func, + useSelector: PropTypes.func }; /** @@ -112,7 +200,8 @@ ProductView.propTypes = { */ ProductView.defaultProps = { t: translate, - useRouteDetail: routerContext.useRouteDetail + useRouteDetail: routerContext.useRouteDetail, + useSelector: storeHooks.reactRedux.useSelector }; export { ProductView as default, ProductView }; diff --git a/src/components/productView/productViewMissing.js b/src/components/productView/productViewMissing.js index 0b68816c3..50308a6a1 100644 --- a/src/components/productView/productViewMissing.js +++ b/src/components/productView/productViewMissing.js @@ -2,22 +2,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button, Card, CardBody, CardFooter, CardTitle, Gallery, Title, PageSection } from '@patternfly/react-core'; import { ArrowRightIcon } from '@patternfly/react-icons'; -import { useMount } from 'react-use'; import { PageLayout, PageHeader } from '../pageLayout/pageLayout'; -import { routerContext, routerHelpers } from '../router'; +import { routerContext } from '../router'; import { helpers } from '../../common'; import { translate } from '../i18n/i18n'; -/** - * Return a list of available products. - * - * @returns {Array} - */ -const filterAvailableProducts = () => { - const { configs, allConfigs } = routerHelpers.getRouteConfigByPath(); - return (configs.length && configs) || allConfigs; -}; - /** * Render a missing product view. * @@ -25,18 +14,24 @@ const filterAvailableProducts = () => { * @param {object} props * @param {number} props.availableProductsRedirect * @param {Function} props.t - * @param {Function} props.useHistory + * @param {Function} props.useNavigate + * @param {Function} props.useRouteDetail * @returns {Node} */ -const ProductViewMissing = ({ availableProductsRedirect, t, useHistory: useAliasHistory }) => { - const history = useAliasHistory({ isSetAppNav: true }); - const availableProducts = filterAvailableProducts(); +const ProductViewMissing = ({ + availableProductsRedirect, + t, + useNavigate: useAliasNavigate, + useRouteDetail: useAliasRouteDetail +}) => { + const navigate = useAliasNavigate(); + const { firstMatch, allConfigs } = useAliasRouteDetail(); + const availableProducts = (firstMatch && [firstMatch]) || allConfigs; - useMount(() => { - if (availableProducts.length <= availableProductsRedirect) { - history.push(availableProducts?.[0]?.productPath); - } - }); + if (availableProducts?.length <= availableProductsRedirect) { + navigate(availableProducts[0].productPath); + return null; + } /** * On click, update history. @@ -45,38 +40,41 @@ const ProductViewMissing = ({ availableProductsRedirect, t, useHistory: useAlias * @param {string} path * @returns {void} */ - const onNavigate = path => history.push(path); + const onNavigate = path => navigate(path); return ( {t(`curiosity-view.title`, { appName: helpers.UI_DISPLAY_NAME })} - {availableProducts.map(product => ( + {availableProducts?.map(({ productGroup, productId, productPath }) => ( onNavigate(product.productPath)} + onClick={() => onNavigate(productPath)} > {t('curiosity-view.title', { appName: helpers.UI_DISPLAY_NAME, - context: product.productId + context: productId })} {t('curiosity-view.description', { appName: helpers.UI_DISPLAY_NAME, - context: product.productId + context: productGroup })}