feature (console): add BigQuery connect DB widget

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7884
Co-authored-by: Matthew Goodwin <49927862+m4ttheweric@users.noreply.github.com>
GitOrigin-RevId: 86a6b490cd4b8ed2a6b1f8d9f85f58f91debe33b
This commit is contained in:
Vijay Prasanna 2023-02-14 13:47:58 +05:30 committed by hasura-bot
parent ef532b5534
commit 31d2542cd2
11 changed files with 503 additions and 0 deletions

View File

@ -0,0 +1,32 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { ConnectBigQueryWidget } from './ConnectBigQueryWidget';
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
import { handlers } from '../../mocks/handlers.mock';
export default {
component: ConnectBigQueryWidget,
decorators: [ReactQueryDecorator()],
parameters: {
msw: handlers(),
},
} as ComponentMeta<typeof ConnectBigQueryWidget>;
export const CreateConnection: ComponentStory<
typeof ConnectBigQueryWidget
> = () => {
return (
<div className="max-w-3xl">
<ConnectBigQueryWidget />
</div>
);
};
export const EditConnection: ComponentStory<
typeof ConnectBigQueryWidget
> = () => {
return (
<div className="max-w-3xl">
<ConnectBigQueryWidget dataSourceName="test_source_bq" />
</div>
);
};

View File

@ -0,0 +1,134 @@
import { InputField, useConsoleForm } from '@/new-components/Form';
import { Tabs } from '@/new-components/Tabs';
import { Button } from '@/new-components/Button';
import { useEffect, useState } from 'react';
import { GraphQLCustomization } from '../GraphQLCustomization/GraphQLCustomization';
import { Configuration } from './parts/Configuration';
import { getDefaultValues, BigQueryConnectionSchema, schema } from './schema';
import { get } from 'lodash';
import { FaExclamationTriangle } from 'react-icons/fa';
import { useManageDatabaseConnection } from '../../hooks/useManageDatabaseConnection';
import { hasuraToast } from '@/new-components/Toasts';
import { useMetadata } from '@/features/hasura-metadata-api';
import { generatePostgresRequestPayload } from './utils/generateRequests';
interface ConnectBigQueryWidgetProps {
dataSourceName?: string;
}
export const ConnectBigQueryWidget = (props: ConnectBigQueryWidgetProps) => {
const { dataSourceName } = props;
const isEditMode = !!dataSourceName;
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 successful!'
: 'Database added successfully!',
});
},
onError: err => {
hasuraToast({
type: 'error',
title: 'Error while adding database',
children: JSON.stringify(err),
});
},
});
const handleSubmit = (formValues: BigQueryConnectionSchema) => {
const payload = generatePostgresRequestPayload({
driver: 'bigquery',
values: formValues,
});
if (isEditMode) {
editConnection(payload);
} else {
createConnection(payload);
}
};
const [tab, setTab] = useState('connection_details');
const {
Form,
methods: { formState, reset },
} = useConsoleForm({
schema,
});
useEffect(() => {
try {
reset(getDefaultValues(metadataSource));
} catch (err) {
hasuraToast({
type: 'error',
title:
'Error while retriving database. Please check if the database is of type postgres',
});
}
}, [metadataSource, reset]);
const connectionDetailsTabErrors = [
get(formState.errors, 'name'),
get(formState.errors, 'configuration.connectionInfo'),
].filter(Boolean);
return (
<div>
<div className="text-xl text-gray-600 font-semibold">
{isEditMode
? 'Edit BigQuery Connection'
: 'Connect New BigQuery 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 display name"
placeholder="Database name"
/>
<Configuration name="configuration" />
</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>
);
};

View File

@ -0,0 +1,31 @@
import { SimpleForm } from '@/new-components/Form';
import { Button } from '@/new-components/Button';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { z } from 'zod';
import { Configuration } from './Configuration';
export default {
component: Configuration,
} as ComponentMeta<typeof Configuration>;
export const Primary: ComponentStory<typeof Configuration> = () => (
<SimpleForm
onSubmit={data => console.log(data)}
schema={z.any()}
options={{
defaultValues: {
details: {
databaseUrl: {
connectionType: 'databaseUrl',
},
},
},
}}
>
<Configuration name="connectionInfo" />
<Button type="submit" className="my-2">
Submit
</Button>
</SimpleForm>
);

View File

@ -0,0 +1,19 @@
import { Datasets } from './Datasets';
import { ProjectId } from './ProjectId';
import { ServiceAccount } from './ServiceAccount';
export const Configuration = ({ name }: { name: string }) => {
return (
<div className="my-2">
<div className="my-2">
<ServiceAccount name={`${name}.serviceAccount`} />
</div>
<div className="my-2">
<ProjectId name={`${name}.projectId`} />
</div>
<div className="my-2">
<Datasets name={`${name}.datasets`} />
</div>
</div>
);
};

View File

@ -0,0 +1,45 @@
import { InputField, Radio } from '@/new-components/Form';
import { useFormContext } from 'react-hook-form';
import { BigQueryConnectionSchema } from '../schema';
export const Datasets = ({ name }: { name: string }) => {
const options = [
{ value: 'valeu', label: 'Datasets' },
{ value: 'envVar', label: 'Enviromnent variable' },
];
const { watch } =
useFormContext<
Record<string, BigQueryConnectionSchema['configuration']['datasets']>
>();
const connectionType = watch(`${name}.type`);
return (
<div className="bg-white border border-hasGray-300 rounded-md shadow-sm overflow-hidden p-4">
<div className="bg-white py-1.5 font-semibold">
<Radio
name={`${name}.type`}
label="Datasets"
options={options}
orientation="horizontal"
tooltip="Enviroment variable recomennded"
/>
</div>
{connectionType === 'value' ? (
<InputField
name={`${name}.value`}
label="Datasets"
placeholder="dataset_1,dataset_2"
/>
) : (
<InputField
name={`${name}.envVar`}
label="Environment variable"
placeholder="HASURA_GRAPHQL_DB_URL_FROM_ENV"
/>
)}
</div>
);
};

View File

@ -0,0 +1,45 @@
import { InputField, Radio } from '@/new-components/Form';
import { useFormContext } from 'react-hook-form';
import { BigQueryConnectionSchema } from '../schema';
export const ProjectId = ({ name }: { name: string }) => {
const options = [
{ value: 'value', label: 'Project ID value' },
{ value: 'envVar', label: 'Enviromnent variable' },
];
const { watch } =
useFormContext<
Record<string, BigQueryConnectionSchema['configuration']['projectId']>
>();
const connectionType = watch(`${name}.type`);
return (
<div className="bg-white border border-hasGray-300 rounded-md shadow-sm overflow-hidden p-4">
<div className="bg-white py-1.5 font-semibold">
<Radio
name={`${name}.type`}
label="Project ID"
options={options}
orientation="horizontal"
tooltip="Enviroment variable recomennded"
/>
</div>
{connectionType === 'value' ? (
<InputField
name={`${name}.value`}
label="Project ID"
placeholder="Project ID"
/>
) : (
<InputField
name={`${name}.envVar`}
label="Environment variable"
placeholder="HASURA_GRAPHQL_DB_URL_FROM_ENV"
/>
)}
</div>
);
};

View File

@ -0,0 +1,48 @@
import { InputField, Radio } from '@/new-components/Form';
import { useFormContext } from 'react-hook-form';
import { BigQueryConnectionSchema } from '../schema';
export const ServiceAccount = ({ name }: { name: string }) => {
const options = [
{ value: 'serviceAccountKey', label: 'Service Account Key' },
{ value: 'envVar', label: 'Enviromnent variable' },
];
const { watch } =
useFormContext<
Record<
string,
BigQueryConnectionSchema['configuration']['serviceAccount']
>
>();
const connectionType = watch(`${name}.type`);
return (
<div className="bg-white border border-hasGray-300 rounded-md shadow-sm overflow-hidden p-4">
<div className="bg-white py-1.5 font-semibold">
<Radio
name={`${name}.type`}
label="Connect Database via"
options={options}
orientation="horizontal"
tooltip="Enviroment variable recomennded"
/>
</div>
{connectionType === 'serviceAccountKey' ? (
<InputField
name={`${name}.value`}
label="Database URL"
placeholder=""
/>
) : (
<InputField
name={`${name}.envVar`}
label="Environment variable"
placeholder="HASURA_GRAPHQL_DB_URL_FROM_ENV"
/>
)}
</div>
);
};

View File

@ -0,0 +1,69 @@
import { z } from 'zod';
import { graphQLCustomizationSchema } from '../GraphQLCustomization/schema';
import { Source } from '@/features/hasura-metadata-types';
import { adaptPostgresConnection } from './utils/adaptResponse';
export const schema = z.object({
name: z.string().min(1, 'Database display name is a required field'),
configuration: z.object({
serviceAccount: z.discriminatedUnion('type', [
z.object({
type: z.literal('envVar'),
envVar: z.string().min(1, 'Env variable cannot be empty'),
}),
z.object({
type: z.literal('serviceAccountKey'),
value: z.string().min(1, 'Service account key cannot be empty'),
}),
]),
projectId: z.discriminatedUnion('type', [
z.object({
type: z.literal('envVar'),
envVar: z.string().min(1, 'Env variable cannot be empty'),
}),
z.object({
type: z.literal('value'),
value: z.string().min(1, 'Project ID cannot be empty'),
}),
]),
datasets: z.discriminatedUnion('type', [
z.object({
type: z.literal('envVar'),
envVar: z.string().min(1, 'Env variable cannot be empty'),
}),
z.object({
type: z.literal('value'),
value: z.string().min(1, 'Service account key cannot be empty'),
}),
]),
}),
customization: graphQLCustomizationSchema.optional(),
});
export const getDefaultValues = (
metadataSource?: Source
): BigQueryConnectionSchema => {
// if there is no exisiting connection, then return this template as default
if (!metadataSource)
return {
name: '',
configuration: {
serviceAccount: {
type: 'envVar',
envVar: '',
},
projectId: {
type: 'value',
value: '',
},
datasets: {
type: 'value',
value: '',
},
},
};
return adaptPostgresConnection(metadataSource);
};
export type BigQueryConnectionSchema = z.infer<typeof schema>;

View File

@ -0,0 +1,43 @@
import {
BigQueryConfiguration,
Source,
} from '@/features/hasura-metadata-types';
import { isArray } from 'lodash';
import { adaptGraphQLCustomization } from '../../GraphQLCustomization/utils/adaptResponse';
import { BigQueryConnectionSchema } from '../schema';
export const adaptPostgresConnection = (
metadataSource: Source
): BigQueryConnectionSchema => {
if (metadataSource.kind !== 'bigquery')
throw Error('Not a bigquery connection');
// This assertion is safe because of the check above.
const configuration = metadataSource.configuration as BigQueryConfiguration;
return {
name: metadataSource.name,
configuration: {
serviceAccount:
typeof configuration.service_account == 'string'
? {
type: 'serviceAccountKey',
value: configuration.service_account,
}
: {
type: 'envVar',
envVar: configuration.service_account.from_env,
},
projectId:
typeof configuration.project_id === 'string'
? { type: 'value', value: configuration.project_id }
: { type: 'envVar', envVar: configuration.project_id.from_env },
datasets: isArray(configuration.datasets)
? { type: 'value', value: configuration.datasets.join() }
: { type: 'envVar', envVar: configuration.datasets.from_env },
},
customization: adaptGraphQLCustomization(
metadataSource.customization ?? {}
),
};
};

View File

@ -0,0 +1,37 @@
import { DatabaseConnection } from '@/features/ConnectDBRedesign/types';
import { generateGraphQLCustomizationInfo } from '../../GraphQLCustomization/utils/generateRequest';
import { BigQueryConnectionSchema } from '../schema';
import { cleanEmpty } from '../../ConnectPostgresWidget/utils/helpers';
export const generatePostgresRequestPayload = ({
driver,
values,
}: {
driver: string;
values: BigQueryConnectionSchema;
}): DatabaseConnection => {
const payload = {
driver,
details: {
name: values.name,
configuration: {
service_account:
values.configuration.serviceAccount.type === 'envVar'
? { from_env: values.configuration.serviceAccount.envVar }
: values.configuration.serviceAccount.value,
project_id:
values.configuration.projectId.type === 'envVar'
? { from_env: values.configuration.projectId.envVar }
: values.configuration.projectId.value,
datasets:
values.configuration.projectId.type === 'envVar'
? { from_env: values.configuration.projectId.envVar }
: values.configuration.projectId.value.split(','),
},
customization: generateGraphQLCustomizationInfo(
values.customization ?? {}
),
},
};
return cleanEmpty(payload);
};