mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 17:31:56 +03:00
console: Refactor cloud console onboarding
[GT-639]: https://hasurahq.atlassian.net/browse/GT-639?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9344 GitOrigin-RevId: 9f135937075baf12d1370508fa6717b7bcdbe4ce
This commit is contained in:
parent
d8a4d254f6
commit
84ae7a0652
@ -73,7 +73,7 @@ export {
|
|||||||
} from './lib/features/Analytics';
|
} from './lib/features/Analytics';
|
||||||
export { CloudOnboarding } from './lib/features/CloudOnboarding';
|
export { CloudOnboarding } from './lib/features/CloudOnboarding';
|
||||||
export { prefetchSurveysData } from './lib/features/Surveys';
|
export { prefetchSurveysData } from './lib/features/Surveys';
|
||||||
export { prefetchOnboardingData } from './lib/features/CloudOnboarding/OnboardingWizard';
|
export { prefetchOnboardingData } from './lib/features/CloudOnboarding/NeonOnboardingWizard';
|
||||||
export {
|
export {
|
||||||
prefetchEELicenseInfo,
|
prefetchEELicenseInfo,
|
||||||
NavbarButton as EntepriseNavbarButton,
|
NavbarButton as EntepriseNavbarButton,
|
||||||
|
@ -1,35 +1,27 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useAppDispatch } from '../../../storeHooks';
|
import { useAppDispatch } from '../../../storeHooks';
|
||||||
import { AllowedSurveyThemes, Survey } from '../../Surveys';
|
import { ConnectDBScreen, TemplateSummary } from './components';
|
||||||
import {
|
import { DialogContainer } from '../components';
|
||||||
ConnectDBScreen,
|
|
||||||
TemplateSummary,
|
|
||||||
DialogContainer,
|
|
||||||
UseCaseScreen,
|
|
||||||
} from './components';
|
|
||||||
|
|
||||||
import { useWizardState } from './hooks';
|
import { useWizardState } from './hooks';
|
||||||
import {
|
import {
|
||||||
NEON_TEMPLATE_BASE_PATH,
|
|
||||||
dialogHeader,
|
dialogHeader,
|
||||||
familiaritySurveySubHeader,
|
NEON_TEMPLATE_BASE_PATH,
|
||||||
stepperNavSteps,
|
stepperNavSteps,
|
||||||
} from './constants';
|
} from '../constants';
|
||||||
|
import { OnboardingResponseData } from '../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parent container for the onboarding wizard. Takes care of assembling and rendering all steps.
|
* Parent container for the onboarding wizard. Takes care of assembling and rendering all steps.
|
||||||
*/
|
*/
|
||||||
export function Root() {
|
export function Root(props: {
|
||||||
|
onboardingData: OnboardingResponseData | undefined;
|
||||||
|
}) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const [stepperIndex, setStepperIndex] = React.useState<number>(1);
|
const [stepperIndex, setStepperIndex] = React.useState<number>(1);
|
||||||
|
|
||||||
const {
|
const { state, setState } = useWizardState(props.onboardingData);
|
||||||
state,
|
|
||||||
setState,
|
|
||||||
familiaritySurveyData,
|
|
||||||
familiaritySurveyOnOptionClick,
|
|
||||||
} = useWizardState();
|
|
||||||
|
|
||||||
const transitionToTemplateSummary = () => {
|
const transitionToTemplateSummary = () => {
|
||||||
setState('template-summary');
|
setState('template-summary');
|
||||||
@ -40,20 +32,6 @@ export function Root() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case 'familiarity-survey': {
|
|
||||||
return (
|
|
||||||
<DialogContainer
|
|
||||||
header={dialogHeader}
|
|
||||||
subHeader={familiaritySurveySubHeader}
|
|
||||||
>
|
|
||||||
<Survey
|
|
||||||
theme={AllowedSurveyThemes.familiaritySurveyTheme}
|
|
||||||
onSubmit={familiaritySurveyOnOptionClick}
|
|
||||||
data={familiaritySurveyData}
|
|
||||||
/>
|
|
||||||
</DialogContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case 'landing-page': {
|
case 'landing-page': {
|
||||||
return (
|
return (
|
||||||
<DialogContainer
|
<DialogContainer
|
||||||
@ -87,12 +65,6 @@ export function Root() {
|
|||||||
</DialogContainer>
|
</DialogContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'use-case-onboarding':
|
|
||||||
return (
|
|
||||||
<DialogContainer header="">
|
|
||||||
<UseCaseScreen dismiss={dismiss} dispatch={dispatch} />
|
|
||||||
</DialogContainer>
|
|
||||||
);
|
|
||||||
case 'hidden':
|
case 'hidden':
|
||||||
default: {
|
default: {
|
||||||
return null;
|
return null;
|
@ -15,8 +15,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
NEON_TEMPLATE_BASE_PATH,
|
NEON_TEMPLATE_BASE_PATH,
|
||||||
skippedNeonOnboardingVariables,
|
skippedNeonOnboardingVariables,
|
||||||
} from '../../../constants';
|
} from '../../../../constants';
|
||||||
import { emitOnboardingEvent } from '../../../utils';
|
import { emitOnboardingEvent } from '../../../../utils';
|
||||||
|
|
||||||
export function NeonOnboarding(props: {
|
export function NeonOnboarding(props: {
|
||||||
dispatch: Dispatch;
|
dispatch: Dispatch;
|
@ -6,9 +6,9 @@ import { HasuraLogoFull } from '../../../../../new-components/HasuraLogo';
|
|||||||
import { Analytics } from '../../../../Analytics';
|
import { Analytics } from '../../../../Analytics';
|
||||||
import { NeonIcon } from './NeonIcon';
|
import { NeonIcon } from './NeonIcon';
|
||||||
import _push from '../../../../../components/Services/Data/push';
|
import _push from '../../../../../components/Services/Data/push';
|
||||||
import { emitOnboardingEvent } from '../../utils';
|
import { emitOnboardingEvent } from '../../../utils';
|
||||||
import { Dispatch } from '../../../../../types';
|
import { Dispatch } from '../../../../../types';
|
||||||
import { skippedNeonOnboardingToConnectOtherDB } from '../../constants';
|
import { skippedNeonOnboardingToConnectOtherDB } from '../../../constants';
|
||||||
|
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
refresh: <MdRefresh />,
|
refresh: <MdRefresh />,
|
@ -6,10 +6,14 @@ import {
|
|||||||
staleTime,
|
staleTime,
|
||||||
templateSummaryRunQueryClickVariables,
|
templateSummaryRunQueryClickVariables,
|
||||||
templateSummaryRunQuerySkipVariables,
|
templateSummaryRunQuerySkipVariables,
|
||||||
} from '../../constants';
|
} from '../../../constants';
|
||||||
import { QueryScreen } from './QueryScreen';
|
import { QueryScreen } from '../../../components/QueryScreen/QueryScreen';
|
||||||
import { fetchTemplateDataQueryFn, emitOnboardingEvent } from '../../utils';
|
import { fetchTemplateDataQueryFn } from '../../utils';
|
||||||
import { runQueryInGraphiQL, fillSampleQueryInGraphiQL } from '../../../utils';
|
import {
|
||||||
|
runQueryInGraphiQL,
|
||||||
|
fillSampleQueryInGraphiQL,
|
||||||
|
emitOnboardingEvent,
|
||||||
|
} from '../../../utils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
templateUrl: string;
|
templateUrl: string;
|
@ -0,0 +1,2 @@
|
|||||||
|
export { TemplateSummary } from './TemplateSummary/TemplateSummary';
|
||||||
|
export { ConnectDBScreen } from './ConnectDBScreen/ConnectDBScreen';
|
@ -2,16 +2,12 @@ import { useEffect } from 'react';
|
|||||||
import { NeonIntegrationStatus } from '../../../../components/Services/Data/DataSources/CreateDataSource/Neon/useNeonIntegration';
|
import { NeonIntegrationStatus } from '../../../../components/Services/Data/DataSources/CreateDataSource/Neon/useNeonIntegration';
|
||||||
import {
|
import {
|
||||||
neonOAuthStartVariables,
|
neonOAuthStartVariables,
|
||||||
// neonOAuthCompleteVariables,
|
|
||||||
neonDbCreationStartVariables,
|
neonDbCreationStartVariables,
|
||||||
// neonDbCreationCompleteVariables,
|
|
||||||
hasuraSourceCreationStartVariables,
|
hasuraSourceCreationStartVariables,
|
||||||
// hasuraSourceCreationCompleteVariables,
|
|
||||||
installTemplateStartVariables,
|
installTemplateStartVariables,
|
||||||
// installTemplateCompleteVariables,
|
|
||||||
getNeonOnboardingErrorVariables,
|
getNeonOnboardingErrorVariables,
|
||||||
} from '../constants';
|
} from '../../constants';
|
||||||
import { emitOnboardingEvent } from '../utils';
|
import { emitOnboardingEvent } from '../../utils';
|
||||||
|
|
||||||
export function useEmitOnboardingEvents(
|
export function useEmitOnboardingEvents(
|
||||||
neonIntegrationStatus: NeonIntegrationStatus,
|
neonIntegrationStatus: NeonIntegrationStatus,
|
@ -6,7 +6,7 @@ import { HasuraMetadataV3 } from '../../../../metadata/types';
|
|||||||
import { MetadataResponse } from '../../../MetadataAPI';
|
import { MetadataResponse } from '../../../MetadataAPI';
|
||||||
import { useAppSelector } from '../../../../storeHooks';
|
import { useAppSelector } from '../../../../storeHooks';
|
||||||
import { useMutation, useQuery } from 'react-query';
|
import { useMutation, useQuery } from 'react-query';
|
||||||
import { staleTime } from '../constants';
|
import { staleTime } from '../../constants';
|
||||||
import { fetchTemplateDataQueryFn, transformOldMetadata } from '../utils';
|
import { fetchTemplateDataQueryFn, transformOldMetadata } from '../utils';
|
||||||
|
|
||||||
type MutationFnArgs = {
|
type MutationFnArgs = {
|
@ -6,7 +6,7 @@ import { Api } from '../../../../hooks/apiUtils';
|
|||||||
import { useAppSelector } from '../../../../storeHooks';
|
import { useAppSelector } from '../../../../storeHooks';
|
||||||
import { useMutation, useQuery } from 'react-query';
|
import { useMutation, useQuery } from 'react-query';
|
||||||
import { fetchTemplateDataQueryFn } from '../utils';
|
import { fetchTemplateDataQueryFn } from '../utils';
|
||||||
import { staleTime } from '../constants';
|
import { staleTime } from '../../constants';
|
||||||
import 'whatwg-fetch';
|
import 'whatwg-fetch';
|
||||||
|
|
||||||
type MutationFnArgs = {
|
type MutationFnArgs = {
|
@ -20,7 +20,7 @@ import {
|
|||||||
serverDownErrorMessage,
|
serverDownErrorMessage,
|
||||||
} from '../mocks/constants';
|
} from '../mocks/constants';
|
||||||
import { useInstallTemplate } from './useInstallTemplate';
|
import { useInstallTemplate } from './useInstallTemplate';
|
||||||
import { NEON_TEMPLATE_BASE_PATH } from '../constants';
|
import { NEON_TEMPLATE_BASE_PATH } from '../../constants';
|
||||||
import 'whatwg-fetch';
|
import 'whatwg-fetch';
|
||||||
|
|
||||||
const server = setupServer();
|
const server = setupServer();
|
@ -1,6 +1,6 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useInstallMigration } from './useInstallMigration';
|
import { useInstallMigration } from './useInstallMigration';
|
||||||
import { getMetadataUrl, getMigrationUrl } from '../constants';
|
import { getMetadataUrl, getMigrationUrl } from '../../constants';
|
||||||
import { useInstallMetadata } from './useInstallMetadata';
|
import { useInstallMetadata } from './useInstallMetadata';
|
||||||
|
|
||||||
/**
|
/**
|
@ -1,8 +1,8 @@
|
|||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { APIError } from '../../../../hooks/error';
|
import { APIError } from '../../../../hooks/error';
|
||||||
import { fetchAllOnboardingDataQueryFn } from '../utils';
|
import { fetchAllOnboardingDataQueryFn } from '../utils';
|
||||||
import { onboardingQueryKey } from '../constants';
|
import { onboardingQueryKey } from '../../constants';
|
||||||
import { OnboardingResponseData } from '../types';
|
import { OnboardingResponseData } from '../../types';
|
||||||
|
|
||||||
export function useOnboardingData() {
|
export function useOnboardingData() {
|
||||||
return useQuery<OnboardingResponseData, APIError>(
|
return useQuery<OnboardingResponseData, APIError>(
|
@ -5,7 +5,7 @@ import {
|
|||||||
getMigrationUrl,
|
getMigrationUrl,
|
||||||
getSampleQueriesUrl,
|
getSampleQueriesUrl,
|
||||||
getSchemaImageUrl,
|
getSchemaImageUrl,
|
||||||
} from '../constants';
|
} from '../../constants';
|
||||||
import { fetchTemplateDataQueryFn } from '../utils';
|
import { fetchTemplateDataQueryFn } from '../utils';
|
||||||
|
|
||||||
/**
|
/**
|
@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { getWizardState } from '../utils';
|
||||||
|
import { OnboardingResponseData } from '../../types';
|
||||||
|
|
||||||
|
export type WizardState = 'landing-page' | 'template-summary' | 'hidden';
|
||||||
|
|
||||||
|
export function useWizardState(
|
||||||
|
onboardingData: OnboardingResponseData | undefined
|
||||||
|
) {
|
||||||
|
const [state, setState] = useState<WizardState>(
|
||||||
|
getWizardState(onboardingData)
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const wizardState = getWizardState(onboardingData);
|
||||||
|
setState(wizardState);
|
||||||
|
}, [onboardingData]);
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
setState,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import { Root } from './Root';
|
||||||
|
|
||||||
|
export { prefetchOnboardingData } from './utils';
|
||||||
|
export { useOnboardingData } from './hooks';
|
||||||
|
export const NeonOnboarding = Root;
|
@ -8,8 +8,8 @@ import {
|
|||||||
getSampleQueriesUrl,
|
getSampleQueriesUrl,
|
||||||
getSchemaImageUrl,
|
getSchemaImageUrl,
|
||||||
NEON_TEMPLATE_BASE_PATH,
|
NEON_TEMPLATE_BASE_PATH,
|
||||||
} from '../constants';
|
} from '../../constants';
|
||||||
import { OnboardingResponseData } from '../types';
|
import { OnboardingResponseData } from '../../types';
|
||||||
|
|
||||||
const userMock = {
|
const userMock = {
|
||||||
id: '59300b64-fb3a-4f17-8a0f-6f698569eade',
|
id: '59300b64-fb3a-4f17-8a0f-6f698569eade',
|
@ -9,7 +9,7 @@ import {
|
|||||||
MOCK_MIGRATION_FILE_CONTENTS,
|
MOCK_MIGRATION_FILE_CONTENTS,
|
||||||
serverDownErrorMessage,
|
serverDownErrorMessage,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import { OnboardingResponseData } from '../types';
|
import { OnboardingResponseData } from '../../types';
|
||||||
import { FetchAllSurveysDataQuery } from '../../../ControlPlane';
|
import { FetchAllSurveysDataQuery } from '../../../ControlPlane';
|
||||||
|
|
||||||
type ResponseBodyOnSuccess = {
|
type ResponseBodyOnSuccess = {
|
@ -0,0 +1,104 @@
|
|||||||
|
import { cloudDataServiceApiClient } from '../../../hooks/cloudDataServiceApiClient';
|
||||||
|
import { Api } from '../../../hooks/apiUtils';
|
||||||
|
import { HasuraMetadataV3 } from '../../../metadata/types';
|
||||||
|
import { reactQueryClient } from '../../../lib/reactQuery';
|
||||||
|
import {
|
||||||
|
fetchAllOnboardingDataQuery,
|
||||||
|
fetchAllOnboardingDataQueryVariables,
|
||||||
|
onboardingQueryKey,
|
||||||
|
} from '../constants';
|
||||||
|
import { WizardState } from './hooks/useWizardState';
|
||||||
|
import { OnboardingResponseData } from '../types';
|
||||||
|
|
||||||
|
export function getWizardState(
|
||||||
|
onboardingData?: OnboardingResponseData
|
||||||
|
): WizardState {
|
||||||
|
// if onbarding data is not present due to api error, or data loading state, then hide the wizard
|
||||||
|
// this early return is required to distinguish between server errors vs data not being present for user
|
||||||
|
// if the request is successful and data is not present for the given user, then we should show the onboarding wizard
|
||||||
|
if (!onboardingData?.data) return 'hidden';
|
||||||
|
|
||||||
|
// if user created account before the launch of onboarding wizard (Oct 17, 2022),
|
||||||
|
// hide the wizard and survey
|
||||||
|
const userCreatedAt = new Date(onboardingData.data.users[0].created_at);
|
||||||
|
if (userCreatedAt.getTime() < 1666008600000) {
|
||||||
|
return 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!onboardingData.data.user_onboarding[0]?.is_onboarded) {
|
||||||
|
return 'landing-page';
|
||||||
|
}
|
||||||
|
return 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloudHeaders = {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to be used as a react query QueryFn, which does a `GET` request to
|
||||||
|
* fetch our requested object, and returns a promise.
|
||||||
|
*/
|
||||||
|
export function fetchTemplateDataQueryFn<
|
||||||
|
ResponseData,
|
||||||
|
TransformedData = ResponseData
|
||||||
|
>(
|
||||||
|
dataUrl: string,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
transformFn?: (data: ResponseData) => TransformedData
|
||||||
|
) {
|
||||||
|
return Api.get<ResponseData, TransformedData>(
|
||||||
|
{
|
||||||
|
url: dataUrl,
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
transformFn
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function which merges the old and additional metadata objects
|
||||||
|
* to create a new metadata object and returns it.
|
||||||
|
*/
|
||||||
|
export const transformOldMetadata = (
|
||||||
|
oldMetadata: HasuraMetadataV3,
|
||||||
|
additionalMetadata: HasuraMetadataV3,
|
||||||
|
source: string
|
||||||
|
) => {
|
||||||
|
const newMetadata: HasuraMetadataV3 = {
|
||||||
|
...oldMetadata,
|
||||||
|
sources:
|
||||||
|
oldMetadata?.sources?.map(oldSource => {
|
||||||
|
if (oldSource.name !== source) {
|
||||||
|
return oldSource;
|
||||||
|
}
|
||||||
|
const metadataObject = additionalMetadata?.sources?.[0];
|
||||||
|
if (!metadataObject) {
|
||||||
|
return oldSource;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...oldSource,
|
||||||
|
tables: [...oldSource.tables, ...(metadataObject.tables ?? [])],
|
||||||
|
functions: [
|
||||||
|
...(oldSource.functions ?? []),
|
||||||
|
...(metadataObject.functions ?? []),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}) ?? [],
|
||||||
|
};
|
||||||
|
return newMetadata;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchAllOnboardingDataQueryFn = () =>
|
||||||
|
cloudDataServiceApiClient<OnboardingResponseData, OnboardingResponseData>(
|
||||||
|
fetchAllOnboardingDataQuery,
|
||||||
|
fetchAllOnboardingDataQueryVariables,
|
||||||
|
cloudHeaders
|
||||||
|
);
|
||||||
|
|
||||||
|
export const prefetchOnboardingData = () => {
|
||||||
|
reactQueryClient.prefetchQuery(
|
||||||
|
onboardingQueryKey,
|
||||||
|
fetchAllOnboardingDataQueryFn
|
||||||
|
);
|
||||||
|
};
|
@ -1,70 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { FaPlayCircle } from 'react-icons/fa';
|
|
||||||
import { Dialog } from '../../../../../new-components/Dialog';
|
|
||||||
import { Button } from '../../../../../new-components/Button';
|
|
||||||
import { Analytics } from '../../../../Analytics';
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
query: string;
|
|
||||||
schemaImage: string;
|
|
||||||
onRunHandler: () => void;
|
|
||||||
onSkipHandler: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function QueryDialog(props: Props) {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
query,
|
|
||||||
schemaImage,
|
|
||||||
onRunHandler,
|
|
||||||
onSkipHandler,
|
|
||||||
} = props;
|
|
||||||
return (
|
|
||||||
<Dialog hasBackdrop title={title} description={description} size="md">
|
|
||||||
<>
|
|
||||||
<div className="mx-4 my-2">
|
|
||||||
<div className="text-md text-gray-700 mb-2">
|
|
||||||
We've created a <b>`sample`</b> schema to help you get started
|
|
||||||
using Hasura. It contains the sample structure of a music directory,
|
|
||||||
as well as a view and function to give you an idea about how Hasura
|
|
||||||
works.
|
|
||||||
</div>
|
|
||||||
<img
|
|
||||||
className="mb-2"
|
|
||||||
data-testid="query-dialog-schema-image"
|
|
||||||
src={schemaImage}
|
|
||||||
alt="getting-started"
|
|
||||||
/>
|
|
||||||
<div data-testid="query-dialog-sample-query">
|
|
||||||
<div className="text-md text-gray-700 mb-2">
|
|
||||||
Give it a try with our example query:
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<pre className="border border-gray-300 bg-gray-200 text-muted font-mono text-sm px-4 py-4">
|
|
||||||
{query}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border border-gray-300 shadow-lg bg-white flex justify-between items-center px-4 py-4">
|
|
||||||
<Analytics name="query-dialog-skip-button">
|
|
||||||
<div
|
|
||||||
className="cursor-pointer text-secondary text-sm hover:text-secondary-dark"
|
|
||||||
onClick={onSkipHandler}
|
|
||||||
>
|
|
||||||
Skip getting started tutorial
|
|
||||||
</div>
|
|
||||||
</Analytics>
|
|
||||||
<Analytics name="query-dialog-get-started-button">
|
|
||||||
<Button mode="primary" onClick={onRunHandler}>
|
|
||||||
Run Query <FaPlayCircle />
|
|
||||||
</Button>
|
|
||||||
</Analytics>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,185 +0,0 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
|
||||||
import { Button } from '../../../../../new-components/Button';
|
|
||||||
import { emitOnboardingEvent } from '../../utils';
|
|
||||||
import {
|
|
||||||
getUseCaseExperimentOnboardingVariables,
|
|
||||||
skippedUseCaseExperimentOnboarding,
|
|
||||||
} from '../../constants';
|
|
||||||
import { Analytics, trackCustomEvent } from '../../../../Analytics';
|
|
||||||
import { Dispatch } from '../../../../../types';
|
|
||||||
import _push from '../../../../../components/Services/Data/push';
|
|
||||||
|
|
||||||
type UseCaseScreenProps = { dismiss: () => void; dispatch: Dispatch };
|
|
||||||
|
|
||||||
export type UseCases =
|
|
||||||
| 'data-api'
|
|
||||||
| 'gql-backend'
|
|
||||||
| 'data-federation'
|
|
||||||
| 'gateway';
|
|
||||||
|
|
||||||
interface UseCaseAssets {
|
|
||||||
id: UseCases;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
image: string;
|
|
||||||
consoleUrl: string;
|
|
||||||
docsUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useCasesAssets: UseCaseAssets[] = [
|
|
||||||
{
|
|
||||||
id: 'data-api',
|
|
||||||
image:
|
|
||||||
'https://storage.googleapis.com/graphql-engine-cdn.hasura.io/cloud-console/assets/common/img/hasura-usecase-data-api.svg',
|
|
||||||
title: 'Data Access Layer',
|
|
||||||
description:
|
|
||||||
'Build an instant, real-time API over your data sources for easy and performant access',
|
|
||||||
consoleUrl: '/',
|
|
||||||
docsUrl: 'https://hasura.io/docs/latest/resources/use-case/data-api/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'gql-backend',
|
|
||||||
image:
|
|
||||||
'https://storage.googleapis.com/graphql-engine-cdn.hasura.io/cloud-console/assets/common/img/hasura-usecase-gql-backend.svg',
|
|
||||||
title: 'Graphql Backend',
|
|
||||||
description:
|
|
||||||
'Build a lightning-fast GraphQL backend and significantly accelerate your application development',
|
|
||||||
consoleUrl: '/',
|
|
||||||
docsUrl: 'https://hasura.io/docs/latest/resources/use-case/gql-backend/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'gateway',
|
|
||||||
image:
|
|
||||||
'https://storage.googleapis.com/graphql-engine-cdn.hasura.io/cloud-console/assets/common/img/hasura-usecase-gateway.svg',
|
|
||||||
title: 'API Gateway',
|
|
||||||
description:
|
|
||||||
'Build a single entry point from client applications into an ecosystem of microservices',
|
|
||||||
consoleUrl: '/',
|
|
||||||
docsUrl: 'https://hasura.io/docs/latest/resources/use-case/api-gateway/',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const UseCaseScreen = (props: UseCaseScreenProps) => {
|
|
||||||
const [selectedUseCase, setSelectedUseCase] = useState<UseCases | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
trackCustomEvent({
|
|
||||||
location: 'Console',
|
|
||||||
action: 'Load',
|
|
||||||
object: 'Use Case Wizard',
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const randomUseCaseAssets = useMemo(() => {
|
|
||||||
return useCasesAssets.sort(() => Math.random() - 0.5);
|
|
||||||
}, [useCasesAssets]);
|
|
||||||
|
|
||||||
const useCase = useCasesAssets.find(
|
|
||||||
useCase => useCase.id === selectedUseCase
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSubmit = () => {
|
|
||||||
if (useCase) {
|
|
||||||
props.dispatch(_push(useCase.consoleUrl));
|
|
||||||
emitOnboardingEvent(getUseCaseExperimentOnboardingVariables(useCase.id));
|
|
||||||
props.dismiss();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="use-case-container border border-solid border-gray-400 rounded-sm flex flex-col flex-wrap p-8 h-full bg-white"
|
|
||||||
style={{
|
|
||||||
width: '902px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="use-case-welcome-header flex justify-between w-full">
|
|
||||||
<div className="use-case-welcome-text font-sans">
|
|
||||||
<h1 className="text-xl font-bold text-cloud-darkest">
|
|
||||||
Welcome to Hasura!
|
|
||||||
</h1>
|
|
||||||
<div className="text-muted-dark font-normal">
|
|
||||||
Hasura can help you supercharge your development
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="use-case-welcome-illustrations h-[93px]">
|
|
||||||
<img
|
|
||||||
src="https://storage.googleapis.com/graphql-engine-cdn.hasura.io/cloud-dashboard/dashboard/hasura-loading-illustration.svg"
|
|
||||||
alt="welcome"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="use-case-intro-text text-[#64748B] font-sans mt-3 mb-3.5">
|
|
||||||
What would you like to build with Hasura?
|
|
||||||
</div>
|
|
||||||
<div className="use-cases flex flex-wrap justify-around gap-y-15 gap-y-8">
|
|
||||||
{randomUseCaseAssets.map((item, index) => (
|
|
||||||
<div className="flex relative h-[250px]" key={index}>
|
|
||||||
<label
|
|
||||||
key={index}
|
|
||||||
htmlFor={item.id}
|
|
||||||
className="use-case-card flex flex-col cursor-pointer border border-solid border-slate-300 rounded focus-within:border focus-within:border-solid focus-within:border-amber-500 transition-shadow shadow-none hover:shadow-md w-[400px]"
|
|
||||||
onChange={event => {
|
|
||||||
const selectedUseCaseNode = event.target as HTMLInputElement;
|
|
||||||
setSelectedUseCase(selectedUseCaseNode.id as UseCases);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img src={item.image} alt={item.id} />
|
|
||||||
|
|
||||||
<div className="absolute bottom-0 data-api-description flex mt-3 ml-6 mb-2">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
id={item.id}
|
|
||||||
name="use-case"
|
|
||||||
className="mt-1"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col font-sans ml-2">
|
|
||||||
<div className="font-semibold text-slate-900">
|
|
||||||
{item.title}
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-dark font-normal">
|
|
||||||
{item.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="use-case-cta flex justify-between w-full mt-8">
|
|
||||||
<Analytics name="use-case-onboarding-skip">
|
|
||||||
<div
|
|
||||||
className="ml-xs mr-4 text-secondary flex items-center cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
props.dismiss();
|
|
||||||
emitOnboardingEvent(skippedUseCaseExperimentOnboarding);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Skip
|
|
||||||
</div>
|
|
||||||
</Analytics>
|
|
||||||
{useCase ? (
|
|
||||||
<Analytics name={`use-case-onboarding-${selectedUseCase}`}>
|
|
||||||
<a
|
|
||||||
href={useCase.docsUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
onClick={onSubmit}
|
|
||||||
>
|
|
||||||
<Button mode="primary">Continue</Button>
|
|
||||||
</a>
|
|
||||||
</Analytics>
|
|
||||||
) : (
|
|
||||||
<Analytics
|
|
||||||
name={`use-case-onboarding-unselected`}
|
|
||||||
passHtmlAttributesToChildren
|
|
||||||
>
|
|
||||||
<Button mode="primary" disabled>
|
|
||||||
Continue
|
|
||||||
</Button>
|
|
||||||
</Analytics>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,46 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useSurveysData } from '../../../Surveys';
|
|
||||||
import { useOnboardingData } from './useOnboardingData';
|
|
||||||
import { getWizardState } from '../utils';
|
|
||||||
import { AllowedSurveyNames } from '../../../Surveys/types';
|
|
||||||
import { getLSItem, LS_KEYS, removeLSItem, setLSItem } from '../../../../utils';
|
|
||||||
|
|
||||||
export type WizardState =
|
|
||||||
| 'familiarity-survey'
|
|
||||||
| 'landing-page'
|
|
||||||
| 'template-summary'
|
|
||||||
| 'use-case-onboarding'
|
|
||||||
| 'hidden';
|
|
||||||
|
|
||||||
export function useWizardState() {
|
|
||||||
const {
|
|
||||||
show: showSurvey,
|
|
||||||
data: familiaritySurveyData,
|
|
||||||
onSubmit: familiaritySurveyOnOptionClick,
|
|
||||||
} = useSurveysData({ surveyName: AllowedSurveyNames.familiaritySurvey });
|
|
||||||
|
|
||||||
const { data: onboardingData } = useOnboardingData();
|
|
||||||
|
|
||||||
const [state, setState] = useState<WizardState>(
|
|
||||||
getWizardState(showSurvey, onboardingData)
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const wizardState = getWizardState(showSurvey, onboardingData);
|
|
||||||
if (wizardState !== 'hidden') {
|
|
||||||
setLSItem(LS_KEYS.showUseCaseOverviewPopup, 'true');
|
|
||||||
}
|
|
||||||
setState(wizardState);
|
|
||||||
|
|
||||||
//removing skipOnboarding key from LocalStorage
|
|
||||||
if (getLSItem(LS_KEYS.skipOnboarding) === 'true') {
|
|
||||||
removeLSItem(LS_KEYS.skipOnboarding);
|
|
||||||
}
|
|
||||||
}, [onboardingData, showSurvey]);
|
|
||||||
return {
|
|
||||||
state,
|
|
||||||
setState,
|
|
||||||
familiaritySurveyData,
|
|
||||||
familiaritySurveyOnOptionClick,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import { Root } from './Root';
|
|
||||||
|
|
||||||
export { prefetchOnboardingData, emitOnboardingEvent } from './utils';
|
|
||||||
export { oneClickDeploymentOnboardingShown } from './constants';
|
|
||||||
export { useOnboardingData } from './hooks';
|
|
||||||
export type { UserOnboarding, OnboardingResponseData } from './types';
|
|
||||||
export { DialogContainer } from './components';
|
|
||||||
export const OnboardingWizard = Root;
|
|
@ -1,38 +0,0 @@
|
|||||||
import { One_Click_Deployment_States_Enum } from '../../ControlPlane';
|
|
||||||
|
|
||||||
export type UserOnboarding = {
|
|
||||||
activity: Record<string, any>;
|
|
||||||
target: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type User = {
|
|
||||||
id: string;
|
|
||||||
created_at: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OneClickDeploymentByProject = {
|
|
||||||
id: number;
|
|
||||||
state: One_Click_Deployment_States_Enum;
|
|
||||||
git_repository_url: string;
|
|
||||||
git_repository_branch?: string;
|
|
||||||
hasura_directory?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OneClickDeploymentSampleApp = {
|
|
||||||
git_repository_url: string;
|
|
||||||
git_repository_branch: string;
|
|
||||||
hasura_directory: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
react_icons_fa_component_name?: string;
|
|
||||||
rank: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OnboardingResponseData = {
|
|
||||||
data: {
|
|
||||||
user_onboarding: UserOnboarding[];
|
|
||||||
users: User[];
|
|
||||||
one_click_deployment: OneClickDeploymentByProject[];
|
|
||||||
one_click_deployment_sample_apps: OneClickDeploymentSampleApp[];
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,221 +0,0 @@
|
|||||||
import { parse, print } from 'graphql';
|
|
||||||
import { cloudDataServiceApiClient } from '../../../hooks/cloudDataServiceApiClient';
|
|
||||||
import { Api } from '../../../hooks/apiUtils';
|
|
||||||
import { HasuraMetadataV3 } from '../../../metadata/types';
|
|
||||||
import { reactQueryClient } from '../../../lib/reactQuery';
|
|
||||||
import { programmaticallyTraceError } from '../../Analytics';
|
|
||||||
import {
|
|
||||||
skippedNeonOnboardingVariables,
|
|
||||||
onboardingCompleteVariables,
|
|
||||||
templateSummaryRunQueryClickVariables,
|
|
||||||
templateSummaryRunQuerySkipVariables,
|
|
||||||
hasuraSourceCreationStartVariables,
|
|
||||||
trackOnboardingActivityMutation,
|
|
||||||
onboardingQueryKey,
|
|
||||||
fetchAllOnboardingDataQuery,
|
|
||||||
fetchAllOnboardingDataQueryVariables,
|
|
||||||
oneClickDeploymentOnboardingShown,
|
|
||||||
useCaseExperimentOnboarding,
|
|
||||||
skippedOnboardingThroughURLParamVariables,
|
|
||||||
skippedUseCaseExperimentOnboarding,
|
|
||||||
skippedNeonOnboardingToConnectOtherDB,
|
|
||||||
} from './constants';
|
|
||||||
import { WizardState } from './hooks/useWizardState';
|
|
||||||
import { OnboardingResponseData, UserOnboarding } from './types';
|
|
||||||
import { getLSItem, LS_KEYS } from '../../../utils';
|
|
||||||
|
|
||||||
export function shouldShowOnboarding(onboardingData: UserOnboarding) {
|
|
||||||
const userActivity = onboardingData?.activity;
|
|
||||||
|
|
||||||
if (
|
|
||||||
userActivity?.[skippedNeonOnboardingVariables.kind]?.value === 'true' ||
|
|
||||||
userActivity?.[skippedUseCaseExperimentOnboarding.kind]?.value === 'true' ||
|
|
||||||
userActivity?.[skippedOnboardingThroughURLParamVariables.kind]?.value ===
|
|
||||||
'true' ||
|
|
||||||
userActivity?.[skippedNeonOnboardingToConnectOtherDB.kind]?.value ===
|
|
||||||
'true' ||
|
|
||||||
userActivity?.[onboardingCompleteVariables.kind]?.value === 'true' ||
|
|
||||||
userActivity?.[hasuraSourceCreationStartVariables.kind]?.value === 'true' ||
|
|
||||||
userActivity?.[templateSummaryRunQuerySkipVariables.kind]?.value ===
|
|
||||||
'true' ||
|
|
||||||
userActivity?.[templateSummaryRunQueryClickVariables.kind]?.value ===
|
|
||||||
'true' ||
|
|
||||||
userActivity?.[oneClickDeploymentOnboardingShown.kind]?.value === 'true' ||
|
|
||||||
userActivity?.[useCaseExperimentOnboarding.kind]?.value === 'true' ||
|
|
||||||
userActivity?.[oneClickDeploymentOnboardingShown.kind]?.value === 'true'
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (getLSItem(LS_KEYS.skipOnboarding) === 'true') {
|
|
||||||
emitOnboardingEvent(skippedOnboardingThroughURLParamVariables);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nullUserOnboardingData = {
|
|
||||||
activity: {},
|
|
||||||
target: 'cloud_console',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms server returned data to the required format.
|
|
||||||
*/
|
|
||||||
function onboardingDataTransformFn(
|
|
||||||
data: OnboardingResponseData
|
|
||||||
): UserOnboarding {
|
|
||||||
return (
|
|
||||||
data?.data?.user_onboarding?.find(
|
|
||||||
onboarding => onboarding.target === 'cloud_console'
|
|
||||||
) || nullUserOnboardingData
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWizardState(
|
|
||||||
showFamiliaritySurvey: boolean,
|
|
||||||
onboardingData?: OnboardingResponseData
|
|
||||||
): WizardState {
|
|
||||||
// if onbarding data is not present due to api error, or data loading state, then hide the wizard
|
|
||||||
// this early return is required to distinguish between server errors vs data not being present for user
|
|
||||||
// if the request is successful and data is not present for the given user, then we should show the onboarding wizard
|
|
||||||
if (!onboardingData?.data) return 'hidden';
|
|
||||||
|
|
||||||
// if user created account before the launch of onboarding wizard (Oct 17, 2022),
|
|
||||||
// hide the wizard and survey
|
|
||||||
const userCreatedAt = new Date(onboardingData.data.users[0].created_at);
|
|
||||||
if (userCreatedAt.getTime() < 1666008600000) {
|
|
||||||
return 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
// transform the onboarding data if present, to a consumable format
|
|
||||||
const transformedOnboardingData = onboardingDataTransformFn(onboardingData);
|
|
||||||
if (shouldShowOnboarding(transformedOnboardingData)) {
|
|
||||||
if (showFamiliaritySurvey) return 'familiarity-survey';
|
|
||||||
return 'use-case-onboarding';
|
|
||||||
}
|
|
||||||
return 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResponseDataOnMutation = {
|
|
||||||
data: {
|
|
||||||
trackOnboardingActivity: {
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const cloudHeaders = {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const emitOnboardingEvent = (variables: Record<string, unknown>) => {
|
|
||||||
// mutate server data
|
|
||||||
cloudDataServiceApiClient<ResponseDataOnMutation, ResponseDataOnMutation>(
|
|
||||||
trackOnboardingActivityMutation,
|
|
||||||
variables,
|
|
||||||
cloudHeaders
|
|
||||||
).catch(error => {
|
|
||||||
programmaticallyTraceError(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.
|
|
||||||
*/
|
|
||||||
export function fetchTemplateDataQueryFn<
|
|
||||||
ResponseData,
|
|
||||||
TransformedData = ResponseData
|
|
||||||
>(
|
|
||||||
dataUrl: string,
|
|
||||||
headers: Record<string, string>,
|
|
||||||
transformFn?: (data: ResponseData) => TransformedData
|
|
||||||
) {
|
|
||||||
return Api.get<ResponseData, TransformedData>(
|
|
||||||
{
|
|
||||||
url: dataUrl,
|
|
||||||
headers,
|
|
||||||
},
|
|
||||||
transformFn
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function which merges the old and additional metadata objects
|
|
||||||
* to create a new metadata object and returns it.
|
|
||||||
*/
|
|
||||||
export const transformOldMetadata = (
|
|
||||||
oldMetadata: HasuraMetadataV3,
|
|
||||||
additionalMetadata: HasuraMetadataV3,
|
|
||||||
source: string
|
|
||||||
) => {
|
|
||||||
const newMetadata: HasuraMetadataV3 = {
|
|
||||||
...oldMetadata,
|
|
||||||
sources:
|
|
||||||
oldMetadata?.sources?.map(oldSource => {
|
|
||||||
if (oldSource.name !== source) {
|
|
||||||
return oldSource;
|
|
||||||
}
|
|
||||||
const metadataObject = additionalMetadata?.sources?.[0];
|
|
||||||
if (!metadataObject) {
|
|
||||||
return oldSource;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...oldSource,
|
|
||||||
tables: [...oldSource.tables, ...(metadataObject.tables ?? [])],
|
|
||||||
functions: [
|
|
||||||
...(oldSource.functions ?? []),
|
|
||||||
...(metadataObject.functions ?? []),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}) ?? [],
|
|
||||||
};
|
|
||||||
return newMetadata;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function to parse a string with multiple graphql queries and pick the one with given operation name
|
|
||||||
* */
|
|
||||||
export function getQueryFromSampleQueries(
|
|
||||||
allQueries: string,
|
|
||||||
operationName: string
|
|
||||||
): string {
|
|
||||||
const doc = parse(allQueries);
|
|
||||||
const { definitions: allDefinitions } = doc;
|
|
||||||
const definitions = allDefinitions.filter(
|
|
||||||
d => d.kind === 'OperationDefinition'
|
|
||||||
);
|
|
||||||
let queryDef = definitions
|
|
||||||
.filter(d => d.kind === 'OperationDefinition')
|
|
||||||
.find(d => {
|
|
||||||
if (d.kind === 'OperationDefinition') {
|
|
||||||
return d.name?.value === operationName;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
if (!queryDef) {
|
|
||||||
if (definitions.length > 0) {
|
|
||||||
queryDef = definitions[0];
|
|
||||||
} else {
|
|
||||||
throw new Error('no valid operations in sample.graphql');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return print({
|
|
||||||
...doc,
|
|
||||||
definitions: [queryDef],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetchAllOnboardingDataQueryFn = () =>
|
|
||||||
cloudDataServiceApiClient<OnboardingResponseData, OnboardingResponseData>(
|
|
||||||
fetchAllOnboardingDataQuery,
|
|
||||||
fetchAllOnboardingDataQueryVariables,
|
|
||||||
cloudHeaders
|
|
||||||
);
|
|
||||||
|
|
||||||
export const prefetchOnboardingData = () => {
|
|
||||||
reactQueryClient.prefetchQuery(
|
|
||||||
onboardingQueryKey,
|
|
||||||
fetchAllOnboardingDataQueryFn
|
|
||||||
);
|
|
||||||
};
|
|
@ -3,7 +3,8 @@ import globals from '../../../Globals';
|
|||||||
import { FaGithub } from 'react-icons/fa';
|
import { FaGithub } from 'react-icons/fa';
|
||||||
import { GraphiqlPopup, WorkflowProgress } from './components';
|
import { GraphiqlPopup, WorkflowProgress } from './components';
|
||||||
import { stepperNavSteps } from './constants';
|
import { stepperNavSteps } from './constants';
|
||||||
import { DialogContainer } from '../OnboardingWizard';
|
import { oneClickDeploymentOnboardingShown } from '../constants';
|
||||||
|
import { DialogContainer } from '../components';
|
||||||
import { useTriggerDeployment } from './hooks';
|
import { useTriggerDeployment } from './hooks';
|
||||||
import { GitRepoDetails, FallbackApp } from './types';
|
import { GitRepoDetails, FallbackApp } from './types';
|
||||||
import {
|
import {
|
||||||
@ -11,6 +12,7 @@ import {
|
|||||||
getGitRepoFullLinkFromDetails,
|
getGitRepoFullLinkFromDetails,
|
||||||
getSampleQueriesUrl,
|
getSampleQueriesUrl,
|
||||||
} from './util';
|
} from './util';
|
||||||
|
import { emitOnboardingEvent } from '../utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parent container for the one click deployment wizard. Takes care of assembling and rendering all steps.
|
* Parent container for the one click deployment wizard. Takes care of assembling and rendering all steps.
|
||||||
@ -39,6 +41,7 @@ export function Root(props: {
|
|||||||
const transitionToQueryPopupSuccessState = () => {
|
const transitionToQueryPopupSuccessState = () => {
|
||||||
setGraphiQlPopupStatus('success');
|
setGraphiQlPopupStatus('success');
|
||||||
setState('graphiql-popup');
|
setState('graphiql-popup');
|
||||||
|
emitOnboardingEvent(oneClickDeploymentOnboardingShown);
|
||||||
};
|
};
|
||||||
const transitionToQueryPopupWithErrorState = () => {
|
const transitionToQueryPopupWithErrorState = () => {
|
||||||
setGraphiQlPopupStatus('error');
|
setGraphiQlPopupStatus('error');
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
ProgressStateStatus,
|
ProgressStateStatus,
|
||||||
GitRepoDetails,
|
GitRepoDetails,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { OnboardingKind, OnboardingResponseData } from '../types';
|
||||||
|
|
||||||
// returns the error status if the step had an error in the latest workflow
|
// returns the error status if the step had an error in the latest workflow
|
||||||
// returns null if the step hasn't had an error in the latest workflow
|
// returns null if the step hasn't had an error in the latest workflow
|
||||||
@ -295,3 +296,19 @@ export const getSampleQueriesUrl = (gitRepoDetails: GitRepoDetails) => {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const oneClickDeploymentOnboardingKind = (
|
||||||
|
onboardingData: OnboardingResponseData
|
||||||
|
): OnboardingKind => ({
|
||||||
|
kind: 'one-click-deployment',
|
||||||
|
deployment: {
|
||||||
|
deploymentId: onboardingData.data.one_click_deployment[0].id,
|
||||||
|
gitRepoDetails: {
|
||||||
|
url: onboardingData.data.one_click_deployment[0].git_repository_url,
|
||||||
|
branch: onboardingData.data.one_click_deployment[0].git_repository_branch,
|
||||||
|
hasuraDirectory:
|
||||||
|
onboardingData.data.one_click_deployment[0].hasura_directory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fallbackApps: onboardingData.data.one_click_deployment_sample_apps || [],
|
||||||
|
});
|
||||||
|
@ -0,0 +1,192 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Button } from '../../../new-components/Button';
|
||||||
|
import { emitOnboardingEvent } from '../utils';
|
||||||
|
import {
|
||||||
|
getUseCaseExperimentOnboardingVariables,
|
||||||
|
skippedUseCaseExperimentOnboarding,
|
||||||
|
} from '../constants';
|
||||||
|
import { Analytics, trackCustomEvent } from '../../Analytics';
|
||||||
|
import _push from '../../../components/Services/Data/push';
|
||||||
|
import { useAppDispatch } from '../../../storeHooks';
|
||||||
|
import { DialogContainer } from '../components';
|
||||||
|
|
||||||
|
type UseCaseScreenProps = {
|
||||||
|
dismiss: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UseCases =
|
||||||
|
| 'data-api'
|
||||||
|
| 'gql-backend'
|
||||||
|
| 'data-federation'
|
||||||
|
| 'gateway';
|
||||||
|
|
||||||
|
interface UseCaseAssets {
|
||||||
|
id: UseCases;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
image: string;
|
||||||
|
consoleUrl: string;
|
||||||
|
docsUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useCasesAssets: UseCaseAssets[] = [
|
||||||
|
{
|
||||||
|
id: 'data-api',
|
||||||
|
image:
|
||||||
|
'https://storage.googleapis.com/graphql-engine-cdn.hasura.io/cloud-console/assets/common/img/hasura-usecase-data-api.svg',
|
||||||
|
title: 'Data Access Layer',
|
||||||
|
description:
|
||||||
|
'Build an instant, real-time API over your data sources for easy and performant access',
|
||||||
|
consoleUrl: '/',
|
||||||
|
docsUrl: 'https://hasura.io/docs/latest/resources/use-case/data-api/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gql-backend',
|
||||||
|
image:
|
||||||
|
'https://storage.googleapis.com/graphql-engine-cdn.hasura.io/cloud-console/assets/common/img/hasura-usecase-gql-backend.svg',
|
||||||
|
title: 'Graphql Backend',
|
||||||
|
description:
|
||||||
|
'Build a lightning-fast GraphQL backend and significantly accelerate your application development',
|
||||||
|
consoleUrl: '/',
|
||||||
|
docsUrl: 'https://hasura.io/docs/latest/resources/use-case/gql-backend/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gateway',
|
||||||
|
image:
|
||||||
|
'https://storage.googleapis.com/graphql-engine-cdn.hasura.io/cloud-console/assets/common/img/hasura-usecase-gateway.svg',
|
||||||
|
title: 'API Gateway',
|
||||||
|
description:
|
||||||
|
'Build a single entry point from client applications into an ecosystem of microservices',
|
||||||
|
consoleUrl: '/',
|
||||||
|
docsUrl: 'https://hasura.io/docs/latest/resources/use-case/api-gateway/',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Root = (props: UseCaseScreenProps) => {
|
||||||
|
const [selectedUseCase, setSelectedUseCase] = useState<UseCases | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
trackCustomEvent({
|
||||||
|
location: 'Console',
|
||||||
|
action: 'Load',
|
||||||
|
object: 'Use Case Wizard',
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const randomUseCaseAssets = useMemo(() => {
|
||||||
|
return useCasesAssets.sort(() => Math.random() - 0.5);
|
||||||
|
}, [useCasesAssets]);
|
||||||
|
|
||||||
|
const useCase = useCasesAssets.find(
|
||||||
|
useCase => useCase.id === selectedUseCase
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
if (useCase) {
|
||||||
|
dispatch(_push(useCase.consoleUrl));
|
||||||
|
emitOnboardingEvent(getUseCaseExperimentOnboardingVariables(useCase.id));
|
||||||
|
props.dismiss();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContainer header={''}>
|
||||||
|
<div
|
||||||
|
className="use-case-container border border-solid border-gray-400 rounded-sm flex flex-col flex-wrap p-8 h-full bg-white"
|
||||||
|
style={{
|
||||||
|
width: '902px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="use-case-welcome-header flex justify-between w-full">
|
||||||
|
<div className="use-case-welcome-text font-sans">
|
||||||
|
<h1 className="text-xl font-bold text-cloud-darkest">
|
||||||
|
Welcome to Hasura!
|
||||||
|
</h1>
|
||||||
|
<div className="text-muted-dark font-normal">
|
||||||
|
Hasura can help you supercharge your development
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="use-case-welcome-illustrations h-[93px]">
|
||||||
|
<img
|
||||||
|
src="https://storage.googleapis.com/graphql-engine-cdn.hasura.io/cloud-dashboard/dashboard/hasura-loading-illustration.svg"
|
||||||
|
alt="welcome"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="use-case-intro-text text-[#64748B] font-sans mt-3 mb-3.5">
|
||||||
|
What would you like to build with Hasura?
|
||||||
|
</div>
|
||||||
|
<div className="use-cases flex flex-wrap justify-around gap-y-15 gap-y-8">
|
||||||
|
{randomUseCaseAssets.map((item, index) => (
|
||||||
|
<div className="flex relative h-[250px]" key={index}>
|
||||||
|
<label
|
||||||
|
key={index}
|
||||||
|
htmlFor={item.id}
|
||||||
|
className="use-case-card flex flex-col cursor-pointer border border-solid border-slate-300 rounded focus-within:border focus-within:border-solid focus-within:border-amber-500 transition-shadow shadow-none hover:shadow-md w-[400px]"
|
||||||
|
onChange={event => {
|
||||||
|
const selectedUseCaseNode = event.target as HTMLInputElement;
|
||||||
|
setSelectedUseCase(selectedUseCaseNode.id as UseCases);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={item.image} alt={item.id} />
|
||||||
|
|
||||||
|
<div className="absolute bottom-0 data-api-description flex mt-3 ml-6 mb-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id={item.id}
|
||||||
|
name="use-case"
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col font-sans ml-2">
|
||||||
|
<div className="font-semibold text-slate-900">
|
||||||
|
{item.title}
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-dark font-normal">
|
||||||
|
{item.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="use-case-cta flex justify-between w-full mt-8">
|
||||||
|
<Analytics name="use-case-onboarding-skip">
|
||||||
|
<div
|
||||||
|
className="ml-xs mr-4 text-secondary flex items-center cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
props.dismiss();
|
||||||
|
emitOnboardingEvent(skippedUseCaseExperimentOnboarding);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Skip
|
||||||
|
</div>
|
||||||
|
</Analytics>
|
||||||
|
{useCase ? (
|
||||||
|
<Analytics name={`use-case-onboarding-${selectedUseCase}`}>
|
||||||
|
<a
|
||||||
|
href={useCase.docsUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
onClick={onSubmit}
|
||||||
|
>
|
||||||
|
<Button mode="primary">Continue</Button>
|
||||||
|
</a>
|
||||||
|
</Analytics>
|
||||||
|
) : (
|
||||||
|
<Analytics
|
||||||
|
name={`use-case-onboarding-unselected`}
|
||||||
|
passHtmlAttributesToChildren
|
||||||
|
>
|
||||||
|
<Button mode="primary" disabled>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</Analytics>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,3 @@
|
|||||||
|
import { Root } from './Root';
|
||||||
|
|
||||||
|
export const UseCaseOnboarding = Root;
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button } from '../../../../../new-components/Button';
|
import { Button } from '../../../../new-components/Button';
|
||||||
import { FaPlayCircle } from 'react-icons/fa';
|
import { FaPlayCircle } from 'react-icons/fa';
|
||||||
import { Analytics } from '../../../../Analytics';
|
import { Analytics } from '../../../Analytics';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
schemaImage: string;
|
schemaImage: string;
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { HasuraLogoFull } from '../../../../../new-components/HasuraLogo';
|
import { HasuraLogoFull } from '../../../../new-components/HasuraLogo';
|
||||||
|
|
||||||
export function TopHeaderBar() {
|
export function TopHeaderBar() {
|
||||||
return (
|
return (
|
@ -1,6 +1,3 @@
|
|||||||
export { ConnectDBScreen } from './ConnectDBScreen/ConnectDBScreen';
|
|
||||||
export { TemplateSummary } from './QueryScreen/TemplateSummary';
|
|
||||||
export { StepperNavbar } from './StepperNavbar/StepperNavbar';
|
export { StepperNavbar } from './StepperNavbar/StepperNavbar';
|
||||||
export type { StepperNavbarStep } from './StepperNavbar/StepperNavbar';
|
export type { StepperNavbarStep } from './StepperNavbar/StepperNavbar';
|
||||||
export { DialogContainer } from './DialogContainer/DialogContainer';
|
export { DialogContainer } from './DialogContainer/DialogContainer';
|
||||||
export { UseCaseScreen } from './UseCaseScreen/UseCaseScreen';
|
|
@ -1,6 +1,6 @@
|
|||||||
import globals from '../../../Globals';
|
import globals from '../../Globals';
|
||||||
import { BASE_URL_TEMPLATE } from '../../../components/Services/Data/Schema/TemplateGallery/templateGalleryConfig';
|
import { BASE_URL_TEMPLATE } from '../../components/Services/Data/Schema/TemplateGallery/templateGalleryConfig';
|
||||||
import { UseCases } from './components/UseCaseScreen/UseCaseScreen';
|
import { UseCases } from './UseCaseOnboarding/Root';
|
||||||
|
|
||||||
// This config is stored in root level index.js, and there is a config file in each directory which stores which
|
// This config is stored in root level index.js, and there is a config file in each directory which stores which
|
||||||
// stores the directory structure.
|
// stores the directory structure.
|
||||||
@ -16,9 +16,10 @@ export const onboardingQueryKey = 'onboardingData';
|
|||||||
*/
|
*/
|
||||||
export const fetchAllOnboardingDataQuery = `
|
export const fetchAllOnboardingDataQuery = `
|
||||||
query fetchAllOnboardingData ($projectId: uuid!) {
|
query fetchAllOnboardingData ($projectId: uuid!) {
|
||||||
user_onboarding {
|
user_onboarding(where: {target: {_eq: "cloud_console"}}) {
|
||||||
activity
|
activity
|
||||||
target
|
target
|
||||||
|
is_onboarded
|
||||||
}
|
}
|
||||||
users {
|
users {
|
||||||
id
|
id
|
||||||
@ -63,8 +64,6 @@ export const getSchemaImageUrl = (baseUrl: string) => {
|
|||||||
return `${baseUrl}/diagram.png`;
|
return `${baseUrl}/diagram.png`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NEON_ONBOARDING_QUERY_KEY = 'neonOnboarding';
|
|
||||||
|
|
||||||
export const trackOnboardingActivityMutation = `
|
export const trackOnboardingActivityMutation = `
|
||||||
mutation trackOnboardingActivity($projectId: uuid!, $kind: String!, $error_code: String, $subkind: String,) {
|
mutation trackOnboardingActivity($projectId: uuid!, $kind: String!, $error_code: String, $subkind: String,) {
|
||||||
trackOnboardingActivity(payload: {kind: $kind, project_id: $projectId, error_code: $error_code, subkind: $subkind}) {
|
trackOnboardingActivity(payload: {kind: $kind, project_id: $projectId, error_code: $error_code, subkind: $subkind}) {
|
||||||
@ -79,11 +78,6 @@ const mutationVariables = {
|
|||||||
...(projectId && { projectId }),
|
...(projectId && { projectId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onboardingCompleteVariables = {
|
|
||||||
...mutationVariables,
|
|
||||||
kind: 'onboarding_complete',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const skippedOnboardingThroughURLParamVariables = {
|
export const skippedOnboardingThroughURLParamVariables = {
|
||||||
...mutationVariables,
|
...mutationVariables,
|
||||||
kind: 'skipped_neon_onboarding_through_url_param',
|
kind: 'skipped_neon_onboarding_through_url_param',
|
||||||
@ -103,41 +97,21 @@ export const neonOAuthStartVariables = {
|
|||||||
kind: 'neon_login_start',
|
kind: 'neon_login_start',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const neonOAuthCompleteVariables = {
|
|
||||||
...mutationVariables,
|
|
||||||
kind: 'neon_login_complete',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const neonDbCreationStartVariables = {
|
export const neonDbCreationStartVariables = {
|
||||||
...mutationVariables,
|
...mutationVariables,
|
||||||
kind: 'neon_db_creation_start',
|
kind: 'neon_db_creation_start',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const neonDbCreationCompleteVariables = {
|
|
||||||
...mutationVariables,
|
|
||||||
kind: 'neon_db_creation_complete',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasuraSourceCreationStartVariables = {
|
export const hasuraSourceCreationStartVariables = {
|
||||||
...mutationVariables,
|
...mutationVariables,
|
||||||
kind: 'hasura_source_creation_start',
|
kind: 'hasura_source_creation_start',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasuraSourceCreationCompleteVariables = {
|
|
||||||
...mutationVariables,
|
|
||||||
kind: 'hasura_source_creation_complete',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const installTemplateStartVariables = {
|
export const installTemplateStartVariables = {
|
||||||
...mutationVariables,
|
...mutationVariables,
|
||||||
kind: 'install_template_start',
|
kind: 'install_template_start',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const installTemplateCompleteVariables = {
|
|
||||||
...mutationVariables,
|
|
||||||
kind: 'install_template_complete',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const templateSummaryRunQueryClickVariables = {
|
export const templateSummaryRunQueryClickVariables = {
|
||||||
...mutationVariables,
|
...mutationVariables,
|
||||||
kind: 'run_query_click',
|
kind: 'run_query_click',
|
@ -1,60 +1,53 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { OnboardingKind, OnboardingResponseData } from '../types';
|
||||||
|
import { APIError } from '../../../hooks/error';
|
||||||
|
import { getLSItem, LS_KEYS, removeLSItem } from '../../../utils';
|
||||||
|
import { skippedOnboardingThroughURLParamVariables } from '../constants';
|
||||||
|
import { emitOnboardingEvent } from '../utils';
|
||||||
|
import { oneClickDeploymentOnboardingKind } from '../OneClickDeployment/util';
|
||||||
import { OneClickDeploymentState } from '../OneClickDeployment';
|
import { OneClickDeploymentState } from '../OneClickDeployment';
|
||||||
import {
|
|
||||||
useOnboardingData,
|
|
||||||
emitOnboardingEvent,
|
|
||||||
oneClickDeploymentOnboardingShown,
|
|
||||||
} from '../OnboardingWizard';
|
|
||||||
import { OnboardingKind } from '../types';
|
|
||||||
|
|
||||||
export const useOnboardingKind = () => {
|
export const useOnboardingKind = (
|
||||||
const { data, error, isLoading } = useOnboardingData();
|
onboardingData: OnboardingResponseData | undefined,
|
||||||
|
error: APIError | null,
|
||||||
|
isLoading: boolean
|
||||||
|
) => {
|
||||||
const [kind, setKind] = React.useState<OnboardingKind>({ kind: 'none' });
|
const [kind, setKind] = React.useState<OnboardingKind>({ kind: 'none' });
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (error || isLoading) {
|
if (error || isLoading || !onboardingData?.data) {
|
||||||
setKind({ kind: 'none' });
|
|
||||||
// TODO: emit onboarding error
|
// TODO: emit onboarding error
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data?.data) {
|
const is_onboarded =
|
||||||
setKind({ kind: 'none' });
|
onboardingData?.data?.user_onboarding[0]?.is_onboarded || false;
|
||||||
// TODO: emit onboarding error
|
const isSkipOnboardingFlagSet =
|
||||||
return;
|
getLSItem(LS_KEYS.skipOnboarding) === 'true';
|
||||||
|
|
||||||
|
if (isSkipOnboardingFlagSet) {
|
||||||
|
emitOnboardingEvent(skippedOnboardingThroughURLParamVariables);
|
||||||
|
removeLSItem(LS_KEYS.skipOnboarding);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if there's a pending one-click-deployment for this project,
|
if (
|
||||||
// show the deployment status
|
onboardingData?.data?.one_click_deployment[0] &&
|
||||||
// if the deployment is complete, show nothing
|
onboardingData?.data?.one_click_deployment[0]?.state !==
|
||||||
const deployment = data.data.one_click_deployment[0];
|
OneClickDeploymentState.Completed
|
||||||
if (deployment?.state === OneClickDeploymentState.Completed) {
|
) {
|
||||||
setKind({ kind: 'none' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (deployment) {
|
|
||||||
// TODO: Skip this block also if state == 'ERROR' && retryCount >= something
|
// TODO: Skip this block also if state == 'ERROR' && retryCount >= something
|
||||||
|
setKind(oneClickDeploymentOnboardingKind(onboardingData));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setKind({
|
if (is_onboarded || isSkipOnboardingFlagSet) {
|
||||||
kind: 'one-click-deployment',
|
setKind({ kind: 'none' });
|
||||||
deployment: {
|
|
||||||
deploymentId: data.data.one_click_deployment[0].id,
|
|
||||||
gitRepoDetails: {
|
|
||||||
url: data.data.one_click_deployment[0].git_repository_url,
|
|
||||||
branch: data.data.one_click_deployment[0].git_repository_branch,
|
|
||||||
hasuraDirectory: data.data.one_click_deployment[0].hasura_directory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fallbackApps: data.data.one_click_deployment_sample_apps || [],
|
|
||||||
});
|
|
||||||
// emit onboarding event denoting onboarding through one click deployment
|
|
||||||
emitOnboardingEvent(oneClickDeploymentOnboardingShown);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// pass control to onboarding wizard
|
// pass control to onboarding wizard
|
||||||
setKind({ kind: 'wizard' });
|
setKind({ kind: 'use-case-onboarding' });
|
||||||
}, [data, error, isLoading]);
|
}, [onboardingData, error, isLoading]);
|
||||||
|
|
||||||
// if there's error fetching onboarding data
|
// if there's error fetching onboarding data
|
||||||
// do not show anything and proceed to the console
|
// do not show anything and proceed to the console
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import globals from '../../Globals';
|
import globals from '../../Globals';
|
||||||
import { isCloudConsole } from '../../utils/cloudConsole';
|
import { isCloudConsole } from '../../utils';
|
||||||
import { OneClickDeployment } from './OneClickDeployment';
|
import { OneClickDeployment } from './OneClickDeployment';
|
||||||
import { OnboardingWizard } from './OnboardingWizard';
|
import { NeonOnboarding, useOnboardingData } from './NeonOnboardingWizard';
|
||||||
|
import { UseCaseOnboarding } from './UseCaseOnboarding';
|
||||||
import { useOnboardingKind } from './hooks/useOnboardingKind';
|
import { useOnboardingKind } from './hooks/useOnboardingKind';
|
||||||
import { useFallbackApps } from './hooks/useFallbackApps';
|
import { useFallbackApps } from './hooks/useFallbackApps';
|
||||||
|
|
||||||
export const CloudOnboardingWithoutCloudCheck = () => {
|
export const CloudOnboardingWithoutCloudCheck = () => {
|
||||||
const cloudOnboarding = useOnboardingKind();
|
const { data, error, isLoading } = useOnboardingData();
|
||||||
|
const cloudOnboarding = useOnboardingKind(data, error, isLoading);
|
||||||
const fallbackApps = useFallbackApps(cloudOnboarding);
|
const fallbackApps = useFallbackApps(cloudOnboarding);
|
||||||
switch (cloudOnboarding.kind) {
|
switch (cloudOnboarding.kind) {
|
||||||
case 'wizard':
|
case 'neon-onboarding':
|
||||||
return <OnboardingWizard />;
|
return <NeonOnboarding onboardingData={data} />;
|
||||||
case 'one-click-deployment':
|
case 'one-click-deployment':
|
||||||
return (
|
return (
|
||||||
<OneClickDeployment
|
<OneClickDeployment
|
||||||
@ -20,6 +22,8 @@ export const CloudOnboardingWithoutCloudCheck = () => {
|
|||||||
fallbackApps={fallbackApps}
|
fallbackApps={fallbackApps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case 'use-case-onboarding':
|
||||||
|
return <UseCaseOnboarding dismiss={cloudOnboarding.dismissOnboarding} />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,47 @@
|
|||||||
import { OnboardingResponseData } from './OnboardingWizard';
|
import { One_Click_Deployment_States_Enum } from '../ControlPlane';
|
||||||
import { GitRepoDetails, FallbackApp } from './OneClickDeployment';
|
import { GitRepoDetails, FallbackApp } from './OneClickDeployment';
|
||||||
|
|
||||||
export type OneClickDeploymentSampleApp =
|
export type UserOnboarding = {
|
||||||
OnboardingResponseData['data']['one_click_deployment_sample_apps'];
|
activity: Record<string, any>;
|
||||||
|
target: string;
|
||||||
|
is_onboarded: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
id: string;
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OneClickDeploymentByProject = {
|
||||||
|
id: number;
|
||||||
|
state: One_Click_Deployment_States_Enum;
|
||||||
|
git_repository_url: string;
|
||||||
|
git_repository_branch?: string;
|
||||||
|
hasura_directory?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OneClickDeploymentSampleApp = {
|
||||||
|
git_repository_url: string;
|
||||||
|
git_repository_branch: string;
|
||||||
|
hasura_directory: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
react_icons_fa_component_name?: string;
|
||||||
|
rank: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnboardingResponseData = {
|
||||||
|
data: {
|
||||||
|
user_onboarding: UserOnboarding[];
|
||||||
|
users: User[];
|
||||||
|
one_click_deployment: OneClickDeploymentByProject[];
|
||||||
|
one_click_deployment_sample_apps: OneClickDeploymentSampleApp[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type OnboardingKind =
|
export type OnboardingKind =
|
||||||
| {
|
| {
|
||||||
kind: 'wizard';
|
kind: 'neon-onboarding';
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
kind: 'one-click-deployment';
|
kind: 'one-click-deployment';
|
||||||
@ -16,6 +51,9 @@ export type OnboardingKind =
|
|||||||
};
|
};
|
||||||
fallbackApps: OnboardingResponseData['data']['one_click_deployment_sample_apps'];
|
fallbackApps: OnboardingResponseData['data']['one_click_deployment_sample_apps'];
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
kind: 'use-case-onboarding';
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
kind: 'none';
|
kind: 'none';
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
clickRunQueryButton,
|
clickRunQueryButton,
|
||||||
forceGraphiQLIntrospection,
|
|
||||||
forceChangeGraphiqlQuery,
|
forceChangeGraphiqlQuery,
|
||||||
|
forceGraphiQLIntrospection,
|
||||||
} from '../../components/Services/ApiExplorer/OneGraphExplorer/utils';
|
} from '../../components/Services/ApiExplorer/OneGraphExplorer/utils';
|
||||||
import { Dispatch } from '../../types';
|
import { Dispatch } from '../../types';
|
||||||
|
import { cloudDataServiceApiClient } from '../../hooks/cloudDataServiceApiClient';
|
||||||
|
import { trackOnboardingActivityMutation } from './constants';
|
||||||
|
import { programmaticallyTraceError } from '../Analytics/core/programmaticallyTraceError';
|
||||||
|
|
||||||
export const runQueryInGraphiQL = () => {
|
export const runQueryInGraphiQL = () => {
|
||||||
clickRunQueryButton();
|
clickRunQueryButton();
|
||||||
@ -21,3 +24,26 @@ export const fillSampleQueryInGraphiQL = (
|
|||||||
forceChangeGraphiqlQuery(query, dispatch);
|
forceChangeGraphiqlQuery(query, dispatch);
|
||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ResponseDataOnMutation = {
|
||||||
|
data: {
|
||||||
|
trackOnboardingActivity: {
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const cloudHeaders = {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const emitOnboardingEvent = (variables: Record<string, unknown>) => {
|
||||||
|
// mutate server data
|
||||||
|
cloudDataServiceApiClient<ResponseDataOnMutation, ResponseDataOnMutation>(
|
||||||
|
trackOnboardingActivityMutation,
|
||||||
|
variables,
|
||||||
|
cloudHeaders
|
||||||
|
).catch(error => {
|
||||||
|
programmaticallyTraceError(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -125,7 +125,6 @@ export const LS_KEYS = {
|
|||||||
notificationsLastSeen: 'notifications:lastSeen',
|
notificationsLastSeen: 'notifications:lastSeen',
|
||||||
authState: 'AUTH_STATE',
|
authState: 'AUTH_STATE',
|
||||||
skipOnboarding: 'SKIP_CLOUD_ONBOARDING',
|
skipOnboarding: 'SKIP_CLOUD_ONBOARDING',
|
||||||
showUseCaseOverviewPopup: 'onboarding:showUseCaseOverviewPopup',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clearGraphiqlLS = () => {
|
export const clearGraphiqlLS = () => {
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
Analytics,
|
|
||||||
Button,
|
|
||||||
getLSItem,
|
|
||||||
LS_KEYS,
|
|
||||||
setLSItem,
|
|
||||||
} from '@hasura/console-legacy-ce';
|
|
||||||
import { MdClose } from 'react-icons/md';
|
|
||||||
|
|
||||||
const ExploreUseCasePopup = () => {
|
|
||||||
const showUseCaseOverviewPopup = getLSItem(LS_KEYS.showUseCaseOverviewPopup);
|
|
||||||
|
|
||||||
const [showUseCasePopup, setShowUseCasePopup] =
|
|
||||||
React.useState<boolean>(false);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
setShowUseCasePopup(showUseCaseOverviewPopup === 'true');
|
|
||||||
}, [showUseCaseOverviewPopup]);
|
|
||||||
|
|
||||||
const useCaseOverviewDocsUrl =
|
|
||||||
'https://hasura.io/docs/latest/resources/use-case/overview/';
|
|
||||||
|
|
||||||
return (
|
|
||||||
showUseCasePopup && (
|
|
||||||
<div className="z-[10] fixed w-96 bottom-14 right-12 border border-slate-300 abc">
|
|
||||||
<div className="p-sm flex space-x-1.5 bg-emerald-50 justify-between">
|
|
||||||
Want to learn more? Find out how you can leverage Hasura! 🚀
|
|
||||||
<Analytics name="use-case-popup-dismiss">
|
|
||||||
<MdClose
|
|
||||||
className="mt-1 cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
setShowUseCasePopup(false);
|
|
||||||
setLSItem(LS_KEYS.showUseCaseOverviewPopup, 'false');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Analytics>
|
|
||||||
</div>
|
|
||||||
<div className="p-sm bg-slate-50 border-t border-slate-300">
|
|
||||||
<Analytics name="use-case-popup-explore" passHtmlAttributesToChildren>
|
|
||||||
<Button
|
|
||||||
mode="default"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => {
|
|
||||||
setShowUseCasePopup(false);
|
|
||||||
setLSItem(LS_KEYS.showUseCaseOverviewPopup, 'false');
|
|
||||||
window.open(
|
|
||||||
useCaseOverviewDocsUrl,
|
|
||||||
'_blank',
|
|
||||||
'noreferrer,noopener'
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Explore
|
|
||||||
</Button>
|
|
||||||
</Analytics>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ExploreUseCasePopup;
|
|
@ -93,7 +93,6 @@ import logoutIcon from './images/log-out.svg';
|
|||||||
import EELogo from './images/hasura-ee-mono-light.svg';
|
import EELogo from './images/hasura-ee-mono-light.svg';
|
||||||
import { isHasuraCollaboratorUser } from '../Login/utils';
|
import { isHasuraCollaboratorUser } from '../Login/utils';
|
||||||
import { ConsoleDevTools } from '@hasura/console-legacy-ce';
|
import { ConsoleDevTools } from '@hasura/console-legacy-ce';
|
||||||
import ExploreUseCasePopup from './ExploreUseCasePopup';
|
|
||||||
|
|
||||||
const { Plan, Project_Entitlement_Types_Enum } = ControlPlane;
|
const { Plan, Project_Entitlement_Types_Enum } = ControlPlane;
|
||||||
class Main extends React.Component {
|
class Main extends React.Component {
|
||||||
@ -727,7 +726,6 @@ class Main extends React.Component {
|
|||||||
metadata={metadata?.metadataObject}
|
metadata={metadata?.metadataObject}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<ExploreUseCasePopup />
|
|
||||||
</div>
|
</div>
|
||||||
<CloudOnboarding />
|
<CloudOnboarding />
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user