From ef0000124273c1a6a137268f2de5e1fad50fe061 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 19 Dec 2024 08:59:07 -0500 Subject: [PATCH 01/10] feat: add API_RESPONSE_DELAY env variable --- .env.template | 3 ++- apps/api/src/configuration/configuration.schema.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.env.template b/.env.template index 270d955e9..708395e04 100644 --- a/.env.template +++ b/.env.template @@ -76,7 +76,8 @@ PLAYGROUND_DEV_SERVER_PORT=3750 GATEWAY_DEV_SERVER_PORT=3500 # The port to use for the Vite (full web app) development server WEB_DEV_SERVER_PORT=3000 - +# Set an arbitrary delay (in milliseconds) for all responses (useful for testing suspense) +API_RESPONSE_DELAY=0 # If set to 'true' and NODE_ENV === 'development', then login is automated VITE_DEV_BYPASS_AUTH=false # The username to use if VITE_DEV_BYPASS_AUTH is set to true diff --git a/apps/api/src/configuration/configuration.schema.ts b/apps/api/src/configuration/configuration.schema.ts index 216670f4b..450b07f74 100644 --- a/apps/api/src/configuration/configuration.schema.ts +++ b/apps/api/src/configuration/configuration.schema.ts @@ -14,6 +14,7 @@ export const $Configuration = z .object({ API_DEV_SERVER_PORT: z.coerce.number().positive().int().optional(), API_PROD_SERVER_PORT: z.coerce.number().positive().int().default(80), + API_RESPONSE_DELAY: z.coerce.number().positive().int().optional(), DANGEROUSLY_DISABLE_PBKDF2_ITERATION: $BooleanString.default(false), DEBUG: $BooleanString, GATEWAY_API_KEY: z.string().min(32), From b29f71eab24feda5963bf8e56bd332c1d24d8888 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 19 Dec 2024 09:14:03 -0500 Subject: [PATCH 02/10] feat: add delay middleware --- apps/api/src/app.module.ts | 13 ++++++++++++- .../src/core/middleware/delay.middleware.ts | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/core/middleware/delay.middleware.ts diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index c7d4a6f0d..191385f48 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -1,6 +1,7 @@ import { CryptoModule } from '@douglasneuroinformatics/libnest/crypto'; import { LoggingModule } from '@douglasneuroinformatics/libnest/logging'; import { Module } from '@nestjs/common'; +import type { MiddlewareConsumer, NestModule } from '@nestjs/common'; import { APP_GUARD } from '@nestjs/core'; import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; @@ -10,6 +11,7 @@ import { AuthenticationGuard } from './auth/guards/authentication.guard'; import { AuthorizationGuard } from './auth/guards/authorization.guard'; import { ConfigurationModule } from './configuration/configuration.module'; import { ConfigurationService } from './configuration/configuration.service'; +import { DelayMiddleware } from './core/middleware/delay.middleware'; import { GatewayModule } from './gateway/gateway.module'; import { GroupsModule } from './groups/groups.module'; import { InstrumentsModule } from './instruments/instruments.module'; @@ -93,4 +95,13 @@ import { UsersModule } from './users/users.module'; } ] }) -export class AppModule {} +export class AppModule implements NestModule { + constructor(private readonly configurationService: ConfigurationService) {} + + configure(consumer: MiddlewareConsumer) { + const isDev = this.configurationService.get('NODE_ENV') === 'development'; + if (isDev) { + consumer.apply(DelayMiddleware).forRoutes('*'); + } + } +} diff --git a/apps/api/src/core/middleware/delay.middleware.ts b/apps/api/src/core/middleware/delay.middleware.ts new file mode 100644 index 000000000..b28f4e678 --- /dev/null +++ b/apps/api/src/core/middleware/delay.middleware.ts @@ -0,0 +1,18 @@ +import { Injectable, type NestMiddleware } from '@nestjs/common'; + +import { ConfigurationService } from '@/configuration/configuration.service'; + +@Injectable() +export class DelayMiddleware implements NestMiddleware { + constructor(private readonly configurationService: ConfigurationService) {} + + use(_req: any, _res: any, next: (error?: any) => void) { + const responseDelay = this.configurationService.get('API_RESPONSE_DELAY'); + if (!responseDelay) { + return next(); + } + setTimeout(() => { + next(); + }, responseDelay); + } +} From a0ae02833519b17b2be65e6e9d1d3b39ca683e82 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 19 Dec 2024 10:33:16 -0500 Subject: [PATCH 03/10] feat: add WithFallback component --- apps/web/src/components/WithFallback.tsx | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 apps/web/src/components/WithFallback.tsx diff --git a/apps/web/src/components/WithFallback.tsx b/apps/web/src/components/WithFallback.tsx new file mode 100644 index 000000000..b9452371d --- /dev/null +++ b/apps/web/src/components/WithFallback.tsx @@ -0,0 +1,42 @@ +/* eslint-disable react/function-component-definition */ + +import { useEffect, useState } from 'react'; + +import { LoadingFallback } from './LoadingFallback'; + +const MIN_DELAY = 300; // ms + +function isDataReady( + props: TProps +): props is TProps & { data: NonNullable } { + return !(props.data === null || props.data === undefined); +} + +export function WithFallback({ + Component, + props +}: { + Component: React.FC; + props: TProps extends { data: infer TData extends NonNullable } + ? Omit & { data: null | TData | undefined } + : never; +}) { + // if the data is not initially ready, set a min delay + const [isMinDelayComplete, setIsMinDelayComplete] = useState(isDataReady(props)); + + useEffect(() => { + let timeout: ReturnType; + if (!isMinDelayComplete) { + timeout = setTimeout(() => { + setIsMinDelayComplete(true); + }, MIN_DELAY); + } + return () => clearTimeout(timeout); + }, []); + + return isMinDelayComplete && isDataReady(props) ? ( + + ) : ( + + ); +} From 9de5086722e86c1c4e3154dcb288c8cffee92d69 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 19 Dec 2024 10:34:27 -0500 Subject: [PATCH 04/10] fix: add WithFallback to data hub page --- .../web/src/features/datahub/pages/DataHubPage.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/web/src/features/datahub/pages/DataHubPage.tsx b/apps/web/src/features/datahub/pages/DataHubPage.tsx index c49bc042b..6bd0ce25f 100644 --- a/apps/web/src/features/datahub/pages/DataHubPage.tsx +++ b/apps/web/src/features/datahub/pages/DataHubPage.tsx @@ -12,6 +12,7 @@ import { useNavigate } from 'react-router-dom'; import { IdentificationForm } from '@/components/IdentificationForm'; import { LoadingFallback } from '@/components/LoadingFallback'; import { PageHeader } from '@/components/PageHeader'; +import { WithFallback } from '@/components/WithFallback'; import { useAppStore } from '@/store'; import { downloadExcel } from '@/utils/excel'; @@ -84,7 +85,7 @@ export const DataHubPage = () => { }> -
+
@@ -115,10 +116,13 @@ export const DataHubPage = () => { />
- { - navigate(`${subject.id}/assignments`); + { + navigate(`${subject.id}/assignments`); + } }} />
From da558a4dde34af4dfd1d0acec463ce8eed0f2159 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 19 Dec 2024 10:39:50 -0500 Subject: [PATCH 05/10] refactor: use WithLoading in Summary --- apps/web/src/components/Layout/Layout.tsx | 12 ++- .../features/dashboard/components/Summary.tsx | 86 ++++++++++--------- 2 files changed, 56 insertions(+), 42 deletions(-) diff --git a/apps/web/src/components/Layout/Layout.tsx b/apps/web/src/components/Layout/Layout.tsx index 048baa302..2ca89ac22 100644 --- a/apps/web/src/components/Layout/Layout.tsx +++ b/apps/web/src/components/Layout/Layout.tsx @@ -1,10 +1,20 @@ -import { Outlet } from 'react-router-dom'; +import { useEffect } from 'react'; + +import { Outlet, useLocation } from 'react-router-dom'; + +import { queryClient } from '@/services/react-query'; import { Footer } from '../Footer'; import { Navbar } from '../Navbar'; import { Sidebar } from '../Sidebar'; export const Layout = () => { + // const location = useLocation(); + + // useEffect(() => { + // queryClient.clear(); + // }, [location.pathname]); + return (
diff --git a/apps/web/src/features/dashboard/components/Summary.tsx b/apps/web/src/features/dashboard/components/Summary.tsx index 365aa7802..234c60de6 100644 --- a/apps/web/src/features/dashboard/components/Summary.tsx +++ b/apps/web/src/features/dashboard/components/Summary.tsx @@ -1,7 +1,8 @@ import { useTranslation } from '@douglasneuroinformatics/libui/hooks'; import { ClipboardDocumentIcon, DocumentTextIcon, UserIcon, UsersIcon } from '@heroicons/react/24/solid'; +import type { Summary as SummaryType } from '@opendatacapture/schemas/summary'; -import { LoadingFallback } from '@/components/LoadingFallback'; +import { WithFallback } from '@/components/WithFallback'; import { useAppStore } from '@/store'; import { StatisticCard } from '../components/StatisticCard'; @@ -17,46 +18,49 @@ export const Summary = () => { } }); - if (!summaryQuery.data) { - return ; - } - return ( -
-
- } - label={t({ - en: 'Total Users', - fr: "Nombre d'utilisateurs" - })} - value={summaryQuery.data.counts.users} - /> - } - label={t({ - en: 'Total Subjects', - fr: 'Nombre de clients' - })} - value={summaryQuery.data.counts.subjects} - /> - } - label={t({ - en: 'Total Instruments', - fr: "Nombre d'instruments" - })} - value={summaryQuery.data.counts.instruments} - /> - } - label={t({ - en: 'Total Records', - fr: "Nombre d'enregistrements" - })} - value={summaryQuery.data.counts.records} - /> -
-
+ ( +
+
+ } + label={t({ + en: 'Total Users', + fr: "Nombre d'utilisateurs" + })} + value={data.counts.users} + /> + } + label={t({ + en: 'Total Subjects', + fr: 'Nombre de clients' + })} + value={data.counts.subjects} + /> + } + label={t({ + en: 'Total Instruments', + fr: "Nombre d'instruments" + })} + value={data.counts.instruments} + /> + } + label={t({ + en: 'Total Records', + fr: "Nombre d'enregistrements" + })} + value={data.counts.records} + /> +
+
+ )} + props={{ + data: summaryQuery.data + }} + /> ); }; From b63769ed6cbf300a861bbe15e57b85571d4bfffb Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 19 Dec 2024 10:43:33 -0500 Subject: [PATCH 06/10] fix: increase delay duration --- apps/web/src/components/WithFallback.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/WithFallback.tsx b/apps/web/src/components/WithFallback.tsx index b9452371d..77ea26d96 100644 --- a/apps/web/src/components/WithFallback.tsx +++ b/apps/web/src/components/WithFallback.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import { LoadingFallback } from './LoadingFallback'; -const MIN_DELAY = 300; // ms +const MIN_DELAY = 500; // ms function isDataReady( props: TProps From 1feaf24249c63d47b126c2d9ea70924ac736509b Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 19 Dec 2024 10:45:46 -0500 Subject: [PATCH 07/10] refactor: pass group form props as data --- .../group/components/ManageGroupForm.tsx | 22 +++++++++---------- .../features/group/pages/ManageGroupPage.tsx | 6 +++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/web/src/features/group/components/ManageGroupForm.tsx b/apps/web/src/features/group/components/ManageGroupForm.tsx index 329291aae..2f8c17a2a 100644 --- a/apps/web/src/features/group/components/ManageGroupForm.tsx +++ b/apps/web/src/features/group/components/ManageGroupForm.tsx @@ -14,23 +14,21 @@ export type AvailableInstrumentOptions = { }; export type ManageGroupFormProps = { - availableInstrumentOptions: AvailableInstrumentOptions; - initialValues: { - accessibleFormInstrumentIds: Set; - accessibleInteractiveInstrumentIds: Set; - defaultIdentificationMethod?: SubjectIdentificationMethod; - idValidationRegex?: null | string; + data: { + availableInstrumentOptions: AvailableInstrumentOptions; + initialValues: { + accessibleFormInstrumentIds: Set; + accessibleInteractiveInstrumentIds: Set; + defaultIdentificationMethod?: SubjectIdentificationMethod; + idValidationRegex?: null | string; + }; }; onSubmit: (data: Partial) => Promisable; readOnly: boolean; }; -export const ManageGroupForm = ({ - availableInstrumentOptions, - initialValues, - onSubmit, - readOnly -}: ManageGroupFormProps) => { +export const ManageGroupForm = ({ data, onSubmit, readOnly }: ManageGroupFormProps) => { + const { availableInstrumentOptions, initialValues } = data; const { t } = useTranslation(); let description = t('group.manage.accessibleInstrumentsDesc'); diff --git a/apps/web/src/features/group/pages/ManageGroupPage.tsx b/apps/web/src/features/group/pages/ManageGroupPage.tsx index a0389502b..11797fbf9 100644 --- a/apps/web/src/features/group/pages/ManageGroupPage.tsx +++ b/apps/web/src/features/group/pages/ManageGroupPage.tsx @@ -71,8 +71,10 @@ export const ManageGroupPage = () => { { const updatedGroup = await updateGroupMutation.mutateAsync(data); From 6cf0d00b8768dfb68bc0c4167c6a28d46a1d7571 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 19 Dec 2024 10:52:17 -0500 Subject: [PATCH 08/10] refactor: use WithFallback in ManageGroup Page --- .../features/group/pages/ManageGroupPage.tsx | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/apps/web/src/features/group/pages/ManageGroupPage.tsx b/apps/web/src/features/group/pages/ManageGroupPage.tsx index 11797fbf9..90bc12eee 100644 --- a/apps/web/src/features/group/pages/ManageGroupPage.tsx +++ b/apps/web/src/features/group/pages/ManageGroupPage.tsx @@ -4,6 +4,7 @@ import { Heading } from '@douglasneuroinformatics/libui/components'; import { useTranslation } from '@douglasneuroinformatics/libui/hooks'; import { PageHeader } from '@/components/PageHeader'; +import { WithFallback } from '@/components/WithFallback'; import { useInstrumentInfoQuery } from '@/hooks/useInstrumentInfoQuery'; import { useSetupState } from '@/hooks/useSetupState'; import { useAppStore } from '@/store'; @@ -19,12 +20,15 @@ export const ManageGroupPage = () => { const changeGroup = useAppStore((store) => store.changeGroup); const setupState = useSetupState(); - const availableInstruments = instrumentInfoQuery.data ?? []; + const availableInstruments = instrumentInfoQuery.data; const accessibleInstrumentIds = currentGroup?.accessibleInstrumentIds; const defaultIdentificationMethod = currentGroup?.settings.defaultIdentificationMethod; - const { availableInstrumentOptions, initialValues } = useMemo(() => { + const data = useMemo(() => { + if (!availableInstruments) { + return null; + } const availableInstrumentOptions: AvailableInstrumentOptions = { form: {}, interactive: {}, @@ -69,16 +73,15 @@ export const ManageGroupPage = () => { {t('manage.pageTitle')} - - { - const updatedGroup = await updateGroupMutation.mutateAsync(data); - changeGroup(updatedGroup); + { + const updatedGroup = await updateGroupMutation.mutateAsync(data); + changeGroup(updatedGroup); + }, + readOnly: Boolean(setupState.data?.isDemo && import.meta.env.PROD) }} /> From f6196238ad71abc5e533f8b92f31c9f696b106c3 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 19 Dec 2024 10:52:49 -0500 Subject: [PATCH 09/10] fix: remove location force render from layout --- apps/web/src/components/Layout/Layout.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/apps/web/src/components/Layout/Layout.tsx b/apps/web/src/components/Layout/Layout.tsx index 2ca89ac22..048baa302 100644 --- a/apps/web/src/components/Layout/Layout.tsx +++ b/apps/web/src/components/Layout/Layout.tsx @@ -1,20 +1,10 @@ -import { useEffect } from 'react'; - -import { Outlet, useLocation } from 'react-router-dom'; - -import { queryClient } from '@/services/react-query'; +import { Outlet } from 'react-router-dom'; import { Footer } from '../Footer'; import { Navbar } from '../Navbar'; import { Sidebar } from '../Sidebar'; export const Layout = () => { - // const location = useLocation(); - - // useEffect(() => { - // queryClient.clear(); - // }, [location.pathname]); - return (
From bdab88503648c881405b6a7bcf2830f1e3f53e73 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Thu, 19 Dec 2024 13:02:15 -0500 Subject: [PATCH 10/10] fix: reduce delay --- apps/web/src/components/WithFallback.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/WithFallback.tsx b/apps/web/src/components/WithFallback.tsx index 77ea26d96..b9452371d 100644 --- a/apps/web/src/components/WithFallback.tsx +++ b/apps/web/src/components/WithFallback.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import { LoadingFallback } from './LoadingFallback'; -const MIN_DELAY = 500; // ms +const MIN_DELAY = 300; // ms function isDataReady( props: TProps