diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectGDCSourceWidget/ConnectGDCSourceWidget.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectGDCSourceWidget/ConnectGDCSourceWidget.stories.tsx new file mode 100644 index 00000000000..709eadf2ae5 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectGDCSourceWidget/ConnectGDCSourceWidget.stories.tsx @@ -0,0 +1,32 @@ +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { ConnectGDCSourceWidget } from './ConnectGDCSourceWidget'; +import { ReactQueryDecorator } from '@/storybook/decorators/react-query'; +import { handlers } from '../../mocks/handlers.mock'; + +export default { + component: ConnectGDCSourceWidget, + decorators: [ReactQueryDecorator()], + parameters: { + msw: handlers(), + }, +} as ComponentMeta; + +export const CreateConnection: ComponentStory< + typeof ConnectGDCSourceWidget +> = () => { + return ( +
+ +
+ ); +}; + +export const EditConnection: ComponentStory< + typeof ConnectGDCSourceWidget +> = () => { + return ( +
+ +
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectGDCSourceWidget/ConnectGDCSourceWidget.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectGDCSourceWidget/ConnectGDCSourceWidget.tsx new file mode 100644 index 00000000000..eaeb2a0418b --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectGDCSourceWidget/ConnectGDCSourceWidget.tsx @@ -0,0 +1,185 @@ +import { DataSource, Feature } from '@/features/DataSource'; +import { useHttpClient } from '@/features/Network'; +import { OpenApi3Form } from '@/features/OpenApi3Form'; +import { Button } from '@/new-components/Button'; +import { transformSchemaToZodObject } from '@/features/OpenApi3Form/utils'; +import { InputField, useConsoleForm } from '@/new-components/Form'; +import { Tabs } from '@/new-components/Tabs'; +import { get } from 'lodash'; +import { useEffect, useState } from 'react'; +import { FaExclamationTriangle } from 'react-icons/fa'; +import { useQuery } from 'react-query'; +import { z, ZodSchema } from 'zod'; +import { graphQLCustomizationSchema } from '../GraphQLCustomization/schema'; +import { GraphQLCustomization } from '../GraphQLCustomization/GraphQLCustomization'; +import { useMetadata } from '@/features/hasura-metadata-api'; +import { adaptGraphQLCustomization } from '../GraphQLCustomization/utils/adaptResponse'; +import { generateGDCRequestPayload } from './utils/generateRequest'; +import { hasuraToast } from '@/new-components/Toasts'; +import { useManageDatabaseConnection } from '../../hooks/useManageDatabaseConnection'; +import { capitaliseFirstLetter } from '@/components/Common/ConfigureTransformation/utils'; + +interface ConnectGDCSourceWidgetProps { + driver: string; + dataSourceName?: string; +} + +const useFormValidationSchema = (driver: string) => { + const httpClient = useHttpClient(); + return useQuery({ + queryKey: ['form-schema', driver], + queryFn: async () => { + const configSchemas = await DataSource( + httpClient + ).connectDB.getConfigSchema(driver); + + if (!configSchemas || configSchemas === Feature.NotImplemented) + throw Error('Could not retrive config schema info for driver'); + + const validationSchema = z.object({ + name: z.string().min(1, 'Name is a required field!'), + configuration: transformSchemaToZodObject( + configSchemas.configSchema, + configSchemas.otherSchemas + ), + customization: graphQLCustomizationSchema.optional(), + }); + + return { validationSchema, configSchemas }; + }, + refetchOnWindowFocus: false, + }); +}; + +export const ConnectGDCSourceWidget = (props: ConnectGDCSourceWidgetProps) => { + const { driver, dataSourceName } = props; + const [tab, setTab] = useState('connection_details'); + + const { data: metadataSource } = useMetadata(m => + m.metadata.sources.find(source => source.name === dataSourceName) + ); + + const { createConnection, editConnection, isLoading } = + useManageDatabaseConnection({ + onSuccess: () => { + hasuraToast({ + type: 'success', + title: isEditMode + ? 'Database updated successfully!' + : 'Database added successfully!', + }); + }, + onError: err => { + hasuraToast({ + type: 'error', + title: 'An error occurred while adding database', + children: JSON.stringify(err), + }); + }, + }); + + const isEditMode = !!dataSourceName; + + const { data } = useFormValidationSchema(driver); + + const [schema, setSchema] = useState(z.any()); + + const { + Form, + methods: { formState, reset }, + } = useConsoleForm({ + schema, + }); + + useEffect(() => { + if (data?.validationSchema) setSchema(data.validationSchema); + }, [data?.validationSchema]); + + useEffect(() => { + if (metadataSource) + reset({ + name: metadataSource?.name, + // This is a particularly weird case with metadata only valid for GDC sources. + configuration: (metadataSource?.configuration as any).value, + customization: adaptGraphQLCustomization( + metadataSource?.customization ?? {} + ), + }); + }, [metadataSource, reset]); + + if (!data?.configSchemas) return null; + + const handleSubmit = (formValues: any) => { + const payload = generateGDCRequestPayload({ + driver, + values: formValues, + }); + + if (isEditMode) { + editConnection(payload); + } else { + createConnection(payload); + } + }; + + const connectionDetailsTabErrors = [ + get(formState.errors, 'name'), + get(formState.errors, 'configuration.connectionInfo'), + ].filter(Boolean); + + console.log(formState.errors); + + return ( +
+
+ {isEditMode + ? `Edit ${capitaliseFirstLetter(driver)} Connection` + : `Connect New ${capitaliseFirstLetter(driver)} Database`} +
+
+ setTab(value)} + items={[ + { + value: 'connection_details', + label: 'Connection Details', + icon: connectionDetailsTabErrors.length ? ( + + ) : undefined, + content: ( +
+ + +
+ ), + }, + { + value: 'customization', + label: 'GraphQL Customization', + content: , + }, + ]} + /> +
+ +
+ +
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectGDCSourceWidget/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectGDCSourceWidget/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectGDCSourceWidget/utils/generateRequest.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectGDCSourceWidget/utils/generateRequest.ts new file mode 100644 index 00000000000..5a5f7f7cd56 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectGDCSourceWidget/utils/generateRequest.ts @@ -0,0 +1,23 @@ +import { DatabaseConnection } from '@/features/ConnectDBRedesign/types'; +import { cleanEmpty } from '../../ConnectPostgresWidget/utils/helpers'; +import { generateGraphQLCustomizationInfo } from '../../GraphQLCustomization'; + +export const generateGDCRequestPayload = ({ + driver, + values, +}: { + driver: string; + values: any; +}): DatabaseConnection => { + const payload = { + driver, + details: { + name: values.name, + configuration: values.configuration, + customization: generateGraphQLCustomizationInfo( + values.customization ?? {} + ), + }, + }; + return cleanEmpty(payload); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/GraphQLCustomization/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/GraphQLCustomization/index.ts new file mode 100644 index 00000000000..04f32e4d7b7 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/GraphQLCustomization/index.ts @@ -0,0 +1,7 @@ +export { GraphQLCustomization } from './GraphQLCustomization'; +export { adaptGraphQLCustomization } from './utils/adaptResponse'; +export { generateGraphQLCustomizationInfo } from './utils/generateRequest'; +export { + GraphQLCustomizationSchema, + graphQLCustomizationSchema, +} from './schema'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/mocks/handlers.mock.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/mocks/handlers.mock.ts index ce7d5ec98d5..77601a64652 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/mocks/handlers.mock.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/mocks/handlers.mock.ts @@ -6,6 +6,21 @@ const mockMetadata: Metadata = { metadata: { version: 3, sources: [ + { + name: 'sqlite_test', + kind: 'sqlite', + tables: [], + configuration: { + template: null, + timeout: null, + value: { + db: './chinook.db', + explicit_main_schema: false, + include_sqlite_meta_tables: false, + tables: ['Album', 'Artist', 'Genre', 'Track'], + }, + }, + }, { name: 'chinook', kind: 'postgres', @@ -77,6 +92,93 @@ const mockMetadata: Metadata = { }, }; +export const mockCapabilitiesResponse = { + capabilities: { + comparisons: { subquery: { supports_relations: true } }, + data_schema: { supports_foreign_keys: true, supports_primary_keys: true }, + explain: {}, + queries: {}, + raw: {}, + relationships: {}, + scalar_types: { + DateTime: { + comparison_operators: { _in_year: 'int' }, + graphql_type: 'String', + }, + bool: { + comparison_operators: { + _and: 'bool', + _nand: 'bool', + _or: 'bool', + _xor: 'bool', + }, + graphql_type: 'Boolean', + }, + decimal: { + aggregate_functions: { max: 'decimal', min: 'decimal', sum: 'decimal' }, + comparison_operators: { _modulus_is_zero: 'decimal' }, + graphql_type: 'Float', + update_column_operators: { + dec: { argument_type: 'decimal' }, + inc: { argument_type: 'decimal' }, + }, + }, + number: { + aggregate_functions: { max: 'number', min: 'number', sum: 'number' }, + comparison_operators: { _modulus_is_zero: 'number' }, + graphql_type: 'Float', + update_column_operators: { + dec: { argument_type: 'number' }, + inc: { argument_type: 'number' }, + }, + }, + string: { + aggregate_functions: { max: 'string', min: 'string' }, + comparison_operators: { _glob: 'string', _like: 'string' }, + graphql_type: 'String', + }, + }, + }, + config_schema_response: { + config_schema: { + nullable: false, + properties: { + DEBUG: { + additionalProperties: true, + description: 'For debugging.', + nullable: true, + type: 'object', + }, + db: { description: 'The SQLite database file to use.', type: 'string' }, + explicit_main_schema: { + default: false, + description: "Prefix all tables with the 'main' schema", + nullable: true, + type: 'boolean', + }, + include_sqlite_meta_tables: { + description: + 'By default index tables, etc are not included, set this to true to include them.', + nullable: true, + type: 'boolean', + }, + tables: { + description: + 'List of tables to make available in the schema and for querying', + items: { $ref: '#/other_schemas/TableName' }, + nullable: true, + type: 'array', + }, + }, + required: ['db'], + type: 'object', + }, + other_schemas: { TableName: { nullable: false, type: 'string' } }, + }, + display_name: 'Hasura SQLite', + options: { uri: 'http://host.docker.internal:8100' }, +}; + export const handlers = () => [ rest.post('http://localhost:8080/v1/metadata', (req, res, ctx) => { const requestBody = req.body as Record; @@ -84,6 +186,9 @@ export const handlers = () => [ if (requestBody.type === 'export_metadata') return res(ctx.json(mockMetadata)); + if (requestBody.type === 'get_source_kind_capabilities') + return res(ctx.json(mockCapabilitiesResponse)); + return res(ctx.json({})); }), rest.get(`http://localhost:8080/v1alpha1/config`, (req, res, ctx) => { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9e852934459..bb61105d53b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -95339,7 +95339,7 @@ "@emotion/core": "^10.0.22", "@mdx-js/react": "^1.5.2", "codemirror": "5.51.0", - "codemirror-graphql": "0.12.2", + "codemirror-graphql": "^0.12.0-alpha.0", "copy-to-clipboard": "^3.2.0", "entities": "^2.0.0", "markdown-it": "^10.0.0", diff --git a/frontend/tsconfig.base.json b/frontend/tsconfig.base.json index d2a8ec403de..9cb1e27dd9c 100644 --- a/frontend/tsconfig.base.json +++ b/frontend/tsconfig.base.json @@ -35,11 +35,11 @@ "libs/console/legacy-ce/src/exports/main.js" ], "@hasura/internal-plugin": ["libs/nx/internal-plugin/src/index.ts"], - "unplugin-dynamic-asset-loader": [ - "libs/nx/unplugin-dynamic-asset-loader/src/index.ts" - ], "storybook-addon-console-env": [ "libs/nx/storybook-addon-console-env/src/index.ts" + ], + "unplugin-dynamic-asset-loader": [ + "libs/nx/unplugin-dynamic-asset-loader/src/index.ts" ] } }, diff --git a/frontend/workspace.json b/frontend/workspace.json index fa15b1bc9b8..ebc5cf70d2d 100644 --- a/frontend/workspace.json +++ b/frontend/workspace.json @@ -10,7 +10,7 @@ "console-legacy-ee": "libs/console/legacy-ee", "nx-internal-plugin": "libs/nx/internal-plugin", "nx-internal-plugin-e2e": "apps/nx/internal-plugin-e2e", - "nx-unplugin-dynamic-asset-loader": "libs/nx/unplugin-dynamic-asset-loader", - "nx-storybook-addon-console-env": "libs/nx/storybook-addon-console-env" + "nx-storybook-addon-console-env": "libs/nx/storybook-addon-console-env", + "nx-unplugin-dynamic-asset-loader": "libs/nx/unplugin-dynamic-asset-loader" } }