console: add onboarding events to neon integration; misc onboarding changes

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6131
Co-authored-by: Abhijeet Khangarot <26903230+abhi40308@users.noreply.github.com>
GitOrigin-RevId: 88ffbeb8539dee04ac4b83c6db124fa5817c303c
This commit is contained in:
Rishichandra Wawhal 2022-09-30 19:26:00 +05:30 committed by hasura-bot
parent 7739f8e4a0
commit 950e8dbe25
13 changed files with 223 additions and 60 deletions

View File

@ -23,7 +23,8 @@ export function Neon(props: { allDatabases: string[]; dispatch: Dispatch }) {
getNeonDBName(allDatabases),
pushToDatasource,
pushToConnectDBPage,
dispatch
dispatch,
'data-manage-create'
);
const neonBannerProps = transformNeonIntegrationStatusToNeonBannerProps(

View File

@ -5,6 +5,7 @@ import {
setDBURLInEnvVars,
verifyProjectHealthAndConnectDataSource,
} from '../utils';
import { NeonIntegrationContext } from './utils';
import { setDBConnectionDetails } from '../../../DataActions';
import {
connectDataSource,
@ -47,7 +48,8 @@ type HasuraDatasourceStatus =
export function useCreateHasuraCloudDatasource(
dbUrl: string,
dataSourceName = 'default',
dispatch: Dispatch
dispatch: Dispatch,
context: NeonIntegrationContext
) {
const [state, setState] = useState<HasuraDatasourceStatus>({
status: 'idle',
@ -81,7 +83,12 @@ export function useCreateHasuraCloudDatasource(
getDefaultState({
dbConnection: connectionConfig,
}),
successCallback
successCallback,
undefined,
undefined,
undefined,
undefined,
context === 'data-manage-create'
);
} catch (e) {
errorCallback();

View File

@ -4,6 +4,7 @@ import { useNeonOAuth } from './useNeonOAuth';
import { useNeonDatabase } from './useNeonDatabase';
import { useCreateHasuraCloudDatasource } from './useCreateHasuraCloudDatasource';
import { setDBConnectionDetails } from '../../../DataActions';
import { NeonIntegrationContext } from './utils';
type EmptyPayload = Record<string, never>;
@ -65,7 +66,8 @@ export function useNeonIntegration(
dataSourceName: string,
dbCreationCallback: (dataSourceName: string) => void, // TODO use NeonIntegrationStatus as a parameter
failureCallback: VoidFunction, // TODO use NeonIntegrationStatus as a parameter
dispatch: Dispatch
dispatch: Dispatch,
context: NeonIntegrationContext
): NeonIntegrationStatus {
const { startNeonOAuth, neonOauthStatus } = useNeonOAuth();
@ -81,7 +83,8 @@ export function useNeonIntegration(
? neonDBCreationStatus.payload.databaseUrl || ''
: '',
dataSourceName,
dispatch
dispatch,
context
);
useEffect(() => {

View File

@ -31,6 +31,8 @@ export function getNeonDBName(allDatabases: string[]) {
return dbName;
}
export type NeonIntegrationContext = 'onboarding' | 'data-manage-create';
export function transformNeonIntegrationStatusToNeonBannerProps(
neonIntegrationStatus: NeonIntegrationStatus
): NeonBannerProps {

View File

@ -31,12 +31,6 @@ export function ConnectDBScreen(props: ConnectDBScreenProps) {
pushToConnectDBPage();
};
const onUnexpectedNeonOnboardingError = (error?: string) => {
// TODO raise sentry alert
console.error(error);
pushToConnectDBPage();
};
const skipLandingPage = React.useCallback(() => {
persistSkippedOnboarding();
dismissOnboarding();
@ -58,9 +52,8 @@ export function ConnectDBScreen(props: ConnectDBScreenProps) {
{hasNeonAccess ? (
<NeonOnboarding
dispatch={dispatch}
onSkip={dismissOnboarding}
onCompletion={proceed}
onError={onUnexpectedNeonOnboardingError}
dismiss={dismissOnboarding}
proceed={proceed}
/>
) : (
<>

View File

@ -7,38 +7,76 @@ import _push from '../../../../components/Services/Data/push';
import {
useInstallTemplate,
usePrefetchNeonOnboardingTemplateData,
useEmitOnboardingEvents,
} from '../../hooks';
import { NEON_TEMPLATE_BASE_PATH } from '../../constants';
import { persistSkippedOnboarding } from '../../utils';
export function NeonOnboarding(props: {
dispatch: Dispatch;
onSkip: VoidFunction;
onCompletion: VoidFunction;
onError: (errorMsg?: string) => void;
dismiss: VoidFunction;
proceed: VoidFunction;
}) {
const { dispatch, onSkip, onCompletion, onError } = props;
const [installingTemplate, setInstallingTemplate] = React.useState(false);
const { dispatch, dismiss, proceed } = props;
const onSkipHandler = () => {
persistSkippedOnboarding();
dismiss();
};
const onSuccessHandler = () => {
proceed();
};
const onErrorHanlder = () => {
dispatch(_push('/data/manage/connect'));
dismiss();
};
// Prefetch Neon related template data from github repo
usePrefetchNeonOnboardingTemplateData();
usePrefetchNeonOnboardingTemplateData(NEON_TEMPLATE_BASE_PATH);
// Memoised function used to install the template
const { install } = useInstallTemplate('default', onCompletion, onError);
const { install } = useInstallTemplate(
'default',
NEON_TEMPLATE_BASE_PATH,
onSuccessHandler,
onErrorHanlder
);
const neonIntegrationStatus = useNeonIntegration(
'default',
() => {
setInstallingTemplate(true);
install();
},
() => {
onError();
dispatch(_push(`/data/manage/connect`));
onErrorHanlder();
},
dispatch
dispatch,
'onboarding'
);
// emit onboarding events to the database
useEmitOnboardingEvents(neonIntegrationStatus, installingTemplate);
// allow skipping only when an action is not in-progress
const allowSkipping =
neonIntegrationStatus.status === 'idle' ||
neonIntegrationStatus.status === 'authentication-error' ||
neonIntegrationStatus.status === 'neon-database-creation-error';
const neonBannerProps = transformNeonIntegrationStatusToNeonBannerProps(
neonIntegrationStatus
);
// show template install status when template is installing
neonBannerProps.buttonText = installingTemplate
? 'Installing Sample Schema'
: neonBannerProps.buttonText;
return (
<div className="w-full">
<div className="w-full mb-sm">
@ -46,9 +84,16 @@ export function NeonOnboarding(props: {
</div>
<div className="flex justify-start items-center w-full">
<a
className="w-auto text-secondary cursor-pointer text-sm hover:text-secondary-dark"
className={`w-auto text-secondary cursor-pointer text-sm hover:text-secondary-dark ${
allowSkipping ? 'cursor-pointer' : 'cursor-not-allowed'
}`}
data-trackid="onboarding-skip-button"
onClick={onSkip}
title={!allowSkipping ? 'Please wait...' : undefined}
onClick={() => {
if (allowSkipping) {
onSkipHandler();
}
}}
>
Skip getting started tutorial
</a>

View File

@ -1,11 +1,17 @@
import * as React from 'react';
import { useQuery } from 'react-query';
import { tracingTools } from '@/features/TracingTools';
import { NEON_ONBOARDING_QUERY_KEY, staleTime } from '../../constants';
import {
NEON_ONBOARDING_QUERY_KEY,
staleTime,
templateSummaryRunQueryClickVariables,
templateSummaryRunQuerySkipVariables,
} from '../../constants';
import { QueryDialog } from './QueryDialog';
import {
fetchTemplateDataQueryFn,
getQueryFromSampleQueries,
emitOnboardingEvent,
} from '../../utils';
const defaultQuery = `
@ -33,11 +39,6 @@ const runSampleQueryInGraphiQL = (query: string) => {
return Promise.resolve(query);
};
// TODO use an actual function
const emitSkipRunQueryEvent = () => {
return Promise.resolve();
};
type Props = {
templateUrl: string;
dismiss: VoidFunction;
@ -86,13 +87,14 @@ export function TemplateSummary(props: Props) {
});
const onRunHandler = () => {
emitOnboardingEvent(templateSummaryRunQueryClickVariables);
runSampleQueryInGraphiQL(sampleQuery).then(() => {
dismiss();
});
};
const onSkipHandler = () => {
emitSkipRunQueryEvent();
emitOnboardingEvent(templateSummaryRunQuerySkipVariables);
dismiss();
};

View File

@ -7,12 +7,23 @@ import { BASE_URL_TEMPLATE } from '@/components/Services/Data/Schema/TemplateGal
// But in order to save api calls, we directly fetch the migraion and metadata files, without querying config.
const ROOT_DIR = 'postgres';
const TEMPLATE_DIR = 'getting-started';
const NEON_TEMPLATE_BASE_PATH = `${BASE_URL_TEMPLATE}/${ROOT_DIR}/${TEMPLATE_DIR}`;
export const NEON_TEMPLATE_BASE_PATH = `${BASE_URL_TEMPLATE}/${ROOT_DIR}/${TEMPLATE_DIR}`;
export const NEON_METADATA_PATH = `${NEON_TEMPLATE_BASE_PATH}/metadata.json`;
export const NEON_MIGRATIONS_PATH = `${NEON_TEMPLATE_BASE_PATH}/migration.sql`;
export const NEON_QUERY_PATH = `${NEON_TEMPLATE_BASE_PATH}/sample.graphql`;
export const NEON_IMAGE_PATH = `${NEON_TEMPLATE_BASE_PATH}/diagram.png`;
export const getMetadataUrl = (baseUrl: string) => {
return `${baseUrl}/metadata.json`;
};
export const getMigrationUrl = (baseUrl: string) => {
return `${baseUrl}/migration.sql`;
};
export const getSampleQueriesUrl = (baseUrl: string) => {
return `${baseUrl}/sample.graphql`;
};
export const getSchemaImageUrl = (baseUrl: string) => {
return `${baseUrl}/diagram.png`;
};
export const NEON_ONBOARDING_QUERY_KEY = 'neonOnboarding';
@ -63,25 +74,43 @@ export const neonDbCreationCompleteVariables = {
kind: 'neon_db_creation_complete',
};
export const hasuraSourceCreationStart = {
export const hasuraSourceCreationStartVariables = {
...mutationVariables,
kind: 'hasura_source_creation_start',
};
export const hasuraSourceCreationComplete = {
export const hasuraSourceCreationCompleteVariables = {
...mutationVariables,
kind: 'hasura_source_creation_complete',
};
export const templateSummaryRunQueryClick = {
export const installTemplateStartVariables = {
...mutationVariables,
kind: 'install_template_start',
};
export const installTemplateCompleteVariables = {
...mutationVariables,
kind: 'install_template_complete',
};
export const templateSummaryRunQueryClickVariables = {
...mutationVariables,
kind: 'run_query_click',
};
export const templateSummaryRunQuerySkip = {
export const templateSummaryRunQuerySkipVariables = {
...mutationVariables,
kind: 'run_query_skip',
};
export const getNeonOnboardingErrorVariables = (code: string) => {
return {
...mutationVariables,
kind: 'neon_onboarding_error',
error_code: code,
};
};
// A stale time of 5 minutes for use in useQuery hook
export const staleTime = 5 * 60 * 1000;

View File

@ -3,3 +3,4 @@ export { usePrefetchNeonOnboardingTemplateData } from './usePrefetchNeonOnboardi
export { useInstallMigration } from './useInstallMigration';
export { useInstallMetadata } from './useInstallMetadata';
export { useInstallTemplate } from './useInstallTemplate';
export { useEmitOnboardingEvents } from './useEmitOnboardingEvents';

View File

@ -0,0 +1,54 @@
import { useEffect } from 'react';
import { NeonIntegrationStatus } from '@/components/Services/Data/DataSources/CreateDataSource/Neon/useNeonIntegration';
import {
neonOAuthStartVariables,
// neonOAuthCompleteVariables,
neonDbCreationStartVariables,
// neonDbCreationCompleteVariables,
hasuraSourceCreationStartVariables,
// hasuraSourceCreationCompleteVariables,
installTemplateStartVariables,
// installTemplateCompleteVariables,
getNeonOnboardingErrorVariables,
} from '../constants';
import { emitOnboardingEvent } from '../utils';
export function useEmitOnboardingEvents(
neonIntegrationStatus: NeonIntegrationStatus,
installingTemplate: boolean
) {
useEffect(() => {
switch (neonIntegrationStatus.status) {
case 'authentication-error':
emitOnboardingEvent(getNeonOnboardingErrorVariables('authentication'));
break;
case 'neon-database-creation-error':
emitOnboardingEvent(
getNeonOnboardingErrorVariables('neon-database-creation')
);
break;
case 'hasura-source-creation-error':
emitOnboardingEvent(
getNeonOnboardingErrorVariables('hasura-source-creation')
);
break;
case 'authentication-loading':
emitOnboardingEvent(neonOAuthStartVariables);
break;
case 'neon-database-creation-loading':
emitOnboardingEvent(neonDbCreationStartVariables);
break;
case 'hasura-source-creation-loading':
emitOnboardingEvent(hasuraSourceCreationStartVariables);
break;
default:
break;
}
}, [neonIntegrationStatus]);
useEffect(() => {
if (installingTemplate) {
emitOnboardingEvent(installTemplateStartVariables);
}
}, [installingTemplate]);
}

View File

@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { useInstallMigration } from './useInstallMigration';
import { NEON_METADATA_PATH, NEON_MIGRATIONS_PATH } from '../constants';
import { getMetadataUrl, getMigrationUrl } from '../constants';
import { useInstallMetadata } from './useInstallMetadata';
/**
@ -10,13 +10,14 @@ import { useInstallMetadata } from './useInstallMetadata';
*/
export function useInstallTemplate(
dataSourceName: string,
templateBaseUrl: string,
onSuccessCb: () => void,
onErrorCb: (errorMsg?: string) => void
) {
// fetch the function to apply metadata
const { updateMetadata } = useInstallMetadata(
dataSourceName,
NEON_METADATA_PATH,
getMetadataUrl(templateBaseUrl),
onSuccessCb,
onErrorCb
);
@ -24,7 +25,7 @@ export function useInstallTemplate(
// fetch the function to apply migration
const { performMigration } = useInstallMigration(
dataSourceName,
NEON_MIGRATIONS_PATH,
getMigrationUrl(templateBaseUrl),
// install metadata only if migrations has been applied successfully
() => {
if (updateMetadata) {

View File

@ -1,10 +1,10 @@
import { useEffect } from 'react';
import { useQueryClient } from 'react-query';
import {
NEON_METADATA_PATH,
NEON_MIGRATIONS_PATH,
NEON_IMAGE_PATH,
NEON_QUERY_PATH,
getMetadataUrl,
getMigrationUrl,
getSampleQueriesUrl,
getSchemaImageUrl,
} from '../constants';
import { fetchTemplateDataQueryFn } from '../utils';
@ -13,21 +13,30 @@ import { fetchTemplateDataQueryFn } from '../utils';
* so data is ready by the time we need it. For example this could be fired while Neon
* DB is being created.
*/
export const usePrefetchNeonOnboardingTemplateData = () => {
export const usePrefetchNeonOnboardingTemplateData = (
templateBaseUrl: string
) => {
const queryClient = useQueryClient();
useEffect(() => {
queryClient.prefetchQuery(NEON_MIGRATIONS_PATH, () =>
fetchTemplateDataQueryFn(NEON_MIGRATIONS_PATH, {})
const metadataUrl = getMetadataUrl(templateBaseUrl);
queryClient.prefetchQuery(metadataUrl, () =>
fetchTemplateDataQueryFn(metadataUrl, {})
);
queryClient.prefetchQuery(NEON_METADATA_PATH, () =>
fetchTemplateDataQueryFn(NEON_METADATA_PATH, {})
const migrationUrl = getMigrationUrl(templateBaseUrl);
queryClient.prefetchQuery(migrationUrl, () =>
fetchTemplateDataQueryFn(migrationUrl, {})
);
queryClient.prefetchQuery(NEON_IMAGE_PATH, () =>
fetchTemplateDataQueryFn(NEON_IMAGE_PATH, {})
const sampleQueriesUrl = getSampleQueriesUrl(templateBaseUrl);
queryClient.prefetchQuery(sampleQueriesUrl, () =>
fetchTemplateDataQueryFn(sampleQueriesUrl, {})
);
queryClient.prefetchQuery(NEON_QUERY_PATH, () =>
fetchTemplateDataQueryFn(NEON_QUERY_PATH, {})
const schemaImageUrl = getSchemaImageUrl(templateBaseUrl);
queryClient.prefetchQuery(schemaImageUrl, () =>
fetchTemplateDataQueryFn(schemaImageUrl, {})
);
}, []);
};

View File

@ -6,6 +6,9 @@ import { HasuraMetadataV3 } from '@/metadata/types';
import {
skippedOnboardingVariables,
onboardingCompleteVariables,
templateSummaryRunQueryClickVariables,
templateSummaryRunQuerySkipVariables,
hasuraSourceCreationStartVariables,
graphQlMutation,
} from './constants';
@ -33,9 +36,11 @@ export function shouldShowOnboarding(
// onboarding skipped/completion is different with the new Neon flow
if (hasNeonAccess) {
if (
userActivity?.skipped_onboarding ||
userActivity?.onboarding_complete ||
userActivity?.hasura_source_creation_complete
userActivity?.[skippedOnboardingVariables.kind] ||
userActivity?.[onboardingCompleteVariables.kind] ||
userActivity?.[hasuraSourceCreationStartVariables.kind] ||
userActivity?.[templateSummaryRunQuerySkipVariables.kind] ||
userActivity?.[templateSummaryRunQueryClickVariables.kind]
) {
return false;
}
@ -86,6 +91,17 @@ export const persistOnboardingCompletion = () => {
});
};
export const emitOnboardingEvent = (variables: Record<string, unknown>) => {
// mutate server data
cloudDataServiceApiClient<ResponseDataOnMutation, ResponseDataOnMutation>(
graphQlMutation,
variables,
cloudHeaders
).catch(error => {
// TODO throw Sentry alert
console.error(error);
});
};
/**
* Utility function to be used as a react query QueryFn, which does a `GET` request to
* fetch our requested object, and returns a promise.