From e3a1edfd5eddc16f25690f1fafeaba312477c6da Mon Sep 17 00:00:00 2001 From: Ruslan Date: Tue, 1 Mar 2022 16:41:22 +0300 Subject: [PATCH] Preparing the project structure for API connection Connect and configure libraries: * axioms * Lodash * types of props * react-redux * redux-preserved * react-router-home * react-helmet-asynchronous * @reduxjs/toolkit * jwt-decode * re-select --- package.json | 8 +- src/App.js | 68 +++++++--- src/App.scss | 6 + src/components/Loader/Loader.js | 27 ++++ src/components/Loader/Loader.scss | 97 ++++++++++++++ src/components/index.js | 1 + src/config/ConfigLoader.js | 25 ++++ src/config/Splash/Splash.js | 11 ++ src/config/Splash/Splash.scss | 7 + src/config/index.js | 19 +++ src/index.js | 28 ++-- src/layouts/dashboard/DashboardSidebar.js | 24 +--- src/pages/MainPage/MainPage.js | 19 +++ src/pages/MainPage/MainPage.scss | 0 src/remotes/index.js | 55 ++++++++ src/routes.js | 6 +- src/store/index.js | 26 ++++ src/store/middlewares.js | 12 ++ src/store/security/index.js | 0 src/store/security/lib/models.js | 0 src/store/security/lib/reducers.js | 19 +++ src/store/security/lib/selectors.js | 17 +++ src/store/security/lib/thunks.js | 120 +++++++++++++++++ src/utils/index.js | 33 +++++ src/utils/reduxUtils.js | 74 +++++++++++ yarn.lock | 150 +++++++++++++--------- 26 files changed, 736 insertions(+), 116 deletions(-) create mode 100644 src/App.scss create mode 100644 src/components/Loader/Loader.js create mode 100644 src/components/Loader/Loader.scss create mode 100644 src/components/index.js create mode 100644 src/config/ConfigLoader.js create mode 100644 src/config/Splash/Splash.js create mode 100644 src/config/Splash/Splash.scss create mode 100644 src/config/index.js create mode 100644 src/pages/MainPage/MainPage.js create mode 100644 src/pages/MainPage/MainPage.scss create mode 100644 src/remotes/index.js create mode 100644 src/store/index.js create mode 100644 src/store/middlewares.js create mode 100644 src/store/security/index.js create mode 100644 src/store/security/lib/models.js create mode 100644 src/store/security/lib/reducers.js create mode 100644 src/store/security/lib/selectors.js create mode 100644 src/store/security/lib/thunks.js create mode 100644 src/utils/index.js create mode 100644 src/utils/reduxUtils.js diff --git a/package.json b/package.json index daa6b1282..2f23265df 100644 --- a/package.json +++ b/package.json @@ -37,13 +37,16 @@ "@mui/lab": "^5.0.0-alpha.69", "@mui/material": "^5.4.2", "@mui/utils": "^5.4.2", + "@reduxjs/toolkit": "^1.8.0", "@testing-library/jest-dom": "^5.16.2", "apexcharts": "^3.33.1", + "axios": "^0.26.0", "change-case": "^4.1.2", "date-fns": "^2.28.0", "formik": "^2.2.9", "framer-motion": "^6.2.6", "history": "^5.2.0", + "jwt-decode": "^3.1.2", "lodash": "^4.17.21", "numeral": "^2.0.6", "prop-types": "^15.8.1", @@ -51,8 +54,11 @@ "react-apexcharts": "^1.3.9", "react-dom": "^17.0.2", "react-helmet-async": "^1.2.3", - "react-router-dom": "^6.2.1", + "react-redux": "^7.2.6", + "react-router-dom": "^6.2.2", "react-scripts": "^5.0.0", + "redux-persist": "^6.0.0", + "reselect": "^4.1.5", "simplebar": "^5.3.6", "simplebar-react": "^2.3.6", "web-vitals": "^2.1.4", diff --git a/src/App.js b/src/App.js index 36b6cefcc..76721fee9 100644 --- a/src/App.js +++ b/src/App.js @@ -1,21 +1,53 @@ -// routes -import Router from './routes' -// theme -import ThemeConfig from './theme' -import GlobalStyles from './theme/globalStyles' -// components -import ScrollToTop from './components/ScrollToTop' -import { BaseOptionChartStyle } from './components/charts/BaseOptionChart' +import { Component } from 'react' +import { connect } from 'react-redux' +import PropTypes from 'prop-types' +import { BrowserRouter, Switch, Route } from 'react-router-dom' +import { initSecurity, selectAuthModel } from '@src/store' +import { Loader } from '@src/components' +import { MainPage } from './pages' +import './App.scss' -// ---------------------------------------------------------------------- +class App extends Component { + static propTypes = { + auth: PropTypes.object, + initSecurity: PropTypes.func, + authModel: PropTypes.shape({ + loaded: PropTypes.bool, + loading: PropTypes.bool, + error: PropTypes.string, + payload: PropTypes.object, + }), + } -export default function App () { - return ( - - - - - - - ) + componentDidMount () { + const params = new URLSearchParams(window.location.search) + const authCode = params.get('code') + this.props.initSecurity(authCode) + } + + render () { + const { authModel } = this.props + + return ( + + + + { + authModel.loading || !authModel.loaded + ?
+ +
+ : + } +
+
+
+ ) + } } + +const mapStateToProps = state => ({ authModel: selectAuthModel(state) }) + +const mapDispatchToProps = { initSecurity } + +export default connect(mapStateToProps, mapDispatchToProps)(App) \ No newline at end of file diff --git a/src/App.scss b/src/App.scss new file mode 100644 index 000000000..a04e1d6cf --- /dev/null +++ b/src/App.scss @@ -0,0 +1,6 @@ +.app-loading { + display: flex row center center; + + width: 100vw; + height: 100vh; +} \ No newline at end of file diff --git a/src/components/Loader/Loader.js b/src/components/Loader/Loader.js new file mode 100644 index 000000000..feb1271ba --- /dev/null +++ b/src/components/Loader/Loader.js @@ -0,0 +1,27 @@ +import { Component } from 'react' +import PropTypes from 'prop-types' +import cn from 'classnames' +import './Loader.scss' + +export default class Loader extends Component { + static propTypes = { + className: PropTypes.string, + small: PropTypes.bool, + } + + render () { + const { className } = this.props + /** + * Loader from @link {https://codemyui.com/material-design-google-loader-in-css/} + */ + return ( +
+
+ + + +
+
+ ) + } +} \ No newline at end of file diff --git a/src/components/Loader/Loader.scss b/src/components/Loader/Loader.scss new file mode 100644 index 000000000..67a997944 --- /dev/null +++ b/src/components/Loader/Loader.scss @@ -0,0 +1,97 @@ +$green: #008744; +$blue: #0057E7; +$red: #D62D20; +$yellow: #FFA700; +$white: #EEE; + +// scaling... any units +$width: 100px; + +.loader { + position: relative; + width: $width; + margin: 0 auto; + + &::before { + display: block; + padding-top: 100%; + content: ""; + } +} + +.circular { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + margin: auto; + animation: rotate 2s linear infinite; + transform-origin: center center; +} + +.path { + animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite; + stroke-dasharray: 1, 200; + stroke-dashoffset: 0; + stroke-linecap: round; +} + +@keyframes rotate { + 100% { + transform: rotate(360deg); + } +} + +@keyframes dash { + 0% { + stroke-dasharray: 1, 200; + stroke-dashoffset: 0; + } + + 50% { + stroke-dasharray: 89, 200; + stroke-dashoffset: -35px; + } + + 100% { + stroke-dasharray: 89, 200; + stroke-dashoffset: -124px; + } +} + +@keyframes color { + 100%, + 0% { + stroke: $red; + } + + 40% { + stroke: $blue; + } + + 66% { + stroke: $green; + } + + 80%, + 90% { + stroke: $yellow; + } +} + +// demo-specific +body { + background-color: $white; +} + +.showbox { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 5%; +} \ No newline at end of file diff --git a/src/components/index.js b/src/components/index.js new file mode 100644 index 000000000..af45ec3ec --- /dev/null +++ b/src/components/index.js @@ -0,0 +1 @@ +export { default as Loader } from './Loader/Loader' \ No newline at end of file diff --git a/src/config/ConfigLoader.js b/src/config/ConfigLoader.js new file mode 100644 index 000000000..ef7b2c40c --- /dev/null +++ b/src/config/ConfigLoader.js @@ -0,0 +1,25 @@ +import React from 'react' +import { loadConfig } from './index' +import { remotesManager } from '@src/remotes' + +export default class ConfigLoader extends React.Component { + constructor (props) { + super(props) + + this.state = { isLoaded: false } + } + + componentDidMount = async () => { + const config = await loadConfig() + + remotesManager.initialize(config) + this.setState({ isLoaded: true }) + } + + render () { + if(!this.state.isLoaded) + return this.props.loading ? this.props.loading() : null + + return this.props.ready(this.state.config) + } +} \ No newline at end of file diff --git a/src/config/Splash/Splash.js b/src/config/Splash/Splash.js new file mode 100644 index 000000000..383a31952 --- /dev/null +++ b/src/config/Splash/Splash.js @@ -0,0 +1,11 @@ +import React from 'react' +import { Loader } from '@src/components' +import './Splash.scss' + +export default class Splash extends React.Component { + render () { + return
+ +
+ } +} \ No newline at end of file diff --git a/src/config/Splash/Splash.scss b/src/config/Splash/Splash.scss new file mode 100644 index 000000000..87ff5cf23 --- /dev/null +++ b/src/config/Splash/Splash.scss @@ -0,0 +1,7 @@ + +.root-splash { + display: flex row center center; + + width: 100vw; + height: 100vh; +} \ No newline at end of file diff --git a/src/config/index.js b/src/config/index.js new file mode 100644 index 000000000..d96879b81 --- /dev/null +++ b/src/config/index.js @@ -0,0 +1,19 @@ +const config = {} + +const loadConfig = () => { + return fetch('/config.json') + .then(res => res.json()) + .then((newconfig) => { + for(let prop in config) { + delete config[prop] + } + for(let prop in newconfig) { + config[prop] = newconfig[prop] + } + + return config + }) +} + +export { loadConfig } +export default config \ No newline at end of file diff --git a/src/index.js b/src/index.js index b13089ead..dee2101ac 100644 --- a/src/index.js +++ b/src/index.js @@ -1,21 +1,33 @@ -// scroll bar -import 'simplebar/src/simplebar.css' - +import React from 'react' +import { Provider } from 'react-redux' +import { PersistGate } from 'redux-persist/integration/react' import ReactDOM from 'react-dom' import { BrowserRouter } from 'react-router-dom' import { HelmetProvider } from 'react-helmet-async' -// +import { store, persistor } from '@src/store' +import ConfigLoader from '@src/config/ConfigLoader' +import Splash from '@src/config/Splash/Splash' +import './index.css' import App from './App' + import * as serviceWorker from './serviceWorker' import reportWebVitals from './reportWebVitals' - -// ---------------------------------------------------------------------- +import 'simplebar/src/simplebar.css' ReactDOM.render( - + + + + } + loading={() => } + /> + + + , , document.getElementById('root') @@ -27,4 +39,4 @@ serviceWorker.unregister() // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals() +reportWebVitals() \ No newline at end of file diff --git a/src/layouts/dashboard/DashboardSidebar.js b/src/layouts/dashboard/DashboardSidebar.js index 4f11240da..12ca05931 100644 --- a/src/layouts/dashboard/DashboardSidebar.js +++ b/src/layouts/dashboard/DashboardSidebar.js @@ -3,7 +3,7 @@ import { useEffect } from 'react' import { Link as RouterLink, useLocation } from 'react-router-dom' // material import { styled } from '@mui/material/styles' -import { Box, Link, Button, Drawer, Typography, Avatar, Stack } from '@mui/material' +import { Box, Link, Drawer, Typography, Avatar, Stack } from '@mui/material' // mocks_ import account from '../../_mocks_/account' // hooks @@ -90,28 +90,6 @@ export default function DashboardSidebar ({ isOpenSidebar, onCloseSidebar }) { spacing={3} sx={{ pt: 5, borderRadius: 2, position: 'relative' }} > - - - - - Get more? - - - From only $69 - - - - diff --git a/src/pages/MainPage/MainPage.js b/src/pages/MainPage/MainPage.js new file mode 100644 index 000000000..da57f9436 --- /dev/null +++ b/src/pages/MainPage/MainPage.js @@ -0,0 +1,19 @@ +import { Component } from 'react' +import { Router } from '@src/routes' +import { ThemeConfig } from '@src/theme' +import { GlobalStyles } from '@src/theme/globalStyles' +import { ScrollToTop } from '@src/components/ScrollToTop' +import { BaseOptionChartStyle } from '@src/components/charts/BaseOptionChart' + +export default class MainPage extends Component { + render () { + return ( + + + + + + + ) + } +} \ No newline at end of file diff --git a/src/pages/MainPage/MainPage.scss b/src/pages/MainPage/MainPage.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/remotes/index.js b/src/remotes/index.js new file mode 100644 index 000000000..8f9a1a108 --- /dev/null +++ b/src/remotes/index.js @@ -0,0 +1,55 @@ +import axios from 'axios' + +const withAuthorization = (authorization, config = {}) => ({ + ...config, + headers: { + ...config.headers, + Authorization: `Bearer ${authorization}`, + }, +}) + +class RemotesManager { + constructor (config) { + this.BACKEND_AUTH = null + this.BACKEND_USER = null + this.BACKEND_DATASETS = null + this.BACKEND_ACTIONS = null + this.BACKEND_API = null + } + + initialize (config) { + console.log('app config', config) + + this.BACKEND_AUTH = axios.create({ + baseURL: config.api.auth.uri, + timeout: 60000, + }) + + this.BACKEND_USER = axios.create({ + baseURL: config.api.user.uri, + timeout: 60000, + }) + + this.BACKEND_DATASETS = axios.create({ + baseURL: config.api.datasets.uri, + timeout: 60000, + }) + + this.BACKEND_ACTIONS = axios.create({ + baseURL: config.api.actions.uri, + timeout: 60000, + }) + + this.BACKEND_API = axios.create({ + baseURL: config.api.api.uri, + timeout: 60000, + }) + } +} + +const remotesManager = new RemotesManager() + +export { + remotesManager, + withAuthorization, +} \ No newline at end of file diff --git a/src/routes.js b/src/routes.js index c403761d3..489970cce 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,8 +1,8 @@ import { Navigate, useRoutes } from 'react-router-dom' -// layouts + import DashboardLayout from './layouts/dashboard' import LogoOnlyLayout from './layouts/LogoOnlyLayout' -// + import Login from './pages/Login' import Register from './pages/Register' import DashboardApp from './pages/DashboardApp' @@ -11,8 +11,6 @@ import Blog from './pages/Blog' import User from './pages/User' import NotFound from './pages/Page404' -// ---------------------------------------------------------------------- - export default function Router () { return useRoutes([ { diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 000000000..7c13c546e --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,26 @@ +import { configureStore } from '@reduxjs/toolkit' +import storage from 'redux-persist/lib/storage' +import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER } from 'redux-persist' + +import securityReducer from './security' +import { refreshJwtMiddleware, notificationsMiddleware } from './middlewares' + +const store = configureStore({ + reducer: { + security: persistReducer({ key: 'security', whitelist: ['auth'], storage }, securityReducer), + }, + middleware: getDefaultMiddleware => getDefaultMiddleware( + { + serializableCheck: false, + ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], + }, + ) + .concat(refreshJwtMiddleware) + .concat(notificationsMiddleware) + , +}) + +const persistor = persistStore(store) + +export { store, persistor } +export * from './security' diff --git a/src/store/middlewares.js b/src/store/middlewares.js new file mode 100644 index 000000000..4af58d87d --- /dev/null +++ b/src/store/middlewares.js @@ -0,0 +1,12 @@ + +export const refreshJwtMiddleware = ({ dispatch, getState }) => next => (action) => { + return next(action) +} + +export const notificationsMiddleware = ({ dispatch, getState }) => next => (action) => { + if (action.type.endsWith('/rejected')) { + console.error(action) + } + + return next(action) +} \ No newline at end of file diff --git a/src/store/security/index.js b/src/store/security/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/store/security/lib/models.js b/src/store/security/lib/models.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/store/security/lib/reducers.js b/src/store/security/lib/reducers.js new file mode 100644 index 000000000..71c7fd6c6 --- /dev/null +++ b/src/store/security/lib/reducers.js @@ -0,0 +1,19 @@ +import { getAuthUri } from '@src/utils' + +const reducers = { + clearSecurity: (state) => { + state.user = null + state.roles = null + state.auth = null + }, + + logout: (state) => { + state.user = null + state.roles = null + state.auth = null + + window.location.replace(getAuthUri({ redirectSuffix: window.location.pathname })) + }, +} + +export default reducers \ No newline at end of file diff --git a/src/store/security/lib/selectors.js b/src/store/security/lib/selectors.js new file mode 100644 index 000000000..0ea3f40c6 --- /dev/null +++ b/src/store/security/lib/selectors.js @@ -0,0 +1,17 @@ +import { createSelector } from 'reselect' + +export const selectSecurity = state => state.security +export const selectAuth = state => state.security.auth +export const selectUser = state => state.security.user +export const selectAccessToken = state => state.security.auth.access_token +export const selectRefreshToken = state => state?.security?.auth?.refresh_token + +export const selectAuthModel = createSelector( + selectSecurity, + security => ({ + loading: security.authLoading, + loaded: security.authLoaded, + error: security.authError, + payload: security.auth, + }), +) \ No newline at end of file diff --git a/src/store/security/lib/thunks.js b/src/store/security/lib/thunks.js new file mode 100644 index 000000000..2e94f58f5 --- /dev/null +++ b/src/store/security/lib/thunks.js @@ -0,0 +1,120 @@ +import jwt_decode from 'jwt-decode' +import { createAsyncThunk } from '@reduxjs/toolkit' +import { remotesManager } from '@src/remotes' +import { getAuthUri, getAuthParams, getRefreshParams, getLogoutParams } from '@src/utils' +import config from '@src/config' +import { clearSecurity, logout } from '../index' +import { selectRefreshToken } from './selectors' + +export const initSecurity = createAsyncThunk('security/init', + async (authCode, { dispatch, getState }) => { + const state = getState() + const refresh_token = selectRefreshToken(state) + const decoded = refresh_token && jwt_decode(refresh_token) + + if (decoded && (decoded.exp * 1000 - Date.now()) > 5000) { + // refresh token is not expired + const res = await dispatch(refreshToken()) + return res.payload + } + + await dispatch(clearSecurity()) + if (!authCode) + window.location.replace(getAuthUri({ redirectSuffix: window.location.pathname })) + + const { data } = await remotesManager.BACKEND_AUTH.post('/realms/master/protocol/openid-connect/token', new URLSearchParams({ + ...getAuthParams({ redirectSuffix: window.location.pathname }), + code: authCode, + }).toString(), { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + + return data + }, +) + +export const refreshToken = createAsyncThunk('security/refreshToken', + async (arg, { dispatch, getState }) => { + const state = getState() + const refresh_token = selectRefreshToken(state) + + try { + const { data } = await remotesManager.BACKEND_AUTH.post('/realms/master/protocol/openid-connect/token', new URLSearchParams({ + ...getRefreshParams({ redirectSuffix: window.location.pathname }), + refresh_token: refresh_token, + }).toString(), { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + return data + } catch (e) { + console.error('can not refresh token', e) + dispatch(logout()) + } + }, +) + +export const loadUser = createAsyncThunk( + 'security/loadUser', + async (arg, { dispatch, getState }) => { + const { data } = await remotesManager.BACKEND_USER.post('/session/set', getState().security.auth) + return data + }, +) + +export const logoutFromApp = createAsyncThunk('security/logoutFromApp', + async (arg, { dispatch, getState }) => { + const refresh_token = selectRefreshToken(getState()) + const redirectParams = new URLSearchParams({ + ...getLogoutParams(), + refresh_token: refresh_token, + }).toString() + await clearSecurity() + window.location.replace(config.api.auth.uri + '/realms/master/protocol/openid-connect/logout?' + redirectParams) + }, +) + +const extraReducers = { + [initSecurity.pending]: (state, action) => { + state.authLoading = true + state.authLoaded = false + state.authError = null + }, + [initSecurity.fulfilled]: (state, action) => { + state.authLoading = false + state.authLoaded = true + state.authError = null + state.auth = action.payload + }, + [initSecurity.rejected]: (state, action) => { + state.authLoading = false + state.authLoaded = false + state.authError = action.error + window.location.replace(getAuthUri({ redirectSuffix: window.location.pathname })) + }, + + [loadUser.fulfilled]: (state, action) => { + state.user = action.payload.user + state.roles = action.payload.roles + state.currentRole = action.payload.roles.find(r => r.is_main) + }, + [loadUser.rejected]: (state, action) => { + console.error('loadUser.rejected', action) + }, + + [refreshToken.fulfilled]: (state, action) => { + state.auth = action.payload + }, + [refreshToken.rejected]: (state, action) => { + console.error('refreshToken.rejected', action) + }, + + [logoutFromApp.fulfilled]: (state, action) => { + + }, +} + +export default extraReducers \ No newline at end of file diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 000000000..6e594cd8e --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,33 @@ +import config from '@src/config' + +export const getAuthParams = ({ redirectSuffix }) => { + return { + grant_type: 'authorization_code', + response_type: 'code', + client_id: config.api.auth.client_id, + redirect_uri: `${config.api.auth.redirect_uri}${redirectSuffix || ''}`, + } +} + +export const getRefreshParams = ({ redirectSuffix }) => { + return { + grant_type: 'refresh_token', + client_id: config.api.auth.client_id, + redirect_uri: `${config.api.auth.redirect_uri}${redirectSuffix || ''}`, + } +} + +export const getLogoutParams = () => { + return { + client_id: config.api.auth.client_id, + post_logout_redirect_uri: config.api.auth.redirect_uri, + } +} + +export const getAuthUri = ({ redirectSuffix }) => { + return `${config.api.auth.am_uri}?${new URLSearchParams(getAuthParams({ redirectSuffix })).toString()}` +} + +export const normalizeStr = str => str.toLowerCase().replace(/\s/g, '') + +export * from './reduxUtils' \ No newline at end of file diff --git a/src/utils/reduxUtils.js b/src/utils/reduxUtils.js new file mode 100644 index 000000000..2e4285f46 --- /dev/null +++ b/src/utils/reduxUtils.js @@ -0,0 +1,74 @@ +import _ from 'lodash' + +export const modelCreator = key => ({ + [key]: { + payload: null, + loading: false, + loaded: false, + error: null, + }, +}) + +export const extraCreator = (thunk, key) => { + return { + [thunk.pending]: (state, action) => { + state[key].loading = true + state[key].loaded = false + state[key].error = null + }, + [thunk.rejected]: (state, action) => { + state[key].loading = false + state[key].loaded = false + state[key].error = action.payload || action.error?.error || action.error?.message || `Error in ${thunk.rejected}` + }, + [thunk.fulfilled]: (state, action) => { + state[key].loading = false + state[key].loaded = true + state[key].error = null + state[key].payload = action.payload + }, + } +} + +export const extraNestedCreator = (thunk, stateKey, pathBuilder) => { + return { + [thunk.pending]: (state, action) => { + const keys = [stateKey, ...pathBuilder(action.meta.arg)] + _.set(state, keys, { + loading: true, + loaded: false, + error: null, + }) + }, + [thunk.rejected]: (state, action) => { + const keys = [stateKey, ...pathBuilder(action.meta.arg)] + _.set(state, keys, { + loading: false, + loaded: false, + error: action?.payload || action.error?.error || action.error?.message || `Error in ${thunk.rejected}`, + }) + }, + [thunk.fulfilled]: (state, action) => { + const keys = [stateKey, ...pathBuilder(action.meta.arg)] + _.set(state, keys, { + payload: action.payload, + loading: false, + loaded: true, + error: null, + }) + }, + } +} + +export const mapDataWithMeta = (data) => { + return data.rowMode === 'array' + ? data.data.map((e) => { + return e.reduce((acc, cur, index) => { + return { + ...acc, + [data.metaData[index].name]: cur, + } + }, {}) + }) + : data.data +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 03c20c3ca..16b5de0c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1668,6 +1668,16 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9" integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA== +"@reduxjs/toolkit@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.8.0.tgz#8ae875e481ed97e4a691aafa034f876bfd0413c4" + integrity sha512-cdfHWfcvLyhBUDicoFwG1u32JqvwKDxLxDd7zSmSoFw/RhYLOygIRtmaMjPRUUHmVmmAGAvquLLsKKU/677kSQ== + dependencies: + immer "^9.0.7" + redux "^4.1.2" + redux-thunk "^2.4.1" + reselect "^4.1.5" + "@rollup/plugin-babel@^5.2.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879" @@ -1977,6 +1987,14 @@ dependencies: "@types/node" "*" +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/html-minifier-terser@^6.0.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" @@ -2078,6 +2096,16 @@ dependencies: "@types/react" "*" +"@types/react-redux@^7.1.20": + version "7.1.22" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.22.tgz#0eab76a37ef477cc4b53665aeaf29cb60631b72a" + integrity sha512-GxIA1kM7ClU73I6wg9IRTVwSO9GS+SAKZKe0Enj+82HMU6aoESFU2HNAdNi3+J53IaOHPiUfT3kSG4L828joDQ== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + "@types/react-transition-group@^4.4.4": version "4.4.4" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e" @@ -2698,6 +2726,13 @@ axe-core@^4.3.5: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413" integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw== +axios@^0.26.0: + version "0.26.0" + resolved "https://npm.js.bars.group/axios/-/axios-0.26.0.tgz#9a318f1c69ec108f8cd5f3c3d390366635e13928" + integrity sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og== + dependencies: + follow-redirects "^1.14.8" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -3290,7 +3325,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -confusing-browser-globals@^1.0.10, confusing-browser-globals@^1.0.11: +confusing-browser-globals@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== @@ -4075,30 +4110,6 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-config-airbnb-base@^15.0.0: - version "15.0.0" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz#6b09add90ac79c2f8d723a2580e07f3925afd236" - integrity sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig== - dependencies: - confusing-browser-globals "^1.0.10" - object.assign "^4.1.2" - object.entries "^1.1.5" - semver "^6.3.0" - -eslint-config-airbnb@^19.0.4: - version "19.0.4" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz#84d4c3490ad70a0ffa571138ebcdea6ab085fdc3" - integrity sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew== - dependencies: - eslint-config-airbnb-base "^15.0.0" - object.assign "^4.1.2" - object.entries "^1.1.5" - -eslint-config-prettier@^8.4.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.4.0.tgz#8e6d17c7436649e98c4c2189868562921ef563de" - integrity sha512-CFotdUcMY18nGRo5KGsnNxpznzhkopOcOo0InID+sgQssPrzjvsyKZPvOgymTFeHrFuC3Tzdf2YndhXtULK9Iw== - eslint-config-react-app@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-7.0.0.tgz#0fa96d5ec1dfb99c029b1554362ab3fa1c3757df" @@ -4143,7 +4154,7 @@ eslint-plugin-flowtype@^8.0.3: lodash "^4.17.21" string-natural-compare "^3.0.1" -eslint-plugin-import@^2.25.3, eslint-plugin-import@^2.25.4: +eslint-plugin-import@^2.25.3: version "2.25.4" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz#322f3f916a4e9e991ac7af32032c25ce313209f1" integrity sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA== @@ -4187,19 +4198,12 @@ eslint-plugin-jsx-a11y@^6.5.1: language-tags "^1.0.5" minimatch "^3.0.4" -eslint-plugin-prettier@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz#8b99d1e4b8b24a762472b4567992023619cb98e0" - integrity sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ== - dependencies: - prettier-linter-helpers "^1.0.0" - eslint-plugin-react-hooks@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz#318dbf312e06fab1c835a4abef00121751ac1172" integrity sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA== -eslint-plugin-react@^7.27.1, eslint-plugin-react@^7.28.0: +eslint-plugin-react@^7.27.1: version "7.28.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz#8f3ff450677571a659ce76efc6d80b6a525adbdf" integrity sha512-IOlFIRHzWfEQQKcAD4iyYDndHwTQiCMcJVJjxempf203jnNLUnW34AXLrV33+nEXoifJE2ZEGmcjKPL8957eSw== @@ -4270,7 +4274,7 @@ eslint-webpack-plugin@^3.1.1: normalize-path "^3.0.0" schema-utils "^3.1.1" -eslint@^8.3.0, eslint@^8.9.0: +eslint@^8.3.0: version "8.9.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.9.0.tgz#a2a8227a99599adc4342fd9b854cb8d8d6412fdb" integrity sha512-PB09IGwv4F4b0/atrbcMFboF/giawbBLVC7fyDamk5Wtey4Jh2K+rYaBhCAbUyEI4QzB1ly09Uglc9iCtFaG2Q== @@ -4445,11 +4449,6 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-diff@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" - integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== - fast-glob@^3.2.11, fast-glob@^3.2.9: version "3.2.11" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" @@ -4596,7 +4595,7 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== -follow-redirects@^1.0.0: +follow-redirects@^1.0.0, follow-redirects@^1.14.8: version "1.14.9" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== @@ -5994,6 +5993,11 @@ jsonpointer@^5.0.0: array-includes "^3.1.3" object.assign "^4.1.2" +jwt-decode@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" + integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== + kind-of@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" @@ -7340,18 +7344,6 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prettier-linter-helpers@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" - integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== - dependencies: - fast-diff "^1.1.2" - -prettier@^2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" - integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== - pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" @@ -7569,23 +7561,35 @@ react-is@^17.0.1, react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-redux@^7.2.6: + version "7.2.6" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.6.tgz#49633a24fe552b5f9caf58feb8a138936ddfe9aa" + integrity sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ== + dependencies: + "@babel/runtime" "^7.15.4" + "@types/react-redux" "^7.1.20" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^17.0.2" + react-refresh@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== -react-router-dom@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.2.1.tgz#32ec81829152fbb8a7b045bf593a22eadf019bec" - integrity sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA== +react-router-dom@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.2.2.tgz#f1a2c88365593c76b9612ae80154a13fcb72e442" + integrity sha512-AtYEsAST7bDD4dLSQHDnk/qxWLJdad5t1HFa1qJyUrCeGgEuCSw0VB/27ARbF9Fi/W5598ujvJOm3ujUCVzuYQ== dependencies: history "^5.2.0" - react-router "6.2.1" + react-router "6.2.2" -react-router@6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.2.1.tgz#be2a97a6006ce1d9123c28934e604faef51448a3" - integrity sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg== +react-router@6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.2.2.tgz#495e683a0c04461eeb3d705fe445d6cf42f0c249" + integrity sha512-/MbxyLzd7Q7amp4gDOGaYvXwhEojkJD5BtExkuKmj39VEE0m3l/zipf6h2WIB2jyAO0lI6NGETh4RDcktRm4AQ== dependencies: history "^5.2.0" @@ -7706,6 +7710,23 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +redux-persist@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8" + integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ== + +redux-thunk@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.1.tgz#0dd8042cf47868f4b29699941de03c9301a75714" + integrity sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q== + +redux@^4.0.0, redux@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104" + integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw== + dependencies: + "@babel/runtime" "^7.9.2" + regenerate-unicode-properties@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56" @@ -7803,6 +7824,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +reselect@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.5.tgz#852c361247198da6756d07d9296c2b51eddb79f6" + integrity sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"