From 8b9b5e168c724af56273c0876c3133493caf9357 Mon Sep 17 00:00:00 2001 From: Ciaran Schutte Date: Wed, 16 Aug 2023 21:05:37 -0400 Subject: [PATCH] Feat/8 Logout (#39) * move global into src, instead of app folder * add some components * configure uikit, emotion and typings * uikit happy * Basic Env Vars * Populating Basic Configs * cleanup * add theme provider * emotion imports * emotion uikit style tpying * First Working Test * Working Redirect w/ URL Join * remove DataCallout uikit re-export wrapper * Basic Reusable Component * Remove .env * Update .gitignore * Remove Duplicate * Stage for UI Kit * Loading Test w/ UI Kit * NavBar Setup * Fix Landing Page * Remove Next logo * Skeleton Cookies * Basic Logout * AuthContext Setup * AuthContext Simplest State * Generic Vars * Major Update w/ Logout + UI Kit * Testing Context + State * Add Copyrights * Update TypeScript, Imports, File Structure * Login Button Component * Use AuthContextValue * Use GoogleLogin * Use URL Join * Local .env updates * Remove Boilerplate * Shuffle Files Around * Refactor w/o Context * Remove Logging * Fix env settings * Add React Query + Mock Login State * Add Copyright * Current Working State * Fix User Badge * Temporary Login Solution * Clean Up State Changes * Remove Redirect * Cleaned Up User Badge * Basic Pathname Solution * Cleaned Up Login Flow * Improved Layout + Header Handling * Remove vscode from gitignore * Fix /public import * Update page names * Refactor Auth / Layout * General Clean Up * Stub Middleware * Tidying * Middleware + Auth Route Setup - Infinite Loop * Current State - Testing * Children prop cleanup * Header UI Updates * Current Working State * Clean Up Use of Stored Token * Remove extra storedToken * First Lint Changes * Hook + Middleware Cleanup * Better fallback, LoggingIn dependency * Move Middleware to Feature Branch * Unnecesary changes * Remove Server side token management * First Setup * Log Out Update * Updated Var name + Conditional * Improve Loading Var Names + Reusable Logout * Use Context logOut * Remove Dummy Logout Button * 1st Header PR Feedback * Remove Unused Imports * Fix Build Issues * ProfileMenu Component * Reusable Login --------- Co-authored-by: Ciaran Schutte Feat/#14 programs list page (#47) * add submission layout * add some components * add All Programs layout * skeleton * cleanup + add linking for programs * remove sidemenu logic for PR * remove program list and search func for PR * fix type error * mmove types around * fix build * clean import --------- Co-authored-by: Ciaran Schutte add All Programs layout skeleton cleanup + add linking for programs remove sidemenu logic for PR remove program list and search func for PR use route groups content additions to sidemenu poor mans sidebar toggle Revert "Merge branch 'develop' into sidemenu-layout-restructure" This reverts commit 640bb4be53ac90eb79b7d9804d885c2e20957dba. clean up add sidemenu toggle fix build lots of styling add page count add table comps, fix font fix percent cell table updates sidemenu styling styling cleanup cleanup --- package-lock.json | 4 +- .../submission/components/ProgramMenu.tsx | 17 ++-- .../submission/components/Search.tsx | 4 +- .../submission/components/SideMenu/types.ts | 20 ---- .../{SideMenu/Menu.tsx => Sidemenu.tsx} | 74 ++++++++++++++- src/app/(post-login)/submission/layout.tsx | 11 +-- src/app/(post-login)/submission/page.tsx | 4 +- .../submission/program-table/DonorStatus.tsx | 2 +- .../submission/program-table/Header.tsx | 2 +- .../submission/program-table/config.tsx | 2 +- src/app/(pre-login)/landing-page/page.tsx | 10 +- src/app/(pre-login)/logging-in/page.tsx | 10 +- src/app/components/Header.tsx | 91 +++++-------------- src/app/components/ProfileMenu.tsx | 62 ------------- src/app/layout.tsx | 8 +- src/app/page.tsx | 2 +- src/global/constants.ts | 5 - src/global/utils/auth.tsx | 51 ++++------- 18 files changed, 152 insertions(+), 227 deletions(-) delete mode 100644 src/app/(post-login)/submission/components/SideMenu/types.ts rename src/app/(post-login)/submission/components/{SideMenu/Menu.tsx => Sidemenu.tsx} (50%) delete mode 100644 src/app/components/ProfileMenu.tsx diff --git a/package-lock.json b/package-lock.json index c844be42..abadb519 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rdpc-ui", - "version": "0.5.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rdpc-ui", - "version": "0.5.0", + "version": "0.2.0", "dependencies": { "@hookform/resolvers": "^3.1.1", "@icgc-argo/ego-token-utils": "^8.2.0", diff --git a/src/app/(post-login)/submission/components/ProgramMenu.tsx b/src/app/(post-login)/submission/components/ProgramMenu.tsx index dff4c559..42267686 100644 --- a/src/app/(post-login)/submission/components/ProgramMenu.tsx +++ b/src/app/(post-login)/submission/components/ProgramMenu.tsx @@ -22,7 +22,7 @@ import { css } from '@/lib/emotion'; import { MenuItem } from '@icgc-argo/uikit'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { MouseEventHandler, useState } from 'react'; +import { useState } from 'react'; export default function ProgramMenu({ programs, @@ -34,14 +34,9 @@ export default function ProgramMenu({ const pathname = usePathname(); const [activeProgramIndex, setActiveProgramIndex] = useState(-1); - const filteredPrograms = !searchQuery.length - ? programs - : programs.filter(({ shortName }) => shortName.search(new RegExp(searchQuery, 'i')) > -1); - - const setActiveProgram = - (index: number): MouseEventHandler => - () => - setActiveProgramIndex(index); + const filteredPrograms = programs.filter( + ({ shortName }) => !searchQuery.length || shortName.search(new RegExp(searchQuery, 'i')) > -1, + ); return ( <> @@ -54,7 +49,7 @@ export default function ProgramMenu({ setActiveProgramIndex(-1)} selected={pathname === '/submission'} /> @@ -64,7 +59,7 @@ export default function ProgramMenu({ level={2} key={program.shortName} content={program.shortName} - onClick={setActiveProgram(programIndex)} + onClick={() => setActiveProgramIndex(programIndex)} selected={programIndex === activeProgramIndex} > {program.shortName} diff --git a/src/app/(post-login)/submission/components/Search.tsx b/src/app/(post-login)/submission/components/Search.tsx index 4f12376f..ed65c1bc 100644 --- a/src/app/(post-login)/submission/components/Search.tsx +++ b/src/app/(post-login)/submission/components/Search.tsx @@ -35,8 +35,8 @@ export default function Search({ content={ { - onChange(event.target.value); + onChange={(e) => { + onChange(e.target.value); }} value={query} css={css` diff --git a/src/app/(post-login)/submission/components/SideMenu/types.ts b/src/app/(post-login)/submission/components/SideMenu/types.ts deleted file mode 100644 index f18bc823..00000000 --- a/src/app/(post-login)/submission/components/SideMenu/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2023 The Ontario Institute for Cancer Research. All rights reserved - * - * This program and the accompanying materials are made available under the terms of - * the GNU Affero General Public License v3.0. You should have received a copy of the - * GNU Affero General Public License along with this program. - * If not, see . - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -export type SideMenuProps = { content: any; onToggle: () => void; isActive: boolean }; diff --git a/src/app/(post-login)/submission/components/SideMenu/Menu.tsx b/src/app/(post-login)/submission/components/Sidemenu.tsx similarity index 50% rename from src/app/(post-login)/submission/components/SideMenu/Menu.tsx rename to src/app/(post-login)/submission/components/Sidemenu.tsx index f69feb09..929b27a9 100644 --- a/src/app/(post-login)/submission/components/SideMenu/Menu.tsx +++ b/src/app/(post-login)/submission/components/Sidemenu.tsx @@ -19,10 +19,76 @@ 'use client'; import { css, useTheme } from '@/lib/emotion'; -import SideMenuContent from './Content'; -import SideMenuToggle, { TOGGLE_HEIGHT_PX } from './Toggle'; -import { SideMenuProps } from './types'; +import { Interpolation, Theme } from '@emotion/react'; +import { Icon, MenuItem, SubMenu, UikitIconNames } from '@icgc-argo/uikit'; +import { useState } from 'react'; +import ProgramMenu from './ProgramMenu'; +import Search from './Search'; +const SideMenuContent = ({ content }: { content: any[] }) => { + const [programNameSearch, setProgramNameSearch] = useState(''); + + return ( + + } content={'My Programs'} selected> + + + + + ); +}; + +const ToggleChevron = ({ css, name }: { css?: Interpolation; name: UikitIconNames }) => ( + +); + +const SideMenuToggle = ({ onToggle, open }: { onToggle: () => void; open: boolean }) => ( +
+
+ + +
+
+ + +
+
+); + +type SideMenuProps = { content: any; onToggle: () => void; isActive: boolean }; const SideMenu = ({ content, onToggle, isActive }: SideMenuProps) => { const theme = useTheme(); @@ -38,7 +104,7 @@ const SideMenu = ({ content, onToggle, isActive }: SideMenuProps) => { >
(true); return ( @@ -41,13 +40,13 @@ export default function AppLayout({ children }: { children: ReactNode }) { >
setSidebarActive((active) => !active)} /> diff --git a/src/app/(post-login)/submission/page.tsx b/src/app/(post-login)/submission/page.tsx index 4299a66a..eceba4cc 100644 --- a/src/app/(post-login)/submission/page.tsx +++ b/src/app/(post-login)/submission/page.tsx @@ -19,9 +19,9 @@ 'use client'; -import mockData from '@/mockData.json'; import ProgramList from './components/ProgramList'; +import programs from './data.temp'; export default function Submission() { - return ; + return ; } diff --git a/src/app/(post-login)/submission/program-table/DonorStatus.tsx b/src/app/(post-login)/submission/program-table/DonorStatus.tsx index 3769ee82..9be3eba5 100644 --- a/src/app/(post-login)/submission/program-table/DonorStatus.tsx +++ b/src/app/(post-login)/submission/program-table/DonorStatus.tsx @@ -48,7 +48,7 @@ const DonorStatus = ({ submittedDonors, commitmentDonors }: DonorStatusProps) => color: ${theme.colors.grey_2}; `} > - {` / `} + {` `}/{` `} {denominator.toLocaleString()} diff --git a/src/app/(post-login)/submission/program-table/Header.tsx b/src/app/(post-login)/submission/program-table/Header.tsx index 19edd8f5..0aa2aec4 100644 --- a/src/app/(post-login)/submission/program-table/Header.tsx +++ b/src/app/(post-login)/submission/program-table/Header.tsx @@ -24,7 +24,7 @@ import { ReactNode } from 'react'; const TableHeader = ({ children }: { children: ReactNode }) => (
[] = [ +export const columns: ColumnDef[] = [ { header: () => Short Name, accessorKey: 'shortName', diff --git a/src/app/(pre-login)/landing-page/page.tsx b/src/app/(pre-login)/landing-page/page.tsx index 561112f3..4230162f 100644 --- a/src/app/(pre-login)/landing-page/page.tsx +++ b/src/app/(pre-login)/landing-page/page.tsx @@ -18,11 +18,10 @@ */ 'use client'; -import { useAppConfigContext } from '@/app/components/ConfigProvider'; +import { useAppConfigContext } from '../../components/ConfigProvider'; export default function LandingPage() { - const { EGO_CLIENT_ID } = useAppConfigContext(); - + const { EGO_CLIENT_ID } = getAppConfig(); return (
@@ -32,6 +31,11 @@ export default function LandingPage() {

Welcome! {EGO_CLIENT_ID}

+
); } diff --git a/src/app/(pre-login)/logging-in/page.tsx b/src/app/(pre-login)/logging-in/page.tsx index 989eb9ce..163bf476 100644 --- a/src/app/(pre-login)/logging-in/page.tsx +++ b/src/app/(pre-login)/logging-in/page.tsx @@ -23,16 +23,18 @@ import { useAuthContext } from '@/global/utils/auth'; import { DnaLoader, css, useTheme } from '@icgc-argo/uikit'; import { useRouter } from 'next/navigation'; import { useQuery } from 'react-query'; +import { useAppConfigContext } from '../../components/ConfigProvider'; export default async function LoggingIn() { const { EGO_LOGIN_URL } = useAppConfigContext(); const router = useRouter(); const theme = useTheme(); - const { egoJwt, authLoading, setAuthLoading, logIn } = useAuthContext(); + const { egoJwt, setEgoJwt, loggingIn, setLoggingIn } = useAuthContext(); + const egoLoginUrl = urlJoin(EGO_API_ROOT, `/api/oauth/ego-token?client_id=${EGO_CLIENT_ID}`); if (egoJwt) router.push('/landing-page'); - if (!authLoading && !egoJwt) setAuthLoading(true); + if (!loggingIn && !egoJwt) setLoggingIn(true); useQuery('egoJwt', () => { fetch(EGO_LOGIN_URL, { @@ -44,7 +46,9 @@ export default async function LoggingIn() { }) .then(async (res) => { const newToken = await res.text(); - logIn(newToken); + storeToken(newToken); + setEgoJwt(newToken); + setLoggingIn(false); }) .catch(console.error); }); diff --git a/src/app/components/Header.tsx b/src/app/components/Header.tsx index 9e31b497..3900a912 100644 --- a/src/app/components/Header.tsx +++ b/src/app/components/Header.tsx @@ -18,45 +18,15 @@ */ 'use client'; -import { useAuthContext } from '@/global/utils/auth'; -import { css, useTheme } from '@/lib/emotion'; -import { AppBarMenuItem, DnaLoader, Link, NavElement } from '@icgc-argo/uikit'; +import { AppBar, css, DnaLoader, UserBadge } from '@icgc-argo/uikit'; +import Link from 'next/link'; import Image from 'next/image'; -import { usePathname } from 'next/navigation'; -import { useState } from 'react'; -import LoginButton from './LoginButton'; -import ProfileMenu from './ProfileMenu'; import argoLogo from '/public/argo-logo.svg'; - -export const HEADER_HEIGHT_PX = '58'; +import { useAuthContext } from '@/global/utils/auth'; +import LoginButton from './LoginButton'; const Header = () => { - const [isDropdownOpen, setDropdownOpen] = useState(false); - const { egoJwt, authLoading, logOut } = useAuthContext(); - const path = usePathname(); - const theme = useTheme(); - const onProfilePage = path === '/landing-page'; - const profileActive = onProfilePage && !!egoJwt.length && !authLoading; - - const profileNavDetails: Array = [ - { - active: profileActive, - href: '/landing-page', - name: 'Profile & Token', - LinkComp: Link, - }, - { - isLink: false, - onClick: () => { - setDropdownOpen(false); - logOut(); - }, - name: 'Logout', - active: false, - href: '', - LinkComp: Link, - }, - ]; + const { egoJwt, loggingIn } = useAuthContext(); return (
{
- {/** "right-aligned" **/} -
div { - border-left: 1px solid ${theme.colors.grey}; - } - `} - > - {authLoading ? ( - - ) : !egoJwt ? ( - - ) : ( - - { - setDropdownOpen(!isDropdownOpen); - }} - profileNavDetails={profileNavDetails} - theme={theme} + {/** keep this div. header will have more items, will be "right-aligned" */} +
+ {egoJwt ? ( + - - )} -
+ ) : loggingIn ? ( + + ) : ( + + )} +
+ ); }; diff --git a/src/app/components/ProfileMenu.tsx b/src/app/components/ProfileMenu.tsx deleted file mode 100644 index 82993524..00000000 --- a/src/app/components/ProfileMenu.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2023 The Ontario Institute for Cancer Research. All rights reserved - * - * This program and the accompanying materials are made available under the terms of - * the GNU Affero General Public License v3.0. You should have received a copy of the - * GNU Affero General Public License along with this program. - * If not, see . - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -'use client'; - -import { css } from '@/lib/emotion'; -import { DropdownMenu, FocusWrapper, NavBarElement, NavElement, UserBadge } from '@icgc-argo/uikit'; -import { Theme } from '@icgc-argo/uikit/ThemeProvider'; - -const ProfileMenu = ({ - isDropdownOpen, - onProfilePage, - onClick, - profileNavDetails, - theme, -}: { - isDropdownOpen: boolean; - onProfilePage: boolean; - onClick: () => void; - profileNavDetails: NavElement[]; - theme: Theme; -}) => ( - - {isDropdownOpen && ( - - {profileNavDetails.map((element, idx) => ( - - ))} - - )} - - -); - -export default ProfileMenu; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 837b9f3d..c85a81e7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -20,9 +20,13 @@ /** @jsxImportSource react */ // ^ force default jsx runtime, @emotion/jsx doesn't play nice with server components -import { BUILD_TIME_VARIABLES } from '@/global/constants'; +import { AuthProvider } from '@/global/utils/auth'; +import { css } from '@/lib/emotion'; import { ReactNode } from 'react'; -import App from './App'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import Footer from './components/Footer'; +import Header from './components/Header'; +import ThemeProvider from './components/ThemeProvider'; async function getAppConfig() { // cache: "no-store" ensures it's run server side diff --git a/src/app/page.tsx b/src/app/page.tsx index e602a653..8b194c82 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -18,12 +18,12 @@ */ 'use client'; +import { getAppConfig } from '@/global/config'; import { css, useTheme } from '@/lib/emotion'; import { DataCallout, Link, Typography, overtureLogo } from '@icgc-argo/uikit'; import Image from 'next/image'; import { ComponentType } from 'react'; import urlJoin from 'url-join'; -import { useAppConfigContext } from './components/ConfigProvider'; import Hero from './components/Hero'; const OvertureBanner: ComponentType = () => { diff --git a/src/global/constants.ts b/src/global/constants.ts index 4bb81d21..895574f9 100644 --- a/src/global/constants.ts +++ b/src/global/constants.ts @@ -18,8 +18,3 @@ */ export const EGO_JWT_KEY = 'EGO_JWT'; -export const LOGIN_NONCE = 'LOGIN_NONCE'; - -export const BUILD_TIME_VARIABLES = { - RUNTIME_CONFIG_URL: process.env.NEXT_PUBLIC_RUNTIME_CONFIG_URL || '', -}; diff --git a/src/global/utils/auth.tsx b/src/global/utils/auth.tsx index 290326c2..4e08acc9 100644 --- a/src/global/utils/auth.tsx +++ b/src/global/utils/auth.tsx @@ -18,37 +18,34 @@ */ 'use client'; -import Header from '@/app/components/Header'; -import { DnaLoader } from '@icgc-argo/uikit'; -import Cookies from 'js-cookie'; -import { usePathname, useRouter } from 'next/navigation'; import { + createContext, Dispatch, ReactNode, SetStateAction, Suspense, - createContext, useContext, + useEffect, useState, } from 'react'; -import { EGO_JWT_KEY, LOGIN_NONCE } from '../constants'; +import Cookies from 'js-cookie'; +import { usePathname } from 'next/navigation'; +import Header from '@/app/components/Header'; +import { DnaLoader } from '@icgc-argo/uikit'; +import { EGO_JWT_KEY } from '../constants'; type AuthContextValue = { egoJwt: string; setEgoJwt: Dispatch>; - authLoading: boolean; - setAuthLoading: Dispatch>; - logIn: (newToken: string) => void; - logOut: () => void; + loggingIn: boolean; + setLoggingIn: Dispatch>; }; const AuthContext = createContext({ egoJwt: '', setEgoJwt: () => '', - authLoading: false, - setAuthLoading: () => false, - logIn: () => undefined, - logOut: () => undefined, + loggingIn: false, + setLoggingIn: () => false, }); export const getStoredToken = () => Cookies.get(EGO_JWT_KEY); @@ -57,30 +54,18 @@ export const storeToken = (egoToken: string) => { Cookies.set(EGO_JWT_KEY, egoToken); }; +export const logOut = () => { + Cookies.remove(EGO_JWT_KEY); +}; + export function AuthProvider({ children }: { children: ReactNode }) { const storedToken = getStoredToken(); const [egoJwt, setEgoJwt] = useState(storedToken || ''); - const router = useRouter(); const path = usePathname(); - const loginStateOnPageLoad = path === '/logging-in' && !egoJwt.length; - const [authLoading, setAuthLoading] = useState(loginStateOnPageLoad); - - const logIn = (newToken: string) => { - storeToken(newToken); - setEgoJwt(newToken); - setAuthLoading(false); - }; - - const logOut = () => { - setAuthLoading(true); - setEgoJwt(''); - Cookies.remove(EGO_JWT_KEY); - Cookies.remove(LOGIN_NONCE); - router.push('/'); - setAuthLoading(false); - }; + const initLoginState = path === '/logging-in' && !egoJwt.length ? true : false; + const [loggingIn, setLoggingIn] = useState(initLoginState); - const value: AuthContextValue = { egoJwt, setEgoJwt, authLoading, setAuthLoading, logIn, logOut }; + const value: AuthContextValue = { egoJwt, setEgoJwt, loggingIn, setLoggingIn }; return (