From 0dfdf51626ebdb879a10f9e6d420d6d4224f12b9 Mon Sep 17 00:00:00 2001 From: Nicolas Humbert Date: Tue, 6 Apr 2021 22:26:30 +0200 Subject: [PATCH] RINGX-116 shell UI integration --- README.md | 8 ++ cypress.json | 3 +- eve/main.yml | 10 ++ eve/ui-test-config-shell.json | 21 +++ eve/ui-test-config.json | 4 +- package-lock.json | 8 ++ package.json | 1 + public/assets/config-shell.json | 21 +++ public/assets/config.json | 4 +- public/assets/logo-dark.svg | 10 ++ src/js/config.js | 10 +- src/js/mock/userManager.js | 63 --------- src/js/userManager.js | 26 ---- src/react/App.js | 4 +- src/react/Auth.js | 69 ---------- src/react/Navbar.js | 105 +++++++++++++++ src/react/Routes.jsx | 92 +++++++------ src/react/ZenkoUI.js | 80 ++++++------ src/react/actions/__tests__/auth.test.js | 121 ++---------------- .../__tests__/utils/dispatchActionsList.js | 23 +--- src/react/actions/__tests__/utils/testUtil.js | 38 +----- src/react/actions/auth.js | 112 ++-------------- src/react/actions/index.js | 1 + src/react/actions/oidc.js | 11 ++ src/react/auth/Login.jsx | 40 ------ src/react/auth/LoginCallback.jsx | 22 ---- src/react/auth/LogoutCallback.jsx | 22 ---- src/react/auth/__tests__/Login.test.jsx | 25 ---- src/react/reducers/auth.js | 18 +-- src/react/reducers/index.js | 4 +- src/react/reducers/initialConstants.js | 7 +- src/react/reducers/oidc.js | 17 +++ src/react/ui-elements/ErrorHandlerModal.jsx | 25 ++-- src/react/ui-elements/ReauthDialog.jsx | 15 +-- .../__tests__/ErrorHandlerModal.test.jsx | 43 ++++--- src/types/actions.js | 34 +++-- src/types/auth.js | 12 -- src/types/entities.js | 10 +- src/types/state.js | 19 +-- 39 files changed, 411 insertions(+), 747 deletions(-) create mode 100644 eve/ui-test-config-shell.json create mode 100644 public/assets/config-shell.json create mode 100644 public/assets/logo-dark.svg delete mode 100644 src/js/mock/userManager.js delete mode 100644 src/js/userManager.js delete mode 100644 src/react/Auth.js create mode 100644 src/react/Navbar.js create mode 100644 src/react/actions/oidc.js delete mode 100644 src/react/auth/Login.jsx delete mode 100644 src/react/auth/LoginCallback.jsx delete mode 100644 src/react/auth/LogoutCallback.jsx delete mode 100644 src/react/auth/__tests__/Login.test.jsx create mode 100644 src/react/reducers/oidc.js diff --git a/README.md b/README.md index 916cdc6a3..3fd0b23b9 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,14 @@ sudo npm run start:dev *Note*: Regular users are not allowed to bind to port 80, ports below 1024 require root/adminstrator rights. You will need to either run as root using sudo, or setup a proxy that redirects requests on port 80 to a port over 1024. +Zenko UI now uses the metalk8s common navbar: +``` +git clone https://github.com/scality/metalk8s +cd metalk8s/shell-ui +docker build -t shell-ui . +docker run -d -p 8082:80 shell-ui +``` + ### Access UI ``` http://ui.zenko.local diff --git a/cypress.json b/cypress.json index f16691e24..3e57e61e2 100644 --- a/cypress.json +++ b/cypress.json @@ -1,4 +1,5 @@ { "baseUrl": "http://127.0.0.1:8383", - "video": false + "video": false, + "chromeWebSecurity": false } diff --git a/eve/main.yml b/eve/main.yml index 78cefa619..2ea6c66d2 100644 --- a/eve/main.yml +++ b/eve/main.yml @@ -8,6 +8,9 @@ branches: stage: post-merge models: + - env: &shell-ui-env + # TODO: update SHELL_UI_IMAGE to a more "official" image. + SHELL_UI_IMAGE: 'registry.scality.com/playground/jbwatenberg/shell-ui:2.9-dev' - env: &deploy-env SCALITY_OCI_REPO: registry.scality.com/zenko-ui/zenko-ui SCALITY_OCI_REPO_DEV: registry.scality.com/zenko-ui-dev/zenko-ui @@ -76,15 +79,21 @@ stages: command: npm run test:coverage haltOnFailure: True - ShellCommand: *npm-build + - ShellCommand: *docker-login - ShellCommand: *docker-build - ShellCommand: name: run end-to-end tests command: >- + docker pull ${SHELL_UI_IMAGE}; + docker run -d -p 8082:80 ${SHELL_UI_IMAGE}; + docker run -d -p 8383:8383 + -v $(pwd)/eve/ui-test-config-shell.json:/usr/share/nginx/html/config-shell.json -v $(pwd)/eve/ui-test-config.json:/usr/share/nginx/html/config.json -v $(pwd)/conf/zenko-ui-nginx.conf:/etc/nginx/conf.d/default.conf ${SCALITY_OCI_REPO_DEV}:%(prop:commit_short_revision)s; set -exu; + bash wait_for_local_port.bash 8082 40; bash wait_for_local_port.bash 8080 40; bash wait_for_local_port.bash 8383 40; CYPRESS_KEYCLOAK_USER_FULLNAME="${KEYCLOAK_USER_FIRSTNAME} ${KEYCLOAK_USER_LASTNAME}" @@ -97,6 +106,7 @@ stages: env: <<: *deploy-env <<: *keycloak-env + <<: *shell-ui-env - ShellCommand: name: upload test coverage haltOnFailure: False diff --git a/eve/ui-test-config-shell.json b/eve/ui-test-config-shell.json new file mode 100644 index 000000000..6dca39460 --- /dev/null +++ b/eve/ui-test-config-shell.json @@ -0,0 +1,21 @@ +{ + "docUrl": "/docs", + "oidc": { + "providerUrl": "http://127.0.0.1:8080/auth/realms/myrealm", + "redirectUrl": "http://127.0.0.1:8383/login/callback", + "clientId": "myclient", + "responseType": "code", + "scopes": "openid profile email roles" + }, + "options": { + "main": { + "http://127.0.0.1:8383/accounts":{ "en": "Accounts" }, + "http://127.0.0.1:8383/buckets":{ "en": "Data Browser" }, + "http://127.0.0.1:8383/workflows":{ "en": "Workflows" } + }, + "subLogin": {} + }, + "logo" : { "dark": "/logo-dark.svg" }, + "favicon": "" + } + \ No newline at end of file diff --git a/eve/ui-test-config.json b/eve/ui-test-config.json index ed2a2f63d..02e0bebb2 100644 --- a/eve/ui-test-config.json +++ b/eve/ui-test-config.json @@ -1,7 +1,7 @@ { + "navbarEndpoint": "http://127.0.0.1:8082/shell/solution-ui-navbar.1.0.0.js", + "navbarConfigUrl": "/config-shell.json", "managementEndpoint": "http://127.0.0.1:5000", - "oidcAuthority": "http://127.0.0.1:8080/auth/realms/myrealm", - "oidcClientId": "myclient", "stsEndpoint": "http://127.0.0.1:8383/sts", "zenkoEndpoint": "http://127.0.0.1:8383/s3" } diff --git a/package-lock.json b/package-lock.json index 107f421e6..c268b83a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12995,6 +12995,14 @@ "prop-types": "^15.7.2" } }, + "react-error-boundary": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.1.tgz", + "integrity": "sha512-W3xCd9zXnanqrTUeViceufD3mIW8Ut29BUD+S2f0eO2XCOU8b6UrJfY46RDGe5lxCJzfe4j0yvIfh0RbTZhKJw==", + "requires": { + "@babel/runtime": "^7.12.5" + } + }, "react-hook-form": { "version": "6.15.1", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.1.tgz", diff --git a/package.json b/package.json index ae8ddfcf2..0cab3652c 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "react-debounce-input": "3.2.0", "react-dom": "^16.12.0", "react-dropzone": "^11.2.0", + "react-error-boundary": "^3.1.1", "react-hook-form": "^6.3.3", "react-redux": "^7.1.3", "react-router-dom": "^5.1.2", diff --git a/public/assets/config-shell.json b/public/assets/config-shell.json new file mode 100644 index 000000000..0ddf6f7ca --- /dev/null +++ b/public/assets/config-shell.json @@ -0,0 +1,21 @@ +{ + "docUrl": "/docs", + "oidc": { + "providerUrl": "http://keycloak.zenko.local/auth/realms/zenko", + "redirectUrl": "http://ui.zenko.local", + "clientId": "zenko-ui", + "responseType": "code", + "scopes": "openid profile email roles" + }, + "options": { + "main": { + "http://ui.zenko.local/accounts":{ "en": "Accounts" }, + "http://ui.zenko.local/buckets":{ "en": "Data Browser" }, + "http://ui.zenko.local/workflows":{ "en": "Workflows" } + }, + "subLogin": {} + }, + "logo" : { "dark": "/logo-dark.svg" }, + "favicon": "", + "providerLogout": true +} diff --git a/public/assets/config.json b/public/assets/config.json index 565ec5d9a..87ec71056 100644 --- a/public/assets/config.json +++ b/public/assets/config.json @@ -1,7 +1,7 @@ { + "navbarEndpoint": "http://127.0.0.1:8082/shell/solution-ui-navbar.1.0.0.js", + "navbarConfigUrl": "/config-shell.json", "managementEndpoint": "http://management.zenko.local", - "oidcAuthority": "http://keycloak.zenko.local/auth/realms/zenko", - "oidcClientId": "zenko-ui", "stsEndpoint": "http://ui.zenko.local/sts", "zenkoEndpoint": "http://ui.zenko.local/s3" } diff --git a/public/assets/logo-dark.svg b/public/assets/logo-dark.svg new file mode 100644 index 000000000..34b5abec6 --- /dev/null +++ b/public/assets/logo-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/js/config.js b/src/js/config.js index 1e77224e5..fdbe186f3 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -4,14 +4,14 @@ export function getAppConfig() { return response.json(); }) .then(config => { - const { managementEndpoint, oidcAuthority, oidcClientId, stsEndpoint, zenkoEndpoint } = config; - if (!managementEndpoint || !oidcAuthority || !oidcClientId || !stsEndpoint || !zenkoEndpoint) { - throw new Error('incorrect or missing mandatory configuration information(s). (i.e. managementEndpoint, oidcAuthority, oidcClientId, stsEndpoint and zenkoEndpoint)'); + const { managementEndpoint, navbarConfigUrl, navbarEndpoint, stsEndpoint, zenkoEndpoint } = config; + if (!navbarEndpoint || !managementEndpoint || !stsEndpoint || !zenkoEndpoint) { + throw new Error('incorrect or missing mandatory configuration information(s). (i.e. managementEndpoint, navbarEndpoint, stsEndpoint and zenkoEndpoint)'); } return { managementEndpoint, - oidcAuthority, - oidcClientId, + navbarConfigUrl, + navbarEndpoint, stsEndpoint, zenkoEndpoint, }; diff --git a/src/js/mock/userManager.js b/src/js/mock/userManager.js deleted file mode 100644 index 5f781fd43..000000000 --- a/src/js/mock/userManager.js +++ /dev/null @@ -1,63 +0,0 @@ -// @flow -import type { AuthUser, UserManager as UserManagerInterface } from '../../types/auth'; -import { ApiErrorObject } from './error'; - -const authUser: AuthUser = { - id_token: 'id_token', - access_token: 'access_token', - token_type: 'bearer', - scope: 'openid profile email', - profile: { sub: 'uuid' }, - expires_at: 1596428238, - state: {}, -}; - -export class MockUserManager { - signinRedirect() { - return Promise.resolve(); - } - - signinRedirectCallback() { - return Promise.resolve(authUser); - } - - removeUser() { - return Promise.resolve(); - } - - signoutPopup() { - return Promise.resolve(); - } - - signoutPopupCallback() { - return Promise.resolve(); - } -} - -export class ErrorUserManager implements UserManagerInterface { - _error: ApiErrorObject; - - constructor(error: ApiErrorObject) { - this._error = error; - } - - signinRedirect() { - return Promise.reject(this._error); - } - - signinRedirectCallback() { - return Promise.reject(this._error); - } - - removeUser() { - return Promise.reject(this._error); - } - - signoutPopup() { - return Promise.reject(this._error); - } - - signoutPopupCallback() { - return Promise.reject(this._error); - } -} diff --git a/src/js/userManager.js b/src/js/userManager.js deleted file mode 100644 index 819565765..000000000 --- a/src/js/userManager.js +++ /dev/null @@ -1,26 +0,0 @@ -// @flow - -import type { UserManager as UserManagerInterface } from '../types/auth'; - -import { createUserManager } from 'redux-oidc'; - -type UserManagerType = { - oidcAuthority: string, - oidcClientId: string, -}; - -const currentEndpoint: string = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}`; - -export function makeUserManager({ oidcAuthority: authority, oidcClientId: clientId }: UserManagerType): UserManagerInterface { - const config = { - authority: authority, - client_id: clientId, - redirect_uri: `${currentEndpoint}/login/callback`, - post_logout_redirect_uri: `${currentEndpoint}/logout/callback`, - response_type: 'code', - scope: 'openid profile email roles', - automaticSilentRenew: true, - }; - - return createUserManager(config); -} diff --git a/src/react/App.js b/src/react/App.js index 06dc1189b..22494e023 100644 --- a/src/react/App.js +++ b/src/react/App.js @@ -1,12 +1,12 @@ import '../css/index.css'; import { history, store } from './store'; -import Auth from './Auth'; import { ConnectedRouter } from 'connected-react-router'; import { Provider } from 'react-redux'; import React from 'react'; import ReactDOM from 'react-dom'; import { ThemeProvider } from 'styled-components'; +import ZenkoUI from './ZenkoUI'; import { theme } from './theme'; // const whyDidYouRender = require('@welldone-software/why-did-you-render'); @@ -21,7 +21,7 @@ ReactDOM.render( - + , diff --git a/src/react/Auth.js b/src/react/Auth.js deleted file mode 100644 index 70c6ad746..000000000 --- a/src/react/Auth.js +++ /dev/null @@ -1,69 +0,0 @@ -// @flow - -import { Container, MainContainer } from './ui-elements/Container'; -import React, { useEffect } from 'react'; -import { Route, Switch } from 'react-router-dom'; -import { useDispatch, useSelector } from 'react-redux'; -import type { Action } from '../types/actions'; -import type { AppState } from '../types/state'; -import { Banner } from '@scality/core-ui'; -import type { DispatchAPI } from 'redux'; -import Loader from './ui-elements/Loader'; -import Login from './auth/Login'; -import LoginCallback from './auth/LoginCallback'; -import LogoutCallback from './auth/LogoutCallback'; -import { OidcProvider } from 'redux-oidc'; -import PrivateRoute from './ui-elements/PrivateRoute'; -import ReauthDialog from './ui-elements/ReauthDialog'; -import ZenkoUI from './ZenkoUI'; -import { loadAppConfig } from './actions'; -import { store } from './store'; - - -function Auth() { - const isUserLoaded = useSelector((state: AppState) => state.auth.isUserLoaded); - const userManager = useSelector((state: AppState) => state.auth.userManager); - const configFailure = useSelector((state: AppState) => state.auth.configFailure); - const errorMessage = useSelector((state: AppState) => state.uiErrors.errorType === 'byComponent' ? - state.uiErrors.errorMsg : ''); - - const dispatch: DispatchAPI = useDispatch(); - - useEffect(() => { - dispatch(loadAppConfig()); - },[dispatch]); - - function content() { - if (configFailure) { - return - } - title="Error: Unable to load the appplication" - variant="danger"> - {errorMessage} - - ; - } - - if (isUserLoaded) { - return - - - - - - - ; - } - - return Login in ; - } - - return - - {content()} - ; -} - - -export default Auth; diff --git a/src/react/Navbar.js b/src/react/Navbar.js new file mode 100644 index 000000000..0c945df29 --- /dev/null +++ b/src/react/Navbar.js @@ -0,0 +1,105 @@ +//@flow +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import type { AppState } from '../types/state'; +import { ErrorBoundary } from 'react-error-boundary'; +import { addOIDCUser } from './actions'; + +function useWebComponent(src?: string, customElementName: string) { + const [hasFailed, setHasFailed] = useState(false); + useLayoutEffect(() => { + const body = document.body; + const element = [...(body?.querySelectorAll('script') || [])].find( + // eslint-disable-next-line flowtype-errors/show-errors + (scriptElement) => scriptElement.attributes.src?.value === src, + ); + if (!element && body && src) { + const scriptElement = document.createElement('script'); + scriptElement.src = src; + scriptElement.onload = () => { + customElements.whenDefined(customElementName).catch(() => { + setHasFailed(true); + }); + }; + scriptElement.onerror = () => { + setHasFailed(true); + }; + body.appendChild(scriptElement); + } + }, [src]); + + if (hasFailed) { + throw new Error(`Failed to load component ${customElementName}`); + } +} + +type NavbarWebComponent = HTMLElement; + +function useLoginEffect(navbarRef: { current: NavbarWebComponent | null }) { + const dispatch = useDispatch(); + + useEffect(() => { + if (!navbarRef.current) { + return; + } + + const navbarElement = navbarRef.current; + + const onAuthenticated = (evt: Event) => { + // eslint-disable-next-line flowtype-errors/show-errors + if (evt.detail && evt.detail.profile) { + dispatch(addOIDCUser(evt.detail)); + } + }; + + navbarElement.addEventListener( + 'solutions-navbar--authenticated', + onAuthenticated, + ); + + return () => { + navbarElement.removeEventListener( + 'solutions-navbar--authenticated', + onAuthenticated, + ); + + }; + }, [navbarRef, dispatch]); +} + +function ErrorFallback({ error }: { error: Error }) { + return ( +
+

Something went wrong:

+
{error.message}
+
+ ); +} + +export function Navbar() { + return ( + + + + ); +} + +function InternalNavbar() { + const navbarEndpoint = useSelector((state: AppState) => state.auth.config.navbarEndpoint); + const navbarConfigUrl = useSelector((state: AppState) => state.auth.config.navbarConfigUrl); + useWebComponent(navbarEndpoint, 'solutions-navbar'); + + const navbarRef = useRef(null); + + useLoginEffect(navbarRef); + + return ( + + ); +} diff --git a/src/react/Routes.jsx b/src/react/Routes.jsx index 3c6a2db7b..3deaa3cdf 100644 --- a/src/react/Routes.jsx +++ b/src/react/Routes.jsx @@ -1,68 +1,62 @@ // @flow -import { Link, Redirect, Route, Switch, matchPath } from 'react-router-dom'; import { NavbarContainer, RouteContainer } from './ui-elements/Container'; +import React, { useEffect } from 'react'; +import { Redirect, Route, Switch } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import AccountCreate from './account/AccountCreate'; import Accounts from './account/Accounts'; -import type { Action } from '../types/actions'; import type { AppState } from '../types/state'; import DataBrowser from './databrowser/DataBrowser'; -import type { DispatchAPI } from 'redux'; +import Loader from './ui-elements/Loader'; import LocationEditor from './backend/location/LocationEditor'; -import { Navbar } from '@scality/core-ui'; +import { Navbar } from './Navbar'; import NoMatch from './NoMatch'; -import React from 'react'; import Workflows from './workflow/Workflows'; -import { signout } from './actions'; +import { loadClients } from './actions'; -function Routes() { - const pathname = useSelector((state: AppState) => state.router.location.pathname); - const userName = useSelector((state: AppState) => state.oidc.user.profile.name || ''); +function PrivateRoutes() { + const dispatch = useDispatch(); + + const isClientsLoaded = useSelector((state: AppState) => state.auth.isClientsLoaded); + const authenticated = useSelector((state: AppState) => !!state.oidc.user && !state.oidc.user.expired); - const dispatch: DispatchAPI = useDispatch(); + useEffect(() => { + if (authenticated) { + // TODO: forbid loading clients when authorization server redirects the user back to ui.zenko.local with an authorization code. + // That will fix management API request being canceled during autentication. + dispatch(loadClients()); + } + },[dispatch, authenticated]); + + if (!isClientsLoaded) { + return Loading clients ; + } return ( - - - , - text: `${userName}`, - type: 'dropdown', - items: [{ label: 'Log out', onClick: () => dispatch(signout()) } ], - }] - } - tabs={[ - { - link: Accounts, - selected: !!matchPath(pathname, { path: '/accounts/:accountName?' }) || - !!matchPath(pathname, { path: '/create-account' }), - }, - { - link: Data Browser, - selected: !!matchPath(pathname, { path: '/buckets' }) || - !!matchPath(pathname, { path: '/create-bucket' }), - }, - { - link: Workflow, - selected: !!matchPath(pathname, { path: '/workflows' }), - }, - ]} - /> - - - }/> + + }/> + + + + + + - - + - - + - - - - + + + ); +} + +function Routes() { + return ( + + + + + ); } diff --git a/src/react/ZenkoUI.js b/src/react/ZenkoUI.js index d7a6cb93c..becc95d40 100644 --- a/src/react/ZenkoUI.js +++ b/src/react/ZenkoUI.js @@ -1,54 +1,58 @@ // @flow - -import React, { useEffect, useState } from 'react'; -import { clearError, loadClients, loadInstanceLatestStatus, loadInstanceStats } from './actions'; +import { Container, MainContainer, ZenkoUIContainer } from './ui-elements/Container'; +import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import Activity from './ui-elements/Activity'; import type { AppState } from '../types/state'; +import { Banner } from '@scality/core-ui'; import ErrorHandlerModal from './ui-elements/ErrorHandlerModal'; import Loader from './ui-elements/Loader'; +import ReauthDialog from './ui-elements/ReauthDialog'; import Routes from './Routes'; -import { ZenkoUIContainer } from './ui-elements/Container'; +import { loadAppConfig } from './actions'; -function ZenkoUI() { - const [ loaded, setLoaded ] = useState(false); - const showError = useSelector((state: AppState) => !!state.uiErrors.errorMsg && state.uiErrors.errorType === 'byModal'); - const errorMessage = useSelector((state: AppState) => state.uiErrors.errorMsg); - const idToken = useSelector((state: AppState) => state.oidc.user.id_token); - const instanceIds = useSelector((state: AppState) => state.oidc.user.profile.instanceIds); +function ZenkoUI() { + const isConfigLoaded = useSelector((state: AppState) => state.auth.isConfigLoaded); + const configFailure = useSelector((state: AppState) => state.auth.configFailure); + const configFailureErrorMessage = useSelector((state: AppState) => state.uiErrors.errorType === 'byComponent' ? + state.uiErrors.errorMsg : ''); const dispatch = useDispatch(); - // When tokens are renewed, clients are updated with the new ID token. - useEffect(() => { - dispatch(loadClients()).then(() => { - setLoaded(true); - }); - }, [dispatch, idToken, instanceIds]); - useEffect(() => { - const refreshIntervalStatsUnit = setInterval( - () => dispatch(loadInstanceLatestStatus()), 10000); - const refreshIntervalStatsSeries = setInterval( - () => dispatch(loadInstanceStats()), 10000); - return () => { - clearInterval(refreshIntervalStatsUnit); - clearInterval(refreshIntervalStatsSeries); - }; - }, [dispatch]); - - return ( - - { loaded ? : Loading } - - dispatch(clearError())} > - {errorMessage} - - - ); + dispatch(loadAppConfig()); + },[dispatch]); + + function content() { + if (configFailure) { + return + } + title="Error: Unable to load the appplication" + variant="danger"> + {configFailureErrorMessage} + + ; + } + + if (isConfigLoaded) { + return ( + + + + + + ); + } + + return Login in ; + } + + return + + {content()} + ; } export default ZenkoUI; diff --git a/src/react/actions/__tests__/auth.test.js b/src/react/actions/__tests__/auth.test.js index 422980c93..7c455029c 100644 --- a/src/react/actions/__tests__/auth.test.js +++ b/src/react/actions/__tests__/auth.test.js @@ -4,17 +4,13 @@ import * as dispatchAction from './utils/dispatchActionsList'; import { APP_CONFIG, INSTANCE_ID, - USER_MANAGER_ERROR_MSG, - errorUserManagerState, initState, - signinRedirectCallbackState, testActionFunction, testDispatchFunction, } from './utils/testUtil'; import { MockManagementClient } from '../../../js/mock/managementClient'; import { MockSTSClient } from '../../../js/mock/STSClient'; -import { MockUserManager } from '../../../js/mock/userManager'; describe('auth actions', () => { const syncTests = [ @@ -28,11 +24,6 @@ describe('auth actions', () => { fn: actions.setSTSClient(new MockSTSClient), expectedActions: [dispatchAction.SET_STS_CLIENT_ACTION], }, - { - it: 'should return SET_USER_MANAGER action', - fn: actions.setUserManager(new MockUserManager), - expectedActions: [dispatchAction.SET_USER_MANAGER_ACTION], - }, { it: 'should return SET_APP_CONFIG action', fn: actions.setAppConfig(APP_CONFIG), @@ -44,121 +35,25 @@ describe('auth actions', () => { expectedActions: [dispatchAction.SELECT_INSTANCE_ACTION], }, { - it: 'should return LOAD_USER_SUCCESS action', - fn: actions.loadUserSuccess(), - expectedActions: [dispatchAction.LOAD_USER_SUCCESS_ACTION], + it: 'should return LOAD_CONFIG_SUCCESS action', + fn: actions.loadConfigSuccess(), + expectedActions: [dispatchAction.LOAD_CONFIG_SUCCESS_ACTION], + }, + { + it: 'should return LOAD_CLIENTS_SUCCESS action', + fn: actions.loadClientsSuccess(), + expectedActions: [dispatchAction.LOAD_CLIENTS_SUCCESS_ACTION], }, { it: 'should return CONFIG_AUTH_FAILURE action', fn: actions.configAuthFailure(), expectedActions: [dispatchAction.CONFIG_AUTH_FAILURE_ACTION], }, - { - it: 'should return SIGNOUT_START action', - fn: actions.signoutStart(), - expectedActions: [dispatchAction.SIGNOUT_START_ACTION], - }, - { - it: 'should return SIGNOUT_END action', - fn: actions.signoutEnd(), - expectedActions: [dispatchAction.SIGNOUT_END_ACTION], - }, ]; syncTests.forEach(testActionFunction); const asyncTests = [ - { - it: 'signin: should return expected actions', - fn: actions.signin(), - storeState: initState, - expectedActions: [], - }, - { - it: 'signin: should handle error', - fn: actions.signin(), - storeState: errorUserManagerState(), - expectedActions: [ - dispatchAction.HANDLE_ERROR_AUTH_ACTION(`Failed to redirect to the authorization endpoint: ${USER_MANAGER_ERROR_MSG}`), - dispatchAction.NETWORK_AUTH_FAILURE_ACTION, - ], - }, - { - it: 'signinCallback: should return expected actions', - fn: actions.signinCallback(), - storeState: initState, - expectedActions: [ - dispatchAction.LOCATION_PUSH_ACTION('/'), - ], - }, - { - it: 'signinCallback: should return push action with expected path "/data"', - fn: actions.signinCallback(), - storeState: signinRedirectCallbackState('/data'), - expectedActions: [ - dispatchAction.LOCATION_PUSH_ACTION('/data'), - ], - }, - { - it: 'signinCallback: should return push action with expected path "/" if state path set to "/login"', - fn: actions.signinCallback(), - storeState: signinRedirectCallbackState('/login'), - expectedActions: [ - dispatchAction.LOCATION_PUSH_ACTION('/'), - ], - }, - { - it: 'signinCallback: should return push action with expected path "/" if state path set to "/login/callback"', - fn: actions.signinCallback(), - storeState: signinRedirectCallbackState('/login/callback'), - expectedActions: [ - dispatchAction.LOCATION_PUSH_ACTION('/'), - ], - }, - { - it: 'signinCallback: should handle error', - fn: actions.signinCallback(), - storeState: errorUserManagerState(), - expectedActions: [ - dispatchAction.HANDLE_ERROR_AUTH_ACTION(`Failed to remove the authenticated user from the session after authencation callback failed: ${USER_MANAGER_ERROR_MSG}`), - dispatchAction.NETWORK_AUTH_FAILURE_ACTION, - ], - }, - { - it: 'signout: should return expected actions', - fn: actions.signout(), - storeState: initState, - expectedActions: [ - dispatchAction.SIGNOUT_START_ACTION, - dispatchAction.SIGNOUT_END_ACTION, - ], - }, - { - it: 'signout: should handle error', - fn: actions.signout(), - storeState: errorUserManagerState(), - expectedActions: [ - dispatchAction.SIGNOUT_START_ACTION, - dispatchAction.HANDLE_ERROR_AUTH_ACTION(`Failed to sign out: ${USER_MANAGER_ERROR_MSG}`), - dispatchAction.NETWORK_AUTH_FAILURE_ACTION, - dispatchAction.SIGNOUT_END_ACTION, - ], - }, - { - it: 'signoutCallback: should return expected actions', - fn: actions.signoutCallback(), - storeState: initState, - expectedActions: [], - }, - { - it: 'signoutCallback: should handle error', - fn: actions.signoutCallback(), - storeState: errorUserManagerState(), - expectedActions: [ - dispatchAction.HANDLE_ERROR_AUTH_ACTION(`An error occurred during the logout process: ${USER_MANAGER_ERROR_MSG}`), - dispatchAction.NETWORK_AUTH_FAILURE_ACTION, - ], - }, { it: 'loadClients: should handle error if user is not authenticated', fn: actions.loadClients(), diff --git a/src/react/actions/__tests__/utils/dispatchActionsList.js b/src/react/actions/__tests__/utils/dispatchActionsList.js index 69cd3fd27..d95e665b0 100644 --- a/src/react/actions/__tests__/utils/dispatchActionsList.js +++ b/src/react/actions/__tests__/utils/dispatchActionsList.js @@ -21,7 +21,8 @@ import type { HandleErrorAction, ListBucketsSuccessAction, ListObjectsSuccessAction, - LoadUserSuccessAction, + LoadClientsSuccessAction, + LoadConfigSuccessAction, NetworkActivityAuthFailureAction, NetworkActivityEndAction, NetworkActivityStartAction, @@ -36,10 +37,7 @@ import type { SetAppConfigAction, SetManagementClientAction, SetSTSClientAction, - SetUserManagerAction, SetZenkoClientAction, - SignoutEndAction, - SignoutStartAction, ToggleAllObjectsAction, ToggleObjectAction, ZenkoAppendSearchListAction, @@ -59,7 +57,6 @@ import { CALL_HISTORY_METHOD } from 'connected-react-router'; import type { LocationName } from '../../../../types/config'; import { MockManagementClient } from '../../../../js/mock/managementClient'; import { MockSTSClient } from '../../../../js/mock/STSClient'; -import { MockUserManager } from '../../../../js/mock/userManager'; // auth actions export const SET_MANAGEMENT_CLIENT_ACTION: SetManagementClientAction = @@ -72,27 +69,21 @@ export function SET_ZENKO_CLIENT_ACTION(zenkoClient: ZenkoClientInterface): SetZ export const SET_STS_CLIENT_ACTION: SetSTSClientAction = { type: 'SET_STS_CLIENT', stsClient: new MockSTSClient() }; -export const SET_USER_MANAGER_ACTION: SetUserManagerAction = - { type: 'SET_USER_MANAGER', userManager: new MockUserManager() }; - export const SET_APP_CONFIG_ACTION: SetAppConfigAction = { type: 'SET_APP_CONFIG', config: APP_CONFIG }; export const SELECT_INSTANCE_ACTION: SelectInstanceAction = { type: 'SELECT_INSTANCE', selectedId: INSTANCE_ID }; -export const LOAD_USER_SUCCESS_ACTION: LoadUserSuccessAction = - { type: 'LOAD_USER_SUCCESS' }; +export const LOAD_CONFIG_SUCCESS_ACTION: LoadConfigSuccessAction = + { type: 'LOAD_CONFIG_SUCCESS' }; + +export const LOAD_CLIENTS_SUCCESS_ACTION: LoadClientsSuccessAction = + { type: 'LOAD_CLIENTS_SUCCESS' }; export const CONFIG_AUTH_FAILURE_ACTION: ConfigAuthFailureAction = { type: 'CONFIG_AUTH_FAILURE' }; -export const SIGNOUT_START_ACTION: SignoutStartAction = - { type: 'SIGNOUT_START' }; - -export const SIGNOUT_END_ACTION: SignoutEndAction = - { type: 'SIGNOUT_END' }; - // * error action export function HANDLE_ERROR_MODAL_ACTION(errorMsg: string): HandleErrorAction { return { type: 'HANDLE_ERROR', errorMsg, errorType: 'byModal' }; diff --git a/src/react/actions/__tests__/utils/testUtil.js b/src/react/actions/__tests__/utils/testUtil.js index 5b33a4553..b46b85a20 100644 --- a/src/react/actions/__tests__/utils/testUtil.js +++ b/src/react/actions/__tests__/utils/testUtil.js @@ -1,6 +1,5 @@ // @flow import { ErrorMockManagementClient, account, latestOverlay, location } from '../../../../js/mock/managementClient'; -import { ErrorUserManager, MockUserManager } from '../../../../js/mock/userManager'; import { bucketInfoResponse, bucketName, @@ -48,10 +47,10 @@ export const mockStore = () => configureStore([thunk]); export const APP_CONFIG = { managementEndpoint: 'https://managementEndpoint', - oidcAuthority: 'oidcAuthority', - oidcClientId: 'oidcClientId', stsEndpoint: 'https://stsEndpoint', s3Endpoint: 'https://s3Endpoint', + navbarEndpoint: 'https://navbarEndpoint', + navbarConfigUrl: 'https://navbarConfigUrl', }; export const INSTANCE_ID = '3d49e1f9-fa2f-40aa-b2d4-c7a8b04c6cde'; @@ -87,17 +86,6 @@ export const TAGS = tags; export const ZENKO_ERROR = zenkoError; export const BUCKET_INFO_RESPONSE = bucketInfoResponse; -export function errorUserManagerState(): AppState { - const state = initState; - return { - ...state, - auth: { - ...state.auth, - userManager: new ErrorUserManager(USER_MANAGER_ERROR), - }, - }; -} - export function errorManagementState(): AppState { const state = initState; return { @@ -132,27 +120,6 @@ export function addNextMarkerToState(state: AppState): AppState { }; } -export function signinRedirectCallbackState(path: string): AppState { - const state = initState; - const userManager = new MockUserManager(); - /*eslint-disable flowtype-errors/show-errors*/ - userManager.signinRedirectCallback = () => { - return Promise.resolve({ - state: { - path, - }, - }); - }; - /*eslint-enable */ - return { - ...state, - auth: { - ...state.auth, - userManager, - }, - }; -} - export function authenticatedUserState(): AppState { const state = initState; return { @@ -190,7 +157,6 @@ export function authenticatedUserState(): AppState { path: '/', }, }, - isLoadingUser: false, }, }; } diff --git a/src/react/actions/auth.js b/src/react/actions/auth.js index 23b5f3e3f..574456aab 100644 --- a/src/react/actions/auth.js +++ b/src/react/actions/auth.js @@ -4,13 +4,11 @@ import type { AppConfig, InstanceId } from '../../types/entities'; import type { ConfigAuthFailureAction, - LoadUserSuccessAction, + LoadClientsSuccessAction, + LoadConfigSuccessAction, SetAppConfigAction, SetManagementClientAction, SetSTSClientAction, - SetUserManagerAction, - SignoutEndAction, - SignoutStartAction, ThunkNonStateAction, ThunkStatePromisedAction, } from '../../types/actions'; @@ -20,15 +18,10 @@ import type { ManagementClient as ManagementClientInterface } from '../../types/ import STSClient from '../../js/STSClient'; import type { STSClient as STSClientInterface } from '../../types/sts'; -import type { UserManager as UserManagerInterface } from '../../types/auth'; import ZenkoClient from '../../js/ZenkoClient'; import { getAppConfig } from '../../js/config'; -import { loadUser } from 'redux-oidc'; import makeMgtClient from '../../js/managementClient'; -import { makeUserManager } from '../../js/userManager'; -import { push } from 'connected-react-router'; -import { store } from '../store'; export function setManagementClient(managementClient: ManagementClientInterface): SetManagementClientAction { return { @@ -44,13 +37,6 @@ export function setSTSClient(stsClient: STSClientInterface): SetSTSClientAction }; } -export function setUserManager(userManager: UserManagerInterface): SetUserManagerAction { - return { - type: 'SET_USER_MANAGER', - userManager, - }; -} - export function setAppConfig(config: AppConfig): SetAppConfigAction { return { type: 'SET_APP_CONFIG', @@ -65,94 +51,21 @@ export function selectInstance(selectedId: InstanceId) { }; } -export function loadUserSuccess(): LoadUserSuccessAction { +export function loadConfigSuccess(): LoadConfigSuccessAction { return { - type: 'LOAD_USER_SUCCESS', + type: 'LOAD_CONFIG_SUCCESS', }; } -export function configAuthFailure(): ConfigAuthFailureAction { +export function loadClientsSuccess(): LoadClientsSuccessAction { return { - type: 'CONFIG_AUTH_FAILURE', + type: 'LOAD_CLIENTS_SUCCESS', }; } -export function signoutStart(): SignoutStartAction { - return { - type: 'SIGNOUT_START', - }; -} - -export function signoutEnd(): SignoutEndAction { +export function configAuthFailure(): ConfigAuthFailureAction { return { - type: 'SIGNOUT_END', - }; -} - -export function signin(pathname: string): ThunkStatePromisedAction { - return (dispatch, getState) => { - const userManager = getState().auth.userManager; - return userManager.signinRedirect({ state: { path: pathname } }) - .catch(error => { - const message = `Failed to redirect to the authorization endpoint: ${error.message || '(unknown reason)'}`; - dispatch(handleErrorMessage(message, 'byAuth')); - dispatch(networkAuthFailure()); - }); - }; -} - -export function signinCallback(): ThunkStatePromisedAction { - return (dispatch, getState) => { - const userManager = getState().auth.userManager; - return userManager.signinRedirectCallback() - .then(user => { - const path = user.state && - user.state.path && - user.state.path !== '/login' && - user.state.path !== '/login/callback' ? user.state.path : '/'; - dispatch(push(path)); - }) - .catch(error => { - // NOTE: removeUser() method is needed to trigger a new "log in" proccess - // issue under investigation here: https://github.com/IdentityModel/oidc-client-js/issues/965 - return userManager.removeUser() - .then(() => { - const message = `Failed to process response from the authorization endpoint: ${error.message || '(unknown reason)'}`; - dispatch(handleErrorMessage(message, 'byAuth')); - dispatch(networkAuthFailure()); - }) - .catch(error => { - const message = `Failed to remove the authenticated user from the session after authencation callback failed: ${error.message || '(unknown reason)'}`; - dispatch(handleErrorMessage(message, 'byAuth')); - dispatch(networkAuthFailure()); - }); - }); - }; -} - -export function signout(): ThunkStatePromisedAction { - return (dispatch, getState) => { - dispatch(signoutStart()); - const userManager = getState().auth.userManager; - return userManager.signoutPopup() - .catch(error => { - const message = `Failed to sign out: ${error.message || '(unknown reason)'}`; - dispatch(handleErrorMessage(message, 'byAuth')); - dispatch(networkAuthFailure()); - }) - .finally(() => dispatch(signoutEnd())); - }; -} - -export function signoutCallback(): ThunkStatePromisedAction { - return (dispatch, getState) => { - const userManager = getState().auth.userManager; - return userManager.signoutPopupCallback() - .catch(error => { - const message = `An error occurred during the logout process: ${error.message || '(unknown reason)'}`; - dispatch(handleErrorMessage(message, 'byAuth')); - dispatch(networkAuthFailure()); - }); + type: 'CONFIG_AUTH_FAILURE', }; } @@ -161,14 +74,9 @@ export function loadAppConfig(): ThunkNonStateAction { return getAppConfig() .then(config => { dispatch(setAppConfig(config)); - const userManager = makeUserManager(config); - dispatch(setUserManager(userManager)); dispatch(setSTSClient(new STSClient({ endpoint: config.stsEndpoint }))); dispatch(setZenkoClient(new ZenkoClient(config.zenkoEndpoint))); - return loadUser(store, userManager); - }) - .then(() => { - dispatch(loadUserSuccess()); + dispatch(loadConfigSuccess()); }) .catch(error => { const message = `Invalid application configuration: ${error.message || '(unknown reason)'}`; @@ -178,7 +86,6 @@ export function loadAppConfig(): ThunkNonStateAction { }; } -// loadClients is called when the ID token gets renewed prior to its expiration. export function loadClients(): ThunkStatePromisedAction { return (dispatch, getState) => { const { oidc, auth: { config } } = getState(); @@ -202,6 +109,7 @@ export function loadClients(): ThunkStatePromisedAction { ]); }) .then(() => dispatch(assumeRoleWithWebIdentity())) + .then(() => dispatch(loadClientsSuccess())) .catch(error => { if (error.message) { dispatch(handleErrorMessage(error.message, 'byAuth')); diff --git a/src/react/actions/index.js b/src/react/actions/index.js index d3741283c..62faa1a13 100644 --- a/src/react/actions/index.js +++ b/src/react/actions/index.js @@ -4,6 +4,7 @@ export * from './configuration'; export * from './error'; export * from './location'; export * from './network'; +export * from './oidc'; export * from './replication'; export * from './secrets'; export * from './s3bucket'; diff --git a/src/react/actions/oidc.js b/src/react/actions/oidc.js new file mode 100644 index 000000000..f6fdb47d2 --- /dev/null +++ b/src/react/actions/oidc.js @@ -0,0 +1,11 @@ +// @flow + +import type { AuthUser } from '../../types/auth'; +import type { OIDCAction } from '../../types/actions'; + +export function addOIDCUser(user: AuthUser): OIDCAction { + return { + type: 'ADD_OIDC_USER', + user, + }; +} diff --git a/src/react/auth/Login.jsx b/src/react/auth/Login.jsx deleted file mode 100644 index 6e1b94d25..000000000 --- a/src/react/auth/Login.jsx +++ /dev/null @@ -1,40 +0,0 @@ -// @flow - -import { Container, ContainerFooter } from '../ui-elements/Container'; -import React, { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import type { Action } from '../../types/actions'; -import type { AppState } from '../../types/state'; -import { Button } from '@scality/core-ui'; -import type { DispatchAPI } from 'redux'; -import Loader from '../ui-elements/Loader'; -import { push } from 'connected-react-router'; -import { signin } from '../actions'; - -function Login() { - const dispatch: DispatchAPI = useDispatch(); - const authenticated = useSelector((state: AppState) => !!state.oidc.user && !state.oidc.user.expired); - const isSigningOut = useSelector((state: AppState) => !!state.auth.isSigningOut); - const path = useSelector((state: AppState) => state.router.location && state.router.location.state && state.router.location.state.path); - - useEffect(() => { - if (!authenticated && !isSigningOut) { - dispatch(signin(path)); - } - }); - - return
- { - authenticated ? - - You are already logged in. - -
; -} - -export default Login; diff --git a/src/react/auth/LoginCallback.jsx b/src/react/auth/LoginCallback.jsx deleted file mode 100644 index f89761404..000000000 --- a/src/react/auth/LoginCallback.jsx +++ /dev/null @@ -1,22 +0,0 @@ -// @flow - -import React, { useEffect } from 'react'; -import type { Action } from '../../types/actions'; -import type { DispatchAPI } from 'redux'; -import Loader from '../ui-elements/Loader'; -import { signinCallback } from '../actions'; -import { useDispatch } from 'react-redux'; - -function LoginCallback() { - const dispatch: DispatchAPI = useDispatch(); - - useEffect(() => { - dispatch(signinCallback()); - }); - - return ( - Redirecting - ); -} - -export default LoginCallback; diff --git a/src/react/auth/LogoutCallback.jsx b/src/react/auth/LogoutCallback.jsx deleted file mode 100644 index c83fb3704..000000000 --- a/src/react/auth/LogoutCallback.jsx +++ /dev/null @@ -1,22 +0,0 @@ -// @flow - -import React, { useEffect } from 'react'; -import type { Action } from '../../types/actions'; -import type { DispatchAPI } from 'redux'; -import Loader from '../ui-elements/Loader'; -import { signoutCallback } from '../actions'; -import { useDispatch } from 'react-redux'; - -function LogoutCallback() { - const dispatch: DispatchAPI = useDispatch(); - - useEffect(() => { - dispatch(signoutCallback()); - }); - - return - You are getting logged out - ; -} - -export default LogoutCallback; diff --git a/src/react/auth/__tests__/Login.test.jsx b/src/react/auth/__tests__/Login.test.jsx deleted file mode 100644 index a9debbacb..000000000 --- a/src/react/auth/__tests__/Login.test.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import Login from '../Login'; -import React from 'react'; -import { reduxMount } from '../../utils/test'; - -describe('Login', () => { - it('should render loader with redirect message if user is not authenticated', () => { - const { component } = reduxMount(, { - router: {}, - }); - - expect(component.find('Login').text()).toContain('Redirecting to the login in page'); - }); - - it('should render "You are already logged in" message if user is already authenticated', () => { - const { component } = reduxMount(, { - oidc: { - user: { expired: false }, - isLoadingUser: false, - }, - router: {}, - }); - - expect(component.find('Login').text()).toContain('You are already logged in'); - }); -}); diff --git a/src/react/reducers/auth.js b/src/react/reducers/auth.js index 6146ec17d..0831f12ba 100644 --- a/src/react/reducers/auth.js +++ b/src/react/reducers/auth.js @@ -15,11 +15,6 @@ export default function auth(state: AuthState = initialAuthState, action: AuthAc ...state, managementClient: action.managementClient, }; - case 'SET_USER_MANAGER': - return { - ...state, - userManager: action.userManager, - }; case 'SET_APP_CONFIG': return { ...state, @@ -30,20 +25,15 @@ export default function auth(state: AuthState = initialAuthState, action: AuthAc ...state, configFailure: true, }; - case 'LOAD_USER_SUCCESS': - return { - ...state, - isUserLoaded: true, - }; - case 'SIGNOUT_START': + case 'LOAD_CONFIG_SUCCESS': return { ...state, - isSigningOut: true, + isConfigLoaded: true, }; - case 'SIGNOUT_END': + case 'LOAD_CLIENTS_SUCCESS': return { ...state, - isSigningOut: false, + isClientsLoaded: true, }; default: return state; diff --git a/src/react/reducers/index.js b/src/react/reducers/index.js index a318170b6..d0db7bd31 100644 --- a/src/react/reducers/index.js +++ b/src/react/reducers/index.js @@ -6,7 +6,7 @@ import { connectRouter } from 'connected-react-router'; import instanceStatus from './instanceStatus'; import instances from './instances'; import networkActivity from './networkActivity'; -import { reducer as oidcReducer } from 'redux-oidc'; +import oidc from './oidc'; import s3 from './s3'; import secrets from './secrets'; import stats from './stats'; @@ -38,7 +38,7 @@ const zenkoUIReducer = history => combineReducers({ s3, secrets, stats, - oidc: oidcReducer, + oidc, router: connectRouter(history), zenko: zenko, }); diff --git a/src/react/reducers/initialConstants.js b/src/react/reducers/initialConstants.js index ff0241e19..9d18fd299 100644 --- a/src/react/reducers/initialConstants.js +++ b/src/react/reducers/initialConstants.js @@ -18,7 +18,6 @@ import { List, Map } from 'immutable'; import { LIST_OBJECTS_S3_TYPE } from '../../react/utils/s3'; import { MockManagementClient } from '../../js/mock/managementClient'; import { MockSTSClient } from '../../js/mock/STSClient'; -import { MockUserManager } from '../../js/mock/userManager'; import { MockZenkoClient } from '../../js/mock/ZenkoClient'; export const initialAccountState: AccountState = { @@ -28,12 +27,11 @@ export const initialAccountState: AccountState = { export const initialAccountsUIState: AccountsUIState = { showDelete: false }; export const initialAuthState: AuthState = { - isUserLoaded: false, + isConfigLoaded: false, + isClientsLoaded: false, configFailure: false, - isSigningOut: false, stsClient: new MockSTSClient(), managementClient: new MockManagementClient(), - userManager: new MockUserManager(), config: { }, }; @@ -152,7 +150,6 @@ export const initialLocationsUIState = { showDeleteLocation: '' }; export const initialNetworkActivityState: NetworkActivityState = { counter: 0, messages: List() }; export const initialOidc: OIDCState = { user: null, - isLoadingUser: false, }; export const initialObjectUIState: ObjectsUIState = { diff --git a/src/react/reducers/oidc.js b/src/react/reducers/oidc.js new file mode 100644 index 000000000..d1bde7f60 --- /dev/null +++ b/src/react/reducers/oidc.js @@ -0,0 +1,17 @@ +// @flow +import type { OIDCAction } from '../../types/actions'; +import type { OIDCState } from '../../types/state'; + +import { initialNetworkActivityState } from './initialConstants'; + +export default function oidc(state: OIDCState=initialNetworkActivityState, action: OIDCAction) { + switch (action.type) { + case 'ADD_OIDC_USER': + return { + ...state, + user: action.user, + }; + default: + return state; + } +} diff --git a/src/react/ui-elements/ErrorHandlerModal.jsx b/src/react/ui-elements/ErrorHandlerModal.jsx index 0241e16a7..d7e25abd3 100644 --- a/src/react/ui-elements/ErrorHandlerModal.jsx +++ b/src/react/ui-elements/ErrorHandlerModal.jsx @@ -1,27 +1,28 @@ // @noflow - +import { useDispatch, useSelector } from 'react-redux'; +import type { Action } from '../../types/actions'; +import type { AppState } from '../../types/state'; import { Button } from '@scality/core-ui'; +import type { DispatchAPI } from 'redux'; import { CustomModal as Modal } from './Modal'; -import type { Node } from 'react'; import React from 'react'; +import { clearError } from '../actions'; -type Props = { - children: Node, - close: () => void, - show: boolean, -}; +const ErrorHandlerModal = () => { + const showError = useSelector((state: AppState) => !!state.uiErrors.errorMsg && state.uiErrors.errorType === 'byModal'); + const errorMessage = useSelector((state: AppState) => state.uiErrors.errorMsg); + const dispatch: DispatchAPI = useDispatch(); -const ErrorHandlerModal = ({ children, close, show }: Props) => { - if (!children) { + if (!showError) { return null; } return ( dispatch(clearError())} footer={