From 74be2e1bb135639ae8eb1eb7a25c107ef74bc9ae Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Mon, 11 Dec 2023 11:33:53 +0300 Subject: [PATCH 1/9] feat(arcgis): auth slice --- .env | 2 +- package.json | 2 + src/app.tsx | 1 + .../layers-control-panel.spec.tsx | 21 +++--- .../layers-panel/layers-control-panel.tsx | 43 ++++++++++- .../arcgis-auth-popup/arcgis-auth-popup.tsx | 58 +++++++++++++++ src/pages/index.ts | 1 + src/redux/slices/arcgis-auth-slice.ts | 46 ++++++++++++ src/redux/store.ts | 3 + src/utils/arcgis-auth.ts | 72 +++++++++++++++++++ webpack.dev.config.js | 1 + 11 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 src/pages/arcgis-auth-popup/arcgis-auth-popup.tsx create mode 100644 src/redux/slices/arcgis-auth-slice.ts create mode 100644 src/utils/arcgis-auth.ts diff --git a/.env b/.env index 756b2061..8f85508b 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -REACT_APP_ARCGIS_REST_CLIENT_ID=abcdef +REACT_APP_ARCGIS_REST_CLIENT_ID=abcd diff --git a/package.json b/package.json index 9938aaa7..bd93774c 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "author": "", "license": "ISC", "dependencies": { + "@esri/arcgis-rest-auth": "^3.7.0", + "@esri/arcgis-rest-request": "^4.2.0", "@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.17", diff --git a/src/app.tsx b/src/app.tsx index 9030c74f..0506380e 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -142,6 +142,7 @@ export const App = () => { } /> } /> } /> + } /> { +const callRender = (renderFunc, props = {}, store = setupStore()) => { return renderFunc( { onPointToLayer={onPointToLayerMock} deleteLayer={onDeleteLayerMock} {...props} - /> + />, + store ); }; describe("Layers Control Panel", () => { it("Should render LayersControlPanel without layers", () => { - const { container } = callRender(renderWithTheme); + const store = setupStore(); + const { container } = callRender(renderWithThemeProviders, undefined, store); expect(container).toBeInTheDocument(); // Insert Buttons should be present @@ -68,7 +71,8 @@ describe("Layers Control Panel", () => { }); it("Should render LayersControlPanel with layers", () => { - const { container } = callRender(renderWithTheme, { + const store = setupStore(); + const { container } = callRender(renderWithThemeProviders, { layers: [ { id: "first", name: "first name", url: "https://first-url.com" }, { id: "second", name: "second name", url: "https://second-url.com" }, @@ -98,7 +102,8 @@ describe("Layers Control Panel", () => { ], }, ], - }); + }, + store); expect(container).toBeInTheDocument(); expect(screen.getByText("ListItem-first")).toBeInTheDocument(); @@ -111,7 +116,7 @@ describe("Layers Control Panel", () => { }); it("Should be able to call functions", () => { - const { container } = callRender(renderWithTheme, { + const { container } = callRender(renderWithThemeProviders, { layers: [ { id: "first", name: "first name", mapUrl: "https://first-url.com" }, ], @@ -139,7 +144,7 @@ describe("Layers Control Panel", () => { }); it("Should render conformation panel", () => { - callRender(renderWithTheme, { + callRender(renderWithThemeProviders, { layers: [ { id: "first", name: "first name", mapUrl: "https://first-url.com" }, // Candidate to delete diff --git a/src/components/layers-panel/layers-control-panel.tsx b/src/components/layers-panel/layers-control-panel.tsx index 1a7a2759..e5da4737 100644 --- a/src/components/layers-panel/layers-control-panel.tsx +++ b/src/components/layers-panel/layers-control-panel.tsx @@ -1,4 +1,4 @@ -import { Fragment, ReactEventHandler, useState } from "react"; +import { Fragment, ReactEventHandler, useState, useEffect } from "react"; import styled from "styled-components"; import { SelectionState, LayerExample, LayerViewState, ListItemType } from "../../types"; @@ -11,6 +11,10 @@ import { LayerOptionsMenu } from "./layer-options-menu/layer-options-menu"; import { handleSelectAllLeafsInGroup } from "../../utils/layer-utils"; import { ButtonSize } from "../../types"; + +import { arcGisLogin, arcGisLogout, selectUser } from "../../redux/slices/arcgis-auth-slice"; +import { useAppDispatch, useAppSelector } from "../../redux/hooks"; + type LayersControlPanelProps = { layers: LayerExample[]; selectedLayerIds: string[]; @@ -69,6 +73,28 @@ export const LayersControlPanel = ({ onPointToLayer, deleteLayer, }: LayersControlPanelProps) => { + + // stub { + const dispatch = useAppDispatch(); + + const handleArcGisLogin = () => { + dispatch(arcGisLogin()); + }; + + const handleArcGisLogout = () => { + dispatch(arcGisLogout()); + }; + + const username = useAppSelector(selectUser); + const [showLogin, setShowLoginButton] = useState(!username); + const [showLogout, setShowLogoutButton] = useState(!!username); + + useEffect(() => { + setShowLoginButton(!username); + setShowLogoutButton(!!username); + }, [username]); +// stub } + const [settingsLayerId, setSettingsLayerId] = useState(""); const [showLayerSettings, setShowLayerSettings] = useState(false); const [layerToDeleteId, setLayerToDeleteId] = useState(""); @@ -184,7 +210,20 @@ export const LayersControlPanel = ({ Insert scene - + + + { showLogin && ( + + Login to ArcGIS + + ) } + { showLogout && ( + + {username} Logout + + ) } + + ); }; diff --git a/src/pages/arcgis-auth-popup/arcgis-auth-popup.tsx b/src/pages/arcgis-auth-popup/arcgis-auth-popup.tsx new file mode 100644 index 00000000..e55891fe --- /dev/null +++ b/src/pages/arcgis-auth-popup/arcgis-auth-popup.tsx @@ -0,0 +1,58 @@ +import Background from "../../../public/images/tools-background.webp"; +import styled from "styled-components"; +import { + getCurrentLayoutProperty, + useAppLayout, +} from "../../utils/hooks/layout"; + +import { ArcGISIdentityManager } from '@esri/arcgis-rest-request'; +import { getAuthOptions } from "../../utils/arcgis-auth"; + +export type LayoutProps = { + layout: string; +}; + +const AuthContainer = styled.div` + display: flex; + flex-direction: column; + width: 100%; + overflow: auto; + overflow-x: hidden; + background: url(${Background}); + background-attachment: fixed; + background-size: cover; + + height: ${getCurrentLayoutProperty({ + desktop: "calc(100vh - 65px)", + tablet: "calc(100vh - 65px)", + mobile: "calc(100vh - 58px)", +})}; + + margin-top: ${getCurrentLayoutProperty({ + desktop: "65px", + tablet: "65px", + mobile: "58px", +})}; +`; + +export const AuthApp = () => { + const layout = useAppLayout(); + + const { redirectUrl, clientId } = getAuthOptions(); + if (!clientId) { + console.error("The ClientId is not defined in .env file."); + } else { + const options = { + clientId: clientId, + redirectUri: redirectUrl, + popup: true, + pkce: true + } + ArcGISIdentityManager.completeOAuth2(options); + } + return ( + + + ); + +} \ No newline at end of file diff --git a/src/pages/index.ts b/src/pages/index.ts index 870791a3..e8552292 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -2,4 +2,5 @@ export { Dashboard } from "./dashboard/dashboard"; export { ViewerApp } from "./viewer-app/viewer-app"; export { DebugApp } from "./debug-app/debug-app"; export { Comparison } from './comparison/comparison'; +export { AuthApp } from './arcgis-auth-popup/arcgis-auth-popup'; diff --git a/src/redux/slices/arcgis-auth-slice.ts b/src/redux/slices/arcgis-auth-slice.ts new file mode 100644 index 00000000..77eceef6 --- /dev/null +++ b/src/redux/slices/arcgis-auth-slice.ts @@ -0,0 +1,46 @@ +import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; +import { RootState } from "../store"; +import { getAuthenticatedUser, arcGisRequestLogin, arcGisRequestLogout } from '../../utils/arcgis-auth'; + +// Define a type for the slice state +export interface ArcGisAuthState { + user: string; +} + +const initialState: ArcGisAuthState = { + user: getAuthenticatedUser(), +} + +const arcGisAuthSlice = createSlice({ + name: "arcGisAuth", + initialState, + reducers: { + }, + extraReducers: (builder) => { + builder.addCase(arcGisLogin.fulfilled, (state, action) => { + state.user = action.payload || ''; + }) + .addCase(arcGisLogout.fulfilled, (state) => { + state.user = ''; + }) + }, +}); + +export const arcGisLogin = createAsyncThunk( + 'arcGisLogin', + async () => { + return await arcGisRequestLogin(); + } +); + +export const arcGisLogout = createAsyncThunk( + 'arcGisLogout', + async () => { + return await arcGisRequestLogout(); + } +); + +export const selectUser = (state: RootState): string => + state.arcGisAuth.user; + +export default arcGisAuthSlice.reducer; diff --git a/src/redux/store.ts b/src/redux/store.ts index 61230baa..51c1ffe5 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -10,6 +10,7 @@ import debugOptionsSliceReducer from "./slices/debug-options-slice"; import i3sStatsSliceReducer from "./slices/i3s-stats-slice"; import baseMapsSliceReducer from "./slices/base-maps-slice"; import symbolizationSliceReducer from "./slices/symbolization-slice"; +import arcGisAuthSliceReducer from "./slices/arcgis-auth-slice"; // Create the root reducer separately so we can extract the RootState type const rootReducer = combineReducers({ @@ -20,6 +21,8 @@ const rootReducer = combineReducers({ baseMaps: baseMapsSliceReducer, symbolization: symbolizationSliceReducer, i3sStats: i3sStatsSliceReducer, + arcGisAuth: arcGisAuthSliceReducer, + }); export const setupStore = (preloadedState?: PreloadedState) => { diff --git a/src/utils/arcgis-auth.ts b/src/utils/arcgis-auth.ts new file mode 100644 index 00000000..8b321641 --- /dev/null +++ b/src/utils/arcgis-auth.ts @@ -0,0 +1,72 @@ +import { ArcGISIdentityManager } from '@esri/arcgis-rest-request'; + +const ARCGIS_REST_USER_SESSION = '__ARCGIS_REST_USER_SESSION__'; +const ARCGIS_REST_USER_INFO = '__ARCGIS_REST_USER_INFO__'; + +const updateSessionInfo = async (session?: ArcGISIdentityManager): Promise => { + let email: string = ''; + if (session) { + localStorage.setItem(ARCGIS_REST_USER_SESSION, session.serialize()); + const user = await session.getUser(); + email = user.email || ''; + localStorage.setItem(ARCGIS_REST_USER_INFO, email); + } else { + localStorage.removeItem(ARCGIS_REST_USER_SESSION); + localStorage.removeItem(ARCGIS_REST_USER_INFO); + } + return email; +} + +/** + * Gets the redirection URL and the client ID to use in the ArcGIS authentication workflow. + * @returns the redirection URL and the client ID. + */ +export const getAuthOptions = () => { + return { + redirectUrl: `${window.location.protocol}//${window.location.hostname}:${window.location.port}/auth`, + clientId: process.env.REACT_APP_ARCGIS_REST_CLIENT_ID + } +} + +/** + * Gets the email of the currently logged in user. + * @returns the user's email or an empty string if the user is not logged in. + */ +export const getAuthenticatedUser = (): string => { + return localStorage.getItem(ARCGIS_REST_USER_INFO) || ''; +} + +/** + * + * @returns + */ +export const arcGisRequestLogin = async () => { + const { redirectUrl, clientId } = getAuthOptions(); + + if (!clientId) { + console.error("The ClientId is not defined in .env file."); + return ''; + } + const options = { + clientId: clientId, + redirectUri: redirectUrl, + popup: true, + pkce: true + } + + let email = ''; + let session: ArcGISIdentityManager | undefined; + try { + session = await ArcGISIdentityManager.beginOAuth2(options); + } + finally { + // In case of an exception the session is not set. + // So the following call will remove any session stored in the local storage. + email = await updateSessionInfo(session); + } + return email; +}; + +export const arcGisRequestLogout = async () => { + return await updateSessionInfo(); +}; diff --git a/webpack.dev.config.js b/webpack.dev.config.js index 3b952f7d..a5288e44 100644 --- a/webpack.dev.config.js +++ b/webpack.dev.config.js @@ -100,6 +100,7 @@ module.exports = (env) => { devServer: { open: true, port: 3000, + server: 'https', client: { overlay: { errors: true, From aafda77aee9ebbe5091c8bc7b279425eea4512a1 Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Mon, 11 Dec 2023 12:05:08 +0300 Subject: [PATCH 2/9] fix lint error --- src/utils/arcgis-auth.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/utils/arcgis-auth.ts b/src/utils/arcgis-auth.ts index 8b321641..b4cad034 100644 --- a/src/utils/arcgis-auth.ts +++ b/src/utils/arcgis-auth.ts @@ -4,7 +4,7 @@ const ARCGIS_REST_USER_SESSION = '__ARCGIS_REST_USER_SESSION__'; const ARCGIS_REST_USER_INFO = '__ARCGIS_REST_USER_INFO__'; const updateSessionInfo = async (session?: ArcGISIdentityManager): Promise => { - let email: string = ''; + let email = ''; if (session) { localStorage.setItem(ARCGIS_REST_USER_SESSION, session.serialize()); const user = await session.getUser(); @@ -33,12 +33,12 @@ export const getAuthOptions = () => { * @returns the user's email or an empty string if the user is not logged in. */ export const getAuthenticatedUser = (): string => { - return localStorage.getItem(ARCGIS_REST_USER_INFO) || ''; + return localStorage.getItem(ARCGIS_REST_USER_INFO) || ''; } /** - * - * @returns + * Makes a ArcGIS login request by opening a popup dialog. + * @returns email of the user logged in or an empty string if the user is not logged in. */ export const arcGisRequestLogin = async () => { const { redirectUrl, clientId } = getAuthOptions(); @@ -67,6 +67,10 @@ export const arcGisRequestLogin = async () => { return email; }; +/** + * Makes a ArcGIS logout request. + * @returns empty string + */ export const arcGisRequestLogout = async () => { return await updateSessionInfo(); }; From 119cb65a504ef422666583624b761216d01035a0 Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Thu, 14 Dec 2023 12:36:16 +0300 Subject: [PATCH 3/9] ModalDialog added --- .env => .env-template | 0 public/icons/download-2.svg | 4 - .../layers-panel/layers-control-panel.tsx | 89 +++++++---- src/components/modal-dialog/modal-dialog.tsx | 142 ++++++++++++++++++ .../arcgis-auth-popup/arcgis-auth-popup.tsx | 21 +-- src/redux/store.ts | 1 - src/utils/arcgis-auth.ts | 76 ++++++---- 7 files changed, 252 insertions(+), 81 deletions(-) rename .env => .env-template (100%) delete mode 100644 public/icons/download-2.svg create mode 100644 src/components/modal-dialog/modal-dialog.tsx diff --git a/.env b/.env-template similarity index 100% rename from .env rename to .env-template diff --git a/public/icons/download-2.svg b/public/icons/download-2.svg deleted file mode 100644 index bdfae70b..00000000 --- a/public/icons/download-2.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/components/layers-panel/layers-control-panel.tsx b/src/components/layers-panel/layers-control-panel.tsx index d0e86707..b6f13f6d 100644 --- a/src/components/layers-panel/layers-control-panel.tsx +++ b/src/components/layers-panel/layers-control-panel.tsx @@ -1,24 +1,20 @@ -import { Fragment, ReactEventHandler, useState, useEffect } from "react"; +import { Fragment, ReactEventHandler, useState } from "react"; import styled from "styled-components"; - import { SelectionState, LayerExample, LayerViewState, ListItemType } from "../../types"; - import { ListItem } from "./list-item/list-item"; import PlusIcon from "../../../public/icons/plus.svg"; import ImportIcon from "../../../public/icons/import.svg"; import EsriImage from "../../../public/images/esri.svg"; import { ActionIconButton } from "../action-icon-button/action-icon-button"; import { AcrGisUser } from "../arcgis-user/arcgis-user"; - import { DeleteConfirmation } from "./delete-confirmation"; import { LayerOptionsMenu } from "./layer-options-menu/layer-options-menu"; import { handleSelectAllLeafsInGroup } from "../../utils/layer-utils"; import { ButtonSize } from "../../types"; import { PanelHorizontalLine } from "../common"; - - import { arcGisLogin, arcGisLogout, selectUser } from "../../redux/slices/arcgis-auth-slice"; import { useAppDispatch, useAppSelector } from "../../redux/hooks"; +import { ModalDialog } from "../modal-dialog/modal-dialog"; type LayersControlPanelProps = { layers: LayerExample[]; @@ -86,37 +82,18 @@ export const LayersControlPanel = ({ deleteLayer, }: LayersControlPanelProps) => { - // stub { - // const dispatch = useAppDispatch(); - - // const handleArcGisLogin = () => { - // dispatch(arcGisLogin()); - // }; - - // const handleArcGisLogout = () => { - // dispatch(arcGisLogout()); - // }; - - // const username = useAppSelector(selectUser); - // const [showLogin, setShowLoginButton] = useState(!username); - // const [showLogout, setShowLogoutButton] = useState(!!username); - - // useEffect(() => { - // setShowLoginButton(!username); - // setShowLogoutButton(!!username); - // }, [username]); -// stub } + const dispatch = useAppDispatch(); + const username = useAppSelector(selectUser); + const isLoggedIn = !!username; + + const [showLogoutWarning, setShowLogoutWarning] = useState(false); const [settingsLayerId, setSettingsLayerId] = useState(""); const [showLayerSettings, setShowLayerSettings] = useState(false); const [layerToDeleteId, setLayerToDeleteId] = useState(""); - /// Stab { - const username = 'Michael-g'; - const [isLoggedIn, setIsLoggedIn] = useState(false); - const onArcGisActionClick = () => { !isLoggedIn && setIsLoggedIn(true) }; - const onArcGisLogoutClick = () => { setIsLoggedIn(false) }; - /// Stab } + const onArcGisActionClick = () => { !isLoggedIn && dispatch(arcGisLogin()); }; + const onArcGisLogoutClick = () => { setShowLogoutWarning(true); }; const isListItemSelected = ( layer: LayerExample, @@ -150,6 +127,38 @@ export const LayersControlPanel = ({ return selectedState; }; + const TextQuestion = styled.div` + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 19px; +`; + + const TextInfo = styled.div` + font-style: normal; + font-weight: 700; + font-size: 16px; + line-height: 19px; +`; + + const renderModalDialogContent = (): JSX.Element => { + return ( + <> + + Are you sure you want to log out? + + + + You are logged in as + + + + {username} + + + ); + } + const renderLayers = ( layers: LayerExample[], parentLayer?: LayerExample, @@ -245,6 +254,22 @@ export const LayersControlPanel = ({ {username} )} + + {showLogoutWarning && ( + { + dispatch(arcGisLogout()); + setShowLogoutWarning(false); + }} + onCancel={() => { setShowLogoutWarning(false); }} + /> + )} + ); diff --git a/src/components/modal-dialog/modal-dialog.tsx b/src/components/modal-dialog/modal-dialog.tsx new file mode 100644 index 00000000..100936f9 --- /dev/null +++ b/src/components/modal-dialog/modal-dialog.tsx @@ -0,0 +1,142 @@ +import styled, { useTheme } from "styled-components"; +import { ActionButton } from "../action-button/action-button"; +import { ActionButtonVariant, LayoutProps } from "../../types"; +import CloseIcon from "../../../public/icons/close.svg"; +import { Popover } from "react-tiny-popover"; + +const Overlay = styled.div` + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + z-index: 103; + background: #00000099; + `; + +const Container = styled.div<{ width: number, height: number }>` + position: absolute; + display: flex; + flex-direction: column; + border-radius: 8px; + background: ${({ theme }) => theme.colors.mainColor}; + width: ${({ width }) => (`${width}px`)}; + height: ${({ height }) => (`${height}px`)}; + left: calc(50% - ${({ width }) => (`${width * 0.5}px`)} ); + top: calc(50% - ${({ height }) => (`${height * 0.5}px`)} ); + z-index: 104; + `; + +const IconContainer = styled.div` + position: absolute; + display: flex; + justify-content: center; + align-items: center; + border-radius: 8px; + width: 44px; + height: 44px; + top: 13px; + right: 14px; + `; + +const ContentContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: start; + height: 100%; + width: 100%; + margin: 32px 32px 0 32px; + row-gap: 16px; + color: ${({ theme }) => theme.colors.fontColor}; +`; + +const Title = styled.div` + font-style: normal; + font-weight: 700; + font-size: 32px; + line-height: 45px; +`; + +const ButtonsContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: center; //space-between; + margin: 32px; + column-gap: 18px; + { > * + { + width: 180px; + } + } +`; + +type LogoutPanelProps = { + title: string; + content: (() => JSX.Element) | JSX.Element; + width: number, + height: number, + onCancel: () => void; + onConfirm: () => void; +}; + +const CloseCrossButton = styled(CloseIcon)` + &:hover { + fill: ${({ theme }) => theme.colors.mainDimColorInverted}; + } + `; + +export const ModalDialog = ({ + title, + content, + width, + height, + onCancel, + onConfirm, +}: LogoutPanelProps) => { + const theme = useTheme(); + + const renderPopoverContent = (): JSX.Element => { + return ( + <> + + + + + + + {title} + {typeof content === 'function' ? content() : content} + + + + Cancel + + Log out + + + + ); + } + + const getPopoverStyle = () => { + return { + zIndex: "104", + width: "100%", + height: "100%" + }; + }; + + return ( + + <> + + ); +}; diff --git a/src/pages/arcgis-auth-popup/arcgis-auth-popup.tsx b/src/pages/arcgis-auth-popup/arcgis-auth-popup.tsx index e55891fe..28ec358c 100644 --- a/src/pages/arcgis-auth-popup/arcgis-auth-popup.tsx +++ b/src/pages/arcgis-auth-popup/arcgis-auth-popup.tsx @@ -5,8 +5,7 @@ import { useAppLayout, } from "../../utils/hooks/layout"; -import { ArcGISIdentityManager } from '@esri/arcgis-rest-request'; -import { getAuthOptions } from "../../utils/arcgis-auth"; +import { arcGisCompleteLogin } from "../../utils/arcgis-auth"; export type LayoutProps = { layout: string; @@ -37,22 +36,10 @@ const AuthContainer = styled.div` export const AuthApp = () => { const layout = useAppLayout(); - - const { redirectUrl, clientId } = getAuthOptions(); - if (!clientId) { - console.error("The ClientId is not defined in .env file."); - } else { - const options = { - clientId: clientId, - redirectUri: redirectUrl, - popup: true, - pkce: true - } - ArcGISIdentityManager.completeOAuth2(options); - } + arcGisCompleteLogin(); return ( ); - -} \ No newline at end of file + +} diff --git a/src/redux/store.ts b/src/redux/store.ts index 51c1ffe5..4b5b407d 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -22,7 +22,6 @@ const rootReducer = combineReducers({ symbolization: symbolizationSliceReducer, i3sStats: i3sStatsSliceReducer, arcGisAuth: arcGisAuthSliceReducer, - }); export const setupStore = (preloadedState?: PreloadedState) => { diff --git a/src/utils/arcgis-auth.ts b/src/utils/arcgis-auth.ts index b4cad034..a6921493 100644 --- a/src/utils/arcgis-auth.ts +++ b/src/utils/arcgis-auth.ts @@ -17,15 +17,32 @@ const updateSessionInfo = async (session?: ArcGISIdentityManager): Promise { - return { - redirectUrl: `${window.location.protocol}//${window.location.hostname}:${window.location.port}/auth`, - clientId: process.env.REACT_APP_ARCGIS_REST_CLIENT_ID +const getAuthOptions = () => { + const port = window.location.port ? `:${window.location.port}` : ''; + const options = { + redirectUri: `${window.location.protocol}//${window.location.hostname}${port}/auth`, + clientId: process.env.REACT_APP_ARCGIS_REST_CLIENT_ID || '', + popup: true, + pkce: true + }; + + if (!options.clientId) { + console.error("The ClientId is not defined in .env file."); } + return options; } /** @@ -37,40 +54,45 @@ export const getAuthenticatedUser = (): string => { } /** - * Makes a ArcGIS login request by opening a popup dialog. + * Makes an ArcGIS login request by opening a popup dialog. * @returns email of the user logged in or an empty string if the user is not logged in. */ export const arcGisRequestLogin = async () => { - const { redirectUrl, clientId } = getAuthOptions(); - - if (!clientId) { - console.error("The ClientId is not defined in .env file."); - return ''; - } - const options = { - clientId: clientId, - redirectUri: redirectUrl, - popup: true, - pkce: true - } - let email = ''; - let session: ArcGISIdentityManager | undefined; - try { - session = await ArcGISIdentityManager.beginOAuth2(options); - } - finally { - // In case of an exception the session is not set. - // So the following call will remove any session stored in the local storage. - email = await updateSessionInfo(session); + + const options = getAuthOptions(); + if (options.clientId) { + let session: ArcGISIdentityManager | undefined; + try { + session = await ArcGISIdentityManager.beginOAuth2(options); + } + finally { + // In case of an exception the session is not set. + // So the following call will remove any session stored in the local storage. + email = await updateSessionInfo(session); + } } return email; }; /** - * Makes a ArcGIS logout request. + * Completes the ArcGIS login request started by {@link arcGisRequestLogin}. + */ +export const arcGisCompleteLogin = async () => { + const options = getAuthOptions(); + if (options.clientId) { + ArcGISIdentityManager.completeOAuth2(options); + } +} + +/** + * Makes an ArcGIS logout request. * @returns empty string */ export const arcGisRequestLogout = async () => { + const session = getArcGisSession(); + if (session) { + await ArcGISIdentityManager.destroy(session); + } return await updateSessionInfo(); }; From ac10454ee17f9c128c30da425e923a900adffa19 Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Thu, 14 Dec 2023 12:48:28 +0300 Subject: [PATCH 4/9] .env added to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index eb7b4d44..8a67cf5e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ build/ coverage/ +.env From c85ce64c0a5278a6abe05aca62d815f24fe5539e Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Mon, 18 Dec 2023 13:48:54 +0300 Subject: [PATCH 5/9] tests added, fix dev review issues --- __mocks__/@esri/arcgis-rest-request.ts | 26 ++ .../action-icon-button.spec.tsx | 6 +- .../action-icon-button/action-icon-button.tsx | 63 +++-- src/components/arcgis-user/arcgis-user.tsx | 17 +- .../layers-control-panel.spec.tsx | 236 ++++++++++++++---- .../layers-panel/layers-control-panel.tsx | 128 ++++++---- src/components/modal-dialog/modal-dialog.tsx | 108 ++++---- .../arcgis-auth-popup.tsx => auth/auth.tsx} | 29 ++- src/pages/auth/e2e.auth.spec.ts | 23 ++ src/pages/index.ts | 2 +- src/redux/slices/arcgis-auth-slice.ts | 43 ++-- src/utils/arcgis-auth.spec.ts | 44 ++++ src/utils/arcgis-auth.ts | 35 +-- 13 files changed, 498 insertions(+), 262 deletions(-) create mode 100644 __mocks__/@esri/arcgis-rest-request.ts rename src/pages/{arcgis-auth-popup/arcgis-auth-popup.tsx => auth/auth.tsx} (67%) create mode 100644 src/pages/auth/e2e.auth.spec.ts create mode 100644 src/utils/arcgis-auth.spec.ts diff --git a/__mocks__/@esri/arcgis-rest-request.ts b/__mocks__/@esri/arcgis-rest-request.ts new file mode 100644 index 00000000..391ef811 --- /dev/null +++ b/__mocks__/@esri/arcgis-rest-request.ts @@ -0,0 +1,26 @@ +const mockEmailExpected = "usermail@gmail.com"; +const mockSessionExpected = '{"usermail": "usermail"}'; + +export class ArcGISIdentityManager { + static beginOAuth2 = async () => { + const session = { + usermail: mockEmailExpected, + serialize: () => { + return mockSessionExpected; + }, + getUser: async () => { + return { email: mockEmailExpected }; + }, + }; + return session; + }; + static completeOAuth2 = async () => { + return; + }; + static destroy = async () => { + return; + }; + static deserialize = (session) => { + return { usermail: "usermail" }; + }; +} diff --git a/src/components/action-icon-button/action-icon-button.spec.tsx b/src/components/action-icon-button/action-icon-button.spec.tsx index 58b97c56..95802fa8 100644 --- a/src/components/action-icon-button/action-icon-button.spec.tsx +++ b/src/components/action-icon-button/action-icon-button.spec.tsx @@ -18,8 +18,8 @@ const callRender = (renderFunc, props = {}) => { ); }; -describe("Plus Button", () => { - it("Should render small Plus button", () => { +describe("ActionIconButton", () => { + it("Should render small Plus icon in the button", () => { const { container } = callRender(renderWithTheme, { children: 'Test Button' }); expect(container).toBeInTheDocument(); const button = screen.getByText('Test Button'); @@ -30,7 +30,7 @@ describe("Plus Button", () => { expect(onClickMock).toHaveBeenCalled(); }); - it("Should render Big Plus button", () => { + it("Should render Big Plus icon in the button", () => { const { container } = callRender(renderWithTheme, { children: 'Test Button', size: ButtonSize.Big }); expect(container).toBeInTheDocument(); const button = screen.getByText('Test Button'); diff --git a/src/components/action-icon-button/action-icon-button.tsx b/src/components/action-icon-button/action-icon-button.tsx index 8e2b6433..c30690d3 100644 --- a/src/components/action-icon-button/action-icon-button.tsx +++ b/src/components/action-icon-button/action-icon-button.tsx @@ -1,10 +1,12 @@ import React from "react"; -import styled, { StyledComponent, DefaultTheme, useTheme } from "styled-components"; +import styled, { + StyledComponent, + DefaultTheme, + useTheme, +} from "styled-components"; import { ButtonSize } from "../../types"; -import { - color_brand_tertiary, -} from "../../constants/colors"; +import { color_brand_tertiary } from "../../constants/colors"; const Button = styled.div<{ grayed?: boolean }>` display: flex; @@ -23,11 +25,10 @@ const Button = styled.div<{ grayed?: boolean }>` } > :nth-child(2) { - color: ${({ theme, grayed }) => ( - grayed - ? theme.colors.actionIconButtonTextDisabledColorHover - : color_brand_tertiary - )}; + color: ${({ theme, grayed }) => + grayed + ? theme.colors.actionIconButtonTextDisabledColorHover + : color_brand_tertiary}; } } `; @@ -37,27 +38,25 @@ const ButtonText = styled.div<{ grayed?: boolean }>` font-weight: 500; font-size: 16px; line-height: 19px; - color: ${({ theme, grayed }) => ( + color: ${({ theme, grayed }) => grayed ? theme.colors.actionIconButtonTextDisabledColor - : color_brand_tertiary - )}; - `; + : color_brand_tertiary}; +`; -const IconContainer = styled.div<{ buttonSize: number, grayed?: boolean }>` +const IconContainer = styled.div<{ buttonSize: number; grayed?: boolean }>` display: flex; justify-content: center; align-items: center; border-radius: 4px; width: ${(props) => (props.buttonSize === ButtonSize.Big ? "40px" : "24px")}; height: ${(props) => (props.buttonSize === ButtonSize.Big ? "40px" : "24px")}; - - background: ${({ theme, grayed }) => ( + + background: ${({ theme, grayed }) => grayed ? theme.colors.actionIconButtonDisabledBG - : `${color_brand_tertiary}66` - )}; - `; + : `${color_brand_tertiary}66`}; +`; type ActionIconButtonProps = { children?: React.ReactNode; @@ -67,18 +66,30 @@ type ActionIconButtonProps = { onClick?: () => void; }; -export const ActionIconButton = ({ Icon, style, size, children, onClick }: ActionIconButtonProps) => { +export const ActionIconButton = ({ + Icon, + style, + size, + children, + onClick, +}: ActionIconButtonProps) => { const grayed = style === "disabled"; const theme = useTheme(); return ( - diff --git a/src/components/arcgis-user/arcgis-user.tsx b/src/components/arcgis-user/arcgis-user.tsx index ad00d99e..8cfb94ca 100644 --- a/src/components/arcgis-user/arcgis-user.tsx +++ b/src/components/arcgis-user/arcgis-user.tsx @@ -1,8 +1,6 @@ import styled from "styled-components"; import LogoutIcon from "../../../public/icons/logout.svg"; -import { - color_canvas_secondary_inverted, -} from "../../constants/colors"; +import { color_canvas_secondary_inverted } from "../../constants/colors"; const Container = styled.div` display: flex; @@ -18,9 +16,7 @@ const UserInfo = styled.div` font-weight: 500; font-size: 14px; line-height: 17px; - color: ${({ theme }) => ( - theme.colors.logoutButtonTextColor - )}; + color: ${({ theme }) => theme.colors.logoutButtonTextColor}; `; const IconButton = styled.div` @@ -31,7 +27,7 @@ const IconButton = styled.div` cursor: pointer; &:hover { > * { - stroke: ${({ theme }) => (theme.colors.logoutButtonIconColorHover)}; + stroke: ${({ theme }) => theme.colors.logoutButtonIconColorHover}; } } `; @@ -46,14 +42,11 @@ type ArcGisUserProps = { onClick?: () => void; }; -export const AcrGisUser = ({ - children, - onClick, -}: ArcGisUserProps) => { +export const AcrGisUser = ({ children, onClick }: ArcGisUserProps) => { return ( {children} - + diff --git a/src/components/layers-panel/layers-control-panel.spec.tsx b/src/components/layers-panel/layers-control-panel.spec.tsx index 778c14bf..eb0dcb1b 100644 --- a/src/components/layers-panel/layers-control-panel.spec.tsx +++ b/src/components/layers-panel/layers-control-panel.spec.tsx @@ -1,6 +1,11 @@ -import { act, screen } from "@testing-library/react"; +import { act, screen, within } from "@testing-library/react"; import { renderWithThemeProviders } from "../../utils/testing-utils/render-with-theme"; import { LayersControlPanel } from "./layers-control-panel"; +import userEvent from "@testing-library/user-event"; +import { + arcGisLogin, + arcGisLogout, +} from "../../redux/slices/arcgis-auth-slice"; // Mocked components import { ActionIconButton } from "../action-icon-button/action-icon-button"; @@ -9,10 +14,18 @@ import { LayerOptionsMenu } from "./layer-options-menu/layer-options-menu"; import { ListItem } from "./list-item/list-item"; import { setupStore } from "../../redux/store"; +import { + getAuthenticatedUser, + arcGisRequestLogin, + arcGisCompleteLogin, + arcGisRequestLogout, +} from "../../utils/arcgis-auth"; + jest.mock("./list-item/list-item"); jest.mock("../action-icon-button/action-icon-button"); jest.mock("./delete-confirmation"); jest.mock("./layer-options-menu/layer-options-menu"); +jest.mock("../../utils/arcgis-auth"); const ListItemMock = ListItem as unknown as jest.Mocked; const PlusButtonMock = ActionIconButton as unknown as jest.Mocked; @@ -20,18 +33,17 @@ const DeleteConfirmationMock = DeleteConfirmation as unknown as jest.Mocked; const LayerOptionsMenuMock = LayerOptionsMenu as unknown as jest.Mocked; -beforeAll(() => { - ListItemMock.mockImplementation((props) => ( -
{`ListItem-${props.id}`}
- )); - PlusButtonMock.mockImplementation(({ children, onClick }) => ( -
{children}
- )); - DeleteConfirmationMock.mockImplementation(() => ( -
Delete Conformation
- )); - LayerOptionsMenuMock.mockImplementation(() =>
Layers Options
); -}); +const getAuthenticatedUserMock = + getAuthenticatedUser as unknown as jest.Mocked; +const arcGisRequestLoginMock = + arcGisRequestLogin as unknown as jest.Mocked; +const arcGisCompleteLoginMock = + arcGisCompleteLogin as unknown as jest.Mocked; +const arcGisRequestLogoutMock = + arcGisRequestLogout as unknown as jest.Mocked; + +const mockEmailExpected = "usermail@gmail.com"; +let mockStorageUserinfo = ""; const onInsertLayerMock = jest.fn(); const onInsertSceneMock = jest.fn(); @@ -59,10 +71,139 @@ const callRender = (renderFunc, props = {}, store = setupStore()) => { ); }; +beforeAll(() => { + ListItemMock.mockImplementation((props) => ( +
{`ListItem-${props.id}`}
+ )); + PlusButtonMock.mockImplementation(({ children, onClick }) => ( +
{children}
+ )); + DeleteConfirmationMock.mockImplementation(() => ( +
Delete Conformation
+ )); + LayerOptionsMenuMock.mockImplementation(() =>
Layers Options
); +}); + +describe("Layers Control Panel - ArcGIS auth", () => { + beforeAll(() => { + arcGisRequestLoginMock.mockImplementation(async () => { + mockStorageUserinfo = mockEmailExpected; + return mockStorageUserinfo; + }); + arcGisCompleteLoginMock.mockImplementation(async () => { + return mockStorageUserinfo; + }); + arcGisRequestLogoutMock.mockImplementation(async () => { + mockStorageUserinfo = ""; + return mockStorageUserinfo; + }); + getAuthenticatedUserMock.mockImplementation(() => { + return mockStorageUserinfo; + }); + }); + + it("Should render ArcGIS Login button", async () => { + const store = setupStore(); + // Let's Log out... + await store.dispatch(arcGisLogout()); + const { container } = callRender( + renderWithThemeProviders, + undefined, + store + ); + expect(container).toBeInTheDocument(); + expect(arcGisRequestLogoutMock).toHaveBeenCalledTimes(1); + + // We are in the "Logged out" state, so the "Log in" button should be there. + const loginButton = await screen.findByText("Login to ArcGIS"); + expect(loginButton).toBeInTheDocument(); + loginButton && userEvent.click(loginButton); + expect(arcGisRequestLoginMock).toHaveBeenCalledTimes(1); + + const importButton = screen.queryByText("Import from ArcGIS"); + expect(importButton).not.toBeInTheDocument(); + }); + + it("Should render ArcGIS Import and Logout buttons", async () => { + const store = setupStore(); + // Let's Log in... + await store.dispatch(arcGisLogin()); + const { container } = callRender( + renderWithThemeProviders, + undefined, + store + ); + expect(container).toBeInTheDocument(); + expect(arcGisRequestLoginMock).toHaveBeenCalledTimes(1); + + // We are in the "Logged in" state, so the "Log in" button should NOT be there. + const importButton = await screen.findByText("Import from ArcGIS"); + expect(importButton).toBeInTheDocument(); + + const logoutUserInfo = await screen.findByText(mockEmailExpected); + expect(logoutUserInfo).toBeInTheDocument(); + + const loginButton = screen.queryByText("Login to ArcGIS"); + expect(loginButton).not.toBeInTheDocument(); + }); + + it("Should respond to action on the ArcGIS Login button", async () => { + const store = setupStore(); + // Let's Log out... + await store.dispatch(arcGisLogout()); + const { container } = callRender( + renderWithThemeProviders, + undefined, + store + ); + expect(container).toBeInTheDocument(); + + const loginButton = screen.getByText("Login to ArcGIS"); + loginButton && userEvent.click(loginButton); + expect(arcGisRequestLoginMock).toHaveBeenCalledTimes(1); + + const importButton = await screen.findByText("Import from ArcGIS"); + expect(importButton).toBeInTheDocument(); + + const loginButtonHidden = screen.queryByText("Login to ArcGIS"); + expect(loginButtonHidden).not.toBeInTheDocument(); + }); + + it("Should respond to action on ArcGIS Logout button", async () => { + const store = setupStore(); + // Let's Log in... + await store.dispatch(arcGisLogin()); + const { container } = callRender( + renderWithThemeProviders, + undefined, + store + ); + + const logoutButton = await screen.findByTestId("userinfo-button"); + logoutButton && userEvent.click(logoutButton); + + const modalDialog = await screen.findByTestId("modal-dialog-content"); + expect(modalDialog).toContainHTML("Are you sure you want to log out?"); + + const cancelButton = within(modalDialog).getByText("Log out"); + cancelButton && userEvent.click(cancelButton); + + const modalDialogHidden = screen.queryByTestId("modal-dialog-content"); + expect(modalDialogHidden).not.toBeInTheDocument(); + + const loginButton = await screen.findByText("Login to ArcGIS"); + expect(loginButton).toBeInTheDocument(); + }); +}); + describe("Layers Control Panel", () => { it("Should render LayersControlPanel without layers", () => { const store = setupStore(); - const { container } = callRender(renderWithThemeProviders, undefined, store); + const { container } = callRender( + renderWithThemeProviders, + undefined, + store + ); expect(container).toBeInTheDocument(); // Insert Buttons should be present @@ -72,38 +213,41 @@ describe("Layers Control Panel", () => { it("Should render LayersControlPanel with layers", () => { const store = setupStore(); - const { container } = callRender(renderWithThemeProviders, { - layers: [ - { id: "first", name: "first name", url: "https://first-url.com" }, - { id: "second", name: "second name", url: "https://second-url.com" }, - { - id: "third", - name: "third name", - url: "", - layers: [ - { - id: "fourth", - name: "fourth name", - url: "https://fourth-url.com", - }, - { id: "fith", name: "fith name", url: "https://fith-url.com" }, - { - id: "sixth", - name: "sixth name", - url: "https://sixth-url.com", - layers: [ - { - id: "seventh", - name: "seventh name", - url: "https://seventh-url.com", - }, - ], - }, - ], - }, - ], - }, - store); + const { container } = callRender( + renderWithThemeProviders, + { + layers: [ + { id: "first", name: "first name", url: "https://first-url.com" }, + { id: "second", name: "second name", url: "https://second-url.com" }, + { + id: "third", + name: "third name", + url: "", + layers: [ + { + id: "fourth", + name: "fourth name", + url: "https://fourth-url.com", + }, + { id: "fith", name: "fith name", url: "https://fith-url.com" }, + { + id: "sixth", + name: "sixth name", + url: "https://sixth-url.com", + layers: [ + { + id: "seventh", + name: "seventh name", + url: "https://seventh-url.com", + }, + ], + }, + ], + }, + ], + }, + store + ); expect(container).toBeInTheDocument(); expect(screen.getByText("ListItem-first")).toBeInTheDocument(); diff --git a/src/components/layers-panel/layers-control-panel.tsx b/src/components/layers-panel/layers-control-panel.tsx index b6f13f6d..6de6fc48 100644 --- a/src/components/layers-panel/layers-control-panel.tsx +++ b/src/components/layers-panel/layers-control-panel.tsx @@ -1,6 +1,11 @@ import { Fragment, ReactEventHandler, useState } from "react"; import styled from "styled-components"; -import { SelectionState, LayerExample, LayerViewState, ListItemType } from "../../types"; +import { + SelectionState, + LayerExample, + LayerViewState, + ListItemType, +} from "../../types"; import { ListItem } from "./list-item/list-item"; import PlusIcon from "../../../public/icons/plus.svg"; import ImportIcon from "../../../public/icons/import.svg"; @@ -12,7 +17,11 @@ import { LayerOptionsMenu } from "./layer-options-menu/layer-options-menu"; import { handleSelectAllLeafsInGroup } from "../../utils/layer-utils"; import { ButtonSize } from "../../types"; import { PanelHorizontalLine } from "../common"; -import { arcGisLogin, arcGisLogout, selectUser } from "../../redux/slices/arcgis-auth-slice"; +import { + arcGisLogin, + arcGisLogout, + selectUser, +} from "../../redux/slices/arcgis-auth-slice"; import { useAppDispatch, useAppSelector } from "../../redux/hooks"; import { ModalDialog } from "../modal-dialog/modal-dialog"; @@ -69,6 +78,20 @@ const EsriStyledImage = styled(EsriImage)` fill: ${({ theme }) => theme.colors.esriImageColor}; `; +const TextInfo = styled.div` + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 19px; +`; + +const TextUser = styled.div` + font-style: normal; + font-weight: 700; + font-size: 16px; + line-height: 19px; +`; + export const LayersControlPanel = ({ layers, type, @@ -81,7 +104,6 @@ export const LayersControlPanel = ({ onPointToLayer, deleteLayer, }: LayersControlPanelProps) => { - const dispatch = useAppDispatch(); const username = useAppSelector(selectUser); @@ -92,8 +114,12 @@ export const LayersControlPanel = ({ const [showLayerSettings, setShowLayerSettings] = useState(false); const [layerToDeleteId, setLayerToDeleteId] = useState(""); - const onArcGisActionClick = () => { !isLoggedIn && dispatch(arcGisLogin()); }; - const onArcGisLogoutClick = () => { setShowLogoutWarning(true); }; + const onArcGisActionClick = () => { + !isLoggedIn && dispatch(arcGisLogin()); + }; + const onArcGisLogoutClick = () => { + setShowLogoutWarning(true); + }; const isListItemSelected = ( layer: LayerExample, @@ -104,60 +130,46 @@ export const LayersControlPanel = ({ let selectedState = SelectionState.unselected; if (!childLayers.length) { - selectedState = selectedLayerIds.includes(layer.id) ? SelectionState.selected : SelectionState.unselected; + selectedState = selectedLayerIds.includes(layer.id) + ? SelectionState.selected + : SelectionState.unselected; } if (childLayers.length && !parentLayer) { - selectedState = groupLeafs.some((leaf) => selectedLayerIds.includes(leaf.id)) ? SelectionState.selected : SelectionState.unselected; + selectedState = groupLeafs.some((leaf) => + selectedLayerIds.includes(leaf.id) + ) + ? SelectionState.selected + : SelectionState.unselected; } if (childLayers.length && parentLayer) { const isAllChildLayersSelected = !groupLeafs.some( - (leaf) => !selectedLayerIds.includes(leaf.id)); - const isAnyChildLayerSelected = groupLeafs.some( - (leaf) => selectedLayerIds.includes(leaf.id)); + (leaf) => !selectedLayerIds.includes(leaf.id) + ); + const isAnyChildLayerSelected = groupLeafs.some((leaf) => + selectedLayerIds.includes(leaf.id) + ); if (isAllChildLayersSelected) { selectedState = SelectionState.selected; } else if (isAnyChildLayerSelected) { - selectedState = SelectionState.indeterminate + selectedState = SelectionState.indeterminate; } } return selectedState; }; - const TextQuestion = styled.div` - font-style: normal; - font-weight: 500; - font-size: 16px; - line-height: 19px; -`; - - const TextInfo = styled.div` - font-style: normal; - font-weight: 700; - font-size: 16px; - line-height: 19px; -`; - const renderModalDialogContent = (): JSX.Element => { return ( <> - - Are you sure you want to log out? - - - - You are logged in as - - - - {username} - + Are you sure you want to log out? + You are logged in as + {username} ); - } + }; const renderLayers = ( layers: LayerExample[], @@ -232,17 +244,30 @@ export const LayersControlPanel = ({ {renderLayers(layers)} - + Insert layer - + Insert scene - + {!isLoggedIn && "Login to ArcGIS"} {isLoggedIn && "Import from ArcGIS"} @@ -250,26 +275,23 @@ export const LayersControlPanel = ({ {isLoggedIn && ( - - {username} - + {username} )} {showLogoutWarning && ( { - dispatch(arcGisLogout()); - setShowLogoutWarning(false); - }} - onCancel={() => { setShowLogoutWarning(false); }} + okButtonText={"Log out"} + onConfirm={() => { + dispatch(arcGisLogout()); + setShowLogoutWarning(false); + }} + onCancel={() => { + setShowLogoutWarning(false); + }} /> )} - ); diff --git a/src/components/modal-dialog/modal-dialog.tsx b/src/components/modal-dialog/modal-dialog.tsx index 100936f9..ad17f85a 100644 --- a/src/components/modal-dialog/modal-dialog.tsx +++ b/src/components/modal-dialog/modal-dialog.tsx @@ -1,31 +1,29 @@ import styled, { useTheme } from "styled-components"; import { ActionButton } from "../action-button/action-button"; -import { ActionButtonVariant, LayoutProps } from "../../types"; +import { ActionButtonVariant } from "../../types"; import CloseIcon from "../../../public/icons/close.svg"; -import { Popover } from "react-tiny-popover"; const Overlay = styled.div` - width: 100%; - height: 100%; - position: absolute; + position: fixed; left: 0; top: 0; - z-index: 103; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; background: #00000099; - `; + z-index: 103; +`; -const Container = styled.div<{ width: number, height: number }>` +const Container = styled.div` position: absolute; display: flex; flex-direction: column; border-radius: 8px; background: ${({ theme }) => theme.colors.mainColor}; - width: ${({ width }) => (`${width}px`)}; - height: ${({ height }) => (`${height}px`)}; - left: calc(50% - ${({ width }) => (`${width * 0.5}px`)} ); - top: calc(50% - ${({ height }) => (`${height * 0.5}px`)} ); z-index: 104; - `; +`; const IconContainer = styled.div` position: absolute; @@ -33,11 +31,11 @@ const IconContainer = styled.div` justify-content: center; align-items: center; border-radius: 8px; - width: 44px; - height: 44px; top: 13px; right: 14px; - `; + width: 44px; + height: 44px; +`; const ContentContainer = styled.div` display: flex; @@ -60,11 +58,11 @@ const Title = styled.div` const ButtonsContainer = styled.div` display: flex; flex-direction: row; - justify-content: center; //space-between; + justify-content: center; margin: 32px; column-gap: 18px; - { > * - { + { + > * { width: 180px; } } @@ -73,8 +71,8 @@ const ButtonsContainer = styled.div` type LogoutPanelProps = { title: string; content: (() => JSX.Element) | JSX.Element; - width: number, - height: number, + okButtonText?: string; + cancelButtonText?: string; onCancel: () => void; onConfirm: () => void; }; @@ -83,60 +81,38 @@ const CloseCrossButton = styled(CloseIcon)` &:hover { fill: ${({ theme }) => theme.colors.mainDimColorInverted}; } - `; +`; export const ModalDialog = ({ title, content, - width, - height, + cancelButtonText = "Cancel", + okButtonText = "Ok", onCancel, onConfirm, }: LogoutPanelProps) => { const theme = useTheme(); - const renderPopoverContent = (): JSX.Element => { - return ( - <> - - - - - - - {title} - {typeof content === 'function' ? content() : content} - - - - Cancel - - Log out - - - - ); - } - - const getPopoverStyle = () => { - return { - zIndex: "104", - width: "100%", - height: "100%" - }; - }; - return ( - - <> - + + + + + + + {title} + {typeof content === "function" ? content() : content} + + + + {cancelButtonText} + + {okButtonText} + + + ); }; diff --git a/src/pages/arcgis-auth-popup/arcgis-auth-popup.tsx b/src/pages/auth/auth.tsx similarity index 67% rename from src/pages/arcgis-auth-popup/arcgis-auth-popup.tsx rename to src/pages/auth/auth.tsx index 28ec358c..4a0275ac 100644 --- a/src/pages/arcgis-auth-popup/arcgis-auth-popup.tsx +++ b/src/pages/auth/auth.tsx @@ -1,3 +1,4 @@ +import { useEffect } from "react"; import Background from "../../../public/images/tools-background.webp"; import styled from "styled-components"; import { @@ -22,24 +23,22 @@ const AuthContainer = styled.div` background-size: cover; height: ${getCurrentLayoutProperty({ - desktop: "calc(100vh - 65px)", - tablet: "calc(100vh - 65px)", - mobile: "calc(100vh - 58px)", -})}; + desktop: "calc(100vh - 65px)", + tablet: "calc(100vh - 65px)", + mobile: "calc(100vh - 58px)", + })}; margin-top: ${getCurrentLayoutProperty({ - desktop: "65px", - tablet: "65px", - mobile: "58px", -})}; + desktop: "65px", + tablet: "65px", + mobile: "58px", + })}; `; export const AuthApp = () => { const layout = useAppLayout(); - arcGisCompleteLogin(); - return ( - - - ); - -} + useEffect(() => { + arcGisCompleteLogin(); + }, []); + return ; +}; diff --git a/src/pages/auth/e2e.auth.spec.ts b/src/pages/auth/e2e.auth.spec.ts new file mode 100644 index 00000000..7f486212 --- /dev/null +++ b/src/pages/auth/e2e.auth.spec.ts @@ -0,0 +1,23 @@ +import puppeteer from "puppeteer"; + +describe("Auth page Default View", () => { + let browser; + let page; + + beforeAll(async () => { + browser = await puppeteer.launch(); + page = await browser.newPage(); + await page.setViewport({ width: 1366, height: 768 }); + await page.goto("http://localhost:3000"); + }); + + afterAll(() => browser.close()); + + it("Should render the page", async () => { + const authPageUrl = "http://localhost:3000/auth"; + + await page.goto(authPageUrl); + const currentUrl = page.url(); + expect(currentUrl).toBe(authPageUrl); + }); +}); diff --git a/src/pages/index.ts b/src/pages/index.ts index e8552292..ae493050 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -2,5 +2,5 @@ export { Dashboard } from "./dashboard/dashboard"; export { ViewerApp } from "./viewer-app/viewer-app"; export { DebugApp } from "./debug-app/debug-app"; export { Comparison } from './comparison/comparison'; -export { AuthApp } from './arcgis-auth-popup/arcgis-auth-popup'; +export { AuthApp } from './auth/auth'; diff --git a/src/redux/slices/arcgis-auth-slice.ts b/src/redux/slices/arcgis-auth-slice.ts index 77eceef6..d4d3a79e 100644 --- a/src/redux/slices/arcgis-auth-slice.ts +++ b/src/redux/slices/arcgis-auth-slice.ts @@ -1,6 +1,10 @@ import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import { RootState } from "../store"; -import { getAuthenticatedUser, arcGisRequestLogin, arcGisRequestLogout } from '../../utils/arcgis-auth'; +import { + getAuthenticatedUser, + arcGisRequestLogin, + arcGisRequestLogout, +} from "../../utils/arcgis-auth"; // Define a type for the slice state export interface ArcGisAuthState { @@ -9,38 +13,31 @@ export interface ArcGisAuthState { const initialState: ArcGisAuthState = { user: getAuthenticatedUser(), -} +}; const arcGisAuthSlice = createSlice({ name: "arcGisAuth", initialState, - reducers: { - }, + reducers: {}, extraReducers: (builder) => { - builder.addCase(arcGisLogin.fulfilled, (state, action) => { - state.user = action.payload || ''; - }) - .addCase(arcGisLogout.fulfilled, (state) => { - state.user = ''; + builder + .addCase(arcGisLogin.fulfilled, (state, action) => { + state.user = action.payload || ""; }) + .addCase(arcGisLogout.fulfilled, (state) => { + state.user = ""; + }); }, }); -export const arcGisLogin = createAsyncThunk( - 'arcGisLogin', - async () => { - return await arcGisRequestLogin(); - } -); +export const arcGisLogin = createAsyncThunk("arcGisLogin", async () => { + return await arcGisRequestLogin(); +}); -export const arcGisLogout = createAsyncThunk( - 'arcGisLogout', - async () => { - return await arcGisRequestLogout(); - } -); +export const arcGisLogout = createAsyncThunk("arcGisLogout", async () => { + return await arcGisRequestLogout(); +}); -export const selectUser = (state: RootState): string => - state.arcGisAuth.user; +export const selectUser = (state: RootState): string => state.arcGisAuth.user; export default arcGisAuthSlice.reducer; diff --git a/src/utils/arcgis-auth.spec.ts b/src/utils/arcgis-auth.spec.ts new file mode 100644 index 00000000..fe15e99c --- /dev/null +++ b/src/utils/arcgis-auth.spec.ts @@ -0,0 +1,44 @@ +import { + getAuthenticatedUser, + arcGisRequestLogin, + arcGisRequestLogout, +} from "./arcgis-auth"; + +jest.mock("@esri/arcgis-rest-request"); + +const ARCGIS_REST_USER_INFO = "__ARCGIS_REST_USER_INFO__"; +const mockEmailExpected = "usermail@gmail.com"; + +let OLD_ENV = {}; + +beforeAll(() => { + OLD_ENV = process.env; +}); + +describe("getAuthenticatedUser", () => { + beforeEach(() => { + jest.resetModules(); // Clear the cache + process.env = { ...OLD_ENV }; + process.env.REACT_APP_ARCGIS_REST_CLIENT_ID = "CLIENT_ID"; + }); + + afterAll(() => { + process.env = OLD_ENV; + }); + + it("Should return email", () => { + localStorage.setItem(ARCGIS_REST_USER_INFO, mockEmailExpected); + const email = getAuthenticatedUser(); + expect(email).toEqual(mockEmailExpected); + }); + + it("Should return session", async () => { + const email = await arcGisRequestLogin(); + expect(email).toBe(mockEmailExpected); + }); + + it("Should return empty session", async () => { + const email = await arcGisRequestLogout(); + expect(email).toBe(""); + }); +}); diff --git a/src/utils/arcgis-auth.ts b/src/utils/arcgis-auth.ts index a6921493..36d397ca 100644 --- a/src/utils/arcgis-auth.ts +++ b/src/utils/arcgis-auth.ts @@ -1,21 +1,23 @@ -import { ArcGISIdentityManager } from '@esri/arcgis-rest-request'; +import { ArcGISIdentityManager } from "@esri/arcgis-rest-request"; -const ARCGIS_REST_USER_SESSION = '__ARCGIS_REST_USER_SESSION__'; -const ARCGIS_REST_USER_INFO = '__ARCGIS_REST_USER_INFO__'; +const ARCGIS_REST_USER_SESSION = "__ARCGIS_REST_USER_SESSION__"; +const ARCGIS_REST_USER_INFO = "__ARCGIS_REST_USER_INFO__"; -const updateSessionInfo = async (session?: ArcGISIdentityManager): Promise => { - let email = ''; +const updateSessionInfo = async ( + session?: ArcGISIdentityManager +): Promise => { + let email = ""; if (session) { localStorage.setItem(ARCGIS_REST_USER_SESSION, session.serialize()); const user = await session.getUser(); - email = user.email || ''; + email = user.email || ""; localStorage.setItem(ARCGIS_REST_USER_INFO, email); } else { localStorage.removeItem(ARCGIS_REST_USER_SESSION); localStorage.removeItem(ARCGIS_REST_USER_INFO); } return email; -} +}; function getArcGisSession(): ArcGISIdentityManager | undefined { let session; @@ -31,42 +33,41 @@ function getArcGisSession(): ArcGISIdentityManager | undefined { * @returns the redirection URL and the client ID. */ const getAuthOptions = () => { - const port = window.location.port ? `:${window.location.port}` : ''; + const port = window.location.port ? `:${window.location.port}` : ""; const options = { redirectUri: `${window.location.protocol}//${window.location.hostname}${port}/auth`, - clientId: process.env.REACT_APP_ARCGIS_REST_CLIENT_ID || '', + clientId: process.env.REACT_APP_ARCGIS_REST_CLIENT_ID || "", popup: true, - pkce: true + pkce: true, }; if (!options.clientId) { console.error("The ClientId is not defined in .env file."); } return options; -} +}; /** * Gets the email of the currently logged in user. * @returns the user's email or an empty string if the user is not logged in. */ export const getAuthenticatedUser = (): string => { - return localStorage.getItem(ARCGIS_REST_USER_INFO) || ''; -} + return localStorage.getItem(ARCGIS_REST_USER_INFO) || ""; +}; /** * Makes an ArcGIS login request by opening a popup dialog. * @returns email of the user logged in or an empty string if the user is not logged in. */ export const arcGisRequestLogin = async () => { - let email = ''; + let email = ""; const options = getAuthOptions(); if (options.clientId) { let session: ArcGISIdentityManager | undefined; try { session = await ArcGISIdentityManager.beginOAuth2(options); - } - finally { + } finally { // In case of an exception the session is not set. // So the following call will remove any session stored in the local storage. email = await updateSessionInfo(session); @@ -83,7 +84,7 @@ export const arcGisCompleteLogin = async () => { if (options.clientId) { ArcGISIdentityManager.completeOAuth2(options); } -} +}; /** * Makes an ArcGIS logout request. From 2ca2a2cd561d67b07def27e51a4001b7a8d1b09f Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Tue, 19 Dec 2023 15:25:48 +0300 Subject: [PATCH 6/9] tests added, fixing dev review issues --- .../action-icon-button.spec.tsx | 31 +++++--- .../arcgis-user/arcgis-user.spec.tsx | 20 ++--- .../layers-panel/layers-control-panel.tsx | 17 ++--- .../modal-dialog/modal-dialog.spec.tsx | 48 ++++++++++++ src/components/modal-dialog/modal-dialog.tsx | 68 +++++++++++------ src/pages/auth/auth.tsx | 2 +- src/redux/slices/arcgis-auth-slice.spec.ts | 76 +++++++++++++++++++ src/redux/slices/arcgis-auth-slice.ts | 5 +- src/utils/arcgis-auth.spec.ts | 8 +- 9 files changed, 209 insertions(+), 66 deletions(-) create mode 100644 src/components/modal-dialog/modal-dialog.spec.tsx create mode 100644 src/redux/slices/arcgis-auth-slice.spec.ts diff --git a/src/components/action-icon-button/action-icon-button.spec.tsx b/src/components/action-icon-button/action-icon-button.spec.tsx index 95802fa8..1771838c 100644 --- a/src/components/action-icon-button/action-icon-button.spec.tsx +++ b/src/components/action-icon-button/action-icon-button.spec.tsx @@ -11,33 +11,42 @@ const callRender = (renderFunc, props = {}) => { return renderFunc( + {...props} + /> ); }; describe("ActionIconButton", () => { it("Should render small Plus icon in the button", () => { - const { container } = callRender(renderWithTheme, { children: 'Test Button' }); + const { container } = callRender(renderWithTheme, { + children: "Test Button", + }); expect(container).toBeInTheDocument(); - const button = screen.getByText('Test Button'); - const buttonHeight = getComputedStyle(button.previousSibling as Element).getPropertyValue("height"); - expect(buttonHeight).toEqual('24px'); + const button = screen.getByText("Test Button"); + const buttonHeight = getComputedStyle( + button.previousSibling as Element + ).getPropertyValue("height"); + expect(buttonHeight).toEqual("24px"); userEvent.click(button); expect(onClickMock).toHaveBeenCalled(); }); it("Should render Big Plus icon in the button", () => { - const { container } = callRender(renderWithTheme, { children: 'Test Button', size: ButtonSize.Big }); + const { container } = callRender(renderWithTheme, { + children: "Test Button", + size: ButtonSize.Big, + }); expect(container).toBeInTheDocument(); - const button = screen.getByText('Test Button'); - const buttonHeight = getComputedStyle(button.previousSibling as Element).getPropertyValue("height"); - expect(buttonHeight).toEqual('40px'); + const button = screen.getByText("Test Button"); + const buttonHeight = getComputedStyle( + button.previousSibling as Element + ).getPropertyValue("height"); + expect(buttonHeight).toEqual("40px"); userEvent.click(button); expect(onClickMock).toHaveBeenCalled(); }); - }); diff --git a/src/components/arcgis-user/arcgis-user.spec.tsx b/src/components/arcgis-user/arcgis-user.spec.tsx index 172d3ab7..e2fa389f 100644 --- a/src/components/arcgis-user/arcgis-user.spec.tsx +++ b/src/components/arcgis-user/arcgis-user.spec.tsx @@ -6,25 +6,21 @@ import { AcrGisUser } from "./arcgis-user"; const onClickMock = jest.fn(); const callRender = (renderFunc, props) => { - return renderFunc( - - ); + return renderFunc(); }; describe("AcrGisUser", () => { it("Should render Logout button", () => { - const { container } = callRender(renderWithTheme, { children: 'Test Button' }); + const { container } = callRender(renderWithTheme, { + children: "Test Button", + }); expect(container).toBeInTheDocument(); - const button = screen.getByText('Test Button'); + const button = screen.getByText("Test Button"); const buttonIcon = button.nextSibling as Element; - const buttonHeight = getComputedStyle(buttonIcon).getPropertyValue( - "height" - ); - expect(buttonHeight).toEqual('17px'); + const buttonHeight = + getComputedStyle(buttonIcon).getPropertyValue("height"); + expect(buttonHeight).toEqual("17px"); userEvent.click(buttonIcon); expect(onClickMock).toHaveBeenCalled(); }); - }); diff --git a/src/components/layers-panel/layers-control-panel.tsx b/src/components/layers-panel/layers-control-panel.tsx index 6de6fc48..1da26ab2 100644 --- a/src/components/layers-panel/layers-control-panel.tsx +++ b/src/components/layers-panel/layers-control-panel.tsx @@ -161,16 +161,6 @@ export const LayersControlPanel = ({ return selectedState; }; - const renderModalDialogContent = (): JSX.Element => { - return ( - <> - Are you sure you want to log out? - You are logged in as - {username} - - ); - }; - const renderLayers = ( layers: LayerExample[], parentLayer?: LayerExample, @@ -281,7 +271,6 @@ export const LayersControlPanel = ({ {showLogoutWarning && ( { dispatch(arcGisLogout()); @@ -290,7 +279,11 @@ export const LayersControlPanel = ({ onCancel={() => { setShowLogoutWarning(false); }} - /> + > + Are you sure you want to log out? + You are logged in as + {username} + )} diff --git a/src/components/modal-dialog/modal-dialog.spec.tsx b/src/components/modal-dialog/modal-dialog.spec.tsx new file mode 100644 index 00000000..77981608 --- /dev/null +++ b/src/components/modal-dialog/modal-dialog.spec.tsx @@ -0,0 +1,48 @@ +import { screen, within } from "@testing-library/react"; +import { renderWithThemeProviders } from "../../utils/testing-utils/render-with-theme"; +import { ModalDialog } from "./modal-dialog"; +import userEvent from "@testing-library/user-event"; +import { setupStore } from "../../redux/store"; + +const onCancel = jest.fn(); +const onConfirm = jest.fn(); + +const callRender = (renderFunc, props = {}, store = setupStore()) => { + return renderFunc( + + <> + , + store + ); +}; + +describe("ModalDialog", () => { + it("Should render dialog", async () => { + const store = setupStore(); + const { container } = callRender( + renderWithThemeProviders, + undefined, + store + ); + expect(container).toBeInTheDocument(); + const dialog = screen.getByTestId("modal-dialog-content"); + expect(dialog).toBeInTheDocument(); + + const title = within(dialog).getByText("Test Title"); + expect(title).toBeInTheDocument(); + + const okButton = within(dialog).getByText("Log out"); + okButton && userEvent.click(okButton); + expect(onConfirm).toHaveBeenCalledTimes(1); + + const cancelButton = within(dialog).getByText("Exit"); + cancelButton && userEvent.click(cancelButton); + expect(onCancel).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/modal-dialog/modal-dialog.tsx b/src/components/modal-dialog/modal-dialog.tsx index ad17f85a..6da831fa 100644 --- a/src/components/modal-dialog/modal-dialog.tsx +++ b/src/components/modal-dialog/modal-dialog.tsx @@ -2,6 +2,7 @@ import styled, { useTheme } from "styled-components"; import { ActionButton } from "../action-button/action-button"; import { ActionButtonVariant } from "../../types"; import CloseIcon from "../../../public/icons/close.svg"; +import { color_brand_primary } from "../../constants/colors"; const Overlay = styled.div` position: fixed; @@ -12,16 +13,29 @@ const Overlay = styled.div` display: flex; justify-content: center; align-items: center; - background: #00000099; + background: ${color_brand_primary}80; z-index: 103; `; +const WrapperContainer = styled.div` + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + visibility: hidden; +`; + const Container = styled.div` position: absolute; display: flex; flex-direction: column; border-radius: 8px; - background: ${({ theme }) => theme.colors.mainColor}; + background: ${({ theme }) => theme.colors.mainHelpPanelColor}; + visibility: visible; z-index: 104; `; @@ -70,7 +84,7 @@ const ButtonsContainer = styled.div` type LogoutPanelProps = { title: string; - content: (() => JSX.Element) | JSX.Element; + children: JSX.Element | JSX.Element[]; okButtonText?: string; cancelButtonText?: string; onCancel: () => void; @@ -85,7 +99,7 @@ const CloseCrossButton = styled(CloseIcon)` export const ModalDialog = ({ title, - content, + children, cancelButtonText = "Cancel", okButtonText = "Ok", onCancel, @@ -94,25 +108,31 @@ export const ModalDialog = ({ const theme = useTheme(); return ( - - - - - - - {title} - {typeof content === "function" ? content() : content} - - - - {cancelButtonText} - - {okButtonText} - - - + <> + + + + + + + + {title} + {children} + + + + {cancelButtonText} + + {okButtonText} + + + + ); }; diff --git a/src/pages/auth/auth.tsx b/src/pages/auth/auth.tsx index 4a0275ac..2bb257d7 100644 --- a/src/pages/auth/auth.tsx +++ b/src/pages/auth/auth.tsx @@ -40,5 +40,5 @@ export const AuthApp = () => { useEffect(() => { arcGisCompleteLogin(); }, []); - return ; + return ; }; diff --git a/src/redux/slices/arcgis-auth-slice.spec.ts b/src/redux/slices/arcgis-auth-slice.spec.ts new file mode 100644 index 00000000..8023ff9a --- /dev/null +++ b/src/redux/slices/arcgis-auth-slice.spec.ts @@ -0,0 +1,76 @@ +import { setupStore } from "../store"; +import reducer, { + selectUser, + arcGisLogin, + arcGisLogout, +} from "./arcgis-auth-slice"; + +import { + getAuthenticatedUser, + arcGisRequestLogin, + arcGisCompleteLogin, + arcGisRequestLogout, +} from "../../utils/arcgis-auth"; + +jest.mock("../../utils/arcgis-auth"); + +const getAuthenticatedUserMock = + getAuthenticatedUser as unknown as jest.Mocked; +const arcGisRequestLoginMock = + arcGisRequestLogin as unknown as jest.Mocked; +const arcGisCompleteLoginMock = + arcGisCompleteLogin as unknown as jest.Mocked; +const arcGisRequestLogoutMock = + arcGisRequestLogout as unknown as jest.Mocked; + +const mockEmailExpected = "usermail@gmail.com"; +let mockStorageUserinfo = mockEmailExpected; + +describe("slice: arcgis-auth", () => { + beforeAll(() => { + arcGisRequestLoginMock.mockImplementation(async () => { + mockStorageUserinfo = mockEmailExpected; + return mockStorageUserinfo; + }); + arcGisCompleteLoginMock.mockImplementation(async () => { + return mockStorageUserinfo; + }); + arcGisRequestLogoutMock.mockImplementation(async () => { + mockStorageUserinfo = ""; + return mockStorageUserinfo; + }); + getAuthenticatedUserMock.mockImplementation(() => { + return mockStorageUserinfo; + }); + }); + + beforeEach(() => { + mockStorageUserinfo = mockEmailExpected; + }); + + it("Reducer should return the initial state", () => { + expect(reducer(undefined, { type: undefined })).toEqual({ + user: mockEmailExpected, + }); + }); + + it("Selector should return the initial state", () => { + const store = setupStore(); + const state = store.getState(); + expect(selectUser(state)).toEqual(mockEmailExpected); + }); + + it("Selector should return the updated value after Login", async () => { + const store = setupStore(); + await store.dispatch(arcGisLogin()); + const state = store.getState(); + expect(selectUser(state)).toEqual(mockEmailExpected); + }); + + it("Selector should return the updated value after Logout", async () => { + const store = setupStore(); + await store.dispatch(arcGisLogout()); + const state = store.getState(); + expect(selectUser(state)).toEqual(""); + }); +}); diff --git a/src/redux/slices/arcgis-auth-slice.ts b/src/redux/slices/arcgis-auth-slice.ts index d4d3a79e..7e7f6ea6 100644 --- a/src/redux/slices/arcgis-auth-slice.ts +++ b/src/redux/slices/arcgis-auth-slice.ts @@ -11,8 +11,9 @@ export interface ArcGisAuthState { user: string; } -const initialState: ArcGisAuthState = { - user: getAuthenticatedUser(), +// "lazy initializer" +const initialState = () => { + return { user: getAuthenticatedUser() }; }; const arcGisAuthSlice = createSlice({ diff --git a/src/utils/arcgis-auth.spec.ts b/src/utils/arcgis-auth.spec.ts index fe15e99c..34ebf69d 100644 --- a/src/utils/arcgis-auth.spec.ts +++ b/src/utils/arcgis-auth.spec.ts @@ -15,7 +15,7 @@ beforeAll(() => { OLD_ENV = process.env; }); -describe("getAuthenticatedUser", () => { +describe("ArcGIS auth functions", () => { beforeEach(() => { jest.resetModules(); // Clear the cache process.env = { ...OLD_ENV }; @@ -26,18 +26,18 @@ describe("getAuthenticatedUser", () => { process.env = OLD_ENV; }); - it("Should return email", () => { + it("Should return email of user logged in", () => { localStorage.setItem(ARCGIS_REST_USER_INFO, mockEmailExpected); const email = getAuthenticatedUser(); expect(email).toEqual(mockEmailExpected); }); - it("Should return session", async () => { + it("Should request login and return email of user", async () => { const email = await arcGisRequestLogin(); expect(email).toBe(mockEmailExpected); }); - it("Should return empty session", async () => { + it("Should request logout and return empty string", async () => { const email = await arcGisRequestLogout(); expect(email).toBe(""); }); From bf4cdee845ceb367a104a7f72f7d8276174a6c7b Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Tue, 19 Dec 2023 15:37:11 +0300 Subject: [PATCH 7/9] fix z-order --- src/components/modal-dialog/modal-dialog.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/modal-dialog/modal-dialog.tsx b/src/components/modal-dialog/modal-dialog.tsx index 6da831fa..3c8d7ea4 100644 --- a/src/components/modal-dialog/modal-dialog.tsx +++ b/src/components/modal-dialog/modal-dialog.tsx @@ -27,6 +27,7 @@ const WrapperContainer = styled.div` justify-content: center; align-items: center; visibility: hidden; + z-index: 104; `; const Container = styled.div` @@ -36,7 +37,7 @@ const Container = styled.div` border-radius: 8px; background: ${({ theme }) => theme.colors.mainHelpPanelColor}; visibility: visible; - z-index: 104; + z-index: 105; `; const IconContainer = styled.div` From a993c1cb164250a10f87ccaf9156ef2ccc73b813 Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Tue, 19 Dec 2023 20:10:28 +0300 Subject: [PATCH 8/9] fix opacity issue --- src/components/common.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/common.tsx b/src/components/common.tsx index 785213fc..5fce3c01 100644 --- a/src/components/common.tsx +++ b/src/components/common.tsx @@ -26,8 +26,7 @@ export const PanelContainer = styled.div` display: flex; flex-direction: column; width: 359px; - background: ${({ theme }) => theme.colors.mainCanvasColor}; - opacity: ${({ theme }) => (theme.name === Theme.Dark ? 0.9 : 1)}; + background: ${({ theme }) => `${theme.colors.mainCanvasColor}${theme.name === Theme.Dark ? 'e6' : 'ff'}` }; border-radius: 8px; padding-bottom: 26px; position: relative; From cf2422787f54dae1612868f0b8ab4013281866c5 Mon Sep 17 00:00:00 2001 From: mspivak-actionengine Date: Wed, 20 Dec 2023 13:15:28 +0300 Subject: [PATCH 9/9] fix minor dev review issues --- src/components/modal-dialog/modal-dialog.spec.tsx | 8 ++------ src/components/modal-dialog/modal-dialog.tsx | 1 + 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/modal-dialog/modal-dialog.spec.tsx b/src/components/modal-dialog/modal-dialog.spec.tsx index 77981608..849f5657 100644 --- a/src/components/modal-dialog/modal-dialog.spec.tsx +++ b/src/components/modal-dialog/modal-dialog.spec.tsx @@ -7,7 +7,7 @@ import { setupStore } from "../../redux/store"; const onCancel = jest.fn(); const onConfirm = jest.fn(); -const callRender = (renderFunc, props = {}, store = setupStore()) => { +const callRender = (renderFunc, store = setupStore()) => { return renderFunc( { describe("ModalDialog", () => { it("Should render dialog", async () => { const store = setupStore(); - const { container } = callRender( - renderWithThemeProviders, - undefined, - store - ); + const { container } = callRender(renderWithThemeProviders, store); expect(container).toBeInTheDocument(); const dialog = screen.getByTestId("modal-dialog-content"); expect(dialog).toBeInTheDocument(); diff --git a/src/components/modal-dialog/modal-dialog.tsx b/src/components/modal-dialog/modal-dialog.tsx index 3c8d7ea4..e2248bba 100644 --- a/src/components/modal-dialog/modal-dialog.tsx +++ b/src/components/modal-dialog/modal-dialog.tsx @@ -93,6 +93,7 @@ type LogoutPanelProps = { }; const CloseCrossButton = styled(CloseIcon)` + cursor: pointer; &:hover { fill: ${({ theme }) => theme.colors.mainDimColorInverted}; }