mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-10-26 10:20:54 +03:00
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:
parent
7739f8e4a0
commit
950e8dbe25
@ -23,7 +23,8 @@ export function Neon(props: { allDatabases: string[]; dispatch: Dispatch }) {
|
||||
getNeonDBName(allDatabases),
|
||||
pushToDatasource,
|
||||
pushToConnectDBPage,
|
||||
dispatch
|
||||
dispatch,
|
||||
'data-manage-create'
|
||||
);
|
||||
|
||||
const neonBannerProps = transformNeonIntegrationStatusToNeonBannerProps(
|
||||
|
@ -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();
|
||||
|
@ -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(() => {
|
||||
|
@ -31,6 +31,8 @@ export function getNeonDBName(allDatabases: string[]) {
|
||||
return dbName;
|
||||
}
|
||||
|
||||
export type NeonIntegrationContext = 'onboarding' | 'data-manage-create';
|
||||
|
||||
export function transformNeonIntegrationStatusToNeonBannerProps(
|
||||
neonIntegrationStatus: NeonIntegrationStatus
|
||||
): NeonBannerProps {
|
||||
|
@ -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}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
@ -3,3 +3,4 @@ export { usePrefetchNeonOnboardingTemplateData } from './usePrefetchNeonOnboardi
|
||||
export { useInstallMigration } from './useInstallMigration';
|
||||
export { useInstallMetadata } from './useInstallMetadata';
|
||||
export { useInstallTemplate } from './useInstallTemplate';
|
||||
export { useEmitOnboardingEvents } from './useEmitOnboardingEvents';
|
||||
|
@ -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]);
|
||||
}
|
@ -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) {
|
||||
|
@ -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, {})
|
||||
);
|
||||
}, []);
|
||||
};
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user