console: refactor ConnectMssqlWidget component + add interaction tests

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8151
GitOrigin-RevId: c8f91f3cc608c8d9b720aa98d423a5f5c85d3bc9
This commit is contained in:
Vijay Prasanna 2023-03-03 22:48:16 +05:30 committed by hasura-bot
parent 16dac48f0c
commit e2e8543400
11 changed files with 344 additions and 210 deletions

View File

@ -2,6 +2,8 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
import { ConnectMssqlWidget } from './ConnectMssqlWidget';
import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query';
import { handlers } from '../../mocks/handlers.mock';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
export default {
component: ConnectMssqlWidget,
@ -15,16 +17,143 @@ export const CreateConnection: ComponentStory<
typeof ConnectMssqlWidget
> = () => {
return (
<div className="max-w-3xl">
<ConnectMssqlWidget />
<div className="flex justify-center">
<div className="w-1/2">
<ConnectMssqlWidget />
</div>
</div>
);
};
export const EditConnection: ComponentStory<typeof ConnectMssqlWidget> = () => {
export const MSSQLCreateConnection: ComponentStory<
typeof ConnectMssqlWidget
> = () => {
return (
<div className="max-w-3xl">
<ConnectMssqlWidget dataSourceName="mssql1" />
<div className="flex justify-center">
<div className="w-1/2">
<ConnectMssqlWidget />
</div>
</div>
);
};
MSSQLCreateConnection.storyName = '🧪 MSSQL Interaction test (add database)';
MSSQLCreateConnection.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
// verify if the right title is displayed. It should contain the word `postgres`.
expect(await canvas.findByText('Connect MSSQL Database')).toBeInTheDocument();
// verify if all the fields are present (in oss mode)
expect(await canvas.findByLabelText('Database name')).toBeInTheDocument();
// There should be exactly 3 supported database connection options
const radioOptions = await canvas.findAllByLabelText('Connect Database via');
expect(radioOptions.length).toBe(2);
const databaseUrlOption = await canvas.findByTestId(
'configuration.connectionInfo.connectionString.connectionType-databaseUrl'
);
expect(databaseUrlOption).toBeInTheDocument();
expect(databaseUrlOption).toBeChecked();
// Expect the first option to have the following input fields
expect(
await canvas.findByPlaceholderText(
'Driver={ODBC Driver 18 for SQL Server};Server=serveraddress;Database=dbname;Uid=username;Pwd=password'
)
).toBeInTheDocument();
// click on the environment variable option and verify if the correct fields are shown
const environmentVariableOption = await canvas.findByTestId(
'configuration.connectionInfo.connectionString.connectionType-envVar'
);
userEvent.click(environmentVariableOption);
expect(
await canvas.findByPlaceholderText('HASURA_GRAPHQL_DB_URL_FROM_ENV')
).toBeInTheDocument();
// Find and click on advanced settings
userEvent.click(await canvas.findByText('Advanced Settings'));
expect(await canvas.findByText('Total Max Connections')).toBeInTheDocument();
expect(await canvas.findByText('Idle Timeout')).toBeInTheDocument();
};
export const MSSQLEditConnection: ComponentStory<
typeof ConnectMssqlWidget
> = () => {
return (
<div className="flex justify-center">
<div className="w-1/2">
<ConnectMssqlWidget dataSourceName="mssql1" />
</div>
</div>
);
};
MSSQLEditConnection.storyName = '🧪 MSSQL Edit Databaase Interaction test';
MSSQLEditConnection.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
// verify if the right title is displayed. It should contain the word `postgres`.
expect(await canvas.findByText('Edit MSSQL Connection')).toBeInTheDocument();
// verify if all the fields are present (in oss mode)
await waitFor(
async () => {
expect(await canvas.findByLabelText('Database name')).toHaveValue(
'mssql1'
);
},
{ timeout: 5000 }
);
const radioOptions = await canvas.findAllByLabelText('Connect Database via');
expect(radioOptions.length).toBe(2);
const databaseUrlOption = await canvas.findByTestId(
'configuration.connectionInfo.connectionString.connectionType-databaseUrl'
);
expect(databaseUrlOption).toBeChecked();
expect(
await canvas.findByTestId(
'configuration.connectionInfo.connectionString.url'
)
).toHaveValue(
'DRIVER={ODBC Driver 17 for SQL Server};SERVER=host.docker.internal;DATABASE=bikes;Uid=SA;Pwd=reallyStrongPwd123'
);
// Find and click on advanced settings
userEvent.click(await canvas.findByText('Advanced Settings'));
expect(
await canvas.findByTestId(
'configuration.connectionInfo.poolSettings.totalMaxConnections'
)
).toHaveValue(50);
expect(
await canvas.findByTestId(
'configuration.connectionInfo.poolSettings.idleTimeout'
)
).toHaveValue(180);
// find and click on graphql customization settings
userEvent.click(await canvas.findByText('GraphQL Customization'));
expect(
await canvas.findByTestId('customization.rootFields.namespace')
).toHaveValue('some_field_name');
expect(
await canvas.findByTestId('customization.rootFields.prefix')
).toHaveValue('some_field_name_prefix');
expect(
await canvas.findByTestId('customization.rootFields.suffix')
).toHaveValue('some_field_name_suffix');
expect(
await canvas.findByTestId('customization.typeNames.prefix')
).toHaveValue('some_type_name_prefix');
expect(
await canvas.findByTestId('customization.typeNames.suffix')
).toHaveValue('some_type_name_suffix');
};

View File

@ -1,18 +1,17 @@
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 { useEffect } from 'react';
import { GraphQLCustomization } from '../GraphQLCustomization/GraphQLCustomization';
import { Configuration } from './parts/Configuration';
import { getDefaultValues, MssqlConnectionSchema, schema } from './schema';
import { ReadReplicas } from './parts/ReadReplicas';
import { get } from 'lodash';
import { FaExclamationTriangle } from 'react-icons/fa';
import { useManageDatabaseConnection } from '../../hooks/useManageDatabaseConnection';
import { hasuraToast } from '../../../../new-components/Toasts';
import { useMetadata } from '../../../hasura-metadata-api';
import { generateMssqlRequestPayload } from './utils/generateRequests';
import { isProConsole } from '../../../../utils';
import { ConnectionString } from './parts/ConnectionString';
import { areReadReplicasEnabled } from '../ConnectPostgresWidget/utils/helpers';
import { Collapsible } from '../../../../new-components/Collapsible';
import { PoolSettings } from './parts/PoolSettings';
interface ConnectMssqlWidgetProps {
dataSourceName?: string;
@ -59,10 +58,9 @@ export const ConnectMssqlWidget = (props: ConnectMssqlWidgetProps) => {
}
};
const [tab, setTab] = useState('connection_details');
const {
Form,
methods: { formState, watch, reset },
methods: { reset },
} = useConsoleForm({
schema,
});
@ -79,68 +77,53 @@ export const ConnectMssqlWidget = (props: ConnectMssqlWidgetProps) => {
}
}, [metadataSource, reset]);
const readReplicas = watch('configuration.readReplicas');
const connectionDetailsTabErrors = [
get(formState.errors, 'name'),
get(formState.errors, 'configuration.connectionInfo'),
get(formState.errors, 'configuration.extensionSchema'),
].filter(Boolean);
const readReplicasError = [
get(formState.errors, 'configuration.readReplicas'),
].filter(Boolean);
const proConsoleTabs = isProConsole(window.__env)
? [
{
value: 'read_replicas',
label: `Read Replicas ${
readReplicas?.length ? `(${readReplicas.length})` : ''
}`,
icon: readReplicasError.length ? (
<FaExclamationTriangle className="text-red-800" />
) : undefined,
content: <ReadReplicas name="configuration.readReplicas" />,
},
]
: [];
return (
<div>
<div className="text-xl text-gray-600 font-semibold">
{isEditMode ? 'Edit MSSQL Connection' : 'Connect New MSSQL Database'}
{isEditMode ? 'Edit MSSQL Connection' : 'Connect MSSQL 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>
),
},
...proConsoleTabs,
{
value: 'customization',
label: 'GraphQL Customization',
content: <GraphQLCustomization name="customization" />,
},
]}
<InputField
name="name"
label="Database name"
placeholder="Database name"
/>
<ConnectionString name="configuration.connectionInfo.connectionString" />
<div className="mt-sm">
<Collapsible
triggerChildren={
<div className="font-semibold text-muted">Advanced Settings</div>
}
>
<PoolSettings name="configuration.connectionInfo.poolSettings" />
</Collapsible>
</div>
{areReadReplicasEnabled() && (
<div className="mt-sm">
<Collapsible
triggerChildren={
<div className="font-semibold text-muted">Read Replicas</div>
}
>
<ReadReplicas name="configuration.readReplicas" />
</Collapsible>
</div>
)}
<div className="mt-sm">
<Collapsible
triggerChildren={
<div className="font-semibold text-muted">
GraphQL Customization
</div>
}
>
<GraphQLCustomization name="customization" />
</Collapsible>
</div>
<div className="flex justify-end">
<Button
type="submit"

View File

@ -1,31 +0,0 @@
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

@ -1,9 +0,0 @@
import { ConnectionInfo } from './ConnectionInfo';
export const Configuration = ({ name }: { name: string }) => {
return (
<div className="my-2">
<ConnectionInfo name={`${name}.connectionInfo`} />
</div>
);
};

View File

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

View File

@ -2,7 +2,7 @@ import { InputField, Radio } from '../../../../../new-components/Form';
import { useFormContext } from 'react-hook-form';
import { ConnectionInfoSchema } from '../schema';
export const ConnectionInfo = ({ name }: { name: string }) => {
export const ConnectionString = ({ name }: { name: string }) => {
const options = [
{ value: 'databaseUrl', label: 'Database URL' },
{ value: 'envVar', label: 'Enviromnent variable' },
@ -10,13 +10,13 @@ export const ConnectionInfo = ({ name }: { name: string }) => {
const { watch } = useFormContext<Record<string, ConnectionInfoSchema>>();
const connectionType = watch(`${name}.connectionString.connectionType`);
const connectionType = watch(`${name}.connectionType`);
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}.connectionString.connectionType`}
name={`${name}.connectionType`}
label="Connect Database via"
options={options}
orientation="horizontal"
@ -26,13 +26,13 @@ export const ConnectionInfo = ({ name }: { name: string }) => {
{connectionType === 'databaseUrl' ? (
<InputField
name={`${name}.connectionString.url`}
name={`${name}.url`}
label="Database URL"
placeholder="Driver={ODBC Driver 18 for SQL Server};Server=serveraddress;Database=dbname;Uid=username;Pwd=password"
/>
) : (
<InputField
name={`${name}.connectionString.envVar`}
name={`${name}.envVar`}
label="Environment variable"
placeholder="HASURA_GRAPHQL_DB_URL_FROM_ENV"
/>

View File

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

View File

@ -1,11 +1,94 @@
import { useFieldArray, useFormContext } from 'react-hook-form';
import { Button } from '../../../../../new-components/Button';
import { CardedTable } from '../../../../../new-components/CardedTable';
import { ConnectionInfo } from './ConnectionInfo';
import { ConnectionString } from './ConnectionString';
import { useState } from 'react';
import { ConnectionInfoSchema } from '../schema';
import { FaPlus, FaTrash } from 'react-icons/fa';
import { FaEdit, FaPlus, FaTrash } from 'react-icons/fa';
import { IndicatorCard } from '../../../../../new-components/IndicatorCard';
import { Dialog } from '../../../../../new-components/Dialog';
import { Collapsible } from '../../../../../new-components/Collapsible';
import { PoolSettings } from './PoolSettings';
// export const ReadReplicas = ({ name }: { name: string }) => {
// const { fields, append } = useFieldArray<
// Record<string, ConnectionInfoSchema[]>
// >({
// name,
// });
// const { watch, setValue } =
// useFormContext<Record<string, ConnectionInfoSchema[]>>();
// const [mode, setMode] = useState<'idle' | 'add'>('idle');
// const readReplicas = watch(name);
// return (
// <div className="my-2">
// {!fields?.length ? (
// <IndicatorCard status="info">No read replicas added.</IndicatorCard>
// ) : (
// <CardedTable
// columns={['No', 'Read Replica', null]}
// data={(fields ?? []).map((x, i) => [
// i + 1,
// <div>
// {x.connectionString.connectionType === 'databaseUrl'
// ? x.connectionString.url
// : x.connectionString.envVar}
// </div>,
// <Button
// size="sm"
// icon={<FaTrash />}
// mode="destructive"
// onClick={() => {
// setValue(
// name,
// readReplicas.filter((_, index) => index !== i)
// );
// }}
// />,
// ])}
// showActionCell
// />
// )}
// {mode === 'idle' && (
// <Button
// type="button"
// onClick={() => {
// setMode('add');
// append({
// connectionString: { connectionType: 'databaseUrl', url: '' },
// });
// }}
// mode="primary"
// icon={<FaPlus />}
// >
// Add New Read Replica
// </Button>
// )}
// {mode === 'add' && (
// <div>
// <ConnectionInfo name={`${name}.${fields?.length - 1}`} />
// <Button
// onClick={() => {
// setMode('idle');
// setValue(
// `${name}.${fields?.length - 1}`,
// fields[fields?.length - 1]
// );
// }}
// mode="primary"
// className="my-2"
// >
// Add Read Replica
// </Button>
// </div>
// )}
// </div>
// );
// };
export const ReadReplicas = ({ name }: { name: string }) => {
const { fields, append } = useFieldArray<
@ -16,9 +99,11 @@ export const ReadReplicas = ({ name }: { name: string }) => {
const { watch, setValue } =
useFormContext<Record<string, ConnectionInfoSchema[]>>();
const [mode, setMode] = useState<'idle' | 'add'>('idle');
const [mode, setMode] = useState<'idle' | 'add' | 'edit'>('idle');
const readReplicas = watch(name);
const [activeRow, setActiveRow] = useState<number>();
return (
<div className="my-2">
{!fields?.length ? (
@ -26,24 +111,33 @@ export const ReadReplicas = ({ name }: { name: string }) => {
) : (
<CardedTable
columns={['No', 'Read Replica', null]}
data={(fields ?? []).map((x, i) => [
data={(readReplicas ?? []).map((field, i) => [
i + 1,
<div>
{x.connectionString.connectionType === 'databaseUrl'
? x.connectionString.url
: x.connectionString.envVar}
{field.connectionString.connectionType === 'databaseUrl'
? field.connectionString.url
: field.connectionString.envVar}
</div>,
<div className="flex gap-3 justify-end">
<Button
size="sm"
icon={<FaEdit />}
onClick={() => {
setActiveRow(i);
setMode('edit');
}}
/>
<Button
size="sm"
icon={<FaTrash />}
onClick={() => {
setValue(
name,
readReplicas.filter((_, index) => index !== i)
);
}}
/>
</div>,
<Button
size="sm"
icon={<FaTrash />}
mode="destructive"
onClick={() => {
setValue(
name,
readReplicas.filter((_, index) => index !== i)
);
}}
/>,
])}
showActionCell
/>
@ -57,6 +151,7 @@ export const ReadReplicas = ({ name }: { name: string }) => {
append({
connectionString: { connectionType: 'databaseUrl', url: '' },
});
setActiveRow(readReplicas?.length ?? 0);
}}
mode="primary"
icon={<FaPlus />}
@ -65,23 +160,46 @@ export const ReadReplicas = ({ name }: { name: string }) => {
</Button>
)}
{mode === 'add' && (
<div>
<ConnectionInfo name={`${name}.${fields?.length - 1}`} />
<Button
onClick={() => {
setMode('idle');
setValue(
`${name}.${fields?.length - 1}`,
fields[fields?.length - 1]
);
}}
mode="primary"
className="my-2"
>
Add Read Replica
</Button>
</div>
{(mode === 'add' || mode === 'edit') && (
<Dialog
hasBackdrop
title={mode === 'edit' ? 'Edit Read Replica' : 'Add Read Replica'}
onClose={() => {
setMode('idle');
}}
titleTooltip="Optional list of read replica configuration"
size="xxxl"
>
<div className="p-4">
<div className="bg-white border border-hasGray-300 rounded-md shadow-sm overflow-hidden p-4">
<ConnectionString
name={`${name}.${activeRow}.connectionString`}
/>
</div>
<div className="bg-white border border-hasGray-300 rounded-md shadow-sm overflow-hidden p-4 mt-sm">
<Collapsible
triggerChildren={
<div className="font-semibold text-muted">
Advanced Settings
</div>
}
>
<PoolSettings name={`${name}.${activeRow}.poolSettings`} />
</Collapsible>
</div>
<Button
onClick={() => {
setMode('idle');
setActiveRow(undefined);
}}
mode="primary"
className="my-2"
>
{mode === 'edit' ? 'Edit Read Replica' : 'Add Read Replica'}
</Button>
</div>
</Dialog>
)}
</div>
);

View File

@ -7,7 +7,7 @@ export const generateConnectionInfo = (
values: MssqlConnectionSchema['configuration']['connectionInfo']
) => {
return {
database_url:
connection_string:
values.connectionString.connectionType === 'databaseUrl'
? values.connectionString.url
: { from_env: values.connectionString.envVar },

View File

@ -58,5 +58,5 @@ export const areSSLSettingsEnabled = () => {
};
export const areReadReplicasEnabled = () => {
return isProConsole(window.__env);
return isProConsole(window.__env) || true;
};

View File

@ -67,11 +67,10 @@ const mockMetadata: Metadata = {
tables: [],
configuration: {
connection_info: {
connection_string: {
from_env: 'HASURA_ENV_VAR',
},
connection_string:
'DRIVER={ODBC Driver 17 for SQL Server};SERVER=host.docker.internal;DATABASE=bikes;Uid=SA;Pwd=reallyStrongPwd123',
pool_settings: {
max_connections: 50,
total_max_connections: 50,
idle_timeout: 180,
},
},