mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
feature (console): add GDC connect DB widget
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7893 Co-authored-by: Matthew Goodwin <49927862+m4ttheweric@users.noreply.github.com> GitOrigin-RevId: c0b5fa4b89a83b953781f78025e7da9c29843c74
This commit is contained in:
parent
fd0ba0013d
commit
50627a3af2
@ -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<typeof ConnectGDCSourceWidget>;
|
||||||
|
|
||||||
|
export const CreateConnection: ComponentStory<
|
||||||
|
typeof ConnectGDCSourceWidget
|
||||||
|
> = () => {
|
||||||
|
return (
|
||||||
|
<div className="max-w-3xl">
|
||||||
|
<ConnectGDCSourceWidget driver="sqlite" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditConnection: ComponentStory<
|
||||||
|
typeof ConnectGDCSourceWidget
|
||||||
|
> = () => {
|
||||||
|
return (
|
||||||
|
<div className="max-w-3xl">
|
||||||
|
<ConnectGDCSourceWidget driver="sqlite" dataSourceName="sqlite_test" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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<ZodSchema>(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 (
|
||||||
|
<div>
|
||||||
|
<div className="text-xl text-gray-600 font-semibold">
|
||||||
|
{isEditMode
|
||||||
|
? `Edit ${capitaliseFirstLetter(driver)} Connection`
|
||||||
|
: `Connect New ${capitaliseFirstLetter(driver)} Database`}
|
||||||
|
</div>
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<Tabs
|
||||||
|
value={tab}
|
||||||
|
onValueChange={value => setTab(value)}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
value: 'connection_details',
|
||||||
|
label: 'Connection Details',
|
||||||
|
icon: connectionDetailsTabErrors.length ? (
|
||||||
|
<FaExclamationTriangle className="text-red-800" />
|
||||||
|
) : undefined,
|
||||||
|
content: (
|
||||||
|
<div className="mt-sm">
|
||||||
|
<InputField
|
||||||
|
name="name"
|
||||||
|
label="Database Name"
|
||||||
|
placeholder="Database name"
|
||||||
|
/>
|
||||||
|
<OpenApi3Form
|
||||||
|
name="configuration"
|
||||||
|
schemaObject={data?.configSchemas.configSchema}
|
||||||
|
references={data?.configSchemas.otherSchemas}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'customization',
|
||||||
|
label: 'GraphQL Customization',
|
||||||
|
content: <GraphQLCustomization name="customization" />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
mode="primary"
|
||||||
|
isLoading={isLoading}
|
||||||
|
loadingText="Saving"
|
||||||
|
>
|
||||||
|
{isEditMode ? 'Update Connection' : 'Connect Database'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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);
|
||||||
|
};
|
@ -0,0 +1,7 @@
|
|||||||
|
export { GraphQLCustomization } from './GraphQLCustomization';
|
||||||
|
export { adaptGraphQLCustomization } from './utils/adaptResponse';
|
||||||
|
export { generateGraphQLCustomizationInfo } from './utils/generateRequest';
|
||||||
|
export {
|
||||||
|
GraphQLCustomizationSchema,
|
||||||
|
graphQLCustomizationSchema,
|
||||||
|
} from './schema';
|
@ -6,6 +6,21 @@ const mockMetadata: Metadata = {
|
|||||||
metadata: {
|
metadata: {
|
||||||
version: 3,
|
version: 3,
|
||||||
sources: [
|
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',
|
name: 'chinook',
|
||||||
kind: 'postgres',
|
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 = () => [
|
export const handlers = () => [
|
||||||
rest.post('http://localhost:8080/v1/metadata', (req, res, ctx) => {
|
rest.post('http://localhost:8080/v1/metadata', (req, res, ctx) => {
|
||||||
const requestBody = req.body as Record<string, any>;
|
const requestBody = req.body as Record<string, any>;
|
||||||
@ -84,6 +186,9 @@ export const handlers = () => [
|
|||||||
if (requestBody.type === 'export_metadata')
|
if (requestBody.type === 'export_metadata')
|
||||||
return res(ctx.json(mockMetadata));
|
return res(ctx.json(mockMetadata));
|
||||||
|
|
||||||
|
if (requestBody.type === 'get_source_kind_capabilities')
|
||||||
|
return res(ctx.json(mockCapabilitiesResponse));
|
||||||
|
|
||||||
return res(ctx.json({}));
|
return res(ctx.json({}));
|
||||||
}),
|
}),
|
||||||
rest.get(`http://localhost:8080/v1alpha1/config`, (req, res, ctx) => {
|
rest.get(`http://localhost:8080/v1alpha1/config`, (req, res, ctx) => {
|
||||||
|
2
frontend/package-lock.json
generated
2
frontend/package-lock.json
generated
@ -95339,7 +95339,7 @@
|
|||||||
"@emotion/core": "^10.0.22",
|
"@emotion/core": "^10.0.22",
|
||||||
"@mdx-js/react": "^1.5.2",
|
"@mdx-js/react": "^1.5.2",
|
||||||
"codemirror": "5.51.0",
|
"codemirror": "5.51.0",
|
||||||
"codemirror-graphql": "0.12.2",
|
"codemirror-graphql": "^0.12.0-alpha.0",
|
||||||
"copy-to-clipboard": "^3.2.0",
|
"copy-to-clipboard": "^3.2.0",
|
||||||
"entities": "^2.0.0",
|
"entities": "^2.0.0",
|
||||||
"markdown-it": "^10.0.0",
|
"markdown-it": "^10.0.0",
|
||||||
|
@ -35,11 +35,11 @@
|
|||||||
"libs/console/legacy-ce/src/exports/main.js"
|
"libs/console/legacy-ce/src/exports/main.js"
|
||||||
],
|
],
|
||||||
"@hasura/internal-plugin": ["libs/nx/internal-plugin/src/index.ts"],
|
"@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": [
|
"storybook-addon-console-env": [
|
||||||
"libs/nx/storybook-addon-console-env/src/index.ts"
|
"libs/nx/storybook-addon-console-env/src/index.ts"
|
||||||
|
],
|
||||||
|
"unplugin-dynamic-asset-loader": [
|
||||||
|
"libs/nx/unplugin-dynamic-asset-loader/src/index.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"console-legacy-ee": "libs/console/legacy-ee",
|
"console-legacy-ee": "libs/console/legacy-ee",
|
||||||
"nx-internal-plugin": "libs/nx/internal-plugin",
|
"nx-internal-plugin": "libs/nx/internal-plugin",
|
||||||
"nx-internal-plugin-e2e": "apps/nx/internal-plugin-e2e",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user