diff --git a/console/src/features/ConnectDB/components/Configuration.tsx b/console/src/features/ConnectDB/components/Configuration.tsx index cd2915eb459..4281b428588 100644 --- a/console/src/features/ConnectDB/components/Configuration.tsx +++ b/console/src/features/ConnectDB/components/Configuration.tsx @@ -3,7 +3,12 @@ import { useHttpClient } from '@/features/Network'; import { useQuery } from 'react-query'; import { useFormContext } from 'react-hook-form'; -import { DataSource, SupportedDrivers, Feature } from '@/features/DataSource'; +import { + DataSource, + SupportedDrivers, + Feature, + isFreeFormObjectField, +} from '@/features/DataSource'; import { IndicatorCard } from '@/new-components/IndicatorCard'; import { Field } from './Fields'; @@ -47,7 +52,11 @@ export const Configuration = ({ name }: Props) => { return Loading configuration info...; } - if (!schema || schema.configSchema.type !== 'object') + if ( + !schema || + schema.configSchema.type !== 'object' || + isFreeFormObjectField(schema.configSchema) + ) return ( Unable to find a valid schema for the {driver} diff --git a/console/src/features/ConnectDB/components/Fields/ArrayInputs.tsx b/console/src/features/ConnectDB/components/Fields/ArrayInputs.tsx new file mode 100644 index 00000000000..2a2356a66a7 --- /dev/null +++ b/console/src/features/ConnectDB/components/Fields/ArrayInputs.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import get from 'lodash.get'; + +import { utils } from '@/features/DataSource'; + +import type { OneOf, Property, Ref } from '@/features/DataSource'; + +import { BasicInput } from './BasicInput'; +import { ObjectArray } from './ObjectArray'; + +const { isRef } = utils; + +interface Props { + name: string; + property: Property & { type: 'array' }; + otherSchemas: Record; +} + +interface ObjectWithProperties { + type: 'object'; + properties: Record; +} + +interface ObjectWithAdditionalProperties { + type: 'object'; + additionalProperties: true; + nullable?: boolean; +} + +type Items = ObjectWithProperties | ObjectWithAdditionalProperties; + +export const isFreeFormArrayItemType = ( + items: Items +): items is { + type: 'object'; + additionalProperties: true; +} => { + if (!('additionalProperties' in items)) return false; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const additionalProperties = items.additionalProperties; + + return true; +}; + +type GetItemsArgs = { + property: Property & { type: 'array' }; + otherSchemas: Record; +}; + +export const getItems = ({ property, otherSchemas }: GetItemsArgs) => { + console.log('getItems', property, otherSchemas); + return isRef(property.items) + ? get(otherSchemas, property.items.$ref.split('/').slice(2).join('.')) + : property.items; +}; + +export const ArrayInput = ({ name, property, otherSchemas }: Props) => { + const items = getItems({ property, otherSchemas }); + + if ( + items.type === 'string' || + items.type === 'number' || + (items.type === 'object' && isFreeFormArrayItemType(items)) + ) + return ( + + ); + + if (items.type === 'object' && !isFreeFormArrayItemType(items)) + return ( + + ); + + return null; // anything outside of the above is not supported +}; diff --git a/console/src/features/ConnectDB/components/Fields/BasicInput.tsx b/console/src/features/ConnectDB/components/Fields/BasicInput.tsx new file mode 100644 index 00000000000..5e836b7c5dc --- /dev/null +++ b/console/src/features/ConnectDB/components/Fields/BasicInput.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { useFieldArray, useFormContext, useWatch } from 'react-hook-form'; + +import { Button } from '@/new-components/Button'; +import { CodeEditorField, InputField } from '@/new-components/Form'; + +interface Props { + name: string; + label: string; + type: 'string' | 'number' | 'object'; +} + +export const BasicInput = ({ name, label, type }: Props) => { + const { fields, append, remove } = useFieldArray({ + name: `basic-array-input-${name}`, + }); + + const { setValue } = useFormContext(); + + const formValues: string[] = useWatch({ name }); + + return ( +
+ +
+ {fields.map((_, fieldIndex) => ( +
+
+ {type === 'object' ? ( + + ) : ( + + )} +
+
+ +
+
+ ))} + +
+
+ ); +}; diff --git a/console/src/features/ConnectDB/components/Fields/ObjectArray.tsx b/console/src/features/ConnectDB/components/Fields/ObjectArray.tsx new file mode 100644 index 00000000000..7f58e3ffe01 --- /dev/null +++ b/console/src/features/ConnectDB/components/Fields/ObjectArray.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useFieldArray, useFormContext, useWatch } from 'react-hook-form'; + +import { OneOf, Property, Ref } from '@/features/DataSource'; +import { Button } from '@/new-components/Button'; +import { RenderProperty } from './RenderProperty'; + +interface Props { + items: { + type: 'object'; + properties: Record; + }; + name: string; + otherSchemas: Record; + label: string; +} + +export const ObjectArray = ({ items, name, otherSchemas, label }: Props) => { + const { fields, append, remove } = useFieldArray({ + name: `${name}-object-array-input`, + }); + + const { setValue } = useFormContext(); + + const formValues: Record[] = useWatch({ name }); + + return ( +
+ +
+ {fields.map((_, index) => ( +
+ +
+ +
+
+ ))} +
+ +
+
+
+ ); +}; diff --git a/console/src/features/ConnectDB/components/Fields/RenderProperty.tsx b/console/src/features/ConnectDB/components/Fields/RenderProperty.tsx index 3be69ef9f47..8bd7ca09e44 100644 --- a/console/src/features/ConnectDB/components/Fields/RenderProperty.tsx +++ b/console/src/features/ConnectDB/components/Fields/RenderProperty.tsx @@ -1,11 +1,13 @@ import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; +// eslint-disable-next-line no-restricted-imports +import { isFreeFormObjectField } from '@/features/DataSource/types'; import { Property } from '@/features/DataSource'; - import { Switch } from '@/new-components/Switch'; -import { InputField, Select } from '@/new-components/Form'; +import { CodeEditorField, InputField, Select } from '@/new-components/Form'; import { Collapse } from '@/new-components/Collapse'; import { Field } from './Fields'; +import { ArrayInput } from './ArrayInputs'; interface RenderPropertyProps { name: string; @@ -64,6 +66,18 @@ export const RenderProperty = ({ ); case 'object': + if (isFreeFormObjectField(property)) { + return ( +
+ +
+ ); + } + if (property.nullable) { // if any of the values are set when editing the form open the collapse const existingValues = watch(name); @@ -110,6 +124,15 @@ export const RenderProperty = ({ })} ); + case 'array': + return ( + + ); + default: throw Error('Case not handled'); } diff --git a/console/src/features/DataSource/common/createZodSchema.ts b/console/src/features/DataSource/common/createZodSchema.ts index a6e9f635989..8d56aa7f36f 100644 --- a/console/src/features/DataSource/common/createZodSchema.ts +++ b/console/src/features/DataSource/common/createZodSchema.ts @@ -1,7 +1,7 @@ import pickBy from 'lodash.pickby'; import { z, ZodSchema } from 'zod'; -import type { OneOf, Property, Ref } from '../types'; +import { isFreeFormObjectField, OneOf, Property, Ref } from '../types'; import { getProperty, isOneOf, isRef } from './utils'; function createZodObject( @@ -71,7 +71,43 @@ export function createZodSchema( .transform(x => parseInt(x, 10) || ''); return z.string().transform(x => parseInt(x, 10) || ''); + case 'array': + if (!isRef(property.items)) { + if (property.items.type === 'string') { + if (property.nullable === false) + return z + .array(z.string()) + .min( + 1, + property.description + ? `${property.description} is Required` + : 'Required!' + ); + + return z.array(z.string()).optional(); + } + + if (property.items.type === 'number') { + if (property.nullable === false) { + return z + .array(z.string()) + .min(1, 'Required') + .transform(x => x.map(y => parseInt(y, 10) || '')); + } + return z + .array(z.string()) + .transform(x => x.map(y => parseInt(y, 10) || '')) + .optional(); + } + } + + return z.any(); + case 'object': + if (isFreeFormObjectField(property)) { + return z.any(); // any valid json + } + const propertiesArray = Object.entries(property.properties); const zodObject = createZodObject(propertiesArray, otherSchemas); diff --git a/console/src/features/DataSource/postgres/introspection/getDatabaseConfiguration.ts b/console/src/features/DataSource/postgres/introspection/getDatabaseConfiguration.ts index 8a7c03259dd..3718f2302ab 100644 --- a/console/src/features/DataSource/postgres/introspection/getDatabaseConfiguration.ts +++ b/console/src/features/DataSource/postgres/introspection/getDatabaseConfiguration.ts @@ -8,12 +8,16 @@ export const getDatabaseConfiguration = async () => { configSchema: { type: 'object', nullable: false, - description: 'Configuration', + description: 'Configuration (complex object example)', properties: { connection_info: { $ref: '#/otherSchemas/ConnectionInfo' }, }, }, otherSchemas: { + TableName: { + nullable: false, + type: 'string', + }, PoolSettings: { type: 'object', description: 'Pool Settings', @@ -119,6 +123,91 @@ export const getDatabaseConfiguration = async () => { nullable: true, enum: ['read-committed', 'repeatable-read', 'serializable'], }, + list_of_table_names: { + type: 'array', + description: 'Tables List (This is an array of strings input)', + items: { + type: 'string', + }, + }, + list_of_ports: { + type: 'array', + description: 'Ports List (This is an array of number input)', + items: { + type: 'number', + }, + }, + ref_array_input: { + description: + 'List of tables (This is an array of strings input but the definition is a ref)', + type: 'array', + items: { + $ref: '#/otherSchemas/TableName', + }, + nullable: true, + }, + list_of_connections: { + description: + 'List of multiple connections (This is a array of objects)', + type: 'array', + items: { + type: 'object', + properties: { + username: { + description: 'Username', + type: 'string', + nullable: false, + }, + password: { + description: 'Password', + type: 'string', + nullable: false, + }, + host: { + description: 'Host', + type: 'string', + nullable: false, + }, + database: { + description: 'Database', + type: 'string', + nullable: false, + }, + port: { + description: 'Port', + type: 'number', + nullable: false, + }, + }, + }, + nullable: true, + }, + DEBUG: { + description: 'For debugging (Free form JSON field example)', + type: 'object', + additionalProperties: true, + nullable: true, + }, + DEBUG_array: { + description: + 'For debugging (Free form array of JSON field example)', + type: 'array', + items: { + type: 'object', + additionalProperties: true, + nullable: true, + }, + nullable: true, + }, + ref_array_object_input: { + description: + 'List of multiple connections (This is a array of objects, object is a ref)', + type: 'array', + items: { + $ref: '#/otherSchemas/ConnectionInfo', + }, + nullable: true, + }, }, }, }, diff --git a/console/src/features/DataSource/types.ts b/console/src/features/DataSource/types.ts index 8bce1b51753..405992b826e 100644 --- a/console/src/features/DataSource/types.ts +++ b/console/src/features/DataSource/types.ts @@ -6,14 +6,32 @@ export type Ref = { $ref: string }; export type OneOf = { oneOf: (Property | Ref)[]; description?: string }; +export const isFreeFormObjectField = ( + property: Property & { type: 'object' } +): property is Property & { + type: 'object'; + additionalProperties: true; +} => { + if (!('additionalProperties' in property)) return false; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const additionalProperties = property.additionalProperties; + + return true; +}; + export type Property = { description?: string; - nullable: boolean; + nullable?: boolean; } & ( | { type: 'object'; properties: Record; } + | { + type: 'object'; + additionalProperties: true; + } | { type: 'string'; enum?: string[]; @@ -24,6 +42,21 @@ export type Property = { | { type: 'boolean'; } + | { + type: 'array'; + items: + | { type: 'string' | 'number' } + | { + type: 'object'; + properties: Record; + } + | { + type: 'object'; + additionalProperties: true; + nullable?: boolean; + } + | Ref; + } ); // export type supportedDrivers = 'postgres' | 'mssql' | 'bigquery' | 'citus' | 'cockroach' | 'gdc'; diff --git a/console/src/new-components/Form/FieldWrapper.tsx b/console/src/new-components/Form/FieldWrapper.tsx index 94a789b9f1c..6a3d3476f27 100644 --- a/console/src/new-components/Form/FieldWrapper.tsx +++ b/console/src/new-components/Form/FieldWrapper.tsx @@ -105,35 +105,38 @@ export const FieldWrapper = (props: FieldWrapperProps) => { : 'max-w-xl' )} > - + {description ? ( + + {description} + + ) : null} + + ) : null} +
{children}
{error ? (