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}} reporting0>",
"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}} reporting0>",
"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
})}