From 451921c7ccf460c39b6174315a370e21b31f58de Mon Sep 17 00:00:00 2001 From: Ann Catton Date: Mon, 15 Mar 2021 16:06:19 -0400 Subject: [PATCH 01/19] =?UTF-8?q?=F0=9F=9A=B8=20wip=20apiToken=20error=20h?= =?UTF-8?q?andling=20display?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ErrorContainer.tsx | 186 +++++++++++++++++++++---- components/pages/repository/index.tsx | 12 +- components/pages/user/ApiTokenInfo.tsx | 126 ++++++++++++++--- components/theme/icons/dismiss.tsx | 26 ++++ components/theme/icons/error.tsx | 4 +- global/types.ts | 1 + global/utils/egoTokenUtils.ts | 33 ++++- static/dismiss.svg | 3 + 8 files changed, 341 insertions(+), 50 deletions(-) create mode 100644 components/theme/icons/dismiss.tsx create mode 100644 static/dismiss.svg diff --git a/components/ErrorContainer.tsx b/components/ErrorContainer.tsx index 0a6bae04..12e7d096 100644 --- a/components/ErrorContainer.tsx +++ b/components/ErrorContainer.tsx @@ -1,51 +1,187 @@ import { css } from '@emotion/core'; +import React from 'react'; + +import theme from './theme'; import { Error } from './theme/icons'; +import Dismiss from './theme/icons/dismiss'; +import { IconProps } from './theme/icons/types'; + +type ErrorSize = 'sm' | 'md' | 'lg'; + +const titleStyles = { + lg: ` + margin: 0.5rem 0 1rem; + font-size: 24px; + line-height: 38px; + `, + md: ` + margin: 0rem; + padding-bottom: 0.4rem; + font-size: 18px; + line-height: 20px + `, + sm: '', +}; + +const containerStyles = { + lg: ` + padding: 1rem 2rem; + line-height: 26px; + max-width: 600px; + `, + md: ` + padding: 1rem; + line-height: 24px; + `, + sm: ` + padding: 0.5rem; + line-height: 20px; + display: flex; + align-items: center; + `, +}; + +type IconDimensions = { + [key in ErrorSize]: { + width: number; + height: number; + }; +}; + +const iconStyles = { + lg: ` + padding-right: 15px + `, + md: ` + padding-right: 15px + `, + sm: ``, +}; + +const iconDimensions: IconDimensions = { + lg: { width: 26, height: 27 }, + md: { width: 21, height: 22 }, + sm: { width: 17, height: 17 }, +}; + +const IconButton = ({ + Icon, + fill, + height, + width, + onClick = () => {}, +}: { + Icon: React.ComponentType; + fill: string; + height: number; + width: number; + onClick: React.MouseEventHandler; +}) => { + return ( + + + + ); +}; -const ErrorContainer = ({ children, title }: { children: React.ReactNode; title: string }) => ( +const ErrorContainer = ({ + children, + title = '', + size, + styles = '', + onDismiss, + dismissible = false, +}: { + children: React.ReactNode; + title?: string; + size: ErrorSize; + styles?: string; + onDismiss?: Function; + dismissible?: boolean; +}) => (
css` - padding: 1rem 2rem; border: 1px solid ${theme.colors.error_2}; border-radius: 5px; ${theme.shadow.default}; ${theme.typography.subheading}; font-weight: normal; - line-height: 26px; - max-width: 600px; background-color: ${theme.colors.error_1}; color: ${theme.colors.accent_dark}; + ${containerStyles[size]}; ` } > -

- +

+ {' '} + {title} + {dismissible && } +

+ {children} +

+ ) : ( +
{' '} - {title} - - {children} + > + +
+ {children} +
+ {dismissible && ( + (onDismiss ? onDismiss() : () => null)} + Icon={Dismiss} + height={12} + width={12} + fill={theme.colors.error_dark} + /> + )} +
+ )}
); diff --git a/components/pages/repository/index.tsx b/components/pages/repository/index.tsx index 0e2df98b..cf7f1a14 100644 --- a/components/pages/repository/index.tsx +++ b/components/pages/repository/index.tsx @@ -101,7 +101,17 @@ const RepositoryPage = () => { return ( {ConfigError ? ( - {ConfigError} + + {ConfigError} + ) : ( { const [existingApiToken, setExistingApiToken] = useState(null); const [isCopyingToken, setIsCopyingToken] = React.useState(false); const [copySuccess, setCopySuccess] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState<{ message: string } | null>(null); const theme: typeof defaultTheme = useTheme(); + // still need to display any errors for the generate request, as permissions may have changed in between + // the time a user signed in and when they attempted to generate a token const generateApiToken = async () => { const { NEXT_PUBLIC_EGO_API_ROOT } = getConfig(); if (user) { @@ -79,27 +84,46 @@ const ApiTokenInfo = () => { return res.json(); }) .then((json) => json.scopes) - .catch((err) => console.warn(err)); + .catch((err) => { + setErrorMessage({ message: err }); + console.warn(err); + return err; + }); - if (scopesResult.length) { + const filteredScopes = Array.isArray(scopesResult) + ? scopesResult + .map((s: string) => parseScope(s)) + .filter((s: ScopeObj) => s.accessLevel !== AccessLevel.DENY) + : []; + + if (filteredScopes.length) { + const scopeParams = filteredScopes.map((f: ScopeObj) => `${f.policy}.${f.accessLevel}`); return fetchWithAuth( - `${EGO_API_KEY_ENDPOINT}?scopes=${encodeURIComponent(scopesResult.join())}&user_id=${ + `${EGO_API_KEY_ENDPOINT}?scopes=${encodeURIComponent(scopeParams.join())}&user_id=${ user.id }`, { method: 'POST' }, ) .then((res) => { if (res.status !== 200) { - throw new Error('Failed to generate api token!'); + throw new Error( + 'User does not have appropriate permissions. Failed to generate api token!', + ); } return res.json(); }) .then((newApiToken: ApiToken) => { setExistingApiToken(newApiToken); }) - .catch((err) => { + .catch(async (err: Error) => { + setErrorMessage({ message: err.message }); return err; }); + } else { + // request for apiToken is skipped if filteredScopes is empty + setErrorMessage({ + message: 'Something bad happened here what was it', + }); } } }; @@ -114,7 +138,10 @@ const ApiTokenInfo = () => { } setExistingApiToken(null); }) - .catch((err) => console.warn(err)) + .catch((err) => { + setErrorMessage({ message: err }); + console.warn(err); + }) ); }; @@ -128,7 +155,10 @@ const ApiTokenInfo = () => { await sleep(); setCopySuccess(false); }) - .catch((err) => console.warn('Failed to copy token!')); + .catch((err) => { + console.warn('Failed to copy token! ', err); + setIsCopyingToken(false); + }); }; const parsedExpiry: number = existingApiToken ? parseExpiry(existingApiToken?.expiryDate) : 0; @@ -154,6 +184,14 @@ const ApiTokenInfo = () => { .catch((err) => console.warn('Could not get api tokens! ', err)); }, [token]); + const userEffectiveScopes = (user?.scope || []) + .map((s) => parseScope(s)) + .filter((s: ScopeObj) => { + return s.accessLevel !== AccessLevel.DENY; + }); + + const userHasScopes = userEffectiveScopes.length > 0; + return (
{ ${theme.typography.subheading}; font-weight: normal; color: ${theme.colors.accent_dark}; - margin-bottom: 2rem; + margin-bottom: 1rem; ` } > @@ -190,6 +228,46 @@ const ApiTokenInfo = () => {
  • When you generate a new token, all previous tokens become invalid.
  • Expired and revoked tokens also become invalid.
  • +
    + {!userHasScopes && ( + + You do not have permission to generate an API token. Please contact the DMS + administrator to gain the correct permission. + + )} +
    + + {errorMessage && ( +
    + css` + background-color: ${theme.colors.error_1}; + color: ${theme.colors.accent_dark}; + `} + dismissible + onDismiss={() => setErrorMessage(null)} + > + + There was a problem generating an API token: {errorMessage.message.toString()} + + +
    + )}
    { `} onClick={() => generateApiToken()} isAsync + disabled={!userHasScopes} > Generate New Token @@ -230,7 +309,7 @@ const ApiTokenInfo = () => { display: flex; flex-direction: row; justify-content: space-between; - margin-bottom: 2rem; + margin-bottom: 1rem; margin-top: 1rem; `} > @@ -340,19 +419,24 @@ const ApiTokenInfo = () => {
    - - - css` - ${theme.typography.subheading}; - font-weight: normal; - color: ${theme.colors.accent_dark}; - ` - } +
    - For more information, please read the{' '} - instructions on how to download data. - + + css` + ${theme.typography.subheading}; + font-weight: normal; + color: ${theme.colors.accent_dark}; + ` + } + > + For more information, please read the{' '} + instructions on how to download data. + +
    ); }; diff --git a/components/theme/icons/dismiss.tsx b/components/theme/icons/dismiss.tsx new file mode 100644 index 00000000..0e68c221 --- /dev/null +++ b/components/theme/icons/dismiss.tsx @@ -0,0 +1,26 @@ +import { css } from '@emotion/core'; +import theme from '..'; +import { IconProps } from './types'; + +const Dismiss = ({ height, width, style, fill }: IconProps) => { + return ( + + + + ); +}; + +export default Dismiss; diff --git a/components/theme/icons/error.tsx b/components/theme/icons/error.tsx index 33bff880..7e550e94 100644 --- a/components/theme/icons/error.tsx +++ b/components/theme/icons/error.tsx @@ -6,8 +6,8 @@ const Error = ({ height, width, style }: IconProps) => { EgoJwtData | null = memoize((egoJ export const extractUser: (decodedToken: EgoJwtData) => UserWithId | undefined = (decodedToken) => { if (decodedToken) { - return { ...decodedToken?.context.user, id: decodedToken?.sub }; + return { + ...decodedToken?.context.user, + scope: decodedToken?.context.scope || [], + id: decodedToken?.sub, + }; } return undefined; }; + +export enum AccessLevel { + READ = 'READ', + WRITE = 'WRITE', + DENY = 'DENY', +} + +export type ScopeObj = { + policy: string; + accessLevel: AccessLevel; +}; + +export const isAccessLevel = (str: any): str is AccessLevel => + Object.values(AccessLevel).includes(str); + +export const parseScope = (scope: string): ScopeObj => { + const splitScope = scope.split('.'); + const accessLevel = splitScope[1]; + if (isAccessLevel(accessLevel)) { + return { + policy: splitScope[0], + accessLevel, + }; + } else { + throw new Error(`invalid scope: ${scope}`); + } +}; diff --git a/static/dismiss.svg b/static/dismiss.svg new file mode 100644 index 00000000..f037f3d1 --- /dev/null +++ b/static/dismiss.svg @@ -0,0 +1,3 @@ + + + From 478f80b9b6df74fe1385462885b309ab32a7f91f Mon Sep 17 00:00:00 2001 From: Ann Catton Date: Tue, 16 Mar 2021 12:09:28 -0400 Subject: [PATCH 02/19] refactor error styling --- components/ErrorContainer.tsx | 189 ------------------------- components/ErrorNotification.tsx | 189 +++++++++++++++++++++++++ components/pages/explorer/index.tsx | 6 +- components/pages/user/ApiTokenInfo.tsx | 10 +- 4 files changed, 197 insertions(+), 197 deletions(-) delete mode 100644 components/ErrorContainer.tsx create mode 100644 components/ErrorNotification.tsx diff --git a/components/ErrorContainer.tsx b/components/ErrorContainer.tsx deleted file mode 100644 index 12e7d096..00000000 --- a/components/ErrorContainer.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import { css } from '@emotion/core'; -import React from 'react'; - -import theme from './theme'; -import { Error } from './theme/icons'; -import Dismiss from './theme/icons/dismiss'; -import { IconProps } from './theme/icons/types'; - -type ErrorSize = 'sm' | 'md' | 'lg'; - -const titleStyles = { - lg: ` - margin: 0.5rem 0 1rem; - font-size: 24px; - line-height: 38px; - `, - md: ` - margin: 0rem; - padding-bottom: 0.4rem; - font-size: 18px; - line-height: 20px - `, - sm: '', -}; - -const containerStyles = { - lg: ` - padding: 1rem 2rem; - line-height: 26px; - max-width: 600px; - `, - md: ` - padding: 1rem; - line-height: 24px; - `, - sm: ` - padding: 0.5rem; - line-height: 20px; - display: flex; - align-items: center; - `, -}; - -type IconDimensions = { - [key in ErrorSize]: { - width: number; - height: number; - }; -}; - -const iconStyles = { - lg: ` - padding-right: 15px - `, - md: ` - padding-right: 15px - `, - sm: ``, -}; - -const iconDimensions: IconDimensions = { - lg: { width: 26, height: 27 }, - md: { width: 21, height: 22 }, - sm: { width: 17, height: 17 }, -}; - -const IconButton = ({ - Icon, - fill, - height, - width, - onClick = () => {}, -}: { - Icon: React.ComponentType; - fill: string; - height: number; - width: number; - onClick: React.MouseEventHandler; -}) => { - return ( - - - - ); -}; - -const ErrorContainer = ({ - children, - title = '', - size, - styles = '', - onDismiss, - dismissible = false, -}: { - children: React.ReactNode; - title?: string; - size: ErrorSize; - styles?: string; - onDismiss?: Function; - dismissible?: boolean; -}) => ( -
    -
    - css` - border: 1px solid ${theme.colors.error_2}; - border-radius: 5px; - ${theme.shadow.default}; - ${theme.typography.subheading}; - font-weight: normal; - background-color: ${theme.colors.error_1}; - color: ${theme.colors.accent_dark}; - ${containerStyles[size]}; - ` - } - > - {title ? ( -
    -

    - {' '} - {title} - {dismissible && } -

    - {children} -
    - ) : ( -
    - -
    - {children} -
    - {dismissible && ( - (onDismiss ? onDismiss() : () => null)} - Icon={Dismiss} - height={12} - width={12} - fill={theme.colors.error_dark} - /> - )} -
    - )} -
    -
    -); - -export default ErrorContainer; diff --git a/components/ErrorNotification.tsx b/components/ErrorNotification.tsx new file mode 100644 index 00000000..157e67cc --- /dev/null +++ b/components/ErrorNotification.tsx @@ -0,0 +1,189 @@ +import { css } from '@emotion/core'; +import styled from '@emotion/styled'; +import React from 'react'; + +import defaultTheme from './theme'; +import { Error as ErrorIcon } from './theme/icons'; +import Dismiss from './theme/icons/dismiss'; +import { IconProps } from './theme/icons/types'; + +enum ErrorSize { + LG = 'lg', + MD = 'md', + SM = 'sm', +} + +const getIconDimensions = ({ size }: { size: ErrorSize }) => + ({ + [ErrorSize.LG]: { width: 26, height: 27 }, + [ErrorSize.MD]: { width: 21, height: 22 }, + [ErrorSize.SM]: { width: 19, height: 19 }, + }[size]); + +const IconButton = ({ + Icon, + fill, + height, + width, + onClick = () => {}, +}: { + Icon: React.ComponentType; + fill: string; + height: number; + width: number; + onClick: React.MouseEventHandler; +}) => { + return ( + + + + ); +}; + +const getContainerStyles = ({ size }: { size: ErrorSize }) => + ({ + [ErrorSize.LG]: ` + padding: 1rem 2rem; + line-height: 26px; + `, + [ErrorSize.MD]: ` + padding: 1rem; + line-height: 24px; + `, + [ErrorSize.SM]: ` + padding: 0.5rem; + line-height: 20px; + display: flex; + align-items: center; + `, + }[size]); + +const ErrorContentContainer = styled('div')` + ${({ theme, size }: { theme: typeof defaultTheme; size: ErrorSize }) => css` + border: 1px solid ${theme.colors.error_2}; + border-radius: 5px; + ${theme.shadow.default}; + ${theme.typography.subheading}; + font-weight: normal; + background-color: ${theme.colors.error_1}; + color: ${theme.colors.accent_dark}; + ${getContainerStyles({ size })}; + max-width: 600px; + `} +`; + +const getIconStyle = ({ size }: { size: ErrorSize }) => + ({ + [ErrorSize.LG]: 'padding-right: 15px', + [ErrorSize.MD]: 'padding-right: 15px', + [ErrorSize.SM]: '', + }[size]); + +const getTitleStyle = ({ size }: { size: ErrorSize }) => + ({ + [ErrorSize.LG]: ` + margin: 0.5rem 0 1rem; + font-size: 24px; + line-height: 38px; + `, + [ErrorSize.MD]: ` + margin: 0rem; + padding-bottom: 0.4rem; + font-size: 18px; + line-height: 20px; + `, + [ErrorSize.SM]: '', + }[size]); + +const ErrorTitle = styled('h1')` + ${({ size }: { size: ErrorSize }) => css` + display: flex; + align-items: center; + ${getTitleStyle({ size })} + `} +`; + +const ErrorNotification = ({ + children, + title, + size, + styles = '', + onDismiss, + dismissible = false, +}: { + children: React.ReactNode; + title?: string; + size: ErrorSize; + styles?: string; + onDismiss?: Function; + dismissible?: boolean; +}) => ( +
    + + {title ? ( +
    + + {' '} + {title} + {dismissible && ( + + )} + + {children} +
    + ) : ( +
    + +
    + {children} +
    + {dismissible && ( + (onDismiss ? onDismiss() : () => null)} + Icon={Dismiss} + height={12} + width={12} + fill={defaultTheme.colors.error_dark} + /> + )} +
    + )} +
    +
    +); + +export default ErrorNotification; diff --git a/components/pages/explorer/index.tsx b/components/pages/explorer/index.tsx index 37d834d0..c20d7674 100644 --- a/components/pages/explorer/index.tsx +++ b/components/pages/explorer/index.tsx @@ -8,7 +8,7 @@ import { RepoFiltersType } from './sqonTypes'; import { getConfig } from '../../../global/config'; import createArrangerFetcher from '../../utils/arrangerFetcher'; import { useEffect, useState } from 'react'; -import ErrorContainer from '../../ErrorContainer'; +import ErrorNotification from '../../ErrorNotification'; import getConfigError from './getConfigError'; const Arranger = dynamic( @@ -101,7 +101,7 @@ const RepositoryPage = () => { return ( {ConfigError ? ( - { `} > {ConfigError} - + ) : ( { `} > {!userHasScopes && ( - + You do not have permission to generate an API token. Please contact the DMS administrator to gain the correct permission. - + )} @@ -244,7 +244,7 @@ const ApiTokenInfo = () => { margin: 1.5rem 0; `} > - css` background-color: ${theme.colors.error_1}; @@ -261,7 +261,7 @@ const ApiTokenInfo = () => { > There was a problem generating an API token: {errorMessage.message.toString()} - + )}
    Date: Tue, 16 Mar 2021 12:36:17 -0400 Subject: [PATCH 03/19] wip error messages --- components/ErrorNotification.tsx | 36 ++++++++++++++------------ components/pages/user/ApiTokenInfo.tsx | 26 ++++++++++--------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/components/ErrorNotification.tsx b/components/ErrorNotification.tsx index 157e67cc..49893bf3 100644 --- a/components/ErrorNotification.tsx +++ b/components/ErrorNotification.tsx @@ -7,17 +7,19 @@ import { Error as ErrorIcon } from './theme/icons'; import Dismiss from './theme/icons/dismiss'; import { IconProps } from './theme/icons/types'; -enum ErrorSize { - LG = 'lg', - MD = 'md', - SM = 'sm', -} +type ErrorSize = 'lg' | 'md' | 'sm'; + +const ERROR_SIZES = { + LG: 'lg' as ErrorSize, + MD: 'md' as ErrorSize, + SM: 'sm' as ErrorSize, +}; const getIconDimensions = ({ size }: { size: ErrorSize }) => ({ - [ErrorSize.LG]: { width: 26, height: 27 }, - [ErrorSize.MD]: { width: 21, height: 22 }, - [ErrorSize.SM]: { width: 19, height: 19 }, + [ERROR_SIZES.LG]: { width: 26, height: 27 }, + [ERROR_SIZES.MD]: { width: 21, height: 22 }, + [ERROR_SIZES.SM]: { width: 19, height: 19 }, }[size]); const IconButton = ({ @@ -47,15 +49,15 @@ const IconButton = ({ const getContainerStyles = ({ size }: { size: ErrorSize }) => ({ - [ErrorSize.LG]: ` + [ERROR_SIZES.LG]: ` padding: 1rem 2rem; line-height: 26px; `, - [ErrorSize.MD]: ` + [ERROR_SIZES.MD]: ` padding: 1rem; line-height: 24px; `, - [ErrorSize.SM]: ` + [ERROR_SIZES.SM]: ` padding: 0.5rem; line-height: 20px; display: flex; @@ -79,25 +81,25 @@ const ErrorContentContainer = styled('div')` const getIconStyle = ({ size }: { size: ErrorSize }) => ({ - [ErrorSize.LG]: 'padding-right: 15px', - [ErrorSize.MD]: 'padding-right: 15px', - [ErrorSize.SM]: '', + [ERROR_SIZES.LG]: 'padding-right: 15px', + [ERROR_SIZES.MD]: 'padding-right: 15px', + [ERROR_SIZES.SM]: '', }[size]); const getTitleStyle = ({ size }: { size: ErrorSize }) => ({ - [ErrorSize.LG]: ` + [ERROR_SIZES.LG]: ` margin: 0.5rem 0 1rem; font-size: 24px; line-height: 38px; `, - [ErrorSize.MD]: ` + [ERROR_SIZES.MD]: ` margin: 0rem; padding-bottom: 0.4rem; font-size: 18px; line-height: 20px; `, - [ErrorSize.SM]: '', + [ERROR_SIZES.SM]: '', }[size]); const ErrorTitle = styled('h1')` diff --git a/components/pages/user/ApiTokenInfo.tsx b/components/pages/user/ApiTokenInfo.tsx index 40d76119..fef06f02 100644 --- a/components/pages/user/ApiTokenInfo.tsx +++ b/components/pages/user/ApiTokenInfo.tsx @@ -79,13 +79,15 @@ const ApiTokenInfo = () => { ) .then((res) => { if (res.status !== 200) { - throw new Error('Error fetching scopes, cannot generate api token.'); + throw new Error( + `Error fetching scopes, cannot generate api token. Response Status: ${res.status}`, + ); } return res.json(); }) .then((json) => json.scopes) - .catch((err) => { - setErrorMessage({ message: err }); + .catch((err: Error) => { + setErrorMessage({ message: err.message }); console.warn(err); return err; }); @@ -106,9 +108,7 @@ const ApiTokenInfo = () => { ) .then((res) => { if (res.status !== 200) { - throw new Error( - 'User does not have appropriate permissions. Failed to generate api token!', - ); + throw new Error(`Failed to generate new Api Token. Response Status: ${res.status}`); } return res.json(); }) @@ -122,7 +122,7 @@ const ApiTokenInfo = () => { } else { // request for apiToken is skipped if filteredScopes is empty setErrorMessage({ - message: 'Something bad happened here what was it', + message: 'User does not have appropriate permissions. Failed to generate api token!', }); } } @@ -131,15 +131,17 @@ const ApiTokenInfo = () => { const revokeApiToken = async () => { return ( existingApiToken && - fetchWithAuth(`${EGO_API_KEY_ENDPOINT}?apiKey=${existingApiToken.name}`, { method: 'DELETE' }) + fetchWithAuth(`${EGO_API_KEY_ENDPOINT}?apiKey=${existingApiToken.name}`, { + method: 'DELETE', + }) .then((res) => { if (res.status !== 200) { - throw new Error('Error revoking api token!'); + throw new Error(`Error revoking api token. Response Status: ${res.status}`); } setExistingApiToken(null); }) - .catch((err) => { - setErrorMessage({ message: err }); + .catch((err: Error) => { + setErrorMessage({ message: err.message }); console.warn(err); }) ); @@ -259,7 +261,7 @@ const ApiTokenInfo = () => { display: block; `} > - There was a problem generating an API token: {errorMessage.message.toString()} + There was a problem: {errorMessage.message.toString()}
    From 20f19beb8428e7bad40ab2fb8692b782b2d22447 Mon Sep 17 00:00:00 2001 From: Ann Catton Date: Tue, 16 Mar 2021 14:36:11 -0400 Subject: [PATCH 04/19] cleanup --- components/pages/user/ApiTokenInfo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/pages/user/ApiTokenInfo.tsx b/components/pages/user/ApiTokenInfo.tsx index fef06f02..6382acab 100644 --- a/components/pages/user/ApiTokenInfo.tsx +++ b/components/pages/user/ApiTokenInfo.tsx @@ -115,7 +115,7 @@ const ApiTokenInfo = () => { .then((newApiToken: ApiToken) => { setExistingApiToken(newApiToken); }) - .catch(async (err: Error) => { + .catch((err: Error) => { setErrorMessage({ message: err.message }); return err; }); From 462ec3ef3f218f63f81e3d5da820c54405288715 Mon Sep 17 00:00:00 2001 From: Ann Catton Date: Tue, 16 Mar 2021 14:56:14 -0400 Subject: [PATCH 05/19] cleanup permissions error message, fix icon --- components/ErrorNotification.tsx | 16 +++++++++------- components/pages/user/ApiTokenInfo.tsx | 7 ++++--- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/components/ErrorNotification.tsx b/components/ErrorNotification.tsx index 49893bf3..31668ed9 100644 --- a/components/ErrorNotification.tsx +++ b/components/ErrorNotification.tsx @@ -19,7 +19,7 @@ const getIconDimensions = ({ size }: { size: ErrorSize }) => ({ [ERROR_SIZES.LG]: { width: 26, height: 27 }, [ERROR_SIZES.MD]: { width: 21, height: 22 }, - [ERROR_SIZES.SM]: { width: 19, height: 19 }, + [ERROR_SIZES.SM]: { width: 18, height: 18 }, }[size]); const IconButton = ({ @@ -156,12 +156,14 @@ const ErrorNotification = ({ flex-direction: row; `} > - + + +
    { } else { // request for apiToken is skipped if filteredScopes is empty setErrorMessage({ - message: 'User does not have appropriate permissions. Failed to generate api token!', + message: + 'You do not have permissions to generate an API token. Your permissions may have changed recently. Please contact the DMS administrator to gain the correct permissions.', }); } } @@ -234,8 +235,8 @@ const ApiTokenInfo = () => { > {!userHasScopes && ( - You do not have permission to generate an API token. Please contact the DMS - administrator to gain the correct permission. + You do not have permissions to generate an API token. Please contact the DMS + administrator to gain the correct permissions. )}
    From 63ed313cd73c4b697aa39531daaa936112781639 Mon Sep 17 00:00:00 2001 From: Ann Catton Date: Wed, 17 Mar 2021 10:23:35 -0400 Subject: [PATCH 06/19] update generic error message --- components/pages/user/ApiTokenInfo.tsx | 23 ++++++++++++++++------- global/utils/constants.ts | 3 +++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/components/pages/user/ApiTokenInfo.tsx b/components/pages/user/ApiTokenInfo.tsx index dea74125..842fb9e6 100644 --- a/components/pages/user/ApiTokenInfo.tsx +++ b/components/pages/user/ApiTokenInfo.tsx @@ -8,7 +8,7 @@ import { Tooltip } from 'react-tippy'; import { parseExpiry, getDayValue } from '../../../global/utils/apiToken'; import { getConfig } from '../../../global/config'; import useAuthContext from '../../../global/hooks/useAuthContext'; -import { EGO_API_KEY_ENDPOINT } from '../../../global/utils/constants'; +import { EGO_API_KEY_ENDPOINT, GENERIC_API_ERROR_MESSAGE } from '../../../global/utils/constants'; import Button from '../../Button'; import StyledLink from '../../Link'; @@ -80,7 +80,7 @@ const ApiTokenInfo = () => { .then((res) => { if (res.status !== 200) { throw new Error( - `Error fetching scopes, cannot generate api token. Response Status: ${res.status}`, + `HTTP error ${res.status}: Error fetching current permissions. Your API token could not be generated. ${GENERIC_API_ERROR_MESSAGE}`, ); } return res.json(); @@ -108,7 +108,9 @@ const ApiTokenInfo = () => { ) .then((res) => { if (res.status !== 200) { - throw new Error(`Failed to generate new Api Token. Response Status: ${res.status}`); + throw new Error( + `HTTP error ${res.status}: Your API token could not be generated. ${GENERIC_API_ERROR_MESSAGE}`, + ); } return res.json(); }) @@ -137,7 +139,9 @@ const ApiTokenInfo = () => { }) .then((res) => { if (res.status !== 200) { - throw new Error(`Error revoking api token. Response Status: ${res.status}`); + throw new Error( + `HTTP error ${res.status}: Your API token could not be revoked. ${GENERIC_API_ERROR_MESSAGE}`, + ); } setExistingApiToken(null); }) @@ -172,7 +176,9 @@ const ApiTokenInfo = () => { fetchWithAuth(`${EGO_API_KEY_ENDPOINT}?user_id=${user.id}`, { method: 'GET' }) .then((res) => { if (res.status !== 200) { - throw new Error(); + throw new Error( + `HTTP error ${res.status}: Your existing API tokens could not be fetched. ${GENERIC_API_ERROR_MESSAGE}`, + ); } return res.json(); }) @@ -184,7 +190,10 @@ const ApiTokenInfo = () => { setExistingApiToken(null); } }) - .catch((err) => console.warn('Could not get api tokens! ', err)); + .catch((err: Error) => { + setErrorMessage({ message: err.message }); + console.warn(err.message); + }); }, [token]); const userEffectiveScopes = (user?.scope || []) @@ -262,7 +271,7 @@ const ApiTokenInfo = () => { display: block; `} > - There was a problem: {errorMessage.message.toString()} + {errorMessage.message.toString()} diff --git a/global/utils/constants.ts b/global/utils/constants.ts index 59a5add4..ac329663 100644 --- a/global/utils/constants.ts +++ b/global/utils/constants.ts @@ -8,3 +8,6 @@ export const EGO_API_KEY_ENDPOINT = `${NEXT_PUBLIC_EGO_API_ROOT}/o/api_key`; export const EXPLORER_PATH = '/explorer'; export const USER_PATH = '/user'; export const LOGIN_PATH = '/login'; + +export const GENERIC_API_ERROR_MESSAGE = + 'Please try again. If the problem persists, please contact the DMS administrator for help troubleshooting the issue.'; From 8d62a3bd93ee9d68a540528ee1923fe5bb62052c Mon Sep 17 00:00:00 2001 From: Ann Catton Date: Wed, 17 Mar 2021 21:23:48 -0400 Subject: [PATCH 07/19] wip loader on explore --- components/Loader.tsx | 28 ++++++++++++++++++++++++ components/pages/explorer/index.tsx | 30 ++++++++++++++++++++++---- components/pages/user/ApiTokenInfo.tsx | 9 ++------ components/utils/sleep.ts | 8 +++++++ pages/logged-in.tsx | 26 +--------------------- 5 files changed, 65 insertions(+), 36 deletions(-) create mode 100644 components/Loader.tsx create mode 100644 components/utils/sleep.ts diff --git a/components/Loader.tsx b/components/Loader.tsx new file mode 100644 index 00000000..5341f391 --- /dev/null +++ b/components/Loader.tsx @@ -0,0 +1,28 @@ +import { css } from '@emotion/core'; + +// TODO: this is a placeholder Loader +const Loader = () => { + return ( +
    css` + border: 14px solid ${theme.colors.grey_3}; + border-top: 14px solid ${theme.colors.secondary_dark}; + border-radius: 50%; + width: 120px; + height: 120px; + animation: spin 2s linear infinite; + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + `} + /> + ); +}; + +export default Loader; diff --git a/components/pages/explorer/index.tsx b/components/pages/explorer/index.tsx index 5a7b14da..b46a1ac8 100644 --- a/components/pages/explorer/index.tsx +++ b/components/pages/explorer/index.tsx @@ -10,6 +10,9 @@ import createArrangerFetcher from '../../utils/arrangerFetcher'; import { useEffect, useState } from 'react'; import ErrorContainer from '../../ErrorContainer'; import getConfigError from './getConfigError'; +import Loader from '../../Loader'; +import { css } from '@emotion/core'; +import sleep from '../../utils/sleep'; const Arranger = dynamic( () => import('@arranger/components/dist/Arranger').then((comp) => comp.Arranger), @@ -67,6 +70,7 @@ const RepositoryPage = () => { } = getConfig(); const [availableProjects, setAvailableProjects] = useState([]); + const [loadingProjects, setLoadingProjects] = useState(true); useEffect(() => { const { NEXT_PUBLIC_ARRANGER_API } = getConfig(); fetch(urlJoin(NEXT_PUBLIC_ARRANGER_API, 'admin/graphql'), { @@ -83,11 +87,15 @@ const RepositoryPage = () => { } return res.json(); }) - .then(({ data: { projects } }: { data: { projects: Project[] } }) => { - setAvailableProjects(projects); + .then(async ({ data: { projects } }: { data: { projects: Project[] } }) => { + await setAvailableProjects(projects); + await sleep(1000); + setLoadingProjects(false); }) - .catch((err) => { + .catch(async (err) => { console.warn(err); + await sleep(1000); + setLoadingProjects(false); }); }, []); @@ -100,7 +108,21 @@ const RepositoryPage = () => { return ( - {ConfigError ? ( + {loadingProjects ? ( +
    + css` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: ${theme.colors.grey_2}; + ` + } + > + +
    + ) : ConfigError ? ( {ConfigError} ) : ( - new Promise((resolve) => { - setTimeout(() => { - resolve(''); - }, time); - }); - const ApiTokenInfo = () => { const { user, token, fetchWithAuth } = useAuthContext(); const [existingApiToken, setExistingApiToken] = useState(null); diff --git a/components/utils/sleep.ts b/components/utils/sleep.ts new file mode 100644 index 00000000..5510a6ff --- /dev/null +++ b/components/utils/sleep.ts @@ -0,0 +1,8 @@ +const sleep = (time: number = 2000) => + new Promise((resolve) => { + setTimeout(() => { + resolve(''); + }, time); + }); + +export default sleep; diff --git a/pages/logged-in.tsx b/pages/logged-in.tsx index 9c4548ab..ca71a289 100644 --- a/pages/logged-in.tsx +++ b/pages/logged-in.tsx @@ -9,6 +9,7 @@ import Router from 'next/router'; import { isValidJwt } from '../global/utils/egoTokenUtils'; import PageLayout from '../components/PageLayout'; import getInternalLink from '../global/utils/getInternalLink'; +import Loader from '../components/Loader'; const fetchEgoToken = () => { const { NEXT_PUBLIC_EGO_API_ROOT, NEXT_PUBLIC_EGO_CLIENT_ID } = getConfig(); @@ -44,31 +45,6 @@ const fetchEgoToken = () => { }); }; -// TODO: this is a placeholder Loader -const Loader = () => { - return ( -
    css` - border: 14px solid ${theme.colors.grey_3}; - border-top: 14px solid ${theme.colors.secondary_dark}; - border-radius: 50%; - width: 120px; - height: 120px; - animation: spin 2s linear infinite; - - @keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } - } - `} - /> - ); -}; - const LoginLoaderPage = createPage({ getInitialProps: async (ctx) => { const { egoJwt, asPath, query } = ctx; From c4445de1b8d7fa7c5a4fb199aaf911fb3bb09a57 Mon Sep 17 00:00:00 2001 From: Ann Catton Date: Thu, 18 Mar 2021 16:24:36 -0400 Subject: [PATCH 08/19] =?UTF-8?q?=F0=9F=94=A7=20Configuration=20for=20cust?= =?UTF-8?q?om=20lab=20name,=20icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 ++ components/NavBar.tsx | 34 ++++++++++++++++++++++------------ components/theme/dimensions.ts | 4 ++++ global/config.ts | 6 ++++++ next.config.js | 3 +++ 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index 51c9420a..1380f0be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,8 @@ WORKDIR /usr/src COPY . /usr/src +VOLUME [ "/usr/src/public/static/dms_user_assets" ] + RUN npm ci RUN npm run build diff --git a/components/NavBar.tsx b/components/NavBar.tsx index bd1ef169..add77312 100644 --- a/components/NavBar.tsx +++ b/components/NavBar.tsx @@ -9,17 +9,30 @@ import useAuthContext from '../global/hooks/useAuthContext'; import { StyledLinkAsButton, InternalLink as Link } from './Link'; import { useTheme } from 'emotion-theming'; import { EXPLORER_PATH, LOGIN_PATH, USER_PATH } from '../global/utils/constants'; +import { getConfig } from '../global/config'; -const NavBar: React.ComponentType = ({ labName = 'Data Management System', labIcon }) => { +const NavBar: React.ComponentType = () => { const { token } = useAuthContext(); const router = useRouter(); const theme: typeof defaultTheme = useTheme(); + const { NEXT_PUBLIC_LAB_NAME, NEXT_PUBLIC_LOGO_FILENAME } = getConfig(); + const activeLinkStyle = ` background-color: ${theme.colors.grey_2}; color: ${theme.colors.accent2_dark}; `; + const labIcon = NEXT_PUBLIC_LOGO_FILENAME ? ( + {NEXT_PUBLIC_LAB_NAME} + ) : ( + + ); return (
    css` @@ -53,17 +66,14 @@ const NavBar: React.ComponentType = ({ labName = 'Data Management System', color: ${theme.colors.accent_dark}; `} > - {labIcon || } - {/* set to default until labname config is implemented */} - {labName && ( - - {labName} - - )} + {labIcon} + + {NEXT_PUBLIC_LAB_NAME} +
    diff --git a/components/theme/dimensions.ts b/components/theme/dimensions.ts index 5a44d92e..d7bfe66d 100644 --- a/components/theme/dimensions.ts +++ b/components/theme/dimensions.ts @@ -9,6 +9,10 @@ const dimensions = { minWidth: 250, maxWidth: 270, }, + labIcon: { + height: 35, + width: 35, + }, }; export default dimensions; diff --git a/global/config.ts b/global/config.ts index fe40aa17..da704122 100644 --- a/global/config.ts +++ b/global/config.ts @@ -15,6 +15,9 @@ export const getConfig = () => { NEXT_PUBLIC_ARRANGER_API: publicConfig.NEXT_PUBLIC_ARRANGER_API || 'http://localhost:5050', NEXT_PUBLIC_ARRANGER_ADMIN_UI: publicConfig.NEXT_PUBLIC_ARRANGER_ADMIN_UI, NEXT_PUBLIC_BASE_PATH: publicConfig.NEXT_PUBLIC_BASE_PATH || '', + NEXT_PUBLIC_ADMIN_EMAIL: publicConfig.NEXT_PUBLIC_ADMIN_EMAIL, + NEXT_PUBLIC_LAB_NAME: publicConfig.NEXT_PUBLIC_LAB_NAME || 'Data Management System', + NEXT_PUBLIC_LOGO_FILENAME: publicConfig.NEXT_PUBLIC_LOGO_FILENAME, } as { NEXT_PUBLIC_EGO_API_ROOT: string; NEXT_PUBLIC_EGO_CLIENT_ID: string; @@ -25,5 +28,8 @@ export const getConfig = () => { NEXT_PUBLIC_ARRANGER_API: string; NEXT_PUBLIC_ARRANGER_ADMIN_UI: string; NEXT_PUBLIC_BASE_PATH: string; + NEXT_PUBLIC_ADMIN_EMAIL: string; + NEXT_PUBLIC_LAB_NAME: string; + NEXT_PUBLIC_LOGO_FILENAME: string; }; }; diff --git a/next.config.js b/next.config.js index 98441130..eb130734 100644 --- a/next.config.js +++ b/next.config.js @@ -13,6 +13,9 @@ module.exports = withCSS({ // using ASSET_PREFIX for the public runtime BASE_PATH because basePath in the top level config was not working // with the dms reverse proxy setup NEXT_PUBLIC_BASE_PATH: process.env.ASSET_PREFIX, + NEXT_PUBLIC_ADMIN_EMAIL: process.env.NEXT_PUBLIC_ADMIN_EMAIL, + NEXT_PUBLIC_LAB_NAME: process.env.NEXT_PUBLIC_LAB_NAME, + NEXT_PUBLIC_LOGO_FILENAME: process.env.NEXT_PUBLIC_LOGO_FILENAME, }, assetPrefix: process.env.ASSET_PREFIX || '', }); From e3287ea4cfb8bb44617ff26afcc768ff3082fe30 Mon Sep 17 00:00:00 2001 From: Ann Catton Date: Thu, 18 Mar 2021 17:44:52 -0400 Subject: [PATCH 09/19] pr feedback. add larger limit to api token GET --- components/ErrorNotification.tsx | 53 +++++++------------------- components/IconButton.tsx | 29 ++++++++++++++ components/pages/user/ApiTokenInfo.tsx | 14 +++---- 3 files changed, 50 insertions(+), 46 deletions(-) create mode 100644 components/IconButton.tsx diff --git a/components/ErrorNotification.tsx b/components/ErrorNotification.tsx index 31668ed9..11e5ab32 100644 --- a/components/ErrorNotification.tsx +++ b/components/ErrorNotification.tsx @@ -1,11 +1,11 @@ import { css } from '@emotion/core'; import styled from '@emotion/styled'; import React from 'react'; +import IconButton from './IconButton'; import defaultTheme from './theme'; import { Error as ErrorIcon } from './theme/icons'; -import Dismiss from './theme/icons/dismiss'; -import { IconProps } from './theme/icons/types'; +import DismissIcon from './theme/icons/dismiss'; type ErrorSize = 'lg' | 'md' | 'sm'; @@ -15,39 +15,14 @@ const ERROR_SIZES = { SM: 'sm' as ErrorSize, }; -const getIconDimensions = ({ size }: { size: ErrorSize }) => +const getIconDimensions = (size: ErrorSize) => ({ [ERROR_SIZES.LG]: { width: 26, height: 27 }, [ERROR_SIZES.MD]: { width: 21, height: 22 }, [ERROR_SIZES.SM]: { width: 18, height: 18 }, }[size]); -const IconButton = ({ - Icon, - fill, - height, - width, - onClick = () => {}, -}: { - Icon: React.ComponentType; - fill: string; - height: number; - width: number; - onClick: React.MouseEventHandler; -}) => { - return ( - - - - ); -}; - -const getContainerStyles = ({ size }: { size: ErrorSize }) => +const getContainerStyles = (size: ErrorSize) => ({ [ERROR_SIZES.LG]: ` padding: 1rem 2rem; @@ -74,19 +49,19 @@ const ErrorContentContainer = styled('div')` font-weight: normal; background-color: ${theme.colors.error_1}; color: ${theme.colors.accent_dark}; - ${getContainerStyles({ size })}; + ${getContainerStyles(size)}; max-width: 600px; `} `; -const getIconStyle = ({ size }: { size: ErrorSize }) => +const getIconStyle = (size: ErrorSize) => ({ [ERROR_SIZES.LG]: 'padding-right: 15px', [ERROR_SIZES.MD]: 'padding-right: 15px', [ERROR_SIZES.SM]: '', }[size]); -const getTitleStyle = ({ size }: { size: ErrorSize }) => +const getTitleStyle = (size: ErrorSize) => ({ [ERROR_SIZES.LG]: ` margin: 0.5rem 0 1rem; @@ -106,7 +81,7 @@ const ErrorTitle = styled('h1')` ${({ size }: { size: ErrorSize }) => css` display: flex; align-items: center; - ${getTitleStyle({ size })} + ${getTitleStyle(size)} `} `; @@ -137,14 +112,14 @@ const ErrorNotification = ({
    {' '} {title} {dismissible && ( - + )} {children} @@ -158,9 +133,9 @@ const ErrorNotification = ({ > @@ -178,7 +153,7 @@ const ErrorNotification = ({ {dismissible && ( (onDismiss ? onDismiss() : () => null)} - Icon={Dismiss} + Icon={DismissIcon} height={12} width={12} fill={defaultTheme.colors.error_dark} diff --git a/components/IconButton.tsx b/components/IconButton.tsx new file mode 100644 index 00000000..056dec4e --- /dev/null +++ b/components/IconButton.tsx @@ -0,0 +1,29 @@ +import { css } from '@emotion/core'; +import { IconProps } from './theme/icons/types'; + +const IconButton = ({ + Icon, + fill, + height, + width, + onClick = () => {}, +}: { + Icon: React.ComponentType; + fill: string; + height: number; + width: number; + onClick: React.MouseEventHandler; +}) => { + return ( + + + + ); +}; + +export default IconButton; diff --git a/components/pages/user/ApiTokenInfo.tsx b/components/pages/user/ApiTokenInfo.tsx index 842fb9e6..8f4c3373 100644 --- a/components/pages/user/ApiTokenInfo.tsx +++ b/components/pages/user/ApiTokenInfo.tsx @@ -100,12 +100,12 @@ const ApiTokenInfo = () => { if (filteredScopes.length) { const scopeParams = filteredScopes.map((f: ScopeObj) => `${f.policy}.${f.accessLevel}`); - return fetchWithAuth( - `${EGO_API_KEY_ENDPOINT}?scopes=${encodeURIComponent(scopeParams.join())}&user_id=${ - user.id - }`, - { method: 'POST' }, - ) + + const apiKeyUrl = new URL(EGO_API_KEY_ENDPOINT); + apiKeyUrl.searchParams.append('scopes', encodeURIComponent(scopeParams.join())); + apiKeyUrl.searchParams.append('user_id', user.id); + + return fetchWithAuth(apiKeyUrl.href, { method: 'POST' }) .then((res) => { if (res.status !== 200) { throw new Error( @@ -173,7 +173,7 @@ const ApiTokenInfo = () => { useEffect(() => { user && - fetchWithAuth(`${EGO_API_KEY_ENDPOINT}?user_id=${user.id}`, { method: 'GET' }) + fetchWithAuth(`${EGO_API_KEY_ENDPOINT}?user_id=${user.id}&limit=1000`, { method: 'GET' }) .then((res) => { if (res.status !== 200) { throw new Error( From 853d3207bf7439d1c891264a5cf47441cc4daa55 Mon Sep 17 00:00:00 2001 From: Ann Catton Date: Fri, 19 Mar 2021 09:07:53 -0400 Subject: [PATCH 10/19] use sort instead of limit for api key fetch --- components/pages/user/ApiTokenInfo.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/components/pages/user/ApiTokenInfo.tsx b/components/pages/user/ApiTokenInfo.tsx index 8f4c3373..9535ec55 100644 --- a/components/pages/user/ApiTokenInfo.tsx +++ b/components/pages/user/ApiTokenInfo.tsx @@ -172,8 +172,13 @@ const ApiTokenInfo = () => { const tokenIsExpired: boolean = has(existingApiToken, 'expiryDate') && parsedExpiry <= 0; useEffect(() => { - user && - fetchWithAuth(`${EGO_API_KEY_ENDPOINT}?user_id=${user.id}&limit=1000`, { method: 'GET' }) + if (user) { + const fetchApiKeysUrl = new URL(EGO_API_KEY_ENDPOINT); + fetchApiKeysUrl.searchParams.append('sort', 'isRevoked'); + fetchApiKeysUrl.searchParams.append('sortOrder', 'ASC'); + fetchApiKeysUrl.searchParams.append('user_id', user.id); + + fetchWithAuth(fetchApiKeysUrl.href, { method: 'GET' }) .then((res) => { if (res.status !== 200) { throw new Error( @@ -194,6 +199,7 @@ const ApiTokenInfo = () => { setErrorMessage({ message: err.message }); console.warn(err.message); }); + } }, [token]); const userEffectiveScopes = (user?.scope || []) From 65b779b2abe61109d91d509076befe89ab9a774e Mon Sep 17 00:00:00 2001 From: Ann Catton Date: Fri, 19 Mar 2021 10:44:57 -0400 Subject: [PATCH 11/19] =?UTF-8?q?=F0=9F=90=9B=20Fix=20scope=20params.=20Re?= =?UTF-8?q?move=20error=20on=20success?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/pages/user/ApiTokenInfo.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/pages/user/ApiTokenInfo.tsx b/components/pages/user/ApiTokenInfo.tsx index 9535ec55..dc129c1c 100644 --- a/components/pages/user/ApiTokenInfo.tsx +++ b/components/pages/user/ApiTokenInfo.tsx @@ -102,7 +102,9 @@ const ApiTokenInfo = () => { const scopeParams = filteredScopes.map((f: ScopeObj) => `${f.policy}.${f.accessLevel}`); const apiKeyUrl = new URL(EGO_API_KEY_ENDPOINT); - apiKeyUrl.searchParams.append('scopes', encodeURIComponent(scopeParams.join())); + scopeParams.map((param) => + apiKeyUrl.searchParams.append('scopes', encodeURIComponent(param)), + ); apiKeyUrl.searchParams.append('user_id', user.id); return fetchWithAuth(apiKeyUrl.href, { method: 'POST' }) @@ -116,6 +118,7 @@ const ApiTokenInfo = () => { }) .then((newApiToken: ApiToken) => { setExistingApiToken(newApiToken); + setErrorMessage(null); }) .catch((err: Error) => { setErrorMessage({ message: err.message }); @@ -144,6 +147,7 @@ const ApiTokenInfo = () => { ); } setExistingApiToken(null); + setErrorMessage(null); }) .catch((err: Error) => { setErrorMessage({ message: err.message }); From ded60c900f239052e413e777ca1fc1430fbfefc6 Mon Sep 17 00:00:00 2001 From: Ann Catton Date: Fri, 19 Mar 2021 13:18:25 -0400 Subject: [PATCH 12/19] =?UTF-8?q?=F0=9F=90=9B=20img=20url=20needs=20base?= =?UTF-8?q?=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/NavBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/NavBar.tsx b/components/NavBar.tsx index add77312..b5a05dc4 100644 --- a/components/NavBar.tsx +++ b/components/NavBar.tsx @@ -16,7 +16,7 @@ const NavBar: React.ComponentType = () => { const router = useRouter(); const theme: typeof defaultTheme = useTheme(); - const { NEXT_PUBLIC_LAB_NAME, NEXT_PUBLIC_LOGO_FILENAME } = getConfig(); + const { NEXT_PUBLIC_LAB_NAME, NEXT_PUBLIC_LOGO_FILENAME, NEXT_PUBLIC_BASE_PATH } = getConfig(); const activeLinkStyle = ` background-color: ${theme.colors.grey_2}; @@ -25,7 +25,7 @@ const NavBar: React.ComponentType = () => { const labIcon = NEXT_PUBLIC_LOGO_FILENAME ? ( {NEXT_PUBLIC_LAB_NAME} Date: Fri, 19 Mar 2021 15:12:33 -0400 Subject: [PATCH 13/19] add comment for loader sleep() call --- components/pages/explorer/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/pages/explorer/index.tsx b/components/pages/explorer/index.tsx index 4b23e15d..dee231f4 100644 --- a/components/pages/explorer/index.tsx +++ b/components/pages/explorer/index.tsx @@ -89,11 +89,13 @@ const RepositoryPage = () => { }) .then(async ({ data: { projects } }: { data: { projects: Project[] } }) => { await setAvailableProjects(projects); + // 1s delay so loader doesn't flicker on and off too quickly await sleep(1000); setLoadingProjects(false); }) .catch(async (err) => { console.warn(err); + // same as above comment await sleep(1000); setLoadingProjects(false); }); From 89bea99f61747b507471420080e9bc5a54dd84c2 Mon Sep 17 00:00:00 2001 From: Ann Catton Date: Fri, 19 Mar 2021 17:35:34 -0400 Subject: [PATCH 14/19] =?UTF-8?q?=F0=9F=90=9B=20Fix=20logic=20for=20api=20?= =?UTF-8?q?token=20display?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/pages/user/ApiTokenInfo.tsx | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/components/pages/user/ApiTokenInfo.tsx b/components/pages/user/ApiTokenInfo.tsx index 3efc2db8..1037ce61 100644 --- a/components/pages/user/ApiTokenInfo.tsx +++ b/components/pages/user/ApiTokenInfo.tsx @@ -1,7 +1,7 @@ import { css, Global } from '@emotion/core'; import styled from '@emotion/styled'; import { useTheme } from 'emotion-theming'; -import { has, isEmpty } from 'lodash'; +import { has, isEmpty, orderBy } from 'lodash'; import React, { useEffect, useState } from 'react'; import { Tooltip } from 'react-tippy'; @@ -174,8 +174,10 @@ const ApiTokenInfo = () => { if (user) { const fetchApiKeysUrl = new URL(EGO_API_KEY_ENDPOINT); fetchApiKeysUrl.searchParams.append('sort', 'isRevoked'); + // sort by asc will get isRevoked=false first fetchApiKeysUrl.searchParams.append('sortOrder', 'ASC'); fetchApiKeysUrl.searchParams.append('user_id', user.id); + fetchApiKeysUrl.searchParams.append('limit', '1000'); fetchWithAuth(fetchApiKeysUrl.href, { method: 'GET' }) .then((res) => { @@ -187,9 +189,20 @@ const ApiTokenInfo = () => { return res.json(); }) .then((json) => { - const activeToken = json.resultSet.find((r: ApiToken) => !r.isRevoked); - if (activeToken) { - setExistingApiToken(activeToken); + // first find all non-revoked tokens + const unrevokedTokens = json.resultSet.filter((r: ApiToken) => !r.isRevoked); + // then sort by expiry date + const unrevokedTokensSortedByExpiry = orderBy(unrevokedTokens, 'expiryDate', ['desc']); + console.log(unrevokedTokensSortedByExpiry); + // find most recent token that is not revoked and not expired, if it exists + const activeToken = unrevokedTokensSortedByExpiry.find((r: ApiToken) => { + const expiry = parseExpiry(r.expiryDate) || 0; + return expiry > 0; + }); + // display either this activeToken, or the most recently expired non-revoked token + const tokenToDisplay = activeToken || unrevokedTokensSortedByExpiry[0]; + if (tokenToDisplay) { + setExistingApiToken(tokenToDisplay); } else { setExistingApiToken(null); } From d2061c42838360889a3a125fe4edf58b2af7422c Mon Sep 17 00:00:00 2001 From: Ann Catton Date: Sat, 20 Mar 2021 10:29:20 -0400 Subject: [PATCH 15/19] =?UTF-8?q?=F0=9F=94=A7=20Display=20only=20configure?= =?UTF-8?q?d=20IdP=20logins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/pages/login/index.tsx | 99 +++++++++++++++++++++----------- global/config.ts | 2 + next.config.js | 1 + 3 files changed, 69 insertions(+), 33 deletions(-) diff --git a/components/pages/login/index.tsx b/components/pages/login/index.tsx index 68ecf0a4..540e5372 100644 --- a/components/pages/login/index.tsx +++ b/components/pages/login/index.tsx @@ -13,6 +13,8 @@ import { import { IconProps } from '../../theme/icons/types'; import { getConfig } from '../../../global/config'; +import { trim } from 'lodash'; +import ErrorNotification from '../../ErrorNotification'; const LoginButton = ({ Icon, @@ -86,22 +88,40 @@ const LoginButton = ({ ); }; -type ProviderType = { - name: string; +enum ProviderType { + GOOGLE = 'GOOGLE', + ORCID = 'ORCID', + LINKEDIN = 'LINKEDIN', + GITHUB = 'GITHUB', + // FACEBOOK = 'FACEBOOK' +} + +type ProviderDetail = { + displayName: string; path: string; icon: any; }; +type ProviderMap = { [k in ProviderType]: ProviderDetail }; -const providers: ProviderType[] = [ - { name: 'Google', path: 'google', icon: GoogleLogo }, - { name: 'ORCiD', path: 'orcid', icon: OrcidLogo }, - { name: 'GitHub', path: 'github', icon: GitHubLogo }, +const providerMap: ProviderMap = { + [ProviderType.GOOGLE]: { displayName: 'Google', path: 'google', icon: GoogleLogo }, + [ProviderType.ORCID]: { displayName: 'ORCiD', path: 'orcid', icon: OrcidLogo }, + [ProviderType.GITHUB]: { displayName: 'GitHub', path: 'github', icon: GitHubLogo }, + [ProviderType.LINKEDIN]: { displayName: 'LinkedIn', path: 'linkedin', icon: LinkedInLogo }, // Facebook will be hidden until provider implementation is fixed in Ego https://github.com/overture-stack/ego/issues/555 - // { name: 'Facebook', path: '', icon: FacebookLogo }, - { name: 'LinkedIn', path: 'linkedin', icon: LinkedInLogo }, -]; + // [ProviderType.FACEBOOK]: { displayName: 'Facebook', path: '', icon: FacebookLogo }, +}; const LoginPage = () => { + const { NEXT_PUBLIC_SSO_PROVIDERS } = getConfig(); + + const configuredProviders = NEXT_PUBLIC_SSO_PROVIDERS.length + ? NEXT_PUBLIC_SSO_PROVIDERS.split(',').map((p) => trim(p)) + : []; + const providers: ProviderDetail[] = configuredProviders.map( + (provider) => providerMap[provider as ProviderType], + ); + console.log(configuredProviders); return (
    { Please choose one of the following log in methods to access your API token for data download: -
      - {providers.map(({ name, icon, path }) => { - return ( -
    • - -
    • - ); - })} -
    + {providers.length ? ( +
      + {providers.map(({ displayName, icon, path }) => { + return ( +
    • + +
    • + ); + })} +
    + ) : ( +
    + + No identity providers have been configured. Please check you dms configuration file. + +
    + )}
    { NEXT_PUBLIC_ADMIN_EMAIL: publicConfig.NEXT_PUBLIC_ADMIN_EMAIL, NEXT_PUBLIC_LAB_NAME: publicConfig.NEXT_PUBLIC_LAB_NAME || 'Data Management System', NEXT_PUBLIC_LOGO_FILENAME: publicConfig.NEXT_PUBLIC_LOGO_FILENAME, + NEXT_PUBLIC_SSO_PROVIDERS: publicConfig.NEXT_PUBLIC_SSO_PROVIDERS || '', } as { NEXT_PUBLIC_EGO_API_ROOT: string; NEXT_PUBLIC_EGO_CLIENT_ID: string; @@ -31,5 +32,6 @@ export const getConfig = () => { NEXT_PUBLIC_ADMIN_EMAIL: string; NEXT_PUBLIC_LAB_NAME: string; NEXT_PUBLIC_LOGO_FILENAME: string; + NEXT_PUBLIC_SSO_PROVIDERS: string; }; }; diff --git a/next.config.js b/next.config.js index eb130734..ac39d12e 100644 --- a/next.config.js +++ b/next.config.js @@ -16,6 +16,7 @@ module.exports = withCSS({ NEXT_PUBLIC_ADMIN_EMAIL: process.env.NEXT_PUBLIC_ADMIN_EMAIL, NEXT_PUBLIC_LAB_NAME: process.env.NEXT_PUBLIC_LAB_NAME, NEXT_PUBLIC_LOGO_FILENAME: process.env.NEXT_PUBLIC_LOGO_FILENAME, + NEXT_PUBLIC_SSO_PROVIDERS: process.env.NEXT_PUBLIC_SSO_PROVIDERS, }, assetPrefix: process.env.ASSET_PREFIX || '', }); From c8b257fe965daf38cef8ce49861f18bd5d06af20 Mon Sep 17 00:00:00 2001 From: Ann Catton Date: Sat, 20 Mar 2021 10:32:49 -0400 Subject: [PATCH 16/19] remove log --- components/pages/user/ApiTokenInfo.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/components/pages/user/ApiTokenInfo.tsx b/components/pages/user/ApiTokenInfo.tsx index 1037ce61..a34da794 100644 --- a/components/pages/user/ApiTokenInfo.tsx +++ b/components/pages/user/ApiTokenInfo.tsx @@ -193,7 +193,6 @@ const ApiTokenInfo = () => { const unrevokedTokens = json.resultSet.filter((r: ApiToken) => !r.isRevoked); // then sort by expiry date const unrevokedTokensSortedByExpiry = orderBy(unrevokedTokens, 'expiryDate', ['desc']); - console.log(unrevokedTokensSortedByExpiry); // find most recent token that is not revoked and not expired, if it exists const activeToken = unrevokedTokensSortedByExpiry.find((r: ApiToken) => { const expiry = parseExpiry(r.expiryDate) || 0; From f712e402335ca432eb233470d2c5fc163c3fd27e Mon Sep 17 00:00:00 2001 From: Ann Catton Date: Mon, 22 Mar 2021 09:20:27 -0400 Subject: [PATCH 17/19] check sso providers are valid ProviderType --- components/pages/login/index.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/components/pages/login/index.tsx b/components/pages/login/index.tsx index 540e5372..6f604506 100644 --- a/components/pages/login/index.tsx +++ b/components/pages/login/index.tsx @@ -116,12 +116,14 @@ const LoginPage = () => { const { NEXT_PUBLIC_SSO_PROVIDERS } = getConfig(); const configuredProviders = NEXT_PUBLIC_SSO_PROVIDERS.length - ? NEXT_PUBLIC_SSO_PROVIDERS.split(',').map((p) => trim(p)) + ? NEXT_PUBLIC_SSO_PROVIDERS.split(',').map((p: string) => trim(p)) : []; - const providers: ProviderDetail[] = configuredProviders.map( - (provider) => providerMap[provider as ProviderType], - ); - console.log(configuredProviders); + // typing p arg as 'any' because typescript complains with 'string' + // check configured providers are valid ProviderTypes + const providers: ProviderDetail[] = configuredProviders + .filter((p: any) => Object.values(ProviderType).includes(p)) + .map((provider: ProviderType) => providerMap[provider as ProviderType]); + return (
    Date: Mon, 22 Mar 2021 11:18:48 -0400 Subject: [PATCH 18/19] fix typing issue --- components/pages/login/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/pages/login/index.tsx b/components/pages/login/index.tsx index 6f604506..26f5cab7 100644 --- a/components/pages/login/index.tsx +++ b/components/pages/login/index.tsx @@ -122,7 +122,7 @@ const LoginPage = () => { // check configured providers are valid ProviderTypes const providers: ProviderDetail[] = configuredProviders .filter((p: any) => Object.values(ProviderType).includes(p)) - .map((provider: ProviderType) => providerMap[provider as ProviderType]); + .map((provider: string) => providerMap[provider as ProviderType]); return ( From 6bb3f6abcbddb9a931fe226c288463b4f3766bfe Mon Sep 17 00:00:00 2001 From: Ann Catton Date: Mon, 22 Mar 2021 12:13:27 -0400 Subject: [PATCH 19/19] =?UTF-8?q?=F0=9F=94=96=20rc=200.8.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8e62ea9b..4c6a6f1d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dms-ui", - "version": "0.7.0", + "version": "0.8.0", "private": true, "scripts": { "dev": "next dev",