mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
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:
parent
265311a4cc
commit
3df2403cf7
@ -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} />;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user