From 1681d1230b62a266ff635a0dd07583ac08638e55 Mon Sep 17 00:00:00 2001 From: Tushar Selvakumar <54372016+macintushar@users.noreply.github.com> Date: Mon, 29 Apr 2024 23:38:12 +0530 Subject: [PATCH] feat: Added Dynamic UI Schema Generation (#80) --- .../JSONSchemaForm/JSONSchemaForm.tsx | 55 +++++++++++++++++++ ui/src/components/JSONSchemaForm/index.ts | 1 + .../rjsf/BaseInputTemplate.tsx | 0 .../rjsf/DescriptionFieldTemplate.tsx | 0 .../JSONSchemaForm}/rjsf/FieldTemplate.tsx | 0 .../rjsf/ObjectFieldTemplate.tsx | 0 .../rjsf/TitleFieldTemplate.tsx | 0 ui/src/utils/generateUiSchema.ts | 26 +++++++++ .../DestinationConfigForm.tsx | 32 +++-------- .../EditDestinations/EditDestinations.tsx | 37 +++---------- .../Sources/EditSource/EditSource.tsx | 38 ++++--------- .../SourceConfigForm/SourceConfigForm.tsx | 30 +++------- 12 files changed, 115 insertions(+), 104 deletions(-) create mode 100644 ui/src/components/JSONSchemaForm/JSONSchemaForm.tsx create mode 100644 ui/src/components/JSONSchemaForm/index.ts rename ui/src/{views/Connectors/Sources => components/JSONSchemaForm}/rjsf/BaseInputTemplate.tsx (100%) rename ui/src/{views/Connectors/Sources => components/JSONSchemaForm}/rjsf/DescriptionFieldTemplate.tsx (100%) rename ui/src/{views/Connectors/Sources => components/JSONSchemaForm}/rjsf/FieldTemplate.tsx (100%) rename ui/src/{views/Connectors/Sources => components/JSONSchemaForm}/rjsf/ObjectFieldTemplate.tsx (100%) rename ui/src/{views/Connectors/Sources => components/JSONSchemaForm}/rjsf/TitleFieldTemplate.tsx (100%) create mode 100644 ui/src/utils/generateUiSchema.ts diff --git a/ui/src/components/JSONSchemaForm/JSONSchemaForm.tsx b/ui/src/components/JSONSchemaForm/JSONSchemaForm.tsx new file mode 100644 index 00000000..0d1b28af --- /dev/null +++ b/ui/src/components/JSONSchemaForm/JSONSchemaForm.tsx @@ -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; + 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['templates'] = { + ObjectFieldTemplate: ObjectFieldTemplate, + TitleFieldTemplate: TitleFieldTemplate, + FieldTemplate: FieldTemplate, + BaseInputTemplate: BaseInputTemplate, + DescriptionFieldTemplate: DescriptionFieldTemplate, + }; + return ( +
onSubmit(formData)} + onChange={({ formData }) => onChange?.(formData)} + > + {children} +
+ ); +}; + +export default JSONSchemaForm; diff --git a/ui/src/components/JSONSchemaForm/index.ts b/ui/src/components/JSONSchemaForm/index.ts new file mode 100644 index 00000000..4b14a8ca --- /dev/null +++ b/ui/src/components/JSONSchemaForm/index.ts @@ -0,0 +1 @@ +export { default } from './JSONSchemaForm'; diff --git a/ui/src/views/Connectors/Sources/rjsf/BaseInputTemplate.tsx b/ui/src/components/JSONSchemaForm/rjsf/BaseInputTemplate.tsx similarity index 100% rename from ui/src/views/Connectors/Sources/rjsf/BaseInputTemplate.tsx rename to ui/src/components/JSONSchemaForm/rjsf/BaseInputTemplate.tsx diff --git a/ui/src/views/Connectors/Sources/rjsf/DescriptionFieldTemplate.tsx b/ui/src/components/JSONSchemaForm/rjsf/DescriptionFieldTemplate.tsx similarity index 100% rename from ui/src/views/Connectors/Sources/rjsf/DescriptionFieldTemplate.tsx rename to ui/src/components/JSONSchemaForm/rjsf/DescriptionFieldTemplate.tsx diff --git a/ui/src/views/Connectors/Sources/rjsf/FieldTemplate.tsx b/ui/src/components/JSONSchemaForm/rjsf/FieldTemplate.tsx similarity index 100% rename from ui/src/views/Connectors/Sources/rjsf/FieldTemplate.tsx rename to ui/src/components/JSONSchemaForm/rjsf/FieldTemplate.tsx diff --git a/ui/src/views/Connectors/Sources/rjsf/ObjectFieldTemplate.tsx b/ui/src/components/JSONSchemaForm/rjsf/ObjectFieldTemplate.tsx similarity index 100% rename from ui/src/views/Connectors/Sources/rjsf/ObjectFieldTemplate.tsx rename to ui/src/components/JSONSchemaForm/rjsf/ObjectFieldTemplate.tsx diff --git a/ui/src/views/Connectors/Sources/rjsf/TitleFieldTemplate.tsx b/ui/src/components/JSONSchemaForm/rjsf/TitleFieldTemplate.tsx similarity index 100% rename from ui/src/views/Connectors/Sources/rjsf/TitleFieldTemplate.tsx rename to ui/src/components/JSONSchemaForm/rjsf/TitleFieldTemplate.tsx diff --git a/ui/src/utils/generateUiSchema.ts b/ui/src/utils/generateUiSchema.ts new file mode 100644 index 00000000..4a355d80 --- /dev/null +++ b/ui/src/utils/generateUiSchema.ts @@ -0,0 +1,26 @@ +export const generateUiSchema = ( + schemaProperties: object, + uiSchema: Record = {}, + path: string[] = [], +): Record => { + 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; +}; diff --git a/ui/src/views/Connectors/Destinations/DestinationsForm/DestinationConfigForm/DestinationConfigForm.tsx b/ui/src/views/Connectors/Destinations/DestinationsForm/DestinationConfigForm/DestinationConfigForm.tsx index 602478a3..30c2a2c3 100644 --- a/ui/src/views/Connectors/Destinations/DestinationsForm/DestinationConfigForm/DestinationConfigForm.tsx +++ b/ui/src/views/Connectors/Destinations/DestinationsForm/DestinationConfigForm/DestinationConfigForm.tsx @@ -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); @@ -43,26 +35,16 @@ const DestinationConfigForm = (): JSX.Element | null => { handleMoveForward(stepInfo?.formKey as string, formData); }; - const templateOverrides: FormProps['templates'] = { - ObjectFieldTemplate: ObjectFieldTemplate, - TitleFieldTemplate: TitleFieldTemplate, - FieldTemplate: FieldTemplate, - BaseInputTemplate: BaseInputTemplate, - DescriptionFieldTemplate: DescriptionFieldTemplate, - }; + const generatedSchema = generateUiSchema(connectorSchema); return ( -
handleFormSubmit(formData)} - templates={templateOverrides} - uiSchema={ - connectorSchema.title ? uiSchemas[connectorSchema.title.toLowerCase()] : undefined - } + uiSchema={generatedSchema} + onSubmit={(formData: FormData) => handleFormSubmit(formData)} > { isDocumentsSectionRequired isContinueCtaRequired /> - +
diff --git a/ui/src/views/Connectors/Destinations/EditDestinations/EditDestinations.tsx b/ui/src/views/Connectors/Destinations/EditDestinations/EditDestinations.tsx index 0aa3006a..00bfa6ca 100644 --- a/ui/src/views/Connectors/Destinations/EditDestinations/EditDestinations.tsx +++ b/ui/src/views/Connectors/Destinations/EditDestinations/EditDestinations.tsx @@ -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'; @@ -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(); @@ -157,14 +150,6 @@ const EditDestination = (): JSX.Element => { } }; - const templateOverrides: FormProps['templates'] = { - ObjectFieldTemplate: ObjectFieldTemplate, - TitleFieldTemplate: TitleFieldTemplate, - FieldTemplate: FieldTemplate, - BaseInputTemplate: BaseInputTemplate, - DescriptionFieldTemplate: DescriptionFieldTemplate, - }; - const EDIT_DESTINATION_STEP: Step[] = [ { name: 'Destinations', @@ -178,6 +163,8 @@ const EditDestination = (): JSX.Element => { if (isConnectorInfoLoading || isConnectorDefinitionLoading) return ; + const generatedSchema = generateUiSchema(connectorSchema?.connection_specification as RJSFSchema); + return ( @@ -217,18 +204,12 @@ const EditDestination = (): JSX.Element => { borderRadius='8px' marginBottom='100px' > -
handleOnTestClick(formData)} - onChange={({ formData }) => setFormData(formData)} - templates={templateOverrides} + onSubmit={(formData: FormData) => handleOnTestClick(formData)} + onChange={(formData: FormData) => setFormData(formData)} > { } /> - +
diff --git a/ui/src/views/Connectors/Sources/EditSource/EditSource.tsx b/ui/src/views/Connectors/Sources/EditSource/EditSource.tsx index ec010e81..eb024da8 100644 --- a/ui/src/views/Connectors/Sources/EditSource/EditSource.tsx +++ b/ui/src/views/Connectors/Sources/EditSource/EditSource.tsx @@ -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'; @@ -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(); @@ -44,7 +38,7 @@ const EditSource = (): JSX.Element => { queryKey: ['connectorInfo', sourceId], queryFn: () => getConnectorInfo(sourceId as string), refetchOnMount: true, - refetchOnWindowFocus: false, + refetchOnWindowFocus: true, enabled: !!sourceId, }); @@ -163,13 +157,7 @@ const EditSource = (): JSX.Element => { }, ]; - const templateOverrides: FormProps['templates'] = { - ObjectFieldTemplate: ObjectFieldTemplate, - TitleFieldTemplate: TitleFieldTemplate, - FieldTemplate: FieldTemplate, - BaseInputTemplate: BaseInputTemplate, - DescriptionFieldTemplate: DescriptionFieldTemplate, - }; + const generatedSchema = generateUiSchema(connectorSchema?.connection_specification as RJSFSchema); return ( @@ -212,18 +200,12 @@ const EditSource = (): JSX.Element => { border='1px' borderColor='gray.400' > -
handleOnTestClick(formData)} - onChange={({ formData }) => setFormData(formData)} - templates={templateOverrides} + onSubmit={(formData: FormData) => handleOnTestClick(formData)} + onChange={(formData: FormData) => setFormData(formData)} > { } /> - +
diff --git a/ui/src/views/Connectors/Sources/SourcesForm/SourceConfigForm/SourceConfigForm.tsx b/ui/src/views/Connectors/Sources/SourcesForm/SourceConfigForm/SourceConfigForm.tsx index 506002b9..02812123 100644 --- a/ui/src/views/Connectors/Sources/SourcesForm/SourceConfigForm/SourceConfigForm.tsx +++ b/ui/src/views/Connectors/Sources/SourcesForm/SourceConfigForm/SourceConfigForm.tsx @@ -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 @@ -78,26 +72,16 @@ const SourceConfigForm = (): JSX.Element | null => { const connectorSchema = data?.data?.connector_spec?.connection_specification; if (!connectorSchema) return null; - const templateOverrides: FormProps['templates'] = { - ObjectFieldTemplate: ObjectFieldTemplate, - TitleFieldTemplate: TitleFieldTemplate, - FieldTemplate: FieldTemplate, - BaseInputTemplate: BaseInputTemplate, - DescriptionFieldTemplate: DescriptionFieldTemplate, - }; + const generatedSchema = generateUiSchema(connectorSchema); return ( -
handleFormSubmit(formData)} + uiSchema={generatedSchema} + onSubmit={(formData: FormData) => handleFormSubmit(formData)} > { isDocumentsSectionRequired isBackRequired /> - +