mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 17:31:56 +03:00
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:
parent
ef532b5534
commit
31d2542cd2
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>;
|
@ -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 ?? {}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
@ -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);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user