Skip to content

Commit

Permalink
Merge pull request #1068 from joshunrau/qol
Browse files Browse the repository at this point in the history
  • Loading branch information
joshunrau authored Dec 19, 2024
2 parents 34755d8 + bdab885 commit 21cf080
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 70 deletions.
3 changes: 2 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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';
Expand Down Expand Up @@ -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('*');
}
}
}
1 change: 1 addition & 0 deletions apps/api/src/configuration/configuration.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
18 changes: 18 additions & 0 deletions apps/api/src/core/middleware/delay.middleware.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
42 changes: 42 additions & 0 deletions apps/web/src/components/WithFallback.tsx
Original file line number Diff line number Diff line change
@@ -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<TProps extends { data: unknown }>(
props: TProps
): props is TProps & { data: NonNullable<TProps['data']> } {
return !(props.data === null || props.data === undefined);
}

export function WithFallback<TProps extends { [key: string]: unknown }>({
Component,
props
}: {
Component: React.FC<TProps>;
props: TProps extends { data: infer TData extends NonNullable<unknown> }
? Omit<TProps, 'data'> & { 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<typeof setTimeout>;
if (!isMinDelayComplete) {
timeout = setTimeout(() => {
setIsMinDelayComplete(true);
}, MIN_DELAY);
}
return () => clearTimeout(timeout);
}, []);

return isMinDelayComplete && isDataReady(props) ? (
<Component {...(props as unknown as TProps)} />
) : (
<LoadingFallback />
);
}
86 changes: 45 additions & 41 deletions apps/web/src/features/dashboard/components/Summary.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,46 +18,49 @@ export const Summary = () => {
}
});

if (!summaryQuery.data) {
return <LoadingFallback />;
}

return (
<div className="body-font">
<div className="grid grid-cols-1 gap-5 text-center lg:grid-cols-2">
<StatisticCard
icon={<UsersIcon className="h-12 w-12" />}
label={t({
en: 'Total Users',
fr: "Nombre d'utilisateurs"
})}
value={summaryQuery.data.counts.users}
/>
<StatisticCard
icon={<UserIcon className="h-12 w-12" />}
label={t({
en: 'Total Subjects',
fr: 'Nombre de clients'
})}
value={summaryQuery.data.counts.subjects}
/>
<StatisticCard
icon={<ClipboardDocumentIcon className="h-12 w-12" />}
label={t({
en: 'Total Instruments',
fr: "Nombre d'instruments"
})}
value={summaryQuery.data.counts.instruments}
/>
<StatisticCard
icon={<DocumentTextIcon className="h-12 w-12" />}
label={t({
en: 'Total Records',
fr: "Nombre d'enregistrements"
})}
value={summaryQuery.data.counts.records}
/>
</div>
</div>
<WithFallback
Component={({ data }: { data: SummaryType }) => (
<div className="body-font">
<div className="grid grid-cols-1 gap-5 text-center lg:grid-cols-2">
<StatisticCard
icon={<UsersIcon className="h-12 w-12" />}
label={t({
en: 'Total Users',
fr: "Nombre d'utilisateurs"
})}
value={data.counts.users}
/>
<StatisticCard
icon={<UserIcon className="h-12 w-12" />}
label={t({
en: 'Total Subjects',
fr: 'Nombre de clients'
})}
value={data.counts.subjects}
/>
<StatisticCard
icon={<ClipboardDocumentIcon className="h-12 w-12" />}
label={t({
en: 'Total Instruments',
fr: "Nombre d'instruments"
})}
value={data.counts.instruments}
/>
<StatisticCard
icon={<DocumentTextIcon className="h-12 w-12" />}
label={t({
en: 'Total Records',
fr: "Nombre d'enregistrements"
})}
value={data.counts.records}
/>
</div>
</div>
)}
props={{
data: summaryQuery.data
}}
/>
);
};
14 changes: 9 additions & 5 deletions apps/web/src/features/datahub/pages/DataHubPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -84,7 +85,7 @@ export const DataHubPage = () => {
</Heading>
</PageHeader>
<React.Suspense fallback={<LoadingFallback />}>
<div>
<div className="flex flex-grow flex-col">
<div className="mb-3 flex flex-col justify-between gap-3 lg:flex-row">
<Dialog open={isLookupOpen} onOpenChange={setIsLookupOpen}>
<Dialog.Trigger className="flex-grow">
Expand Down Expand Up @@ -115,10 +116,13 @@ export const DataHubPage = () => {
/>
</div>
</div>
<MasterDataTable
data={data ?? []}
onSelect={(subject) => {
navigate(`${subject.id}/assignments`);
<WithFallback
Component={MasterDataTable}
props={{
data,
onSelect: (subject) => {
navigate(`${subject.id}/assignments`);
}
}}
/>
</div>
Expand Down
22 changes: 10 additions & 12 deletions apps/web/src/features/group/components/ManageGroupForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,21 @@ export type AvailableInstrumentOptions = {
};

export type ManageGroupFormProps = {
availableInstrumentOptions: AvailableInstrumentOptions;
initialValues: {
accessibleFormInstrumentIds: Set<string>;
accessibleInteractiveInstrumentIds: Set<string>;
defaultIdentificationMethod?: SubjectIdentificationMethod;
idValidationRegex?: null | string;
data: {
availableInstrumentOptions: AvailableInstrumentOptions;
initialValues: {
accessibleFormInstrumentIds: Set<string>;
accessibleInteractiveInstrumentIds: Set<string>;
defaultIdentificationMethod?: SubjectIdentificationMethod;
idValidationRegex?: null | string;
};
};
onSubmit: (data: Partial<UpdateGroupData>) => Promisable<any>;
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');
Expand Down
25 changes: 15 additions & 10 deletions apps/web/src/features/group/pages/ManageGroupPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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: {},
Expand Down Expand Up @@ -69,14 +73,15 @@ export const ManageGroupPage = () => {
{t('manage.pageTitle')}
</Heading>
</PageHeader>

<ManageGroupForm
availableInstrumentOptions={availableInstrumentOptions}
initialValues={initialValues}
readOnly={Boolean(setupState.data?.isDemo && import.meta.env.PROD)}
onSubmit={async (data) => {
const updatedGroup = await updateGroupMutation.mutateAsync(data);
changeGroup(updatedGroup);
<WithFallback
Component={ManageGroupForm}
props={{
data,
onSubmit: async (data) => {
const updatedGroup = await updateGroupMutation.mutateAsync(data);
changeGroup(updatedGroup);
},
readOnly: Boolean(setupState.data?.isDemo && import.meta.env.PROD)
}}
/>
</React.Fragment>
Expand Down

0 comments on commit 21cf080

Please sign in to comment.