Skip to content

Commit

Permalink
refactor(router): sw-625 move to router v6
Browse files Browse the repository at this point in the history
* build, routing package, module ref, spelling
* configs, sorted products, aliases, simplify routes
* locale, OpenShift strings
* AppEntry, remove router component
* app, locale loading checks
* helpers, add memoize, object freeze
* authenticationContext, remove nav history push
* i18n, minor check to avoid reload
* productView, useRouteDetail hook update
* productViewMissing, add useNavigate, RouteDetail hooks
* redux, viewReducer, appTypes for storing route ref
* router, simplify, useSetRouteDetail for config load
* routerContext, useRouteDetail, wrap location, navigate
* routerHelpers, restructure getRouteConfigByPath, memo
  • Loading branch information
cdcabrera committed Feb 24, 2023
1 parent 79fbda6 commit 37189ba
Show file tree
Hide file tree
Showing 54 changed files with 1,749 additions and 3,430 deletions.
5 changes: 4 additions & 1 deletion config/build.plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'] } }
]
})
];

Expand Down
1 change: 1 addition & 0 deletions config/cspell.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"ibmz",
"iife",
"ipsum",
"ized",
"keycloak",
"kubernetes",
"labelledby",
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions public/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,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</0>",
"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</0>",
"description_OpenShift-dedicated-metrics": "Monitor your OpenShift Dedicated usage for On-Demand subscriptions.",
Expand Down
6 changes: 1 addition & 5 deletions src/AppEntry.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
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 = () => (
<Provider store={store}>
<BrowserRouter basename={routerHelpers.dynamicBaseName()}>
<App />
</BrowserRouter>
<App />
</Provider>
);

Expand Down
6 changes: 4 additions & 2 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,17 @@ const App = ({ getLocale, useDispatch: useAliasDispatch, useSelector: useAliasSe
let platformNotifications = null;

useMount(() => {
dispatch(getLocale());
if (!locale) {
dispatch(getLocale());
}
});

if (!helpers.UI_DISABLED_NOTIFICATIONS) {
platformNotifications = <NotificationsPortal />;
}

return (
<I18n locale={locale || null}>
<I18n locale={locale}>
{platformNotifications}
<Authentication>
<Router />
Expand Down
35 changes: 35 additions & 0 deletions src/common/__tests__/__snapshots__/helpers.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ exports[`Helpers should expose a window object: limited window object 1`] = `
"isDate": [Function],
"isPromise": [Function],
"lorem": "ipsum",
"memo": [Function],
"noop": [Function],
"noopPromise": Promise {},
"noopTranslate": [Function],
"numberDisplay": [Function],
"objFreeze": [Function],
}
`;

Expand Down Expand Up @@ -85,10 +87,12 @@ exports[`Helpers should expose a window object: window object 1`] = `
"isDate": [Function],
"isPromise": [Function],
"lorem": "ipsum",
"memo": [Function],
"noop": [Function],
"noopPromise": Promise {},
"noopTranslate": [Function],
"numberDisplay": [Function],
"objFreeze": [Function],
}
`;

Expand Down Expand Up @@ -139,13 +143,44 @@ exports[`Helpers should have specific functions: helpers 1`] = `
"generateId": [Function],
"isDate": [Function],
"isPromise": [Function],
"memo": [Function],
"noop": [Function],
"noopPromise": Promise {},
"noopTranslate": [Function],
"numberDisplay": [Function],
"objFreeze": [Function],
}
`;

exports[`Helpers should produce an immutable like object: clone 1`] = `
{
"dolor": {
"hello": [
"world",
"clone",
],
"sit": {
"lorem": "ipsum",
},
},
"lorem": "ipsum",
}
`;

exports[`Helpers should produce an immutable like object: delete property 1`] = `"Cannot delete property 'sit' of #<Object>"`;
exports[`Helpers should produce an immutable like object: set nested property 1`] = `"Cannot assign to read only property 'lorem' of object '#<Object>'"`;
exports[`Helpers should produce an immutable like object: set property 1`] = `"Cannot assign to read only property 'lorem' of object '#<Object>'"`;
exports[`Helpers should produce an immutable like object: shallow clone 1`] = `"Cannot add property 1, object is not extensible"`;
exports[`Helpers should produce an immutable like object: update nested property list 1`] = `"Cannot add property 1, object is not extensible"`;
exports[`Helpers should produce an immutable like object: update nested property list length 1`] = `"Cannot assign to read only property 'length' of object '[object Array]'"`;
exports[`Helpers should produce an immutable like object: update nested property list values 1`] = `"Cannot delete property '0' of [object Array]"`;
exports[`Helpers should support generated consistent hashes from objects, primitive values: hash, object and primitive values 1`] = `
{
"valueArray": "41d49497cb062fec8dd0a1e9298650aeb2364f35",
Expand Down
67 changes: 67 additions & 0 deletions src/common/__tests__/helpers.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import cryptoMd5 from 'crypto-js/md5';
import _cloneDeep from 'lodash/cloneDeep';
import { helpers } from '../helpers';

describe('Helpers', () => {
Expand Down Expand Up @@ -96,13 +97,79 @@ describe('Helpers', () => {
expect(helpers.isPromise(() => 'lorem')).toBe(false);
});

it('should memoize function return values with memo', () => {
const testArr = [];
const testMemoReturnValue = helpers.memo(
str => {
const arr = ['lorem', 'ipsum', 'dolor', 'sit'];
const randomStr = Math.floor(Math.random() * arr.length);
const genStr = `${arr[randomStr]}-${str}`;
testArr.push(genStr);
return genStr;
},
{ cacheLimit: 3 }
);

testMemoReturnValue('one');
testMemoReturnValue('one');
testMemoReturnValue('one');
testMemoReturnValue('two');
testMemoReturnValue('three');

expect(testArr[0] === testMemoReturnValue('one')).toBe(true);
expect(testArr[1] === testMemoReturnValue('two')).toBe(true);
expect(testArr[2] === testMemoReturnValue('three')).toBe(true);
expect(testArr[2] === testMemoReturnValue('three')).toBe(true);

testMemoReturnValue('four');
expect(testArr[3] === testMemoReturnValue('four')).toBe(true);
});

it('should apply a number display function', () => {
expect(helpers.numberDisplay(null)).toBe(null);
expect(helpers.numberDisplay(undefined)).toBe(undefined);
expect(helpers.numberDisplay(NaN)).toBe(NaN);
expect(helpers.numberDisplay(11)).toMatchSnapshot('number display function result');
});

it('should produce an immutable like object', () => {
const mockObj = {};

mockObj.lorem = 'ipsum';
mockObj.dolor = {
sit: {
lorem: 'ipsum'
},
hello: ['world']
};

helpers.objFreeze(mockObj);

expect(() => delete mockObj.dolor.sit).toThrowErrorMatchingSnapshot('delete property');
expect(() => {
mockObj.lorem = 'hello world';
}).toThrowErrorMatchingSnapshot('set property');
expect(() => {
mockObj.dolor.sit.lorem = 'hello world';
}).toThrowErrorMatchingSnapshot('set nested property');
expect(() => {
mockObj.dolor.hello.push('hello');
}).toThrowErrorMatchingSnapshot('update nested property list');
expect(() => {
mockObj.dolor.hello.pop();
}).toThrowErrorMatchingSnapshot('update nested property list values');
expect(() => {
mockObj.dolor.hello.length = 0;
}).toThrowErrorMatchingSnapshot('update nested property list length');
expect(() => {
({ ...mockObj }).dolor.hello.push('shallow clone');
}).toThrowErrorMatchingSnapshot('shallow clone');

const clone = _cloneDeep(mockObj);
clone.dolor.hello.push('clone');
expect(clone).toMatchSnapshot('clone');
});

it('should expose a window object', () => {
helpers.browserExpose({ lorem: 'ipsum' });
expect(window[helpers.UI_WINDOW_ID]).toMatchSnapshot('window object');
Expand Down
62 changes: 62 additions & 0 deletions src/common/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,37 @@ const generateHash = (anyValue, { method = cryptoSha1 } = {}) =>
})
).toString();

/**
* Simple memoize, cache based arguments with adjustable limit.
*
* @param {Function} func
* @param {object} options
* @param {number} options.cacheLimit
* @returns {Function}
*/
const memo = (func, { cacheLimit = 1 } = {}) => {
// eslint-disable-next-line func-names
const ized = function () {
const cache = [];

return (...args) => {
const key = JSON.stringify({ value: [...args].map(arg => (typeof arg === 'function' && arg.toString()) || arg) });
const keyIndex = cache.indexOf(key);

if (keyIndex < 0) {
const result = func.call(null, ...args);
cache.unshift(key, result);
cache.length = cacheLimit * 2;
return cache[1];
}

return cache[keyIndex + 1];
};
};

return ized();
};

/**
* An empty function.
* Typically used as a default prop.
Expand Down Expand Up @@ -134,6 +165,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.
Expand Down Expand Up @@ -366,10 +426,12 @@ const helpers = {
generateId,
isDate,
isPromise,
memo,
noop,
noopPromise,
noopTranslate,
numberDisplay,
objFreeze,
DEV_MODE,
PROD_MODE,
REVIEW_MODE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,6 @@ exports[`AuthenticationContext should apply a hook for retrieving auth data from
},
],
],
[
[Function],
],
]
`;

Expand Down
17 changes: 3 additions & 14 deletions src/components/authentication/authenticationContext.js
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -27,25 +27,19 @@ 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}}
*/
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 },
Expand All @@ -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) || [];
Expand Down
Loading

0 comments on commit 37189ba

Please sign in to comment.