Skip to content

Commit

Permalink
NEOS-1061 Improves render performance on AI Generate Forms (nucleuscl…
Browse files Browse the repository at this point in the history
  • Loading branch information
nickzelei authored and zackerydev committed May 2, 2024
1 parent e08e287 commit 5193cd1
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 52 deletions.
21 changes: 21 additions & 0 deletions frontend/apps/web/app/(mgmt)/FormPersist.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ReactElement } from 'react';
import { FieldValues, UseFormReturn } from 'react-hook-form';
import useFormPersist from './useFormPersist';

interface FormPersistProps<T extends FieldValues> {
form: UseFormReturn<T>;
formKey: string;
}
const isBrowser = () => typeof window !== 'undefined';

export default function FormPersist<T extends FieldValues>(
props: FormPersistProps<T>
): ReactElement {
const { form, formKey } = props;
useFormPersist(formKey, {
control: form.control,
setValue: form.setValue,
storage: isBrowser() ? window.sessionStorage : undefined,
});
return <></>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,11 @@ export default function AiDataGenConnectionCard({
isLoading: isJobLoading,
} = useGetJob(account?.id ?? '', jobId);

const { isLoading: isConnectionsLoading, data: connectionsData } =
useGetConnections(account?.id ?? '');
const {
isLoading: isConnectionsLoading,
isValidating: isConnectionsValidating,
data: connectionsData,
} = useGetConnections(account?.id ?? '');
const connections = connectionsData?.connections ?? [];

const form = useForm<SingleTableEditAiSourceFormValues>({
Expand All @@ -92,7 +95,7 @@ export default function AiDataGenConnectionCard({
context: { accountId: account?.id },
});

const fkSourceConnectionId = form.getValues('source.fkSourceConnectionId');
const fkSourceConnectionId = form.watch('source.fkSourceConnectionId');

const {
data: connectionSchemaDataMap,
Expand Down Expand Up @@ -127,6 +130,11 @@ export default function AiDataGenConnectionCard({

const [selectedTables, setSelectedTables] = useState<Set<string>>(new Set());

const connectionsMap = useMemo(
() => new Map(connections.map((c) => [c.id, c])),
[isConnectionsValidating]
);

useEffect(() => {
if (
isJobLoading ||
Expand All @@ -135,12 +143,38 @@ export default function AiDataGenConnectionCard({
return;
}
const js = getJobSource(data.job);
onSelectedTableToggle(
new Set([`${js.schema.schema}.${js.schema.table}`]),
'add'
);
if (js.schema.schema && js.schema.table) {
onSelectedTableToggle(
new Set([`${js.schema.schema}.${js.schema.table}`]),
'add'
);
}
}, [isJobLoading]);

const [formSchema, formTable, formSourceId] = form.watch([
'schema.schema',
'schema.table',
'source.sourceId',
]);

const [tableData, columns] = useMemo(() => {
const tdata: AiSchemaTableRecord[] = [];
const cols: ColumnDef<SampleRecord>[] = [];
if (formSchema && formTable && connectionSchemaDataMap?.schemaMap) {
const tableSchema =
connectionSchemaDataMap.schemaMap[`${formSchema}.${formTable}`];
if (tableSchema) {
tdata.push(...tableSchema);
cols.push(
...getAiSampleTableColumns(tableSchema.map((dbcol) => dbcol.column))
);
}
}
return [tdata, cols];
}, [formSchema, formTable, isSchemaMapValidating]);

const sourceConn = connectionsMap.get(formSourceId);

if (isJobLoading || isSchemaDataMapLoading || isConnectionsLoading) {
return <SchemaPageSkeleton />;
}
Expand Down Expand Up @@ -239,28 +273,6 @@ export default function AiDataGenConnectionCard({
}
}

const formVals = form.watch();
let tableData: AiSchemaTableRecord[] = [];
let columns: ColumnDef<SampleRecord>[] = [];
if (
formVals.schema.schema &&
formVals.schema.table &&
connectionSchemaDataMap?.schemaMap
) {
const tableSchema =
connectionSchemaDataMap.schemaMap[
`${formVals.schema.schema}.${formVals.schema.table}`
];
if (tableSchema) {
tableData.push(...tableSchema);
columns = getAiSampleTableColumns(
tableSchema.map((dbcol) => dbcol.column)
);
}
}
const connectionsMap = new Map(connections.map((c) => [c.id, c]));
const sourceConn = connectionsMap.get(formVals.source.sourceId);

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
Expand Down Expand Up @@ -414,7 +426,13 @@ export default function AiDataGenConnectionCard({
appended to the end of this prompt automatically.
</FormDescription>
<FormControl>
<Textarea {...field} />
<Textarea
{...field}
autoComplete="off"
autoCapitalize="off"
autoCorrect="off"
spellCheck={false}
/>
</FormControl>
<FormMessage />
</FormItem>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import FormPersist from '@/app/(mgmt)/FormPersist';
import ButtonText from '@/components/ButtonText';
import { Action } from '@/components/DualListBox/DualListBox';
import Spinner from '@/components/Spinner';
Expand Down Expand Up @@ -58,7 +59,6 @@ import { ColumnDef } from '@tanstack/react-table';
import { useRouter } from 'next/navigation';
import { ReactElement, useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import useFormPersist from 'react-hook-form-persist';
import { useSessionStorage } from 'usehooks-ts';
import JobsProgressSteps, {
getJobProgressSteps,
Expand All @@ -72,7 +72,6 @@ import SampleTable from './SampleTable/SampleTable';
import { getAiSampleTableColumns } from './SampleTable/SampleTableColumns';
import SelectModelNames from './SelectModelNames';
import { SampleRecord } from './types';
const isBrowser = () => typeof window !== 'undefined';

export default function Page({ searchParams }: PageProps): ReactElement {
const { account } = useAccount();
Expand Down Expand Up @@ -135,18 +134,12 @@ export default function Page({ searchParams }: PageProps): ReactElement {
);

const form = useForm<SingleTableAiSchemaFormValues>({
mode: 'onChange',
resolver: yupResolver<SingleTableAiSchemaFormValues>(
SingleTableAiSchemaFormValues
),
values: schemaFormData,
});

useFormPersist(formKey, {
watch: form.watch,
setValue: form.setValue,
storage: isBrowser() ? window.sessionStorage : undefined,
});
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
Expand Down Expand Up @@ -249,7 +242,9 @@ export default function Page({ searchParams }: PageProps): ReactElement {
return;
}
const js = schemaFormData;
onSelectedTableToggle(new Set([`${js.schema}.${js.table}`]), 'add');
if (js.schema && js.table) {
onSelectedTableToggle(new Set([`${js.schema}.${js.table}`]), 'add');
}
}, [isSchemaMapLoading]);

async function onSampleClick(): Promise<void> {
Expand Down Expand Up @@ -294,22 +289,27 @@ export default function Page({ searchParams }: PageProps): ReactElement {
}
}

const formVals = form.watch();
let tableData: AiSchemaTableRecord[] = [];
let columns: ColumnDef<SampleRecord>[] = [];
if (formVals.schema && formVals.table && connectionSchemaDataMap?.schemaMap) {
const tableSchema =
connectionSchemaDataMap.schemaMap[`${formVals.schema}.${formVals.table}`];
if (tableSchema) {
tableData.push(...tableSchema);
columns = getAiSampleTableColumns(
tableSchema.map((dbcol) => dbcol.column)
);
const [formSchema, formTable] = form.watch(['schema', 'table']);

const [tableData, columns] = useMemo(() => {
const tdata: AiSchemaTableRecord[] = [];
const cols: ColumnDef<SampleRecord>[] = [];
if (formSchema && formTable && connectionSchemaDataMap?.schemaMap) {
const tableSchema =
connectionSchemaDataMap.schemaMap[`${formSchema}.${formTable}`];
if (tableSchema) {
tdata.push(...tableSchema);
cols.push(
...getAiSampleTableColumns(tableSchema.map((dbcol) => dbcol.column))
);
}
}
}
return [tdata, cols];
}, [formSchema, formTable, isSchemaMapValidating]);

return (
<div className="flex flex-col gap-5">
<FormPersist formKey={formKey} form={form} />
<OverviewContainer
Header={
<PageHeader
Expand Down Expand Up @@ -375,7 +375,13 @@ export default function Page({ searchParams }: PageProps): ReactElement {
appended to the end of this prompt automatically.
</FormDescription>
<FormControl>
<Textarea {...field} />
<Textarea
{...field}
autoComplete="off"
autoCapitalize="off"
autoCorrect="off"
spellCheck={false}
/>
</FormControl>
<FormMessage />
</FormItem>
Expand Down
94 changes: 94 additions & 0 deletions frontend/apps/web/app/(mgmt)/useFormPersist.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useEffect } from 'react';
import { Control, SetFieldValue, useWatch } from 'react-hook-form';

interface FormPersistConfig {
storage?: Storage;
control: Control<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
setValue: SetFieldValue<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
exclude?: string[];
onDataRestored?: (data: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
validate?: boolean;
dirty?: boolean;
touch?: boolean;
onTimeout?: () => void;
timeout?: number;
}

interface UseFormPersistResult {
clear(): void;
}

export default function useFormPersist(
name: string,
{
storage,
control,
setValue,
exclude = [],
onDataRestored,
validate = false,
dirty = false,
touch = false,
onTimeout,
timeout,
}: FormPersistConfig
): UseFormPersistResult {
const watchedValues = useWatch({
control,
});

const getStorage = () => storage || window.sessionStorage;

const clearStorage = () => getStorage().removeItem(name);

useEffect(() => {
const str = getStorage().getItem(name);

if (str) {
const { _timestamp = null, ...values } = JSON.parse(str);
const dataRestored: { [key: string]: any } = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
const currTimestamp = Date.now();

if (timeout && currTimestamp - _timestamp > timeout) {
onTimeout && onTimeout();
clearStorage();
return;
}

Object.keys(values).forEach((key) => {
const shouldSet = !exclude.includes(key);
if (shouldSet) {
dataRestored[key] = values[key];
setValue(key, values[key], {
shouldValidate: validate,
shouldDirty: dirty,
shouldTouch: touch,
});
}
});

if (onDataRestored) {
onDataRestored(dataRestored);
}
}
}, [storage, name, onDataRestored, setValue]);

useEffect(() => {
const values: Record<string, unknown> = exclude.length
? Object.entries(watchedValues)
.filter(([key]) => !exclude.includes(key))
.reduce((obj, [key, val]) => Object.assign(obj, { [key]: val }), {})
: Object.assign({}, watchedValues);

if (Object.entries(values).length) {
if (timeout !== undefined) {
values._timestamp = Date.now();
}
getStorage().setItem(name, JSON.stringify(values));
}
}, [watchedValues, timeout]);

return {
clear: () => getStorage().removeItem(name),
};
}

0 comments on commit 5193cd1

Please sign in to comment.