From 6b464928e46b643dd33d6bf5a86de6848228b1ac Mon Sep 17 00:00:00 2001 From: David Lyon Date: Thu, 12 Oct 2023 12:35:40 -0700 Subject: [PATCH 01/14] Fixed auth, added login, authed collections, aligned topbar --- src/app/App.tsx | 7 +- src/app/Routes.tsx | 12 ++- src/common/components/Select.tsx | 5 +- src/features/layout/TopBar.module.scss | 20 ++++ src/features/layout/TopBar.tsx | 133 +++++++++++++++++++++---- src/features/layout/layoutSlice.ts | 23 +++-- 6 files changed, 167 insertions(+), 33 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index 2ae39010..3e4d4d28 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -30,10 +30,15 @@ const useInitApp = () => { useLoggedInProfileUser(username); // Placeholder code for determining environment. + // TODO: needs to properly identify PROD, should we do this with an env var? useEffect(() => { // eslint-disable-next-line no-console console.info('Static Deploy Domain:', process.env.REACT_APP_KBASE_DOMAIN); - dispatch(setEnvironment('ci-europa')); + dispatch( + setEnvironment( + process.env.REACT_APP_KBASE_DOMAIN?.split('.')[0] ?? 'unknown' + ) + ); }, [dispatch]); return { isLoading: !initialized }; diff --git a/src/app/Routes.tsx b/src/app/Routes.tsx index b30153c9..ad1f8b95 100644 --- a/src/app/Routes.tsx +++ b/src/app/Routes.tsx @@ -64,9 +64,15 @@ const Routes: FC = () => { {/* Collections */} - } /> - } /> - } /> + } />} /> + } />} + /> + } />} + /> } /> diff --git a/src/common/components/Select.tsx b/src/common/components/Select.tsx index 283decac..2bcd2474 100644 --- a/src/common/components/Select.tsx +++ b/src/common/components/Select.tsx @@ -12,6 +12,7 @@ export interface SelectOption { label: ReactNode; value: string | number; icon?: ReactNode; + fullWidth?: boolean; // ignores icon padding when icons are present } export type OptionsArray = OptionsOrGroups< @@ -104,9 +105,9 @@ export const Select: FC = (props) => { const handleFormatOptionLabel = (data: SelectOption) => { return ( - {hasIcons && ( + {hasIcons && !data.fullWidth ? ( {data.icon} - )} + ) : null} {data.label} ); diff --git a/src/features/layout/TopBar.module.scss b/src/features/layout/TopBar.module.scss index fea3185f..c49acbae 100644 --- a/src/features/layout/TopBar.module.scss +++ b/src/features/layout/TopBar.module.scss @@ -62,6 +62,20 @@ } } +.login_prompt, +.login_prompt:visited { + color: use-color("primary"); + display: flex; + flex-flow: column nowrap; + text-align: center; + text-decoration: none; + width: 77px; + + svg { + font-size: 24px; + } +} + .login_menu { display: block; width: 77px; @@ -74,6 +88,7 @@ align-items: center; color: use_color("black"); display: flex; + gap: 5px; justify-content: left; padding: 4px; @@ -83,4 +98,9 @@ margin-right: 5px; } } + + .name_item { + text-align: center; + width: 100%; + } } diff --git a/src/features/layout/TopBar.tsx b/src/features/layout/TopBar.tsx index f55fd6a1..30ba3d34 100644 --- a/src/features/layout/TopBar.tsx +++ b/src/features/layout/TopBar.tsx @@ -11,23 +11,29 @@ import { faQuestionCircle, faSearch, faServer, + faSignIn, faSignOutAlt, faSortDown, - faSquare, faUser, faWrench, } from '@fortawesome/free-solid-svg-icons'; -import { FC } from 'react'; +import { FC, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import logo from '../../common/assets/logo/46_square.png'; import { Dropdown } from '../../common/components'; -import { useAppSelector } from '../../common/hooks'; -import { authUsername } from '../auth/authSlice'; -import { profileRealname } from '../profile/profileSlice'; +import { useAppDispatch, useAppSelector } from '../../common/hooks'; +import { authUsername, setAuth } from '../auth/authSlice'; import classes from './TopBar.module.scss'; +import { Link } from 'react-router-dom'; +import { getUserProfile } from '../../common/api/userProfileApi'; +import { revokeToken } from '../../common/api/authService'; +import { toast } from 'react-hot-toast'; +import { noOp } from '../common'; export default function TopBar() { + const username = useAppSelector(authUsername); + return (
@@ -43,16 +49,33 @@ export default function TopBar() {
- + {username ? : }
); } -const LoginMenu: FC = () => { +const LoginPrompt: FC = () => ( + + + Sign In + +); + +const UserMenu: FC = () => { const username = useAppSelector(authUsername); - const realname = useAppSelector(profileRealname); + const { data: profData } = getUserProfile.useQuery( + useMemo( + () => ({ + usernames: [username || ''], + }), + [username] + ), + { skip: !username } + ); + const realname = profData?.[0]?.[0]?.user.realname; const navigate = useNavigate(); + const logout = useLogout(); return (
{ { value: '', icon: undefined, + fullWidth: true, label: ( -
+
{realname}
{username} @@ -77,12 +101,12 @@ const LoginMenu: FC = () => { { options: [ { - value: '/profile', + value: '/legacy/people', icon: , label: 'Your Profile', }, { - value: '#your_account', + value: '/legacy/account', icon: , label: 'Your Account', }, @@ -91,7 +115,7 @@ const LoginMenu: FC = () => { { options: [ { - value: '#sign_out', + value: 'LOGOUT', icon: , label: 'Sign Out', }, @@ -99,11 +123,17 @@ const LoginMenu: FC = () => { }, ]} onChange={(opt) => { - if (opt?.[0]) navigate(opt[0].value as string); + if (opt?.[0]) { + if (opt[0].value === 'LOGOUT') { + logout(); + } else { + navigate(opt[0].value as string); + } + } }} >
- +
@@ -111,7 +141,30 @@ const LoginMenu: FC = () => { ); }; +const useLogout = () => { + const tokenId = useAppSelector(({ auth }) => auth.tokenInfo?.id); + const dispatch = useAppDispatch(); + const [revoke] = revokeToken.useMutation(); + const navigate = useNavigate(); + + if (!tokenId) return noOp; + + return () => { + revoke(tokenId) + .unwrap() + .then(() => { + dispatch(setAuth(null)); + toast('You have been signed out'); + navigate('/legacy/auth2/signedout'); + }) + .catch(() => { + toast('Error, could not log out.'); + }); + }; +}; + const HamburgerMenu: FC = () => { + const navigate = useNavigate(); return (
{ { options: [ { - value: window.location.origin + '/#narrativemanager/start', + value: '/legacy/narrativemanager/start', icon: , label: 'Narrative Interface', }, { - value: window.location.origin + '/#narrativemanager/new', + value: '/legacy/narrativemanager/new', icon: , label: 'New Narrative', }, { - value: window.location.origin + '/#jgi-search', + value: '/legacy/jgi-search', icon: , label: 'JGI Search', }, { - value: window.location.origin + '/#biochem-search', + value: '/legacy/biochem-search', icon: , label: 'Biochem Search', }, @@ -143,7 +196,7 @@ const HamburgerMenu: FC = () => { { options: [ { - value: window.location.origin + '/#about/services', + value: '/legacy/about/services', icon: , label: 'KBase Services Status', }, @@ -152,7 +205,7 @@ const HamburgerMenu: FC = () => { { options: [ { - value: window.location.origin + '/#about', + value: '/legacy/about', icon: , label: 'About', }, @@ -170,7 +223,13 @@ const HamburgerMenu: FC = () => { }, ]} onChange={(opt) => { - if (opt?.[0]) window.location.href = opt[0].value as string; + if (typeof opt?.[0]?.value === 'string') { + if (opt[0].value.startsWith('http')) { + window.location.href = opt[0].value; + } else { + navigate(opt[0].value, { relative: 'path' }); + } + } }} > @@ -179,6 +238,36 @@ const HamburgerMenu: FC = () => { ); }; +const UserAvatar = () => { + const username = useAppSelector(authUsername); + const { data: profData } = getUserProfile.useQuery( + useMemo( + () => ({ + usernames: [username || ''], + }), + [username] + ), + { skip: !username } + ); + const avatarUri = useMemo(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const profile = profData ? (profData[0][0]?.profile as any) : undefined; + const avatarOption = profile?.userdata?.avatarOption || 'gravatar'; + if (avatarOption === 'gravatar') { + const gravatarDefault = profile?.userdata?.gravatarDefault || 'identicon'; + const gravatarHash = profile?.synced?.gravatarHash; + if (gravatarHash) { + return `https://www.gravatar.com/avatar/${gravatarHash}?s=300&r=pg&d=${gravatarDefault}`; + } else { + return `https://${process.env.REACT_APP_KBASE_LEGACY_DOMAIN}/images/nouserpic.png`; + } + } else { + return `https://${process.env.REACT_APP_KBASE_LEGACY_DOMAIN}/images/nouserpic.png`; + } + }, [profData]); + return {'avatar'}; +}; + const PageTitle: FC = () => { const title = useAppSelector((state) => state.layout.pageTitle); return ( @@ -196,10 +285,12 @@ const Enviroment: FC = () => { 'ci-europa': faFlask, unknown: faQuestionCircle, appdev: faWrench, + 'narrative-dev': faWrench, }[env]; const txt = { ci: 'CI', 'ci-europa': 'EUR', + 'narrative-dev': 'NARDEV', unknown: '??', appdev: 'APPDEV', }[env]; diff --git a/src/features/layout/layoutSlice.ts b/src/features/layout/layoutSlice.ts index 8992668e..068d1b67 100644 --- a/src/features/layout/layoutSlice.ts +++ b/src/features/layout/layoutSlice.ts @@ -2,8 +2,17 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { useEffect } from 'react'; import { useAppDispatch } from '../../common/hooks'; +const environments = [ + 'unknown', + 'production', + 'ci', + 'appdev', + 'ci-europa', + 'narrative-dev', +] as const; + interface PageState { - environment: 'unknown' | 'production' | 'ci' | 'appdev' | 'ci-europa'; + environment: typeof environments[number]; pageTitle: string; modalDialogId?: string; } @@ -17,11 +26,13 @@ export const pageSlice = createSlice({ name: 'page', initialState, reducers: { - setEnvironment: ( - state, - action: PayloadAction - ) => { - state.environment = action.payload; + setEnvironment: (state, action: PayloadAction) => { + const env = action.payload.toLowerCase(); + if (environments.includes(env as typeof environments[number])) { + state.environment = env as typeof environments[number]; + } else { + state.environment = 'unknown'; + } }, setModalDialogId: ( state, From cedda491ee6759a0f8e2758704a31cffefadcc7d Mon Sep 17 00:00:00 2001 From: David Lyon Date: Fri, 13 Oct 2023 12:21:58 -0700 Subject: [PATCH 02/14] Add config file and env configuration --- .env | 2 ++ Dockerfile | 2 +- config.json | 30 ++++++++++++++++++++++++++++++ scripts/build_deploy.sh | 29 ++++++++++++++--------------- src/app/App.tsx | 7 +------ 5 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 config.json diff --git a/.env b/.env index 2eca9bc6..187d3a48 100644 --- a/.env +++ b/.env @@ -1,6 +1,8 @@ # Base URL path for the enviroment PUBLIC_URL = '/dev/' +# Name of enviroment for build +REACT_APP_KBASE_ENV=ci-europa # Domain of enviroment for build REACT_APP_KBASE_DOMAIN=ci-europa.kbase.us # The following must be a subdomain of REACT_APP_KBASE_DOMAIN diff --git a/Dockerfile b/Dockerfile index bb19c5df..10136a1e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM nginx:stable-alpine3.17-slim USER root ## Install sed command needed for startup script -RUN apk add --no-cache sed +RUN apk add --no-cache sed jq ## Copy built static files for all enviroments to image COPY ./deploy /deploy/ diff --git a/config.json b/config.json new file mode 100644 index 00000000..0c7f2c98 --- /dev/null +++ b/config.json @@ -0,0 +1,30 @@ +{ + "environments": { + "appdev": { + "domain": "appdev.kbase.us", + "legacy": "legacy.appdev.kbase.us", + "public_url": "/" + }, + + "ci": { + "domain": "ci.kbase.us", + "legacy": "legacy.kbase.us", + "public_url": "/" + }, + "ci-europa": { + "domain": "ci-europa.kbase.us", + "legacy": "legacy.ci-europa.kbase.us", + "public_url": "/" + }, + "narrative-dev": { + "domain": "narrative-dev.kbase.us", + "legacy": "legacy.narrative-dev.kbase.us", + "public_url": "/" + }, + "production": { + "domain": "narrative.kbase.us", + "legacy": "legacy.narrative.kbase.us", + "public_url": "/" + } + } +} diff --git a/scripts/build_deploy.sh b/scripts/build_deploy.sh index 02cfce77..572c0b1e 100755 --- a/scripts/build_deploy.sh +++ b/scripts/build_deploy.sh @@ -1,21 +1,20 @@ #!/usr/bin/env bash +# Here we are using bash "here strings" +IFS=$'\n' read -d '' -r -a enviromentsConfig <<< "$(jq -r '.environments + | keys[] as $k + | [($k), (.[$k]["domain"]) , (.[$k]["legacy"]) , (.[$k]["public_url"])] + | join(" ")' config.json)" -declare -a enviroments=( -# " " - "ci ci.kbase.us legacy.kbase.us /" - "ci-europa ci-europa.kbase.us legacy.ci-europa.kbase.us /" - "narrative-dev narrative-dev.kbase.us legacy.narrative-dev.kbase.us /" -) +for enviro in "${enviromentsConfig[@]}"; do + read -a envConf <<< "$enviro" + echo "Building static files for enviroment \"${envConf[0]}\"..."; -for enviro in "${enviroments[@]}"; do - read -a strarr <<< "$enviro" - echo "Building static files for enviroment \"${strarr[0]}\"..."; - - BUILD_PATH="./deploy/${strarr[0]}" \ - REACT_APP_KBASE_DOMAIN="${strarr[1]}" \ - REACT_APP_KBASE_LEGACY_DOMAIN="${strarr[2]}" \ - PUBLIC_URL="${strarr[3]}" \ + BUILD_PATH="./deploy/${envConf[0]}" \ + REACT_APP_KBASE_ENV="${envConf[0]}" \ + REACT_APP_KBASE_DOMAIN="${envConf[1]}" \ + REACT_APP_KBASE_LEGACY_DOMAIN="${envConf[2]}" \ + PUBLIC_URL="${envConf[3]}" \ npm run build && \ - echo "Built static files for enviroment \"${strarr[0]}\"."; + echo "Built static files for enviroment \"${envConf[0]}\"."; done diff --git a/src/app/App.tsx b/src/app/App.tsx index 3e4d4d28..e85996a0 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -30,15 +30,10 @@ const useInitApp = () => { useLoggedInProfileUser(username); // Placeholder code for determining environment. - // TODO: needs to properly identify PROD, should we do this with an env var? useEffect(() => { // eslint-disable-next-line no-console console.info('Static Deploy Domain:', process.env.REACT_APP_KBASE_DOMAIN); - dispatch( - setEnvironment( - process.env.REACT_APP_KBASE_DOMAIN?.split('.')[0] ?? 'unknown' - ) - ); + dispatch(setEnvironment(process.env.REACT_APP_KBASE_ENV ?? 'unknown')); }, [dispatch]); return { isLoading: !initialized }; From 47c3224ef7f27083e44a523c670fbc6b2d961aa5 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Fri, 13 Oct 2023 12:51:34 -0700 Subject: [PATCH 03/14] a11 dropdowns, dropdown touches, env indicator clarity --- src/common/components/Dropdown.module.scss | 12 +++++++++++- src/features/layout/TopBar.module.scss | 2 +- src/features/layout/TopBar.tsx | 8 ++++---- .../NarrativeControl/NarrativeControl.module.scss | 5 ----- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/common/components/Dropdown.module.scss b/src/common/components/Dropdown.module.scss index e69b7486..4c3740da 100644 --- a/src/common/components/Dropdown.module.scss +++ b/src/common/components/Dropdown.module.scss @@ -1,7 +1,17 @@ .dropdown { // these prefixed global classnames are inserted by react-select + + // uses the a11-compatible visually hidden props as per + // https://www.a11yproject.com/posts/how-to-hide-content/ + // this preserves element selectability :global(.react-select__value-container) { - display: none; + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 0; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 0; } :global(.react-select__indicator-separator) { diff --git a/src/features/layout/TopBar.module.scss b/src/features/layout/TopBar.module.scss index c49acbae..acebe33c 100644 --- a/src/features/layout/TopBar.module.scss +++ b/src/features/layout/TopBar.module.scss @@ -79,6 +79,7 @@ .login_menu { display: block; width: 77px; + padding: 4px; .login_menu_username { font-style: italic; @@ -90,7 +91,6 @@ display: flex; gap: 5px; justify-content: left; - padding: 4px; .login_menu_icon { color: use_color("mid-blue"); diff --git a/src/features/layout/TopBar.tsx b/src/features/layout/TopBar.tsx index 30ba3d34..1430b3e7 100644 --- a/src/features/layout/TopBar.tsx +++ b/src/features/layout/TopBar.tsx @@ -281,18 +281,18 @@ const Enviroment: FC = () => { const env = useAppSelector((state) => state.layout.environment); if (env === 'production') return null; const icon = { + appdev: faWrench, ci: faFlask, 'ci-europa': faFlask, - unknown: faQuestionCircle, - appdev: faWrench, 'narrative-dev': faWrench, + unknown: faQuestionCircle, }[env]; const txt = { + appdev: 'APPDEV', ci: 'CI', 'ci-europa': 'EUR', 'narrative-dev': 'NARDEV', - unknown: '??', - appdev: 'APPDEV', + unknown: 'ENV?', }[env]; return (
diff --git a/src/features/navigator/NarrativeControl/NarrativeControl.module.scss b/src/features/navigator/NarrativeControl/NarrativeControl.module.scss index 69c10edd..75c415b8 100644 --- a/src/features/navigator/NarrativeControl/NarrativeControl.module.scss +++ b/src/features/navigator/NarrativeControl/NarrativeControl.module.scss @@ -15,11 +15,6 @@ font-size: 1.5rem; justify-content: space-between; } - - :global(.react-select__control .react-select__value-container) { - padding: 0; - width: 0; - } } .permission { From 1540b7c429fd81667afbfd4c073e3ac87ba6fbbc Mon Sep 17 00:00:00 2001 From: David Lyon Date: Fri, 13 Oct 2023 13:04:46 -0700 Subject: [PATCH 04/14] lint fixes (oops) --- config.json | 3 +++ src/features/layout/TopBar.module.scss | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/config.json b/config.json index 0c7f2c98..2e9b429f 100644 --- a/config.json +++ b/config.json @@ -11,16 +11,19 @@ "legacy": "legacy.kbase.us", "public_url": "/" }, + "ci-europa": { "domain": "ci-europa.kbase.us", "legacy": "legacy.ci-europa.kbase.us", "public_url": "/" }, + "narrative-dev": { "domain": "narrative-dev.kbase.us", "legacy": "legacy.narrative-dev.kbase.us", "public_url": "/" }, + "production": { "domain": "narrative.kbase.us", "legacy": "legacy.narrative.kbase.us", diff --git a/src/features/layout/TopBar.module.scss b/src/features/layout/TopBar.module.scss index acebe33c..ecbeccd3 100644 --- a/src/features/layout/TopBar.module.scss +++ b/src/features/layout/TopBar.module.scss @@ -78,8 +78,8 @@ .login_menu { display: block; - width: 77px; padding: 4px; + width: 77px; .login_menu_username { font-style: italic; From cf46d7ded7d191f2f82cbcd44c40a13ab59e4820 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Mon, 16 Oct 2023 10:22:50 -0700 Subject: [PATCH 05/14] add loggedout postessage event --- src/features/legacy/Legacy.test.tsx | 37 +++++++++++++++++++---------- src/features/legacy/Legacy.tsx | 14 +++++++++-- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/features/legacy/Legacy.test.tsx b/src/features/legacy/Legacy.test.tsx index 794f592c..4f26fe7b 100644 --- a/src/features/legacy/Legacy.test.tsx +++ b/src/features/legacy/Legacy.test.tsx @@ -13,7 +13,8 @@ import * as layoutSlice from '../layout/layoutSlice'; import Legacy, { formatLegacyUrl, getLegacyPart, - isAuthMessage, + isLoginMessage, + isLogoutMessage, isRouteMessage, isTitleMessage, LEGACY_BASE_ROUTE, @@ -36,11 +37,15 @@ const routeMessage = { source: 'kbase-ui.app.route-component', payload: { request: { original: '#/some/hash/path' } }, }; -const authMessage = { +const loginMessage = { source: 'kbase-ui.session.loggedin', payload: { token: 'some-token' }, }; -const nullAuthMessage = { +const logoutMessage = { + source: 'kbase-ui.session.loggedout', + payload: undefined, +}; +const nullLoginMessage = { source: 'kbase-ui.session.loggedin', payload: { token: null }, }; @@ -68,22 +73,30 @@ describe('Legacy', () => { test('isTitleMessage', () => { expect(isTitleMessage(titleMessage)).toBe(true); expect(isTitleMessage(routeMessage)).toBe(false); - expect(isTitleMessage(authMessage)).toBe(false); - expect(isTitleMessage(nullAuthMessage)).toBe(false); + expect(isTitleMessage(loginMessage)).toBe(false); + expect(isTitleMessage(nullLoginMessage)).toBe(false); }); test('isRouteMessage', () => { expect(isRouteMessage(titleMessage)).toBe(false); expect(isRouteMessage(routeMessage)).toBe(true); - expect(isRouteMessage(authMessage)).toBe(false); - expect(isRouteMessage(nullAuthMessage)).toBe(false); + expect(isRouteMessage(loginMessage)).toBe(false); + expect(isRouteMessage(nullLoginMessage)).toBe(false); + }); + + test('isLoginMessage', () => { + expect(isLoginMessage(titleMessage)).toBe(false); + expect(isLoginMessage(routeMessage)).toBe(false); + expect(isLoginMessage(loginMessage)).toBe(true); + expect(isLoginMessage(nullLoginMessage)).toBe(true); }); - test('isAuthMessage', () => { - expect(isAuthMessage(titleMessage)).toBe(false); - expect(isAuthMessage(routeMessage)).toBe(false); - expect(isAuthMessage(authMessage)).toBe(true); - expect(isAuthMessage(nullAuthMessage)).toBe(true); + test('isLogoutMessage', () => { + expect(isLogoutMessage(titleMessage)).toBe(false); + expect(isLogoutMessage(routeMessage)).toBe(false); + expect(isLogoutMessage(loginMessage)).toBe(false); + expect(isLogoutMessage(nullLoginMessage)).toBe(false); + expect(isLogoutMessage(logoutMessage)).toBe(false); }); test('getLegacyPart', () => { diff --git a/src/features/legacy/Legacy.tsx b/src/features/legacy/Legacy.tsx index 1963a899..d967514b 100644 --- a/src/features/legacy/Legacy.tsx +++ b/src/features/legacy/Legacy.tsx @@ -2,6 +2,8 @@ import { RefObject, useEffect, useRef, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { usePageTitle } from '../layout/layoutSlice'; import { useTryAuthFromToken } from '../auth/hooks'; +import { setAuth } from '../auth/authSlice'; +import { useAppDispatch } from '../../common/hooks'; export const LEGACY_BASE_ROUTE = '/legacy'; @@ -13,6 +15,7 @@ export default function Legacy() { const location = useLocation(); const navigate = useNavigate(); + const dispatch = useAppDispatch(); const legacyContentRef = useRef(null); const [legacyTitle, setLegacyTitle] = useState(''); @@ -43,10 +46,12 @@ export default function Legacy() { } } else if (isTitleMessage(d)) { setLegacyTitle(d.payload); - } else if (isAuthMessage(d)) { + } else if (isLoginMessage(d)) { if (d.payload.token) { setReceivedToken(d.payload.token); } + } else if (isLogoutMessage(d)) { + dispatch(setAuth(null)); } }); @@ -179,7 +184,7 @@ export const isRouteMessage = messageGuard( .original === 'string' ); -export const isAuthMessage = messageGuard( +export const isLoginMessage = messageGuard( 'kbase-ui.session.loggedin', (payload): payload is { token: string | null } => !!payload && @@ -188,3 +193,8 @@ export const isAuthMessage = messageGuard( (typeof (payload as Record).token === 'string' || (payload as Record).token === null) ); + +export const isLogoutMessage = messageGuard( + 'kbase-ui.session.loggedout', + (payload): payload is undefined => payload === undefined +); From 8cb717c4a4b6e25431e89213be36f5f007c90437 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Wed, 18 Oct 2023 11:08:08 -0700 Subject: [PATCH 06/14] add store reset action --- src/app/store.ts | 36 ++++++++++++++++++++++------------ src/features/layout/TopBar.tsx | 2 ++ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/app/store.ts b/src/app/store.ts index 1d810154..a568801c 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -1,4 +1,4 @@ -import { configureStore } from '@reduxjs/toolkit'; +import { Action, combineReducers, configureStore } from '@reduxjs/toolkit'; import { baseApi } from '../common/api'; import auth from '../features/auth/authSlice'; import collections from '../features/collections/collectionsSlice'; @@ -9,20 +9,30 @@ import navigator from '../features/navigator/navigatorSlice'; import params from '../features/params/paramsSlice'; import profile from '../features/profile/profileSlice'; +const everyReducer = combineReducers({ + auth, + collections, + count, + icons, + layout, + navigator, + params, + profile, + [baseApi.reducerPath]: baseApi.reducer, +}); + +const rootReducer: typeof everyReducer = (state, action) => { + if (action.type === 'RESET_STATE') { + return everyReducer(undefined, action); + } + + return everyReducer(state, action); +}; + const createStore = (additionalOptions?: T) => { return configureStore({ devTools: true, - reducer: { - auth, - collections, - count, - icons, - layout, - navigator, - params, - profile, - [baseApi.reducerPath]: baseApi.reducer, - }, + reducer: rootReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(baseApi.middleware), ...additionalOptions, @@ -35,6 +45,8 @@ export const store = createStore(); export type RootState = ReturnType; export type AppDispatch = typeof store.dispatch; +export const resetStateAction = (): Action => ({ type: 'RESET_STATE' }); + export const createTestStore = (preloadedState: Partial = {}) => { return createStore({ preloadedState: preloadedState }); }; diff --git a/src/features/layout/TopBar.tsx b/src/features/layout/TopBar.tsx index 1430b3e7..3011cc1f 100644 --- a/src/features/layout/TopBar.tsx +++ b/src/features/layout/TopBar.tsx @@ -30,6 +30,7 @@ import { getUserProfile } from '../../common/api/userProfileApi'; import { revokeToken } from '../../common/api/authService'; import { toast } from 'react-hot-toast'; import { noOp } from '../common'; +import { resetStateAction } from '../../app/store'; export default function TopBar() { const username = useAppSelector(authUsername); @@ -155,6 +156,7 @@ const useLogout = () => { .then(() => { dispatch(setAuth(null)); toast('You have been signed out'); + dispatch(resetStateAction()); navigate('/legacy/auth2/signedout'); }) .catch(() => { From 3f1960766cc5efda366fb7e244398497faeb534a Mon Sep 17 00:00:00 2001 From: David Lyon Date: Wed, 18 Oct 2023 11:10:06 -0700 Subject: [PATCH 07/14] reset state on legacy logout event --- src/features/legacy/Legacy.test.tsx | 2 +- src/features/legacy/Legacy.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/legacy/Legacy.test.tsx b/src/features/legacy/Legacy.test.tsx index 4f26fe7b..b283a239 100644 --- a/src/features/legacy/Legacy.test.tsx +++ b/src/features/legacy/Legacy.test.tsx @@ -96,7 +96,7 @@ describe('Legacy', () => { expect(isLogoutMessage(routeMessage)).toBe(false); expect(isLogoutMessage(loginMessage)).toBe(false); expect(isLogoutMessage(nullLoginMessage)).toBe(false); - expect(isLogoutMessage(logoutMessage)).toBe(false); + expect(isLogoutMessage(logoutMessage)).toBe(true); }); test('getLegacyPart', () => { diff --git a/src/features/legacy/Legacy.tsx b/src/features/legacy/Legacy.tsx index d967514b..13451126 100644 --- a/src/features/legacy/Legacy.tsx +++ b/src/features/legacy/Legacy.tsx @@ -2,8 +2,8 @@ import { RefObject, useEffect, useRef, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { usePageTitle } from '../layout/layoutSlice'; import { useTryAuthFromToken } from '../auth/hooks'; -import { setAuth } from '../auth/authSlice'; import { useAppDispatch } from '../../common/hooks'; +import { resetStateAction } from '../../app/store'; export const LEGACY_BASE_ROUTE = '/legacy'; @@ -51,7 +51,7 @@ export default function Legacy() { setReceivedToken(d.payload.token); } } else if (isLogoutMessage(d)) { - dispatch(setAuth(null)); + dispatch(resetStateAction()); } }); From dbfe7d0fcf7a9fe3825fe2c71e138e3c53140e2d Mon Sep 17 00:00:00 2001 From: David Lyon Date: Wed, 18 Oct 2023 11:34:57 -0700 Subject: [PATCH 08/14] add logged out console message --- src/features/legacy/Legacy.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/features/legacy/Legacy.tsx b/src/features/legacy/Legacy.tsx index 13451126..b55b9d03 100644 --- a/src/features/legacy/Legacy.tsx +++ b/src/features/legacy/Legacy.tsx @@ -51,6 +51,8 @@ export default function Legacy() { setReceivedToken(d.payload.token); } } else if (isLogoutMessage(d)) { + // eslint-disable-next-line no-console + console.info('Logged Out'); dispatch(resetStateAction()); } }); From 0b6845ad527ea5ea7787df0957c31d6b62c723cd Mon Sep 17 00:00:00 2001 From: David Lyon Date: Thu, 12 Oct 2023 12:35:40 -0700 Subject: [PATCH 09/14] Fixed auth, added login, authed collections, aligned topbar --- src/app/App.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/App.tsx b/src/app/App.tsx index e85996a0..c95478ad 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -30,6 +30,7 @@ const useInitApp = () => { useLoggedInProfileUser(username); // Placeholder code for determining environment. + // TODO: needs to properly identify PROD, should we do this with an env var? useEffect(() => { // eslint-disable-next-line no-console console.info('Static Deploy Domain:', process.env.REACT_APP_KBASE_DOMAIN); From b5a033db98d6056f1a42aa62c118a8fc59831748 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Fri, 13 Oct 2023 12:21:58 -0700 Subject: [PATCH 10/14] Add config file and env configuration --- config.json | 3 --- src/app/App.tsx | 1 - 2 files changed, 4 deletions(-) diff --git a/config.json b/config.json index 2e9b429f..0c7f2c98 100644 --- a/config.json +++ b/config.json @@ -11,19 +11,16 @@ "legacy": "legacy.kbase.us", "public_url": "/" }, - "ci-europa": { "domain": "ci-europa.kbase.us", "legacy": "legacy.ci-europa.kbase.us", "public_url": "/" }, - "narrative-dev": { "domain": "narrative-dev.kbase.us", "legacy": "legacy.narrative-dev.kbase.us", "public_url": "/" }, - "production": { "domain": "narrative.kbase.us", "legacy": "legacy.narrative.kbase.us", diff --git a/src/app/App.tsx b/src/app/App.tsx index c95478ad..e85996a0 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -30,7 +30,6 @@ const useInitApp = () => { useLoggedInProfileUser(username); // Placeholder code for determining environment. - // TODO: needs to properly identify PROD, should we do this with an env var? useEffect(() => { // eslint-disable-next-line no-console console.info('Static Deploy Domain:', process.env.REACT_APP_KBASE_DOMAIN); From 91cb755b740ed0e1aec061d89f2758c1bcdded82 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Fri, 13 Oct 2023 12:51:34 -0700 Subject: [PATCH 11/14] a11 dropdowns, dropdown touches, env indicator clarity --- src/features/layout/TopBar.module.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/features/layout/TopBar.module.scss b/src/features/layout/TopBar.module.scss index ecbeccd3..2ff19d71 100644 --- a/src/features/layout/TopBar.module.scss +++ b/src/features/layout/TopBar.module.scss @@ -80,6 +80,7 @@ display: block; padding: 4px; width: 77px; + padding: 4px; .login_menu_username { font-style: italic; From 05a2c422fa55d079997a34e16c8795619e9f346f Mon Sep 17 00:00:00 2001 From: David Lyon Date: Fri, 13 Oct 2023 13:04:46 -0700 Subject: [PATCH 12/14] lint fixes (oops) --- config.json | 3 +++ src/features/layout/TopBar.module.scss | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/config.json b/config.json index 0c7f2c98..2e9b429f 100644 --- a/config.json +++ b/config.json @@ -11,16 +11,19 @@ "legacy": "legacy.kbase.us", "public_url": "/" }, + "ci-europa": { "domain": "ci-europa.kbase.us", "legacy": "legacy.ci-europa.kbase.us", "public_url": "/" }, + "narrative-dev": { "domain": "narrative-dev.kbase.us", "legacy": "legacy.narrative-dev.kbase.us", "public_url": "/" }, + "production": { "domain": "narrative.kbase.us", "legacy": "legacy.narrative.kbase.us", diff --git a/src/features/layout/TopBar.module.scss b/src/features/layout/TopBar.module.scss index 2ff19d71..ecbeccd3 100644 --- a/src/features/layout/TopBar.module.scss +++ b/src/features/layout/TopBar.module.scss @@ -80,7 +80,6 @@ display: block; padding: 4px; width: 77px; - padding: 4px; .login_menu_username { font-style: italic; From cf828a64dd52a5e5f4d0d0ea15e91a494756aa15 Mon Sep 17 00:00:00 2001 From: David Lyon Date: Wed, 18 Oct 2023 13:26:21 -0700 Subject: [PATCH 13/14] initializes the auth state on logout to fix loading issue, fixes enviroment detection --- src/app/App.tsx | 4 ++-- src/features/layout/TopBar.tsx | 3 ++- src/features/legacy/Legacy.tsx | 7 +++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index e85996a0..8476c4ba 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -27,14 +27,14 @@ const useInitApp = () => { // Use authenticated username to load user's profile const username = useAppSelector(authUsername); const initialized = useAppSelector(authInitialized); + const environment = useAppSelector((state) => state.layout.environment); useLoggedInProfileUser(username); - // Placeholder code for determining environment. useEffect(() => { // eslint-disable-next-line no-console console.info('Static Deploy Domain:', process.env.REACT_APP_KBASE_DOMAIN); dispatch(setEnvironment(process.env.REACT_APP_KBASE_ENV ?? 'unknown')); - }, [dispatch]); + }, [dispatch, environment]); return { isLoading: !initialized }; }; diff --git a/src/features/layout/TopBar.tsx b/src/features/layout/TopBar.tsx index 3011cc1f..251f2a0b 100644 --- a/src/features/layout/TopBar.tsx +++ b/src/features/layout/TopBar.tsx @@ -154,9 +154,10 @@ const useLogout = () => { revoke(tokenId) .unwrap() .then(() => { + dispatch(resetStateAction()); + // setAuth(null) follow the state reset to initialize the page as un-Authed dispatch(setAuth(null)); toast('You have been signed out'); - dispatch(resetStateAction()); navigate('/legacy/auth2/signedout'); }) .catch(() => { diff --git a/src/features/legacy/Legacy.tsx b/src/features/legacy/Legacy.tsx index b55b9d03..aae94569 100644 --- a/src/features/legacy/Legacy.tsx +++ b/src/features/legacy/Legacy.tsx @@ -4,6 +4,8 @@ import { usePageTitle } from '../layout/layoutSlice'; import { useTryAuthFromToken } from '../auth/hooks'; import { useAppDispatch } from '../../common/hooks'; import { resetStateAction } from '../../app/store'; +import { setAuth } from '../auth/authSlice'; +import { toast } from 'react-hot-toast'; export const LEGACY_BASE_ROUTE = '/legacy'; @@ -51,9 +53,10 @@ export default function Legacy() { setReceivedToken(d.payload.token); } } else if (isLogoutMessage(d)) { - // eslint-disable-next-line no-console - console.info('Logged Out'); dispatch(resetStateAction()); + dispatch(setAuth(null)); + toast('You have been signed out'); + navigate('/legacy/auth2/signedout'); } }); From cb8cbd0a75b496e8bb904f9ff84a76f47b9bdfa3 Mon Sep 17 00:00:00 2001 From: Jason S Fillman <6155956+jsfillman@users.noreply.github.com> Date: Tue, 24 Oct 2023 01:06:04 -0700 Subject: [PATCH 14/14] Fix legacy.ci URL in config.json --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index 2e9b429f..1e3340a8 100644 --- a/config.json +++ b/config.json @@ -8,7 +8,7 @@ "ci": { "domain": "ci.kbase.us", - "legacy": "legacy.kbase.us", + "legacy": "legacy.ci.kbase.us", "public_url": "/" },