Skip to content

Commit

Permalink
feat: Added Dynamic UI Schema Generation (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
macintushar authored Apr 29, 2024
1 parent 85eed5e commit 1681d12
Show file tree
Hide file tree
Showing 12 changed files with 115 additions and 104 deletions.
55 changes: 55 additions & 0 deletions ui/src/components/JSONSchemaForm/JSONSchemaForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Form from '@rjsf/chakra-ui';
import { RJSFSchema } from '@rjsf/utils';
import validator from '@rjsf/validator-ajv8';

import ObjectFieldTemplate from '@/components/JSONSchemaForm/rjsf/ObjectFieldTemplate';
import TitleFieldTemplate from '@/components/JSONSchemaForm/rjsf/TitleFieldTemplate';
import FieldTemplate from '@/components/JSONSchemaForm/rjsf/FieldTemplate';
import BaseInputTemplate from '@/components/JSONSchemaForm/rjsf/BaseInputTemplate';
import DescriptionFieldTemplate from '@/components/JSONSchemaForm/rjsf/DescriptionFieldTemplate';
import { FormProps } from '@rjsf/core';

type JSONSchemaFormProps = {
schema: RJSFSchema;
uiSchema: Record<string, string>;
onSubmit: (formData: FormData) => void;
onChange?: (formData: FormData) => void;
children?: JSX.Element;
formData?: unknown;
};

const JSONSchemaForm = ({
schema,
uiSchema,
onSubmit,
onChange,
children,
formData,
}: JSONSchemaFormProps): JSX.Element => {
const templateOverrides: FormProps<any, RJSFSchema, any>['templates'] = {
ObjectFieldTemplate: ObjectFieldTemplate,
TitleFieldTemplate: TitleFieldTemplate,
FieldTemplate: FieldTemplate,
BaseInputTemplate: BaseInputTemplate,
DescriptionFieldTemplate: DescriptionFieldTemplate,
};
return (
<Form
// TODO: Removed Hardcoded UI Schema
// uiSchema={
// connectorSchema.title ? uiSchemas[connectorSchema.title.toLowerCase()] : undefined
// }
uiSchema={uiSchema}
schema={schema}
validator={validator}
templates={templateOverrides}
formData={formData}
onSubmit={({ formData }) => onSubmit(formData)}
onChange={({ formData }) => onChange?.(formData)}
>
{children}
</Form>
);
};

export default JSONSchemaForm;
1 change: 1 addition & 0 deletions ui/src/components/JSONSchemaForm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './JSONSchemaForm';
26 changes: 26 additions & 0 deletions ui/src/utils/generateUiSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const generateUiSchema = (
schemaProperties: object,
uiSchema: Record<string, any> = {},
path: string[] = [],
): Record<string, string> => {
Object.entries(schemaProperties).forEach(([key, value]) => {
if (key === 'properties' && typeof value === 'object' && value !== null) {
generateUiSchema(value, uiSchema, path);
} else if (typeof value === 'object' && value !== null) {
const nestedObject = key === 'properties' ? path : path.concat(key);
generateUiSchema(value, uiSchema, nestedObject);
} else if (key === 'multiwoven_secret' && value === true) {
let current = uiSchema;
path.forEach((schemaPath, index) => {
if (index === path.length - 1) {
current[schemaPath] = { 'ui:widget': 'password' };
} else {
current[schemaPath] = current[schemaPath] || {};
}
current = current[schemaPath];
});
}
});

return uiSchema;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,11 @@ import { Box } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useContext } from 'react';

import validator from '@rjsf/validator-ajv8';
import { Form } from '@rjsf/chakra-ui';
import Loader from '@/components/Loader';
import ContentContainer from '@/components/ContentContainer';
import SourceFormFooter from '@/views/Connectors/Sources/SourcesForm/SourceFormFooter';
import ObjectFieldTemplate from '@/views/Connectors/Sources/rjsf/ObjectFieldTemplate';
import TitleFieldTemplate from '@/views/Connectors/Sources/rjsf/TitleFieldTemplate';
import FieldTemplate from '@/views/Connectors/Sources/rjsf/FieldTemplate';
import BaseInputTemplate from '@/views/Connectors/Sources/rjsf/BaseInputTemplate';
import DescriptionFieldTemplate from '@/views/Connectors/Sources/rjsf/DescriptionFieldTemplate';
import { FormProps } from '@rjsf/core';
import { RJSFSchema } from '@rjsf/utils';
import { uiSchemas } from '@/views/Connectors/Sources/SourcesForm/SourceConfigForm/SourceConfigForm';
import JSONSchemaForm from '@/components/JSONSchemaForm';
import { generateUiSchema } from '@/utils/generateUiSchema';

const DestinationConfigForm = (): JSX.Element | null => {
const { state, stepInfo, handleMoveForward } = useContext(SteppedFormContext);
Expand All @@ -43,26 +35,16 @@ const DestinationConfigForm = (): JSX.Element | null => {
handleMoveForward(stepInfo?.formKey as string, formData);
};

const templateOverrides: FormProps<any, RJSFSchema, any>['templates'] = {
ObjectFieldTemplate: ObjectFieldTemplate,
TitleFieldTemplate: TitleFieldTemplate,
FieldTemplate: FieldTemplate,
BaseInputTemplate: BaseInputTemplate,
DescriptionFieldTemplate: DescriptionFieldTemplate,
};
const generatedSchema = generateUiSchema(connectorSchema);

return (
<Box width='100%' display='flex' justifyContent='center'>
<ContentContainer>
<Box backgroundColor='gray.300' padding='20px' borderRadius='8px' marginBottom='100px'>
<Form
<JSONSchemaForm
schema={connectorSchema}
validator={validator}
onSubmit={({ formData }) => handleFormSubmit(formData)}
templates={templateOverrides}
uiSchema={
connectorSchema.title ? uiSchemas[connectorSchema.title.toLowerCase()] : undefined
}
uiSchema={generatedSchema}
onSubmit={(formData: FormData) => handleFormSubmit(formData)}
>
<SourceFormFooter
ctaName='Finish'
Expand All @@ -71,7 +53,7 @@ const DestinationConfigForm = (): JSX.Element | null => {
isDocumentsSectionRequired
isContinueCtaRequired
/>
</Form>
</JSONSchemaForm>
</Box>
</ContentContainer>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import {
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useNavigate, useParams } from 'react-router-dom';

import validator from '@rjsf/validator-ajv8';
import { Form } from '@rjsf/chakra-ui';
import { Box, Button, Divider, Text } from '@chakra-ui/react';
import TopBar from '@/components/TopBar';
import ContentContainer from '@/components/ContentContainer';
Expand All @@ -17,19 +15,14 @@ import { CreateConnectorPayload, TestConnectionPayload } from '../../types';
import { RJSFSchema } from '@rjsf/utils';
import SourceFormFooter from '../../Sources/SourcesForm/SourceFormFooter';
import Loader from '@/components/Loader';
import ObjectFieldTemplate from '@/views/Connectors/Sources/rjsf/ObjectFieldTemplate';
import TitleFieldTemplate from '@/views/Connectors/Sources/rjsf/TitleFieldTemplate';
import FieldTemplate from '@/views/Connectors/Sources/rjsf/FieldTemplate';
import { FormProps } from '@rjsf/core';
import BaseInputTemplate from '@/views/Connectors/Sources/rjsf/BaseInputTemplate';
import DescriptionFieldTemplate from '@/views/Connectors/Sources/rjsf/DescriptionFieldTemplate';
import { uiSchemas } from '../../Sources/SourcesForm/SourceConfigForm/SourceConfigForm';
import { Step } from '@/components/Breadcrumbs/types';
import EntityItem from '@/components/EntityItem';
import moment from 'moment';
import SourceActions from '../../Sources/EditSource/SourceActions';
import { CustomToastStatus } from '@/components/Toast/index';
import useCustomToast from '@/hooks/useCustomToast';
import { generateUiSchema } from '@/utils/generateUiSchema';
import JSONSchemaForm from '../../../../components/JSONSchemaForm';

const EditDestination = (): JSX.Element => {
const { destinationId } = useParams();
Expand Down Expand Up @@ -157,14 +150,6 @@ const EditDestination = (): JSX.Element => {
}
};

const templateOverrides: FormProps<any, RJSFSchema, any>['templates'] = {
ObjectFieldTemplate: ObjectFieldTemplate,
TitleFieldTemplate: TitleFieldTemplate,
FieldTemplate: FieldTemplate,
BaseInputTemplate: BaseInputTemplate,
DescriptionFieldTemplate: DescriptionFieldTemplate,
};

const EDIT_DESTINATION_STEP: Step[] = [
{
name: 'Destinations',
Expand All @@ -178,6 +163,8 @@ const EditDestination = (): JSX.Element => {

if (isConnectorInfoLoading || isConnectorDefinitionLoading) return <Loader />;

const generatedSchema = generateUiSchema(connectorSchema?.connection_specification as RJSFSchema);

return (
<Box width='100%' display='flex' justifyContent='center'>
<ContentContainer>
Expand Down Expand Up @@ -217,18 +204,12 @@ const EditDestination = (): JSX.Element => {
borderRadius='8px'
marginBottom='100px'
>
<Form
uiSchema={
connectorSchema?.connection_specification?.title
? uiSchemas[connectorSchema?.connection_specification?.title.toLowerCase()]
: undefined
}
<JSONSchemaForm
schema={connectorSchema?.connection_specification as RJSFSchema}
validator={validator}
uiSchema={generatedSchema}
formData={formData}
onSubmit={({ formData }) => handleOnTestClick(formData)}
onChange={({ formData }) => setFormData(formData)}
templates={templateOverrides}
onSubmit={(formData: FormData) => handleOnTestClick(formData)}
onChange={(formData: FormData) => setFormData(formData)}
>
<SourceFormFooter
ctaName='Save Changes'
Expand All @@ -253,7 +234,7 @@ const EditDestination = (): JSX.Element => {
</Button>
}
/>
</Form>
</JSONSchemaForm>
</Box>
</ContentContainer>
</Box>
Expand Down
38 changes: 10 additions & 28 deletions ui/src/views/Connectors/Sources/EditSource/EditSource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import {
import { useMutation, useQuery } from '@tanstack/react-query';
import { useNavigate, useParams } from 'react-router-dom';

import validator from '@rjsf/validator-ajv8';
import { Form } from '@rjsf/chakra-ui';
import { Box, Button, Divider, Text } from '@chakra-ui/react';
import SourceFormFooter from '../SourcesForm/SourceFormFooter';
import TopBar from '@/components/TopBar';
Expand All @@ -20,16 +18,12 @@ import Loader from '@/components/Loader';
import { Step } from '@/components/Breadcrumbs/types';
import EntityItem from '@/components/EntityItem';
import moment from 'moment';
import ObjectFieldTemplate from '@/views/Connectors/Sources/rjsf/ObjectFieldTemplate';
import TitleFieldTemplate from '@/views/Connectors/Sources/rjsf/TitleFieldTemplate';
import FieldTemplate from '@/views/Connectors/Sources/rjsf/FieldTemplate';
import { FormProps } from '@rjsf/core';
import BaseInputTemplate from '@/views/Connectors/Sources/rjsf/BaseInputTemplate';
import DescriptionFieldTemplate from '@/views/Connectors/Sources/rjsf/DescriptionFieldTemplate';
import { uiSchemas } from '../SourcesForm/SourceConfigForm/SourceConfigForm';

import SourceActions from './SourceActions';
import { CustomToastStatus } from '@/components/Toast/index';
import useCustomToast from '@/hooks/useCustomToast';
import JSONSchemaForm from '../../../../components/JSONSchemaForm';
import { generateUiSchema } from '@/utils/generateUiSchema';

const EditSource = (): JSX.Element => {
const { sourceId } = useParams();
Expand All @@ -44,7 +38,7 @@ const EditSource = (): JSX.Element => {
queryKey: ['connectorInfo', sourceId],
queryFn: () => getConnectorInfo(sourceId as string),
refetchOnMount: true,
refetchOnWindowFocus: false,
refetchOnWindowFocus: true,
enabled: !!sourceId,
});

Expand Down Expand Up @@ -163,13 +157,7 @@ const EditSource = (): JSX.Element => {
},
];

const templateOverrides: FormProps<any, RJSFSchema, any>['templates'] = {
ObjectFieldTemplate: ObjectFieldTemplate,
TitleFieldTemplate: TitleFieldTemplate,
FieldTemplate: FieldTemplate,
BaseInputTemplate: BaseInputTemplate,
DescriptionFieldTemplate: DescriptionFieldTemplate,
};
const generatedSchema = generateUiSchema(connectorSchema?.connection_specification as RJSFSchema);

return (
<Box width='100%' display='flex' justifyContent='center'>
Expand Down Expand Up @@ -212,18 +200,12 @@ const EditSource = (): JSX.Element => {
border='1px'
borderColor='gray.400'
>
<Form
uiSchema={
connectorSchema?.connection_specification?.title
? uiSchemas[connectorSchema?.connection_specification?.title.toLowerCase()]
: undefined
}
<JSONSchemaForm
schema={connectorSchema?.connection_specification as RJSFSchema}
validator={validator}
uiSchema={generatedSchema}
formData={formData}
onSubmit={({ formData }) => handleOnTestClick(formData)}
onChange={({ formData }) => setFormData(formData)}
templates={templateOverrides}
onSubmit={(formData: FormData) => handleOnTestClick(formData)}
onChange={(formData: FormData) => setFormData(formData)}
>
<SourceFormFooter
ctaName='Save Changes'
Expand All @@ -248,7 +230,7 @@ const EditSource = (): JSX.Element => {
</Button>
}
/>
</Form>
</JSONSchemaForm>
</Box>
</ContentContainer>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,14 @@ import { getConnectorDefinition } from '@/services/connectors';
import { useContext } from 'react';
import { Box } from '@chakra-ui/react';

import validator from '@rjsf/validator-ajv8';
import { Form } from '@rjsf/chakra-ui';
import SourceFormFooter from '@/views/Connectors/Sources/SourcesForm/SourceFormFooter';

import Loader from '@/components/Loader';
import { processFormData } from '@/views/Connectors/helpers';
import ContentContainer from '@/components/ContentContainer';
import ObjectFieldTemplate from '@/views/Connectors/Sources/rjsf/ObjectFieldTemplate';
import TitleFieldTemplate from '@/views/Connectors/Sources/rjsf/TitleFieldTemplate';
import FieldTemplate from '@/views/Connectors/Sources/rjsf/FieldTemplate';
import { FormProps } from '@rjsf/core';
import { RJSFSchema } from '@rjsf/utils';
import BaseInputTemplate from '@/views/Connectors/Sources/rjsf/BaseInputTemplate';
import DescriptionFieldTemplate from '@/views/Connectors/Sources/rjsf/DescriptionFieldTemplate';
import { generateUiSchema } from '@/utils/generateUiSchema';
import JSONSchemaForm from '@/components/JSONSchemaForm';

/**
* TODO: Discuss with backend team and move this to backend
Expand Down Expand Up @@ -78,26 +72,16 @@ const SourceConfigForm = (): JSX.Element | null => {
const connectorSchema = data?.data?.connector_spec?.connection_specification;
if (!connectorSchema) return null;

const templateOverrides: FormProps<any, RJSFSchema, any>['templates'] = {
ObjectFieldTemplate: ObjectFieldTemplate,
TitleFieldTemplate: TitleFieldTemplate,
FieldTemplate: FieldTemplate,
BaseInputTemplate: BaseInputTemplate,
DescriptionFieldTemplate: DescriptionFieldTemplate,
};
const generatedSchema = generateUiSchema(connectorSchema);

return (
<Box display='flex' justifyContent='center' marginBottom='80px'>
<ContentContainer>
<Box backgroundColor='gray.200' padding='24px' borderRadius='8px'>
<Form
uiSchema={
connectorSchema.title ? uiSchemas[connectorSchema.title.toLowerCase()] : undefined
}
<JSONSchemaForm
schema={connectorSchema}
validator={validator}
templates={templateOverrides}
onSubmit={({ formData }) => handleFormSubmit(formData)}
uiSchema={generatedSchema}
onSubmit={(formData: FormData) => handleFormSubmit(formData)}
>
<SourceFormFooter
ctaName='Continue'
Expand All @@ -106,7 +90,7 @@ const SourceConfigForm = (): JSX.Element | null => {
isDocumentsSectionRequired
isBackRequired
/>
</Form>
</JSONSchemaForm>
</Box>
</ContentContainer>
</Box>
Expand Down

0 comments on commit 1681d12

Please sign in to comment.