From a8838bf91eb05051098b85824ffb64c2d1ddcad1 Mon Sep 17 00:00:00 2001 From: atomicgamedev Date: Thu, 21 Nov 2024 20:30:08 +0100 Subject: [PATCH 1/8] Add VerifyConfig.tsx with route --- client/package.json | 3 +- client/src/page/LayoutPublic.tsx | 12 +- client/src/route/auth/VerifyConfig.tsx | 222 +++++++++++++++++++++++++ client/src/routes.tsx | 9 + 4 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 client/src/route/auth/VerifyConfig.tsx diff --git a/client/package.json b/client/package.json index 66489c896..6b73c41b8 100644 --- a/client/package.json +++ b/client/package.json @@ -90,7 +90,8 @@ "resize-observer-polyfill": "^1.5.1", "serve": "^14.2.1", "styled-components": "^6.1.1", - "typescript": "5.1.6" + "typescript": "5.1.6", + "zod": "^3.23.8" }, "devDependencies": { "@babel/core": "7.25.8", diff --git a/client/src/page/LayoutPublic.tsx b/client/src/page/LayoutPublic.tsx index 9acc61376..25ae94135 100644 --- a/client/src/page/LayoutPublic.tsx +++ b/client/src/page/LayoutPublic.tsx @@ -4,7 +4,7 @@ import AppBar from '@mui/material/AppBar'; import Footer from 'page/Footer'; import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; -import { Container } from '@mui/material'; +import { Breakpoint, Container } from '@mui/material'; import LinkButtons from 'components/LinkButtons'; import toolbarLinkValues from 'util/toolbarUtil'; @@ -26,7 +26,10 @@ const DTappBar = () => ( ); -function LayoutPublic(props: { children: React.ReactNode }) { +function LayoutPublic(props: { + children: React.ReactNode; + containerMaxWidth?: Breakpoint; +}) { return ( - + {props.children} diff --git a/client/src/route/auth/VerifyConfig.tsx b/client/src/route/auth/VerifyConfig.tsx new file mode 100644 index 000000000..9328cf69e --- /dev/null +++ b/client/src/route/auth/VerifyConfig.tsx @@ -0,0 +1,222 @@ +import { Paper, Tooltip, Typography } from '@mui/material'; +import * as React from 'react'; +import { z } from 'zod'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; + +const EnvironmentEnum = z.enum(['dev', 'local', 'prod', 'test']); +const PathString = z.string(); +const ScopesString = z.literal('openid profile read_user read_repository api'); + +async function urlRespondsWithOK(url: string): Promise { + // localhost is usually run insecurely so we let it pass + const insecurePattern = /^http:\/\//; + if (insecurePattern.test(url)) { + return url; + } + const cleanURL = url.replace(/^https?:\/\/(www.)?/, '').split('/')[0]; + try { + const response = await fetch(cleanURL, { redirect: 'follow' }); + if (response.ok) { + return cleanURL; + } + throw new Error(`${cleanURL} responded with ${response.status}`); + } catch (error) { + throw new Error( + `Error: Could not fetch ${cleanURL}. ${(error as Error).message}`, + ); + } +} + +const VerifyConfig = () => { + const [validationResults, setValidationResults] = React.useState<{ + [key: string]: boolean; + }>({}); + const root = document.getElementById('root'); + + React.useEffect(() => { + const checkValidations = async () => { + const results: { [key: string]: boolean } = {}; + try { + results.environment = EnvironmentEnum.safeParse( + window.env.REACT_APP_ENVIRONMENT, + ).success; + results.url = await urlRespondsWithOK(window.env.REACT_APP_URL) + .then(() => true) + .catch(() => false); + results.url_basename = PathString.safeParse( + window.env.REACT_APP_URL_BASENAME, + ).success; + results.url_dtlink = PathString.safeParse( + window.env.REACT_APP_URL_DTLINK, + ).success; + results.url_liblink = PathString.safeParse( + window.env.REACT_APP_URL_LIBLINK, + ).success; + results.workbenchlink_vncdesktop = PathString.safeParse( + window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, + ).success; + results.workbenchlink_vscode = PathString.safeParse( + window.env.REACT_APP_WORKBENCHLINK_VSCODE, + ).success; + results.workbenchlink_jupyterlab = PathString.safeParse( + window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, + ).success; + results.workbenchlink_jupyternotebook = PathString.safeParse( + window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, + ).success; + results.client_id = PathString.safeParse( + window.env.REACT_APP_CLIENT_ID, + ).success; + results.auth_authority = await urlRespondsWithOK( + window.env.REACT_APP_AUTH_AUTHORITY, + ) + .then(() => true) + .catch(() => false); + results.redirect_uri = await urlRespondsWithOK( + window.env.REACT_APP_REDIRECT_URI, + ) + .then(() => true) + .catch(() => false); + results.logout_redirect_uri = await urlRespondsWithOK( + window.env.REACT_APP_LOGOUT_REDIRECT_URI, + ) + .then(() => true) + .catch(() => false); + results.gitlab_scopes = ScopesString.safeParse( + window.env.REACT_APP_GITLAB_SCOPES, + ).success; + + setValidationResults(results); + } catch (error) { + throw new Error(error); + } + }; + + checkValidations(); + }, []); + + const ConfigItem = ({ + label, + value, + isValid, + }: { + label: string; + value: string; + isValid?: boolean; + }) => ( +
+ {isValid !== undefined && + (isValid ? ( + + + + ) : ( + + + + ))} +
+ {label}: {value} +
+
+ ); + + return ( + + Configuration Verification +
+ + + + + + + + + + + + + + +
+
+ ); +}; + +export default VerifyConfig; diff --git a/client/src/routes.tsx b/client/src/routes.tsx index 676481ae0..bc3b1f8d0 100644 --- a/client/src/routes.tsx +++ b/client/src/routes.tsx @@ -7,6 +7,7 @@ import DigitalTwins from './route/digitaltwins/DigitalTwins'; import DigitalTwinsPreview from './preview/route/digitaltwins/DigitalTwinsPreview'; import SignIn from './route/auth/Signin'; import Account from './route/auth/Account'; +import VerifyConfig from './route/auth/VerifyConfig'; export const routes = [ { @@ -17,6 +18,14 @@ export const routes = [ ), }, + { + path: 'verify', + element: ( + + + + ), + }, { path: 'library', element: ( From 89afd3a9d77e0b1a05979c14dcefc08503210558 Mon Sep 17 00:00:00 2001 From: atomicgamedev Date: Fri, 22 Nov 2024 21:16:07 +0100 Subject: [PATCH 2/8] Add edge-case for opaque responses --- client/src/route/auth/VerifyConfig.tsx | 168 +++++++++++++++---------- 1 file changed, 99 insertions(+), 69 deletions(-) diff --git a/client/src/route/auth/VerifyConfig.tsx b/client/src/route/auth/VerifyConfig.tsx index 9328cf69e..250bc6296 100644 --- a/client/src/route/auth/VerifyConfig.tsx +++ b/client/src/route/auth/VerifyConfig.tsx @@ -8,26 +8,67 @@ const EnvironmentEnum = z.enum(['dev', 'local', 'prod', 'test']); const PathString = z.string(); const ScopesString = z.literal('openid profile read_user read_repository api'); -async function urlRespondsWithOK(url: string): Promise { - // localhost is usually run insecurely so we let it pass - const insecurePattern = /^http:\/\//; - if (insecurePattern.test(url)) { - return url; - } - const cleanURL = url.replace(/^https?:\/\/(www.)?/, '').split('/')[0]; +type reachableUrlType = Promise<{ + url: string; + status: number | undefined; + error: string | undefined; +}>; +async function urlIsReachable(url: string): reachableUrlType { try { - const response = await fetch(cleanURL, { redirect: 'follow' }); - if (response.ok) { - return cleanURL; + const response = await fetch(url, { method: 'HEAD' }); + + if (response.ok || response.status === 302) { + return { url, status: response.status, error: undefined }; + } + return { + url, + status: response.status, + error: `$Unexpected response code ${response.status} from ${url}.`, + }; + } catch { + try { + const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' }); + if (response.type === 'opaque') { + return { url, status: response.status, error: undefined }; + } + return { + url, + status: response.status, + error: `$Unexpected response code ${response.status} from ${url}.`, + }; + } catch (error) { + return { + url, + status: undefined, + error: `$An error occured when fetching ${url}. ${error}`, + }; } - throw new Error(`${cleanURL} responded with ${response.status}`); - } catch (error) { - throw new Error( - `Error: Could not fetch ${cleanURL}. ${(error as Error).message}`, - ); } } +/* +async function urlIsReachable(url: string): Promise { + const cleanURL = url.replace(/^https:\/\/(www.)?/, '').split('/')[0]; + try { + console.log(`Checking ${url}`); + const response = await fetch(cleanURL); + if (!response.ok) { + throw new Error(`Response status: ${response.status}`); + } + if (response.ok || response.redirected) { + console.log(`Checking succeeded for ${url}`); + return cleanURL; + } + console.log(`Checking failed for ${url} code: ${response.status}`); + throw new Error(`${cleanURL} responded with ${response.status}`); + } catch (error) { + console.log(`Checking failed for ${url}.`); + throw new Error( + `Error: Could not fetch ${cleanURL}. ${(error as Error).message}`, + ); + } +} */ + const VerifyConfig = () => { const [validationResults, setValidationResults] = React.useState<{ [key: string]: boolean; @@ -37,60 +78,49 @@ const VerifyConfig = () => { React.useEffect(() => { const checkValidations = async () => { const results: { [key: string]: boolean } = {}; - try { - results.environment = EnvironmentEnum.safeParse( - window.env.REACT_APP_ENVIRONMENT, - ).success; - results.url = await urlRespondsWithOK(window.env.REACT_APP_URL) - .then(() => true) - .catch(() => false); - results.url_basename = PathString.safeParse( - window.env.REACT_APP_URL_BASENAME, - ).success; - results.url_dtlink = PathString.safeParse( - window.env.REACT_APP_URL_DTLINK, - ).success; - results.url_liblink = PathString.safeParse( - window.env.REACT_APP_URL_LIBLINK, - ).success; - results.workbenchlink_vncdesktop = PathString.safeParse( - window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, - ).success; - results.workbenchlink_vscode = PathString.safeParse( - window.env.REACT_APP_WORKBENCHLINK_VSCODE, - ).success; - results.workbenchlink_jupyterlab = PathString.safeParse( - window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, - ).success; - results.workbenchlink_jupyternotebook = PathString.safeParse( - window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, - ).success; - results.client_id = PathString.safeParse( - window.env.REACT_APP_CLIENT_ID, - ).success; - results.auth_authority = await urlRespondsWithOK( - window.env.REACT_APP_AUTH_AUTHORITY, - ) - .then(() => true) - .catch(() => false); - results.redirect_uri = await urlRespondsWithOK( - window.env.REACT_APP_REDIRECT_URI, - ) - .then(() => true) - .catch(() => false); - results.logout_redirect_uri = await urlRespondsWithOK( - window.env.REACT_APP_LOGOUT_REDIRECT_URI, - ) - .then(() => true) - .catch(() => false); - results.gitlab_scopes = ScopesString.safeParse( - window.env.REACT_APP_GITLAB_SCOPES, - ).success; - - setValidationResults(results); - } catch (error) { - throw new Error(error); - } + results.environment = EnvironmentEnum.safeParse( + window.env.REACT_APP_ENVIRONMENT, + ).success; + results.url = await urlIsReachable(window.env.REACT_APP_URL).then( + (response) => !response.error, + ); + results.url_basename = PathString.safeParse( + window.env.REACT_APP_URL_BASENAME, + ).success; + results.url_dtlink = PathString.safeParse( + window.env.REACT_APP_URL_DTLINK, + ).success; + results.url_liblink = PathString.safeParse( + window.env.REACT_APP_URL_LIBLINK, + ).success; + results.workbenchlink_vncdesktop = PathString.safeParse( + window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, + ).success; + results.workbenchlink_vscode = PathString.safeParse( + window.env.REACT_APP_WORKBENCHLINK_VSCODE, + ).success; + results.workbenchlink_jupyterlab = PathString.safeParse( + window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, + ).success; + results.workbenchlink_jupyternotebook = PathString.safeParse( + window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, + ).success; + results.client_id = PathString.safeParse( + window.env.REACT_APP_CLIENT_ID, + ).success; + results.auth_authority = await urlIsReachable( + window.env.REACT_APP_AUTH_AUTHORITY, + ).then((response) => !response.error); + results.redirect_uri = await urlIsReachable( + window.env.REACT_APP_REDIRECT_URI, + ).then((response) => !response.error); + results.logout_redirect_uri = await urlIsReachable( + window.env.REACT_APP_LOGOUT_REDIRECT_URI, + ).then((response) => !response.error); + results.gitlab_scopes = ScopesString.safeParse( + window.env.REACT_APP_GITLAB_SCOPES, + ).success; + setValidationResults(results); }; checkValidations(); From 740e8b3f5310e8999dc6e7d9950a35307b82153a Mon Sep 17 00:00:00 2001 From: atomicgamedev Date: Thu, 28 Nov 2024 23:15:33 +0100 Subject: [PATCH 3/8] Add status code and more useful error messages --- client/src/route/auth/VerifyConfig.tsx | 460 ++++++++++++------------- 1 file changed, 229 insertions(+), 231 deletions(-) diff --git a/client/src/route/auth/VerifyConfig.tsx b/client/src/route/auth/VerifyConfig.tsx index 250bc6296..a6a09a014 100644 --- a/client/src/route/auth/VerifyConfig.tsx +++ b/client/src/route/auth/VerifyConfig.tsx @@ -8,245 +8,243 @@ const EnvironmentEnum = z.enum(['dev', 'local', 'prod', 'test']); const PathString = z.string(); const ScopesString = z.literal('openid profile read_user read_repository api'); -type reachableUrlType = Promise<{ - url: string; - status: number | undefined; - error: string | undefined; -}>; -async function urlIsReachable(url: string): reachableUrlType { - try { - const response = await fetch(url, { method: 'HEAD' }); - - if (response.ok || response.status === 302) { - return { url, status: response.status, error: undefined }; - } - return { - url, - status: response.status, - error: `$Unexpected response code ${response.status} from ${url}.`, - }; - } catch { - try { - const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' }); - if (response.type === 'opaque') { - return { url, status: response.status, error: undefined }; - } - return { - url, - status: response.status, - error: `$Unexpected response code ${response.status} from ${url}.`, - }; - } catch (error) { - return { - url, - status: undefined, - error: `$An error occured when fetching ${url}. ${error}`, - }; - } - } -} +type validationType = { + value?: string; + status?: number; + error?: string; +}; -/* -async function urlIsReachable(url: string): Promise { - const cleanURL = url.replace(/^https:\/\/(www.)?/, '').split('/')[0]; +async function urlIsReachable(url: string): Promise { try { - console.log(`Checking ${url}`); - const response = await fetch(cleanURL); - if (!response.ok) { - throw new Error(`Response status: ${response.status}`); + const response = await fetch(url, { method: 'HEAD' }); + + if (response.ok || response.status === 302) { + return { value: url, status: response.status, error: undefined }; } - if (response.ok || response.redirected) { - console.log(`Checking succeeded for ${url}`); - return cleanURL; + return { + value: url, + status: response.status, + error: `Unexpected response code ${response.status} from ${url}.`, + }; + } catch { + try { + const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' }); + if (response.type === 'opaque') { + return { value: url, status: response.status, error: undefined }; + } + return { + value: url, + status: response.status, + error: `Unexpected response code ${response.status} from ${url}.`, + }; + } catch (error) { + return { + value: url, + status: undefined, + error: `An error occurred when fetching ${url}. ${error}`, + }; } - console.log(`Checking failed for ${url} code: ${response.status}`); - throw new Error(`${cleanURL} responded with ${response.status}`); - } catch (error) { - console.log(`Checking failed for ${url}.`); - throw new Error( - `Error: Could not fetch ${cleanURL}. ${(error as Error).message}`, - ); } -} */ +} const VerifyConfig = () => { - const [validationResults, setValidationResults] = React.useState<{ - [key: string]: boolean; - }>({}); - const root = document.getElementById('root'); - - React.useEffect(() => { - const checkValidations = async () => { - const results: { [key: string]: boolean } = {}; - results.environment = EnvironmentEnum.safeParse( - window.env.REACT_APP_ENVIRONMENT, - ).success; - results.url = await urlIsReachable(window.env.REACT_APP_URL).then( - (response) => !response.error, - ); - results.url_basename = PathString.safeParse( - window.env.REACT_APP_URL_BASENAME, - ).success; - results.url_dtlink = PathString.safeParse( - window.env.REACT_APP_URL_DTLINK, - ).success; - results.url_liblink = PathString.safeParse( - window.env.REACT_APP_URL_LIBLINK, - ).success; - results.workbenchlink_vncdesktop = PathString.safeParse( - window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, - ).success; - results.workbenchlink_vscode = PathString.safeParse( - window.env.REACT_APP_WORKBENCHLINK_VSCODE, - ).success; - results.workbenchlink_jupyterlab = PathString.safeParse( - window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, - ).success; - results.workbenchlink_jupyternotebook = PathString.safeParse( - window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, - ).success; - results.client_id = PathString.safeParse( - window.env.REACT_APP_CLIENT_ID, - ).success; - results.auth_authority = await urlIsReachable( - window.env.REACT_APP_AUTH_AUTHORITY, - ).then((response) => !response.error); - results.redirect_uri = await urlIsReachable( - window.env.REACT_APP_REDIRECT_URI, - ).then((response) => !response.error); - results.logout_redirect_uri = await urlIsReachable( - window.env.REACT_APP_LOGOUT_REDIRECT_URI, - ).then((response) => !response.error); - results.gitlab_scopes = ScopesString.safeParse( - window.env.REACT_APP_GITLAB_SCOPES, - ).success; - setValidationResults(results); + const [validationResults, setValidationResults] = React.useState<{ + [key: string]: validationType; + }>({}); + + React.useEffect(() => { + const checkValidations = async () => { + const results: { [key: string]: validationType } = {}; + results.environment = EnvironmentEnum.safeParse( + window.env.REACT_APP_ENVIRONMENT + ).success + ? { value: window.env.REACT_APP_ENVIRONMENT, error: undefined } + : { value: undefined, error: EnvironmentEnum.safeParse(window.env.REACT_APP_ENVIRONMENT).error?.message }; + + results.url = await urlIsReachable(window.env.REACT_APP_URL); + results.url_basename = PathString.safeParse( + window.env.REACT_APP_URL_BASENAME + ).success + ? { value: window.env.REACT_APP_URL_BASENAME, error: undefined } + : { value: undefined, error: PathString.safeParse(window.env.REACT_APP_URL_BASENAME).error?.message }; + + results.url_dtlink = PathString.safeParse( + window.env.REACT_APP_URL_DTLINK + ).success + ? { value: window.env.REACT_APP_URL_DTLINK, error: undefined } + : { value: undefined, error: PathString.safeParse(window.env.REACT_APP_URL_DTLINK).error?.message }; + + results.url_liblink = PathString.safeParse( + window.env.REACT_APP_URL_LIBLINK + ).success + ? { value: window.env.REACT_APP_URL_LIBLINK, error: undefined } + : { value: undefined, error: PathString.safeParse(window.env.REACT_APP_URL_LIBLINK).error?.message }; + + results.workbenchlink_vncdesktop = PathString.safeParse( + window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP + ).success + ? { value: window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, error: undefined } + : { value: undefined, error: PathString.safeParse(window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP).error?.message }; + + results.workbenchlink_vscode = PathString.safeParse( + window.env.REACT_APP_WORKBENCHLINK_VSCODE + ).success + ? { value: window.env.REACT_APP_WORKBENCHLINK_VSCODE, error: undefined } + : { value: undefined, error: PathString.safeParse(window.env.REACT_APP_WORKBENCHLINK_VSCODE).error?.message }; + + results.workbenchlink_jupyterlab = PathString.safeParse( + window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB + ).success + ? { value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, error: undefined } + : { value: undefined, error: PathString.safeParse(window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB).error?.message }; + + results.workbenchlink_jupyternotebook = PathString.safeParse( + window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK + ).success + ? { value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, error: undefined } + : { value: undefined, error: PathString.safeParse(window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK).error?.message }; + + results.client_id = PathString.safeParse( + window.env.REACT_APP_CLIENT_ID + ).success + ? { value: window.env.REACT_APP_CLIENT_ID, error: undefined } + : { value: undefined, error: PathString.safeParse(window.env.REACT_APP_CLIENT_ID).error?.message }; + + results.auth_authority = await urlIsReachable( + window.env.REACT_APP_AUTH_AUTHORITY + ); + + results.redirect_uri = await urlIsReachable( + window.env.REACT_APP_REDIRECT_URI + ); + + results.logout_redirect_uri = await urlIsReachable( + window.env.REACT_APP_LOGOUT_REDIRECT_URI + ); + + results.gitlab_scopes = ScopesString.safeParse( + window.env.REACT_APP_GITLAB_SCOPES + ).success + ? { value: window.env.REACT_APP_GITLAB_SCOPES, error: undefined } + : { value: undefined, error: ScopesString.safeParse(window.env.REACT_APP_GITLAB_SCOPES).error?.message }; + + setValidationResults(results); + }; + checkValidations(); + }, []); + + const getIcon = (validation: validationType, label: string, root: HTMLElement | null) => { + if (!validation.error) { + const title = `${label} field is configured correctly. ${validation.status !== undefined ? `${validation.value} responded with status code ${validation.status}.` : ``}`; + return validation.status === undefined || (validation.status >= 200 && validation.status < 300) + ? + : ; + } + return ; }; - checkValidations(); - }, []); - - const ConfigItem = ({ - label, - value, - isValid, - }: { - label: string; - value: string; - isValid?: boolean; - }) => ( -
- {isValid !== undefined && - (isValid ? ( - - - - ) : ( - - - - ))} -
- {label}: {value} -
-
- ); - - return ( - - Configuration Verification -
- - - - - - - - - - - - - - -
-
- ); + const ConfigItem = React.memo(({ label, value, validation = { error: 'Validation not available' } }: { + label: string; + value: string; + validation?: validationType; + }) => { + const root = document.getElementById('root'); + return ( +
+ {getIcon(validation, label, root)} +
+ {label}: {value} +
+
+ ); + }); + ConfigItem.displayName = 'ConfigItem'; + + return ( + + Configuration Verification +
+ + + + + + + + + + + + + + +
+
+ ); }; -export default VerifyConfig; +export default VerifyConfig; \ No newline at end of file From 50928247693929bfe138f44b1bd6c735ade8de82 Mon Sep 17 00:00:00 2001 From: atomicgamedev Date: Fri, 29 Nov 2024 11:10:54 +0100 Subject: [PATCH 4/8] Refactor VerifyConfig --- client/src/route/auth/VerifyConfig.tsx | 450 +++++++++++++------------ 1 file changed, 228 insertions(+), 222 deletions(-) diff --git a/client/src/route/auth/VerifyConfig.tsx b/client/src/route/auth/VerifyConfig.tsx index a6a09a014..f6943fe70 100644 --- a/client/src/route/auth/VerifyConfig.tsx +++ b/client/src/route/auth/VerifyConfig.tsx @@ -9,242 +9,248 @@ const PathString = z.string(); const ScopesString = z.literal('openid profile read_user read_repository api'); type validationType = { - value?: string; - status?: number; - error?: string; + value?: string; + status?: number; + error?: string; }; async function urlIsReachable(url: string): Promise { - try { - const response = await fetch(url, { method: 'HEAD' }); + try { + const response = await fetch(url, { method: 'HEAD' }); - if (response.ok || response.status === 302) { - return { value: url, status: response.status, error: undefined }; - } - return { - value: url, - status: response.status, - error: `Unexpected response code ${response.status} from ${url}.`, - }; - } catch { - try { - const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' }); - if (response.type === 'opaque') { - return { value: url, status: response.status, error: undefined }; - } - return { - value: url, - status: response.status, - error: `Unexpected response code ${response.status} from ${url}.`, - }; - } catch (error) { - return { - value: url, - status: undefined, - error: `An error occurred when fetching ${url}. ${error}`, - }; - } + if (response.ok || response.status === 302) { + return { value: url, status: response.status, error: undefined }; } + return { + value: url, + status: response.status, + error: `Unexpected response code ${response.status} from ${url}.`, + }; + } catch { + try { + const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' }); + if (response.type === 'opaque') { + return { value: url, status: response.status, error: undefined }; + } + return { + value: url, + status: response.status, + error: `Unexpected response code ${response.status} from ${url}.`, + }; + } catch (error) { + return { + value: url, + status: undefined, + error: `An error occurred when fetching ${url}. ${error}`, + }; + } + } } -const VerifyConfig = () => { - const [validationResults, setValidationResults] = React.useState<{ - [key: string]: validationType; - }>({}); - - React.useEffect(() => { - const checkValidations = async () => { - const results: { [key: string]: validationType } = {}; - results.environment = EnvironmentEnum.safeParse( - window.env.REACT_APP_ENVIRONMENT - ).success - ? { value: window.env.REACT_APP_ENVIRONMENT, error: undefined } - : { value: undefined, error: EnvironmentEnum.safeParse(window.env.REACT_APP_ENVIRONMENT).error?.message }; - - results.url = await urlIsReachable(window.env.REACT_APP_URL); - results.url_basename = PathString.safeParse( - window.env.REACT_APP_URL_BASENAME - ).success - ? { value: window.env.REACT_APP_URL_BASENAME, error: undefined } - : { value: undefined, error: PathString.safeParse(window.env.REACT_APP_URL_BASENAME).error?.message }; - - results.url_dtlink = PathString.safeParse( - window.env.REACT_APP_URL_DTLINK - ).success - ? { value: window.env.REACT_APP_URL_DTLINK, error: undefined } - : { value: undefined, error: PathString.safeParse(window.env.REACT_APP_URL_DTLINK).error?.message }; - - results.url_liblink = PathString.safeParse( - window.env.REACT_APP_URL_LIBLINK - ).success - ? { value: window.env.REACT_APP_URL_LIBLINK, error: undefined } - : { value: undefined, error: PathString.safeParse(window.env.REACT_APP_URL_LIBLINK).error?.message }; - - results.workbenchlink_vncdesktop = PathString.safeParse( - window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP - ).success - ? { value: window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, error: undefined } - : { value: undefined, error: PathString.safeParse(window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP).error?.message }; - - results.workbenchlink_vscode = PathString.safeParse( - window.env.REACT_APP_WORKBENCHLINK_VSCODE - ).success - ? { value: window.env.REACT_APP_WORKBENCHLINK_VSCODE, error: undefined } - : { value: undefined, error: PathString.safeParse(window.env.REACT_APP_WORKBENCHLINK_VSCODE).error?.message }; - - results.workbenchlink_jupyterlab = PathString.safeParse( - window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB - ).success - ? { value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, error: undefined } - : { value: undefined, error: PathString.safeParse(window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB).error?.message }; - - results.workbenchlink_jupyternotebook = PathString.safeParse( - window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK - ).success - ? { value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, error: undefined } - : { value: undefined, error: PathString.safeParse(window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK).error?.message }; - - results.client_id = PathString.safeParse( - window.env.REACT_APP_CLIENT_ID - ).success - ? { value: window.env.REACT_APP_CLIENT_ID, error: undefined } - : { value: undefined, error: PathString.safeParse(window.env.REACT_APP_CLIENT_ID).error?.message }; - - results.auth_authority = await urlIsReachable( - window.env.REACT_APP_AUTH_AUTHORITY - ); +const parseField = ( + parser: { + safeParse: (value: string) => { + success: boolean; + error?: { message?: string }; + }; + }, + value: string, +): validationType => { + const result = parser.safeParse(value); + return result.success + ? { value, error: undefined } + : { value: undefined, error: result.error?.message }; +}; - results.redirect_uri = await urlIsReachable( - window.env.REACT_APP_REDIRECT_URI - ); +const getValidationResults = async (): Promise<{ + [key: string]: validationType; +}> => { + const results: { [key: string]: validationType } = { + environment: parseField(EnvironmentEnum, window.env.REACT_APP_ENVIRONMENT), + url: await urlIsReachable(window.env.REACT_APP_URL), + url_basename: parseField(PathString, window.env.REACT_APP_URL_BASENAME), + url_dtlink: parseField(PathString, window.env.REACT_APP_URL_DTLINK), + url_liblink: parseField(PathString, window.env.REACT_APP_URL_LIBLINK), + workbenchlink_vncdesktop: parseField( + PathString, + window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, + ), + workbenchlink_vscode: parseField( + PathString, + window.env.REACT_APP_WORKBENCHLINK_VSCODE, + ), + workbenchlink_jupyterlab: parseField( + PathString, + window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, + ), + workbenchlink_jupyternotebook: parseField( + PathString, + window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, + ), + client_id: parseField(PathString, window.env.REACT_APP_CLIENT_ID), + auth_authority: await urlIsReachable(window.env.REACT_APP_AUTH_AUTHORITY), + redirect_uri: await urlIsReachable(window.env.REACT_APP_REDIRECT_URI), + logout_redirect_uri: await urlIsReachable( + window.env.REACT_APP_LOGOUT_REDIRECT_URI, + ), + gitlab_scopes: parseField(ScopesString, window.env.REACT_APP_GITLAB_SCOPES), + }; + return results; +}; - results.logout_redirect_uri = await urlIsReachable( - window.env.REACT_APP_LOGOUT_REDIRECT_URI - ); +const configIcon = (validation: validationType, label: string): JSX.Element => { + const root = document.getElementById('root'); + if (!validation.error) { + const title = `${label} field is configured correctly. ${validation.status !== undefined ? `${validation.value} responded with status code ${validation.status}.` : ``}`; + return validation.status === undefined || + (validation.status >= 200 && validation.status < 300) ? ( + + + + ) : ( + + + + ); + } + return ( + + + + ); +}; - results.gitlab_scopes = ScopesString.safeParse( - window.env.REACT_APP_GITLAB_SCOPES - ).success - ? { value: window.env.REACT_APP_GITLAB_SCOPES, error: undefined } - : { value: undefined, error: ScopesString.safeParse(window.env.REACT_APP_GITLAB_SCOPES).error?.message }; +const ConfigItem: React.FC<{ + label: string; + value: string; + validation?: validationType; +}> = React.memo( + ({ label, value, validation = { error: 'Validation is not available' } }) => ( +
+ {configIcon(validation, label)} +
+ {label}: {value} +
+
+ ), +); +ConfigItem.displayName = 'ConfigItem'; - setValidationResults(results); - }; - checkValidations(); - }, []); +const VerifyConfig: React.FC = () => { + const [validationResults, setValidationResults] = React.useState<{ + [key: string]: validationType; + }>({}); - const getIcon = (validation: validationType, label: string, root: HTMLElement | null) => { - if (!validation.error) { - const title = `${label} field is configured correctly. ${validation.status !== undefined ? `${validation.value} responded with status code ${validation.status}.` : ``}`; - return validation.status === undefined || (validation.status >= 200 && validation.status < 300) - ? - : ; - } - return ; + React.useEffect(() => { + const fetchValidations = async () => { + const results = await getValidationResults(); + setValidationResults(results); }; + fetchValidations(); + }, []); - const ConfigItem = React.memo(({ label, value, validation = { error: 'Validation not available' } }: { - label: string; - value: string; - validation?: validationType; - }) => { - const root = document.getElementById('root'); - return ( -
- {getIcon(validation, label, root)} -
- {label}: {value} -
-
- ); - }); - ConfigItem.displayName = 'ConfigItem'; + const configItems = [ + { + label: 'APP ENVIRONMENT', + value: window.env.REACT_APP_ENVIRONMENT, + key: 'environment', + }, + { label: 'APP URL', value: window.env.REACT_APP_URL, key: 'url' }, + { + label: 'APP URL BASENAME', + value: window.env.REACT_APP_URL_BASENAME, + key: 'url_basename', + }, + { + label: 'APP URL DTLINK', + value: window.env.REACT_APP_URL_DTLINK, + key: 'url_dtlink', + }, + { + label: 'APP URL LIBLINK', + value: window.env.REACT_APP_URL_LIBLINK, + key: 'url_liblink', + }, + { + label: 'WORKBENCHLINK VNCDESKTOP', + value: window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, + key: 'workbenchlink_vncdesktop', + }, + { + label: 'WORKBENCHLINK VSCODE', + value: window.env.REACT_APP_WORKBENCHLINK_VSCODE, + key: 'workbenchlink_vscode', + }, + { + label: 'WORKBENCHLINK JUPYTERLAB', + value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, + key: 'workbenchlink_jupyterlab', + }, + { + label: 'WORKBENCHLINK JUPYTERNOTEBOOK', + value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, + key: 'workbenchlink_jupyternotebook', + }, + { + label: 'CLIENT ID', + value: window.env.REACT_APP_CLIENT_ID, + key: 'client_id', + }, + { + label: 'AUTH AUTHORITY', + value: window.env.REACT_APP_AUTH_AUTHORITY, + key: 'auth_authority', + }, + { + label: 'REDIRECT URI', + value: window.env.REACT_APP_REDIRECT_URI, + key: 'redirect_uri', + }, + { + label: 'LOGOUT REDIRECT URI', + value: window.env.REACT_APP_LOGOUT_REDIRECT_URI, + key: 'logout_redirect_uri', + }, + { + label: 'GITLAB SCOPES', + value: window.env.REACT_APP_GITLAB_SCOPES, + key: 'gitlab_scopes', + }, + ]; - return ( - - Configuration Verification -
- - - - - - - - - - - - - - -
-
- ); + return ( + + Configuration Verification +
+ {configItems.map(({ label, value, key }) => ( + + ))} +
+
+ ); }; -export default VerifyConfig; \ No newline at end of file +export default VerifyConfig; From 6976bbf08608aa7592fc3d6ec6c9da2ca7183a8c Mon Sep 17 00:00:00 2001 From: atomicgamedev Date: Fri, 29 Nov 2024 18:01:18 +0100 Subject: [PATCH 5/8] Reinstate status code information in tool tips --- client/src/route/auth/VerifyConfig.tsx | 428 ++++++++++++------------- 1 file changed, 214 insertions(+), 214 deletions(-) diff --git a/client/src/route/auth/VerifyConfig.tsx b/client/src/route/auth/VerifyConfig.tsx index f6943fe70..b4ed6d17b 100644 --- a/client/src/route/auth/VerifyConfig.tsx +++ b/client/src/route/auth/VerifyConfig.tsx @@ -9,248 +9,248 @@ const PathString = z.string(); const ScopesString = z.literal('openid profile read_user read_repository api'); type validationType = { - value?: string; - status?: number; - error?: string; + value?: string; + status?: number; + error?: string; }; async function urlIsReachable(url: string): Promise { - try { - const response = await fetch(url, { method: 'HEAD' }); - - if (response.ok || response.status === 302) { - return { value: url, status: response.status, error: undefined }; - } - return { - value: url, - status: response.status, - error: `Unexpected response code ${response.status} from ${url}.`, - }; - } catch { try { - const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' }); - if (response.type === 'opaque') { - return { value: url, status: response.status, error: undefined }; - } - return { - value: url, - status: response.status, - error: `Unexpected response code ${response.status} from ${url}.`, - }; - } catch (error) { - return { - value: url, - status: undefined, - error: `An error occurred when fetching ${url}. ${error}`, - }; + const response = await fetch(url, { method: 'HEAD' }); + + if (response.ok || response.status === 302) { + return { value: url, status: response.status, error: undefined }; + } + return { + value: url, + status: response.status, + error: `Unexpected response code ${response.status} from ${url}.`, + }; + } catch { + try { + const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' }); + if (response.type === 'opaque') { + return { value: url, status: response.status, error: undefined }; + } + return { + value: url, + status: response.status, + error: `Unexpected response code ${response.status} from ${url}.`, + }; + } catch (error) { + return { + value: url, + status: undefined, + error: `An error occurred when fetching ${url}. ${error}`, + }; + } } - } } const parseField = ( - parser: { - safeParse: (value: string) => { - success: boolean; - error?: { message?: string }; - }; - }, - value: string, + parser: { + safeParse: (value: string) => { + success: boolean; + error?: { message?: string }; + }; + }, + value: string, ): validationType => { - const result = parser.safeParse(value); - return result.success - ? { value, error: undefined } - : { value: undefined, error: result.error?.message }; + const result = parser.safeParse(value); + return result.success + ? { value, error: undefined } + : { value: undefined, error: result.error?.message }; }; const getValidationResults = async (): Promise<{ - [key: string]: validationType; + [key: string]: validationType; }> => { - const results: { [key: string]: validationType } = { - environment: parseField(EnvironmentEnum, window.env.REACT_APP_ENVIRONMENT), - url: await urlIsReachable(window.env.REACT_APP_URL), - url_basename: parseField(PathString, window.env.REACT_APP_URL_BASENAME), - url_dtlink: parseField(PathString, window.env.REACT_APP_URL_DTLINK), - url_liblink: parseField(PathString, window.env.REACT_APP_URL_LIBLINK), - workbenchlink_vncdesktop: parseField( - PathString, - window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, - ), - workbenchlink_vscode: parseField( - PathString, - window.env.REACT_APP_WORKBENCHLINK_VSCODE, - ), - workbenchlink_jupyterlab: parseField( - PathString, - window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, - ), - workbenchlink_jupyternotebook: parseField( - PathString, - window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, - ), - client_id: parseField(PathString, window.env.REACT_APP_CLIENT_ID), - auth_authority: await urlIsReachable(window.env.REACT_APP_AUTH_AUTHORITY), - redirect_uri: await urlIsReachable(window.env.REACT_APP_REDIRECT_URI), - logout_redirect_uri: await urlIsReachable( - window.env.REACT_APP_LOGOUT_REDIRECT_URI, - ), - gitlab_scopes: parseField(ScopesString, window.env.REACT_APP_GITLAB_SCOPES), - }; - return results; + const results: { [key: string]: validationType } = { + environment: parseField(EnvironmentEnum, window.env.REACT_APP_ENVIRONMENT), + url: await urlIsReachable(window.env.REACT_APP_URL), + url_basename: parseField(PathString, window.env.REACT_APP_URL_BASENAME), + url_dtlink: parseField(PathString, window.env.REACT_APP_URL_DTLINK), + url_liblink: parseField(PathString, window.env.REACT_APP_URL_LIBLINK), + workbenchlink_vncdesktop: parseField( + PathString, + window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, + ), + workbenchlink_vscode: parseField( + PathString, + window.env.REACT_APP_WORKBENCHLINK_VSCODE, + ), + workbenchlink_jupyterlab: parseField( + PathString, + window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, + ), + workbenchlink_jupyternotebook: parseField( + PathString, + window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, + ), + client_id: parseField(PathString, window.env.REACT_APP_CLIENT_ID), + auth_authority: await urlIsReachable(window.env.REACT_APP_AUTH_AUTHORITY), + redirect_uri: await urlIsReachable(window.env.REACT_APP_REDIRECT_URI), + logout_redirect_uri: await urlIsReachable( + window.env.REACT_APP_LOGOUT_REDIRECT_URI, + ), + gitlab_scopes: parseField(ScopesString, window.env.REACT_APP_GITLAB_SCOPES), + }; + return results; }; const configIcon = (validation: validationType, label: string): JSX.Element => { - const root = document.getElementById('root'); - if (!validation.error) { - const title = `${label} field is configured correctly. ${validation.status !== undefined ? `${validation.value} responded with status code ${validation.status}.` : ``}`; - return validation.status === undefined || - (validation.status >= 200 && validation.status < 300) ? ( - - - - ) : ( - - - + const root = document.getElementById('root'); + if (!validation.error) { + const statusMessage = ` ${validation.status !== undefined ? `${validation.value} responded with status code ${validation.status}.` : ``}`; + return validation.status === undefined || + (validation.status >= 200 && validation.status < 300) ? ( + + + + ) : ( + + + + ); + } + return ( + + + ); - } - return ( - - - - ); }; const ConfigItem: React.FC<{ - label: string; - value: string; - validation?: validationType; + label: string; + value: string; + validation?: validationType; }> = React.memo( - ({ label, value, validation = { error: 'Validation is not available' } }) => ( -
- {configIcon(validation, label)} -
- {label}: {value} -
-
- ), + ({ label, value, validation = { error: 'Validation is not available' } }) => ( +
+ {configIcon(validation, label)} +
+ {label}: {value} +
+
+ ), ); ConfigItem.displayName = 'ConfigItem'; const VerifyConfig: React.FC = () => { - const [validationResults, setValidationResults] = React.useState<{ - [key: string]: validationType; - }>({}); + const [validationResults, setValidationResults] = React.useState<{ + [key: string]: validationType; + }>({}); - React.useEffect(() => { - const fetchValidations = async () => { - const results = await getValidationResults(); - setValidationResults(results); - }; - fetchValidations(); - }, []); + React.useEffect(() => { + const fetchValidations = async () => { + const results = await getValidationResults(); + setValidationResults(results); + }; + fetchValidations(); + }, []); - const configItems = [ - { - label: 'APP ENVIRONMENT', - value: window.env.REACT_APP_ENVIRONMENT, - key: 'environment', - }, - { label: 'APP URL', value: window.env.REACT_APP_URL, key: 'url' }, - { - label: 'APP URL BASENAME', - value: window.env.REACT_APP_URL_BASENAME, - key: 'url_basename', - }, - { - label: 'APP URL DTLINK', - value: window.env.REACT_APP_URL_DTLINK, - key: 'url_dtlink', - }, - { - label: 'APP URL LIBLINK', - value: window.env.REACT_APP_URL_LIBLINK, - key: 'url_liblink', - }, - { - label: 'WORKBENCHLINK VNCDESKTOP', - value: window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, - key: 'workbenchlink_vncdesktop', - }, - { - label: 'WORKBENCHLINK VSCODE', - value: window.env.REACT_APP_WORKBENCHLINK_VSCODE, - key: 'workbenchlink_vscode', - }, - { - label: 'WORKBENCHLINK JUPYTERLAB', - value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, - key: 'workbenchlink_jupyterlab', - }, - { - label: 'WORKBENCHLINK JUPYTERNOTEBOOK', - value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, - key: 'workbenchlink_jupyternotebook', - }, - { - label: 'CLIENT ID', - value: window.env.REACT_APP_CLIENT_ID, - key: 'client_id', - }, - { - label: 'AUTH AUTHORITY', - value: window.env.REACT_APP_AUTH_AUTHORITY, - key: 'auth_authority', - }, - { - label: 'REDIRECT URI', - value: window.env.REACT_APP_REDIRECT_URI, - key: 'redirect_uri', - }, - { - label: 'LOGOUT REDIRECT URI', - value: window.env.REACT_APP_LOGOUT_REDIRECT_URI, - key: 'logout_redirect_uri', - }, - { - label: 'GITLAB SCOPES', - value: window.env.REACT_APP_GITLAB_SCOPES, - key: 'gitlab_scopes', - }, - ]; + const configItems = [ + { + label: 'APP ENVIRONMENT', + value: window.env.REACT_APP_ENVIRONMENT, + key: 'environment', + }, + { label: 'APP URL', value: window.env.REACT_APP_URL, key: 'url' }, + { + label: 'APP URL BASENAME', + value: window.env.REACT_APP_URL_BASENAME, + key: 'url_basename', + }, + { + label: 'APP URL DTLINK', + value: window.env.REACT_APP_URL_DTLINK, + key: 'url_dtlink', + }, + { + label: 'APP URL LIBLINK', + value: window.env.REACT_APP_URL_LIBLINK, + key: 'url_liblink', + }, + { + label: 'WORKBENCHLINK VNCDESKTOP', + value: window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, + key: 'workbenchlink_vncdesktop', + }, + { + label: 'WORKBENCHLINK VSCODE', + value: window.env.REACT_APP_WORKBENCHLINK_VSCODE, + key: 'workbenchlink_vscode', + }, + { + label: 'WORKBENCHLINK JUPYTERLAB', + value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, + key: 'workbenchlink_jupyterlab', + }, + { + label: 'WORKBENCHLINK JUPYTERNOTEBOOK', + value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, + key: 'workbenchlink_jupyternotebook', + }, + { + label: 'CLIENT ID', + value: window.env.REACT_APP_CLIENT_ID, + key: 'client_id', + }, + { + label: 'AUTH AUTHORITY', + value: window.env.REACT_APP_AUTH_AUTHORITY, + key: 'auth_authority', + }, + { + label: 'REDIRECT URI', + value: window.env.REACT_APP_REDIRECT_URI, + key: 'redirect_uri', + }, + { + label: 'LOGOUT REDIRECT URI', + value: window.env.REACT_APP_LOGOUT_REDIRECT_URI, + key: 'logout_redirect_uri', + }, + { + label: 'GITLAB SCOPES', + value: window.env.REACT_APP_GITLAB_SCOPES, + key: 'gitlab_scopes', + }, + ]; - return ( - - Configuration Verification -
- {configItems.map(({ label, value, key }) => ( - - ))} -
-
- ); + return ( + + Configuration Verification +
+ {configItems.map(({ label, value, key }) => ( + + ))} +
+
+ ); }; export default VerifyConfig; From 3260e98c5cfc60973cd0cfb9ee5e911a2bc984d3 Mon Sep 17 00:00:00 2001 From: atomicgamedev Date: Fri, 29 Nov 2024 23:55:40 +0100 Subject: [PATCH 6/8] Integrate verification and sign in --- client/src/route/auth/Signin.tsx | 55 ++- client/src/route/auth/VerifyConfig.tsx | 455 +++++++++++++------------ 2 files changed, 293 insertions(+), 217 deletions(-) diff --git a/client/src/route/auth/Signin.tsx b/client/src/route/auth/Signin.tsx index a113ab265..46c9ee1c6 100644 --- a/client/src/route/auth/Signin.tsx +++ b/client/src/route/auth/Signin.tsx @@ -2,17 +2,70 @@ import * as React from 'react'; import Avatar from '@mui/material/Avatar'; import Box from '@mui/material/Box'; import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; - import { useAuth } from 'react-oidc-context'; import Button from '@mui/material/Button'; +import { useState } from 'react'; +import { CircularProgress } from '@mui/material'; +import VerifyConfig, { + getValidationResults, + validationType, +} from './VerifyConfig'; function SignIn() { const auth = useAuth(); + const [validationResults, setValidationResults] = useState<{ + [key: string]: validationType; + }>({}); + const [isLoading, setIsLoading] = useState(true); + const configsToVerify = [ + 'url', + 'auth_authority', + 'redirect_uri', + 'logout_redirect_uri', + ]; + const verifyConfig = VerifyConfig({ + keys: configsToVerify, + title: 'Config validation failed', + }); + + React.useEffect(() => { + const fetchValidationResults = async () => { + const results = await getValidationResults(); + setValidationResults(results); + setIsLoading(false); + }; + fetchValidationResults(); + }, []); const startAuthProcess = () => { auth.signinRedirect(); }; + if (isLoading) { + return ( + + Verifying configuration + + + ); + } + if ( + configsToVerify.reduce( + (accumulator, currentValue) => + accumulator || validationResults[currentValue].error !== undefined, + false, + ) + ) { + return verifyConfig; + } + return ( { - try { - const response = await fetch(url, { method: 'HEAD' }); + try { + const response = await fetch(url, { method: 'HEAD' }); - if (response.ok || response.status === 302) { - return { value: url, status: response.status, error: undefined }; - } - return { - value: url, - status: response.status, - error: `Unexpected response code ${response.status} from ${url}.`, - }; - } catch { - try { - const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' }); - if (response.type === 'opaque') { - return { value: url, status: response.status, error: undefined }; - } - return { - value: url, - status: response.status, - error: `Unexpected response code ${response.status} from ${url}.`, - }; - } catch (error) { - return { - value: url, - status: undefined, - error: `An error occurred when fetching ${url}. ${error}`, - }; - } + if (response.ok || response.status === 302) { + return { value: url, status: response.status, error: undefined }; } + return { + value: url, + status: response.status, + error: `Unexpected response code ${response.status} from ${url}.`, + }; + } catch { + try { + const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' }); + if (response.type === 'opaque') { + return { value: url, status: response.status, error: undefined }; + } + return { + value: url, + status: response.status, + error: `Unexpected response code ${response.status} from ${url}.`, + }; + } catch (error) { + return { + value: url, + status: undefined, + error: `An error occurred when fetching ${url}. ${error}`, + }; + } + } } const parseField = ( - parser: { - safeParse: (value: string) => { - success: boolean; - error?: { message?: string }; - }; - }, - value: string, + parser: { + safeParse: (value: string) => { + success: boolean; + error?: { message?: string }; + }; + }, + value: string, ): validationType => { - const result = parser.safeParse(value); - return result.success - ? { value, error: undefined } - : { value: undefined, error: result.error?.message }; + const result = parser.safeParse(value); + return result.success + ? { value, error: undefined } + : { value: undefined, error: result.error?.message }; }; -const getValidationResults = async (): Promise<{ - [key: string]: validationType; +export const getValidationResults = async (): Promise<{ + [key: string]: validationType; }> => { - const results: { [key: string]: validationType } = { - environment: parseField(EnvironmentEnum, window.env.REACT_APP_ENVIRONMENT), - url: await urlIsReachable(window.env.REACT_APP_URL), - url_basename: parseField(PathString, window.env.REACT_APP_URL_BASENAME), - url_dtlink: parseField(PathString, window.env.REACT_APP_URL_DTLINK), - url_liblink: parseField(PathString, window.env.REACT_APP_URL_LIBLINK), - workbenchlink_vncdesktop: parseField( - PathString, - window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, - ), - workbenchlink_vscode: parseField( - PathString, - window.env.REACT_APP_WORKBENCHLINK_VSCODE, - ), - workbenchlink_jupyterlab: parseField( - PathString, - window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, - ), - workbenchlink_jupyternotebook: parseField( - PathString, - window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, - ), - client_id: parseField(PathString, window.env.REACT_APP_CLIENT_ID), - auth_authority: await urlIsReachable(window.env.REACT_APP_AUTH_AUTHORITY), - redirect_uri: await urlIsReachable(window.env.REACT_APP_REDIRECT_URI), - logout_redirect_uri: await urlIsReachable( - window.env.REACT_APP_LOGOUT_REDIRECT_URI, - ), - gitlab_scopes: parseField(ScopesString, window.env.REACT_APP_GITLAB_SCOPES), - }; - return results; + const results: { [key: string]: validationType } = { + environment: parseField(EnvironmentEnum, window.env.REACT_APP_ENVIRONMENT), + url: await urlIsReachable(window.env.REACT_APP_URL), + url_basename: parseField(PathString, window.env.REACT_APP_URL_BASENAME), + url_dtlink: parseField(PathString, window.env.REACT_APP_URL_DTLINK), + url_liblink: parseField(PathString, window.env.REACT_APP_URL_LIBLINK), + workbenchlink_vncdesktop: parseField( + PathString, + window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, + ), + workbenchlink_vscode: parseField( + PathString, + window.env.REACT_APP_WORKBENCHLINK_VSCODE, + ), + workbenchlink_jupyterlab: parseField( + PathString, + window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, + ), + workbenchlink_jupyternotebook: parseField( + PathString, + window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, + ), + client_id: parseField(PathString, window.env.REACT_APP_CLIENT_ID), + auth_authority: await urlIsReachable(window.env.REACT_APP_AUTH_AUTHORITY), + redirect_uri: await urlIsReachable(window.env.REACT_APP_REDIRECT_URI), + logout_redirect_uri: await urlIsReachable( + window.env.REACT_APP_LOGOUT_REDIRECT_URI, + ), + gitlab_scopes: parseField(ScopesString, window.env.REACT_APP_GITLAB_SCOPES), + }; + return results; }; const configIcon = (validation: validationType, label: string): JSX.Element => { - const root = document.getElementById('root'); - if (!validation.error) { - const statusMessage = ` ${validation.status !== undefined ? `${validation.value} responded with status code ${validation.status}.` : ``}`; - return validation.status === undefined || - (validation.status >= 200 && validation.status < 300) ? ( - - - - ) : ( - - - - ); - } + const root = document.getElementById('root'); + if (validation.error === 'Validation is not available') { return ( - - - + + + ); + } + if (!validation.error) { + const statusMessage = ` ${validation.status !== undefined ? `${validation.value} responded with status code ${validation.status}.` : ``}`; + return validation.status === undefined || + (validation.status >= 200 && validation.status < 300) ? ( + + + + ) : ( + + + + ); + } + return ( + + + + ); }; const ConfigItem: React.FC<{ - label: string; - value: string; - validation?: validationType; + label: string; + value: string; + validation?: validationType; }> = React.memo( - ({ label, value, validation = { error: 'Validation is not available' } }) => ( -
- {configIcon(validation, label)} -
- {label}: {value} -
-
- ), + ({ label, value, validation = { error: 'Validation is not available' } }) => ( +
+ {configIcon(validation, label)} +
+ {label}: {value} +
+
+ ), ); ConfigItem.displayName = 'ConfigItem'; -const VerifyConfig: React.FC = () => { - const [validationResults, setValidationResults] = React.useState<{ - [key: string]: validationType; - }>({}); +const configItems: { label: string; value: string; key: string }[] = [ + { + label: 'APP ENVIRONMENT', + value: window.env.REACT_APP_ENVIRONMENT, + key: 'environment', + }, + { label: 'APP URL', value: window.env.REACT_APP_URL, key: 'url' }, + { + label: 'APP URL BASENAME', + value: window.env.REACT_APP_URL_BASENAME, + key: 'url_basename', + }, + { + label: 'APP URL DTLINK', + value: window.env.REACT_APP_URL_DTLINK, + key: 'url_dtlink', + }, + { + label: 'APP URL LIBLINK', + value: window.env.REACT_APP_URL_LIBLINK, + key: 'url_liblink', + }, + { + label: 'WORKBENCHLINK VNCDESKTOP', + value: window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, + key: 'workbenchlink_vncdesktop', + }, + { + label: 'WORKBENCHLINK VSCODE', + value: window.env.REACT_APP_WORKBENCHLINK_VSCODE, + key: 'workbenchlink_vscode', + }, + { + label: 'WORKBENCHLINK JUPYTERLAB', + value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, + key: 'workbenchlink_jupyterlab', + }, + { + label: 'WORKBENCHLINK JUPYTERNOTEBOOK', + value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, + key: 'workbenchlink_jupyternotebook', + }, + { + label: 'CLIENT ID', + value: window.env.REACT_APP_CLIENT_ID, + key: 'client_id', + }, + { + label: 'AUTH AUTHORITY', + value: window.env.REACT_APP_AUTH_AUTHORITY, + key: 'auth_authority', + }, + { + label: 'REDIRECT URI', + value: window.env.REACT_APP_REDIRECT_URI, + key: 'redirect_uri', + }, + { + label: 'LOGOUT REDIRECT URI', + value: window.env.REACT_APP_LOGOUT_REDIRECT_URI, + key: 'logout_redirect_uri', + }, + { + label: 'GITLAB SCOPES', + value: window.env.REACT_APP_GITLAB_SCOPES, + key: 'gitlab_scopes', + }, +]; - React.useEffect(() => { - const fetchValidations = async () => { - const results = await getValidationResults(); - setValidationResults(results); - }; - fetchValidations(); - }, []); +const VerifyConfig: React.FC<{ keys?: string[]; title?: string }> = ({ + keys = [], + title = 'Config verification', +}) => { + const [validationResults, setValidationResults] = React.useState<{ + [key: string]: validationType; + }>({}); - const configItems = [ - { - label: 'APP ENVIRONMENT', - value: window.env.REACT_APP_ENVIRONMENT, - key: 'environment', - }, - { label: 'APP URL', value: window.env.REACT_APP_URL, key: 'url' }, - { - label: 'APP URL BASENAME', - value: window.env.REACT_APP_URL_BASENAME, - key: 'url_basename', - }, - { - label: 'APP URL DTLINK', - value: window.env.REACT_APP_URL_DTLINK, - key: 'url_dtlink', - }, - { - label: 'APP URL LIBLINK', - value: window.env.REACT_APP_URL_LIBLINK, - key: 'url_liblink', - }, - { - label: 'WORKBENCHLINK VNCDESKTOP', - value: window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, - key: 'workbenchlink_vncdesktop', - }, - { - label: 'WORKBENCHLINK VSCODE', - value: window.env.REACT_APP_WORKBENCHLINK_VSCODE, - key: 'workbenchlink_vscode', - }, - { - label: 'WORKBENCHLINK JUPYTERLAB', - value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, - key: 'workbenchlink_jupyterlab', - }, - { - label: 'WORKBENCHLINK JUPYTERNOTEBOOK', - value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, - key: 'workbenchlink_jupyternotebook', - }, - { - label: 'CLIENT ID', - value: window.env.REACT_APP_CLIENT_ID, - key: 'client_id', - }, - { - label: 'AUTH AUTHORITY', - value: window.env.REACT_APP_AUTH_AUTHORITY, - key: 'auth_authority', - }, - { - label: 'REDIRECT URI', - value: window.env.REACT_APP_REDIRECT_URI, - key: 'redirect_uri', - }, - { - label: 'LOGOUT REDIRECT URI', - value: window.env.REACT_APP_LOGOUT_REDIRECT_URI, - key: 'logout_redirect_uri', - }, - { - label: 'GITLAB SCOPES', - value: window.env.REACT_APP_GITLAB_SCOPES, - key: 'gitlab_scopes', - }, - ]; + React.useEffect(() => { + const fetchValidations = async () => { + const results = await getValidationResults(); + setValidationResults(results); + }; + fetchValidations(); + }, []); - return ( - - Configuration Verification -
- {configItems.map(({ label, value, key }) => ( - - ))} -
-
- ); + const displayedConfigs = configItems.filter( + (configItem) => keys.length === 0 || keys.includes(configItem.key), + ); + return ( + + {title} +
+ {displayedConfigs.map(({ label, value, key }) => ( + + ))} +
+
+ ); }; export default VerifyConfig; From a2b9df147e49841a68e418de843c6ae65dff2a62 Mon Sep 17 00:00:00 2001 From: atomicgamedev Date: Fri, 6 Dec 2024 18:35:05 +0100 Subject: [PATCH 7/8] Clean up ConfigItems.tsx, Signin.tsx and VerifyConfig.tsx --- client/src/route/auth/ConfigItems.tsx | 127 +++++++++++++++ client/src/route/auth/Signin.tsx | 145 ++++++++--------- client/src/route/auth/VerifyConfig.tsx | 206 +++++-------------------- 3 files changed, 239 insertions(+), 239 deletions(-) create mode 100644 client/src/route/auth/ConfigItems.tsx diff --git a/client/src/route/auth/ConfigItems.tsx b/client/src/route/auth/ConfigItems.tsx new file mode 100644 index 000000000..3aed5d7d1 --- /dev/null +++ b/client/src/route/auth/ConfigItems.tsx @@ -0,0 +1,127 @@ +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import { Tooltip } from '@mui/material'; +import React from 'react'; +import { validationType } from './VerifyConfig'; + +const ConfigIcon = (toolTipTitle: string, icon: JSX.Element): JSX.Element => ( + + {icon} + +); + +export const getConfigIcon = ( + validation: validationType, + label: string, +): JSX.Element => { + let icon = ; + let toolTipTitle = `${label} threw the following error: ${validation.error}`; + const configHasStatus = validation.status !== undefined; + const configHasError = validation.error !== undefined; + if (!configHasError) { + const statusMessage = configHasStatus + ? `${validation.value} responded with status code ${validation.status}.` + : ''; + const validationStatusIsOK = + configHasStatus && + ((validation.status! >= 200 && validation.status! <= 299) || + validation.status! === 302); + icon = + validationStatusIsOK || !configHasStatus ? ( + + ) : ( + + ); + toolTipTitle = + validationStatusIsOK || !configHasStatus + ? `${label} field is configured correctly.` + : `${label} field may not be configured correctly.`; + toolTipTitle += ` ${statusMessage}`; + } + return ConfigIcon(toolTipTitle, icon); +}; + +export const ConfigItem: React.FC<{ + label: string; + value: string; + validation?: validationType; +}> = React.memo( + ({ label, value, validation = { error: 'Validating unavailable' } }) => ( +
+ {getConfigIcon(validation, label)} +
+ {label}: {value} +
+
+ ), +); +ConfigItem.displayName = 'ConfigItem'; + +export const windowEnvironmentVariables: { value: string; key: string }[] = [ + { + value: window.env.REACT_APP_ENVIRONMENT, + key: 'environment', + }, + { + value: window.env.REACT_APP_URL, + key: 'url', + }, + { + value: window.env.REACT_APP_URL_BASENAME, + key: 'url_basename', + }, + { + value: window.env.REACT_APP_URL_DTLINK, + key: 'url_dtlink', + }, + { + value: window.env.REACT_APP_URL_LIBLINK, + key: 'url_liblink', + }, + { + value: window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, + key: 'workbenchlink_vncdesktop', + }, + { + value: window.env.REACT_APP_WORKBENCHLINK_VSCODE, + key: 'workbenchlink_vscode', + }, + { + value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, + key: 'workbenchlink_jupyterlab', + }, + { + value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, + key: 'workbenchlink_jupyternotebook', + }, + { + value: window.env.REACT_APP_CLIENT_ID, + key: 'client_id', + }, + { + value: window.env.REACT_APP_AUTH_AUTHORITY, + key: 'auth_authority', + }, + { + value: window.env.REACT_APP_REDIRECT_URI, + key: 'redirect_uri', + }, + { + value: window.env.REACT_APP_LOGOUT_REDIRECT_URI, + key: 'logout_redirect_uri', + }, + { + value: window.env.REACT_APP_GITLAB_SCOPES, + key: 'gitlab_scopes', + }, +]; diff --git a/client/src/route/auth/Signin.tsx b/client/src/route/auth/Signin.tsx index 46c9ee1c6..0005b0ac5 100644 --- a/client/src/route/auth/Signin.tsx +++ b/client/src/route/auth/Signin.tsx @@ -11,6 +11,70 @@ import VerifyConfig, { validationType, } from './VerifyConfig'; +const loadingComponent = ( + + Verifying configuration + + +); + +const signInComponent = (startAuthProcess: () => void) => ( + + + + + + +); + function SignIn() { const auth = useAuth(); const [validationResults, setValidationResults] = useState<{ @@ -23,11 +87,10 @@ function SignIn() { 'redirect_uri', 'logout_redirect_uri', ]; - const verifyConfig = VerifyConfig({ + const verifyConfigComponent = VerifyConfig({ keys: configsToVerify, title: 'Config validation failed', }); - React.useEffect(() => { const fetchValidationResults = async () => { const results = await getValidationResults(); @@ -41,80 +104,18 @@ function SignIn() { auth.signinRedirect(); }; - if (isLoading) { - return ( - - Verifying configuration - - - ); - } - if ( + let displayedComponent: React.ReactNode = loadingComponent; + const configHasKeyErrors = + !isLoading && configsToVerify.reduce( (accumulator, currentValue) => accumulator || validationResults[currentValue].error !== undefined, false, - ) - ) { - return verifyConfig; - } - - return ( - - - - - - - ); + ); + displayedComponent = configHasKeyErrors + ? verifyConfigComponent + : signInComponent(startAuthProcess); + return displayedComponent; } export default SignIn; diff --git a/client/src/route/auth/VerifyConfig.tsx b/client/src/route/auth/VerifyConfig.tsx index cfb8a84b9..87e188956 100644 --- a/client/src/route/auth/VerifyConfig.tsx +++ b/client/src/route/auth/VerifyConfig.tsx @@ -1,9 +1,7 @@ -import { Paper, Tooltip, Typography } from '@mui/material'; +import { Paper, Typography } from '@mui/material'; import * as React from 'react'; import { z } from 'zod'; -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; -import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; -import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty'; +import { ConfigItem, windowEnvironmentVariables } from './ConfigItems'; const EnvironmentEnum = z.enum(['dev', 'local', 'prod', 'test']); const PathString = z.string(); @@ -15,37 +13,44 @@ export type validationType = { error?: string; }; -async function urlIsReachable(url: string): Promise { +async function opaqueRequest(url: string): Promise { + const urlValidation: validationType = { + value: url, + status: undefined, + error: undefined, + }; try { - const response = await fetch(url, { method: 'HEAD' }); + await fetch(url, { method: 'HEAD', mode: 'no-cors' }); + urlValidation.status = 0; + } catch (error) { + urlValidation.error = `An error occurred when fetching ${url}: ${error}`; + } + return urlValidation; +} - if (response.ok || response.status === 302) { - return { value: url, status: response.status, error: undefined }; - } - return { - value: url, - status: response.status, - error: `Unexpected response code ${response.status} from ${url}.`, - }; +async function corsRequest(url: string): Promise { + const urlValidation: validationType = { + value: url, + status: undefined, + error: undefined, + }; + const response = await fetch(url, { method: 'HEAD' }); + const responseIsAcceptable = response.ok || response.status === 302; + if (!responseIsAcceptable) { + urlValidation.error = `Unexpected response code ${response.status} from ${url}.`; + } + urlValidation.status = response.status; + return urlValidation; +} + +async function urlIsReachable(url: string): Promise { + let urlValidation: validationType; + try { + urlValidation = await corsRequest(url); } catch { - try { - const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' }); - if (response.type === 'opaque') { - return { value: url, status: response.status, error: undefined }; - } - return { - value: url, - status: response.status, - error: `Unexpected response code ${response.status} from ${url}.`, - }; - } catch (error) { - return { - value: url, - status: undefined, - error: `An error occurred when fetching ${url}. ${error}`, - }; - } + urlValidation = await opaqueRequest(url); } + return urlValidation; } const parseField = ( @@ -99,139 +104,6 @@ export const getValidationResults = async (): Promise<{ return results; }; -const configIcon = (validation: validationType, label: string): JSX.Element => { - const root = document.getElementById('root'); - if (validation.error === 'Validation is not available') { - return ( - - - - ); - } - if (!validation.error) { - const statusMessage = ` ${validation.status !== undefined ? `${validation.value} responded with status code ${validation.status}.` : ``}`; - return validation.status === undefined || - (validation.status >= 200 && validation.status < 300) ? ( - - - - ) : ( - - - - ); - } - return ( - - - - ); -}; - -const ConfigItem: React.FC<{ - label: string; - value: string; - validation?: validationType; -}> = React.memo( - ({ label, value, validation = { error: 'Validation is not available' } }) => ( -
- {configIcon(validation, label)} -
- {label}: {value} -
-
- ), -); -ConfigItem.displayName = 'ConfigItem'; - -const configItems: { label: string; value: string; key: string }[] = [ - { - label: 'APP ENVIRONMENT', - value: window.env.REACT_APP_ENVIRONMENT, - key: 'environment', - }, - { label: 'APP URL', value: window.env.REACT_APP_URL, key: 'url' }, - { - label: 'APP URL BASENAME', - value: window.env.REACT_APP_URL_BASENAME, - key: 'url_basename', - }, - { - label: 'APP URL DTLINK', - value: window.env.REACT_APP_URL_DTLINK, - key: 'url_dtlink', - }, - { - label: 'APP URL LIBLINK', - value: window.env.REACT_APP_URL_LIBLINK, - key: 'url_liblink', - }, - { - label: 'WORKBENCHLINK VNCDESKTOP', - value: window.env.REACT_APP_WORKBENCHLINK_VNCDESKTOP, - key: 'workbenchlink_vncdesktop', - }, - { - label: 'WORKBENCHLINK VSCODE', - value: window.env.REACT_APP_WORKBENCHLINK_VSCODE, - key: 'workbenchlink_vscode', - }, - { - label: 'WORKBENCHLINK JUPYTERLAB', - value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERLAB, - key: 'workbenchlink_jupyterlab', - }, - { - label: 'WORKBENCHLINK JUPYTERNOTEBOOK', - value: window.env.REACT_APP_WORKBENCHLINK_JUPYTERNOTEBOOK, - key: 'workbenchlink_jupyternotebook', - }, - { - label: 'CLIENT ID', - value: window.env.REACT_APP_CLIENT_ID, - key: 'client_id', - }, - { - label: 'AUTH AUTHORITY', - value: window.env.REACT_APP_AUTH_AUTHORITY, - key: 'auth_authority', - }, - { - label: 'REDIRECT URI', - value: window.env.REACT_APP_REDIRECT_URI, - key: 'redirect_uri', - }, - { - label: 'LOGOUT REDIRECT URI', - value: window.env.REACT_APP_LOGOUT_REDIRECT_URI, - key: 'logout_redirect_uri', - }, - { - label: 'GITLAB SCOPES', - value: window.env.REACT_APP_GITLAB_SCOPES, - key: 'gitlab_scopes', - }, -]; - const VerifyConfig: React.FC<{ keys?: string[]; title?: string }> = ({ keys = [], title = 'Config verification', @@ -248,7 +120,7 @@ const VerifyConfig: React.FC<{ keys?: string[]; title?: string }> = ({ fetchValidations(); }, []); - const displayedConfigs = configItems.filter( + const displayedConfigs = windowEnvironmentVariables.filter( (configItem) => keys.length === 0 || keys.includes(configItem.key), ); return ( @@ -263,10 +135,10 @@ const VerifyConfig: React.FC<{ keys?: string[]; title?: string }> = ({ > {title}
- {displayedConfigs.map(({ label, value, key }) => ( + {displayedConfigs.map(({ value, key }) => ( From 258aadae4a3265bf23b1882692aaf6b18e80e52c Mon Sep 17 00:00:00 2001 From: atomicgamedev Date: Fri, 6 Dec 2024 20:14:03 +0100 Subject: [PATCH 8/8] Unmemoise ConfigItem --- client/src/route/auth/ConfigItems.tsx | 28 +++++++++++++-------------- client/src/route/auth/Signin.tsx | 12 ++++++------ 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/client/src/route/auth/ConfigItems.tsx b/client/src/route/auth/ConfigItems.tsx index 3aed5d7d1..c27c287bc 100644 --- a/client/src/route/auth/ConfigItems.tsx +++ b/client/src/route/auth/ConfigItems.tsx @@ -48,22 +48,20 @@ export const ConfigItem: React.FC<{ label: string; value: string; validation?: validationType; -}> = React.memo( - ({ label, value, validation = { error: 'Validating unavailable' } }) => ( -
- {getConfigIcon(validation, label)} -
- {label}: {value} -
+}> = ({ label, value, validation = { error: 'Validating unavailable' } }) => ( +
+ {getConfigIcon(validation, label)} +
+ {label}: {value}
- ), +
); ConfigItem.displayName = 'ConfigItem'; diff --git a/client/src/route/auth/Signin.tsx b/client/src/route/auth/Signin.tsx index 0005b0ac5..336f1aeb5 100644 --- a/client/src/route/auth/Signin.tsx +++ b/client/src/route/auth/Signin.tsx @@ -105,16 +105,16 @@ function SignIn() { }; let displayedComponent: React.ReactNode = loadingComponent; - const configHasKeyErrors = - !isLoading && - configsToVerify.reduce( + if (!isLoading) { + const configHasKeyErrors = configsToVerify.reduce( (accumulator, currentValue) => accumulator || validationResults[currentValue].error !== undefined, false, ); - displayedComponent = configHasKeyErrors - ? verifyConfigComponent - : signInComponent(startAuthProcess); + displayedComponent = configHasKeyErrors + ? verifyConfigComponent + : signInComponent(startAuthProcess); + } return displayedComponent; }