mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +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