console: integrate the generalised hook useNeonIntegration hook to render Neon integration in the onboarding wizard

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6055
GitOrigin-RevId: 9b0fc398599e0e7d33b11fd657f93f8d7baaa3c6
This commit is contained in:
Rishichandra Wawhal 2022-09-27 18:46:25 +05:30 committed by hasura-bot
parent 265311a4cc
commit 3df2403cf7
6 changed files with 492 additions and 97 deletions

View File

@ -1,10 +1,10 @@
import * as React from 'react';
import { Dispatch } from '@/types';
import { NeonBanner } from './components/Neon/NeonBanner';
import {
NeonBanner,
Props as NeonBannerProps,
} from './components/Neon/NeonBanner';
import { getNeonDBName } from './utils';
getNeonDBName,
transformNeonIntegrationStatusToNeonBannerProps,
} from './utils';
import { useNeonIntegration } from './useNeonIntegration';
import _push from '../../../push';
@ -26,85 +26,9 @@ export function Neon(props: { allDatabases: string[]; dispatch: Dispatch }) {
dispatch
);
let neonBannerProps: NeonBannerProps;
switch (neonIntegrationStatus.status) {
case 'idle':
neonBannerProps = {
status: {
status: 'default',
},
buttonText: 'Connect Neon Database',
onClickConnect: neonIntegrationStatus.action,
};
break;
case 'authentication-loading':
neonBannerProps = {
status: {
status: 'loading',
},
buttonText: 'Authenticating with Neon',
onClickConnect: () => null,
};
break;
case 'authentication-error':
neonBannerProps = {
status: {
status: 'error',
errorTitle: neonIntegrationStatus.title,
errorDescription: neonIntegrationStatus.description,
},
buttonText: 'Try again',
onClickConnect: neonIntegrationStatus.action,
icon: 'refresh',
};
break;
case 'authentication-success':
case 'neon-database-creation-loading':
neonBannerProps = {
status: {
status: 'loading',
},
buttonText: 'Creating Database',
onClickConnect: () => null,
};
break;
case 'neon-database-creation-error':
neonBannerProps = {
status: {
status: 'error',
errorTitle: neonIntegrationStatus.title,
errorDescription: neonIntegrationStatus.description,
},
buttonText: 'Try again',
onClickConnect: neonIntegrationStatus.action,
icon: 'refresh',
};
break;
case 'neon-database-creation-success':
case 'env-var-creation-loading':
case 'env-var-creation-success':
case 'env-var-creation-error':
case 'hasura-source-creation-loading':
case 'hasura-source-creation-error':
case 'hasura-source-creation-success':
neonBannerProps = {
status: {
status: 'loading',
},
buttonText: 'Connecting to Hasura',
onClickConnect: () => null,
};
break;
default:
neonBannerProps = {
status: {
status: 'default',
},
buttonText: 'Connect Neon Database',
onClickConnect: () => null,
};
break;
}
const neonBannerProps = transformNeonIntegrationStatusToNeonBannerProps(
neonIntegrationStatus
);
return <NeonBanner {...neonBannerProps} />;
}

View File

@ -46,7 +46,7 @@ type Success<Status, Payload> = {
payload: Payload;
};
type NeonIntegrationStatus =
export type NeonIntegrationStatus =
| Idle<'idle', EmptyPayload>
| Loading<'authentication-loading', EmptyPayload>
| Success<'authentication-success', EmptyPayload>

View File

@ -0,0 +1,304 @@
import { transformNeonIntegrationStatusToNeonBannerProps } from './utils';
import type { NeonIntegrationStatus } from './useNeonIntegration';
import type { Props as NeonBannerProps } from './components/Neon/NeonBanner';
const transformNeonIntegrationStatusTestCases: {
name: string;
input: NeonIntegrationStatus;
output: NeonBannerProps;
}[] = [
{
name: 'transforms the idle state correctly',
input: {
status: 'idle',
payload: {},
action: jest.fn(),
},
output: {
status: {
status: 'default',
},
icon: undefined,
buttonText: 'Connect Neon Database',
onClickConnect: jest.fn(),
},
},
{
name: 'transforms the authentication-loading state correctly',
input: {
status: 'authentication-loading',
payload: {},
},
output: {
status: {
status: 'loading',
},
icon: undefined,
buttonText: 'Authenticating with Neon',
onClickConnect: jest.fn(),
},
},
{
name: 'transforms the authentication-success state correctly',
input: {
status: 'authentication-success',
payload: {},
},
output: {
status: {
status: 'loading',
},
icon: undefined,
buttonText: 'Creating Database',
onClickConnect: jest.fn(),
},
},
{
name: 'transforms the neon-db-creation-loading state correctly',
input: {
status: 'neon-database-creation-loading',
payload: {},
},
output: {
status: {
status: 'loading',
},
icon: undefined,
buttonText: 'Creating Database',
onClickConnect: jest.fn(),
},
},
{
name: 'transforms the neon-db-creation-success state correctly',
input: {
status: 'neon-database-creation-success',
payload: {
email: 'email@email.com',
databaseUrl: 'dbUrl',
},
},
output: {
status: {
status: 'loading',
},
icon: undefined,
buttonText: 'Connecting to Hasura',
onClickConnect: jest.fn(),
},
},
{
name: 'transforms the env-db-creation-loading state correctly',
input: {
status: 'env-var-creation-loading',
payload: {
databaseUrl: 'dbUrl',
dataSourceName: 'dsName',
},
},
output: {
status: {
status: 'loading',
},
icon: undefined,
buttonText: 'Connecting to Hasura',
onClickConnect: jest.fn(),
},
},
{
name: 'transforms the env-db-creation-success state correctly',
input: {
status: 'env-var-creation-success',
payload: {
databaseUrl: 'dbUrl',
dataSourceName: 'dsName',
envVar: 'envVarName',
},
},
output: {
status: {
status: 'loading',
},
icon: undefined,
buttonText: 'Connecting to Hasura',
onClickConnect: jest.fn(),
},
},
{
name: 'transforms the hasura source creation loading state state correctly',
input: {
status: 'hasura-source-creation-loading',
payload: {
dataSourceName: 'dsName',
envVar: 'envVarName',
databaseUrl: 'dbUrl',
},
},
output: {
status: {
status: 'loading',
},
icon: undefined,
buttonText: 'Connecting to Hasura',
onClickConnect: jest.fn(),
},
},
{
name: 'transforms the hasura source creation success state state correctly',
input: {
status: 'hasura-source-creation-loading',
payload: {
dataSourceName: 'dsName',
envVar: 'envVarName',
databaseUrl: 'dbUrl',
},
},
output: {
status: {
status: 'loading',
},
icon: undefined,
buttonText: 'Connecting to Hasura',
onClickConnect: jest.fn(),
},
},
];
const transformErrorStatesTestCases: {
name: string;
input: NeonIntegrationStatus;
output: NeonBannerProps;
}[] = [
{
name: 'transforms authentication-error state correctly',
input: {
status: 'authentication-error',
payload: {},
action: jest.fn(),
title: 'Error title',
description: 'Error description',
},
output: {
status: {
status: 'error',
errorTitle: 'Error title',
errorDescription: 'Error description',
},
icon: 'refresh',
buttonText: 'Try again',
onClickConnect: jest.fn(),
},
},
{
name: 'transforms neon-db-creation-error state correctly',
input: {
status: 'neon-database-creation-error',
payload: {},
action: jest.fn(),
title: 'Error title',
description: 'Error description',
},
output: {
status: {
status: 'error',
errorTitle: 'Error title',
errorDescription: 'Error description',
},
icon: 'refresh',
buttonText: 'Try again',
onClickConnect: jest.fn(),
},
},
{
name: 'transforms env-var-creation-error state correctly',
input: {
status: 'env-var-creation-error',
payload: {
databaseUrl: 'dbUrl',
dataSourceName: 'dsName',
},
action: jest.fn(),
title: 'Error title',
description: 'Error description',
},
output: {
status: {
status: 'loading',
},
buttonText: 'Connecting to Hasura',
onClickConnect: jest.fn(),
},
},
{
name: 'transforms hasura-source-creation-error state correctly',
input: {
status: 'hasura-source-creation-error',
payload: {
databaseUrl: 'dbUrl',
dataSourceName: 'dsName',
envVar: 'envVar',
},
action: jest.fn(),
title: 'Error title',
description: 'Error description',
},
output: {
status: {
status: 'loading',
},
buttonText: 'Connecting to Hasura',
onClickConnect: jest.fn(),
},
},
];
describe('transformNeonIntegrationStatusToNeonBannerProps', () => {
transformNeonIntegrationStatusTestCases.forEach(t => {
it(t.name, () => {
const output = transformNeonIntegrationStatusToNeonBannerProps(t.input);
// assert that the status of the output is as expected
expect(output.status.status).toEqual(t.output.status.status);
// assert that the icon of the output is as expected
expect(output.icon).toEqual(t.output.icon);
// assert that the button text is as expected
expect(output.buttonText).toEqual(t.output.buttonText);
// if `input` has a corresponding action, assert that the same action is presented to the user in banner props
if ('action' in t.input) {
output.onClickConnect();
expect(t.input.action).toHaveBeenCalled();
}
});
});
transformErrorStatesTestCases.forEach(t => {
it(t.name, () => {
const output = transformNeonIntegrationStatusToNeonBannerProps(t.input);
// assert that the status of the output is as expected
expect(output.status).toEqual(t.output.status);
// coerce the expected output and actual output to `any` type for
// making assertions irrespective of the descriminant
const actualOutput: any = output;
const expectedOutput: any = t.output;
// expect the button text of the output to be as expected
expect(actualOutput.buttonText).toEqual(expectedOutput.buttonText);
if ('action' in t.input) {
// some errors are not presented to users as errors and they're masked with a loading status
// loading status does not have an actionable, so we assert the right actionable only in
// cases where we present the status as "error"
if (expectedOutput.status.status === 'error') {
actualOutput.onClickConnect();
expect(t.input.action).toHaveBeenCalled();
}
}
// assert the presented icon in the output is as expected
expect(actualOutput.icon).toEqual(expectedOutput.icon);
});
});
});

View File

@ -1,4 +1,6 @@
import { LS_KEYS } from '@/utils/localStorage';
import { NeonIntegrationStatus } from './useNeonIntegration';
import type { Props as NeonBannerProps } from './components/Neon/NeonBanner';
export const NEON_CALLBACK_SEARCH = LS_KEYS.neonCallbackSearch;
@ -28,3 +30,88 @@ export function getNeonDBName(allDatabases: string[]) {
return dbName;
}
export function transformNeonIntegrationStatusToNeonBannerProps(
neonIntegrationStatus: NeonIntegrationStatus
): NeonBannerProps {
let neonBannerProps: NeonBannerProps;
switch (neonIntegrationStatus.status) {
case 'idle':
neonBannerProps = {
status: {
status: 'default',
},
buttonText: 'Connect Neon Database',
onClickConnect: neonIntegrationStatus.action,
};
break;
case 'authentication-loading':
neonBannerProps = {
status: {
status: 'loading',
},
buttonText: 'Authenticating with Neon',
onClickConnect: () => null,
};
break;
case 'authentication-error':
neonBannerProps = {
status: {
status: 'error',
errorTitle: neonIntegrationStatus.title,
errorDescription: neonIntegrationStatus.description,
},
buttonText: 'Try again',
onClickConnect: neonIntegrationStatus.action,
icon: 'refresh',
};
break;
case 'authentication-success':
case 'neon-database-creation-loading':
neonBannerProps = {
status: {
status: 'loading',
},
buttonText: 'Creating Database',
onClickConnect: () => null,
};
break;
case 'neon-database-creation-error':
neonBannerProps = {
status: {
status: 'error',
errorTitle: neonIntegrationStatus.title,
errorDescription: neonIntegrationStatus.description,
},
buttonText: 'Try again',
onClickConnect: neonIntegrationStatus.action,
icon: 'refresh',
};
break;
case 'neon-database-creation-success':
case 'env-var-creation-loading':
case 'env-var-creation-success':
case 'env-var-creation-error':
case 'hasura-source-creation-loading':
case 'hasura-source-creation-error':
case 'hasura-source-creation-success':
neonBannerProps = {
status: {
status: 'loading',
},
buttonText: 'Connecting to Hasura',
onClickConnect: () => null,
};
break;
default:
neonBannerProps = {
status: {
status: 'default',
},
buttonText: 'Connect Neon Database',
onClickConnect: () => null,
};
break;
}
return neonBannerProps;
}

View File

@ -1,8 +1,11 @@
import React from 'react';
import { useAppDispatch } from '@/store';
import { Button } from '@/new-components/Button';
import _push from '../../../../components/Services/Data/push';
import Globals from '@/Globals';
import { hasLuxFeatureAccess } from '@/utils/cloudConsole';
import { OnboardingAnimation, OnboardingAnimationNavbar } from './components';
import { NeonOnboarding } from './NeonOnboarding';
import _push from '../../../../components/Services/Data/push';
type ConnectDBScreenProps = {
skipOnboarding: () => void;
@ -33,18 +36,32 @@ export function ConnectDBScreen(props: ConnectDBScreenProps) {
</div>
<div className="flex items-center justify-between">
<div className="cursor-pointer text-secondary text-sm hover:text-secondary-dark">
<div data-trackid="onboarding-skip-button" onClick={skipOnboarding}>
Skip setup, continue to dashboard
</div>
</div>
<Button
data-trackid="onboarding-connect-db-button"
mode="primary"
onClick={onClick}
>
Connect Your Database
</Button>
{hasLuxFeatureAccess(Globals, 'NeonDatabaseIntegration') ? (
<NeonOnboarding
dispatch={dispatch}
onSkip={skipOnboarding}
onCompletion={completeOnboarding}
onError={() => console.log('error')}
/>
) : (
<>
<div className="cursor-pointer text-secondary text-sm hover:text-secondary-dark">
<div
data-trackid="onboarding-skip-button"
onClick={skipOnboarding}
>
Skip setup, continue to dashboard
</div>
</div>
<Button
data-trackid="onboarding-connect-db-button"
mode="primary"
onClick={onClick}
>
Connect Your Database
</Button>
</>
)}
</div>
</>
);

View File

@ -0,0 +1,63 @@
import * as React from 'react';
import { Dispatch } from '@/types';
import { useNeonIntegration } from '@/components/Services/Data/DataSources/CreateDataSource/Neon/useNeonIntegration';
import { transformNeonIntegrationStatusToNeonBannerProps } from '@/components/Services/Data/DataSources/CreateDataSource/Neon/utils';
import { NeonBanner } from '../NeonConnectBanner/NeonBanner';
import _push from '../../../../components/Services/Data/push';
const useTemplateGallery = (
onSuccess: VoidFunction,
onError: VoidFunction,
dispatch: Dispatch
) => {
return {
install: () => {
dispatch(_push(`/data/default`));
},
};
};
export function NeonOnboarding(props: {
dispatch: Dispatch;
onSkip: VoidFunction;
onCompletion: VoidFunction;
onError: VoidFunction;
}) {
const { dispatch, onSkip, onCompletion, onError } = props;
// Sample function
const { install } = useTemplateGallery(onCompletion, onError, dispatch);
const neonIntegrationStatus = useNeonIntegration(
'default',
() => {
install();
},
() => {
onError();
dispatch(_push(`/data/manage/connect`));
},
dispatch
);
const neonBannerProps = transformNeonIntegrationStatusToNeonBannerProps(
neonIntegrationStatus
);
return (
<div>
<div className="w-full mb-sm">
<NeonBanner {...neonBannerProps} />
</div>
<div className="flex justify-start items-center w-full">
<a
className="w-auto text-secondary cursor-pointer text-sm hover:text-secondary-dark"
data-trackid="onboarding-skip-button"
onClick={onSkip}
>
Skip setup, continue to console
</a>
</div>
</div>
);
}