From 948ace94e79d468f6d8bca407dd4cac75f779399 Mon Sep 17 00:00:00 2001 From: Abhijeet Khangarot Date: Mon, 10 Oct 2022 15:17:20 +0530 Subject: [PATCH] console: Neon integraiton design updates PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6212 Co-authored-by: nevermore <31686586+OjasWadhwani@users.noreply.github.com> Co-authored-by: Rishichandra Wawhal <27274869+wawhal@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> GitOrigin-RevId: 71cc4923a2dda178839464d91b3214456fd207fa --- .../Neon/components/HerokuBanner/Banner.tsx | 32 +- .../Neon/components/Neon/NeonBanner.tsx | 8 +- .../NeonDashboardLink.stories.tsx | 10 + .../NeonDashboardLink/NeonDashboardLink.tsx | 46 + .../NeonDashboardLink/NeonIconSmall.tsx | 37 + .../components/NeonDashboardLink/index.ts | 2 + .../components/NeonDashboardLink/utils.ts | 83 ++ .../CreateDataSource/Neon/index.tsx | 7 + .../Services/Data/Schema/ManageDatabase.tsx | 3 + .../src/features/OnboardingWizard/Root.tsx | 105 +- .../ConnectDBScreen.stories.tsx | 33 +- .../ConnectDBScreen/ConnectDBScreen.tsx | 24 +- .../components/HasuraOnboardingSVG.tsx | 903 ++++++++++++++++++ .../{ => components}/NeonOnboarding.tsx | 25 +- .../components/OnboardingAnimation.tsx | 167 +--- .../components/OnboardingAnimationNavbar.tsx | 66 -- .../ConnectDBScreen/components/index.ts | 2 - .../DialogContainer.stories.tsx | 24 + .../DialogContainer/DialogContainer.tsx | 38 + .../NeonConnectBanner/NeonBanner.stories.tsx | 3 + .../NeonConnectBanner/NeonBanner.tsx | 20 +- .../QueryDialog/QueryDialog.stories.tsx | 76 -- .../components/QueryDialog/QueryDialog.tsx | 70 -- .../QueryScreen/QueryScreen.stories.tsx | 76 ++ .../components/QueryScreen/QueryScreen.tsx | 77 ++ .../TemplateSummary.tsx | 34 +- .../StepperNavbar/StepperNavbar.stories.tsx | 10 + .../StepperNavbar/StepperNavbar.tsx | 70 ++ .../components/CustomRightChevron.tsx | 0 .../OnboardingWizard/components/index.ts | 5 +- .../features/OnboardingWizard/constants.ts | 5 + .../OnboardingWizard/hooks/useWizardState.ts | 47 +- .../src/features/OnboardingWizard/utils.ts | 17 + .../components/HasuraFamiliaritySurvey.tsx | 7 - .../HasuraLogo/HasuraLogoFull.stories.mdx | 4 + .../HasuraLogo/HasuraLogoFull.tsx | 23 +- 36 files changed, 1622 insertions(+), 537 deletions(-) create mode 100644 console/src/components/Services/Data/DataSources/CreateDataSource/Neon/components/NeonDashboardLink/NeonDashboardLink.stories.tsx create mode 100644 console/src/components/Services/Data/DataSources/CreateDataSource/Neon/components/NeonDashboardLink/NeonDashboardLink.tsx create mode 100644 console/src/components/Services/Data/DataSources/CreateDataSource/Neon/components/NeonDashboardLink/NeonIconSmall.tsx create mode 100644 console/src/components/Services/Data/DataSources/CreateDataSource/Neon/components/NeonDashboardLink/index.ts create mode 100644 console/src/components/Services/Data/DataSources/CreateDataSource/Neon/components/NeonDashboardLink/utils.ts create mode 100644 console/src/features/OnboardingWizard/components/ConnectDBScreen/components/HasuraOnboardingSVG.tsx rename console/src/features/OnboardingWizard/components/ConnectDBScreen/{ => components}/NeonOnboarding.tsx (72%) delete mode 100644 console/src/features/OnboardingWizard/components/ConnectDBScreen/components/OnboardingAnimationNavbar.tsx delete mode 100644 console/src/features/OnboardingWizard/components/ConnectDBScreen/components/index.ts create mode 100644 console/src/features/OnboardingWizard/components/DialogContainer/DialogContainer.stories.tsx create mode 100644 console/src/features/OnboardingWizard/components/DialogContainer/DialogContainer.tsx delete mode 100644 console/src/features/OnboardingWizard/components/QueryDialog/QueryDialog.stories.tsx delete mode 100644 console/src/features/OnboardingWizard/components/QueryDialog/QueryDialog.tsx create mode 100644 console/src/features/OnboardingWizard/components/QueryScreen/QueryScreen.stories.tsx create mode 100644 console/src/features/OnboardingWizard/components/QueryScreen/QueryScreen.tsx rename console/src/features/OnboardingWizard/components/{QueryDialog => QueryScreen}/TemplateSummary.tsx (79%) create mode 100644 console/src/features/OnboardingWizard/components/StepperNavbar/StepperNavbar.stories.tsx create mode 100644 console/src/features/OnboardingWizard/components/StepperNavbar/StepperNavbar.tsx rename console/src/features/OnboardingWizard/components/{ConnectDBScreen => StepperNavbar}/components/CustomRightChevron.tsx (100%) diff --git a/console/src/components/Services/Data/DataSources/CreateDataSource/Neon/components/HerokuBanner/Banner.tsx b/console/src/components/Services/Data/DataSources/CreateDataSource/Neon/components/HerokuBanner/Banner.tsx index 320aa1f161a..55d346b5ed6 100644 --- a/console/src/components/Services/Data/DataSources/CreateDataSource/Neon/components/HerokuBanner/Banner.tsx +++ b/console/src/components/Services/Data/DataSources/CreateDataSource/Neon/components/HerokuBanner/Banner.tsx @@ -1,29 +1,21 @@ import React from 'react'; -import { Button } from '@/new-components/Button'; import { GrHeroku } from 'react-icons/gr'; export function HerokuBanner() { return ( -
-
-
- -
-
- Starting November 28th, 2022, free Heroku Dynos, free Heroku - Postgres, and free Heroku Data for Redis will no longer be available. -
-
-
- - - +
+ +
+ Heroku free database integration support has been deprecated.
+ + (Know More) +
); } diff --git a/console/src/components/Services/Data/DataSources/CreateDataSource/Neon/components/Neon/NeonBanner.tsx b/console/src/components/Services/Data/DataSources/CreateDataSource/Neon/components/Neon/NeonBanner.tsx index f89df2f32f1..e18c702b6a6 100644 --- a/console/src/components/Services/Data/DataSources/CreateDataSource/Neon/components/Neon/NeonBanner.tsx +++ b/console/src/components/Services/Data/DataSources/CreateDataSource/Neon/components/Neon/NeonBanner.tsx @@ -40,6 +40,9 @@ export function NeonBanner(props: Props) { Free + + Preview +
- The multi-cloud fully managed Postgres with a generous free tier. We - separated storage and compute to offer autoscaling, branching, and - bottomless storage. + Fully managed Postgres with separate storage and compute, that scales + to zero on inactivity and provides seamless scaling and branching.
) : null} + +
); diff --git a/console/src/features/OnboardingWizard/Root.tsx b/console/src/features/OnboardingWizard/Root.tsx index ad8f39c8e20..7d30a4facea 100644 --- a/console/src/features/OnboardingWizard/Root.tsx +++ b/console/src/features/OnboardingWizard/Root.tsx @@ -1,14 +1,21 @@ import React from 'react'; -import * as Dialog from '@radix-ui/react-dialog'; import { useAppDispatch } from '@/store'; import globals from '@/Globals'; import { hasLuxFeatureAccess, isCloudConsole } from '@/utils/cloudConsole'; -import { TopHeaderBar, ConnectDBScreen, TemplateSummary } from './components'; +import { + ConnectDBScreen, + TemplateSummary, + DialogContainer, +} from './components'; import { useWizardState } from './hooks'; -import { NEON_TEMPLATE_BASE_PATH } from './constants'; +import { + NEON_TEMPLATE_BASE_PATH, + dialogHeader, + familiaritySurveySubHeader, +} from './constants'; import { GrowthExperimentsClient } from '../GrowthExperiments'; -import { useFamiliaritySurveyData, HasuraFamiliaritySurvey } from '../Surveys'; +import { HasuraFamiliaritySurvey } from '../Surveys'; type Props = { growthExperimentsClient: GrowthExperimentsClient; @@ -24,20 +31,15 @@ function Root(props: Props) { const hasNeonAccess = hasLuxFeatureAccess(globals, 'NeonDatabaseIntegration'); - // dialog cannot be reopened once closed - const { state, setState } = useWizardState( - growthExperimentsClient, - hasNeonAccess - ); + const [stepperIndex, setStepperIndex] = React.useState(1); const { - showFamiliaritySurvey, - data: familiaritySurveyData, - onSkip: familiaritySurveyOnSkip, - onOptionClick: familiaritySurveyOnOptionClick, - } = useFamiliaritySurveyData(); - - const templateBaseUrl = NEON_TEMPLATE_BASE_PATH; + state, + setState, + familiaritySurveyData, + familiaritySurveyOnOptionClick, + familiaritySurveyOnSkip, + } = useWizardState(growthExperimentsClient, hasNeonAccess); const transitionToTemplateSummary = () => { setState('template-summary'); @@ -48,43 +50,46 @@ function Root(props: Props) { }; switch (state) { + case 'familiarity-survey': { + return ( + + + + ); + } case 'landing-page': { return ( - // Radix dialog is being used for creating a layover component over the whole app. - // It does not make sense to extend common dialog component to fit this one-off use case. - // - // modal={false} is set to prevent focus issues when multiple modals are visible, - // for example survey modal and onboarding modal - - - -
- {showFamiliaritySurvey ? ( - - ) : ( - - )} -
-
-
+ + + ); } case 'template-summary': { return ( - + + + ); } case 'hidden': @@ -99,10 +104,10 @@ export function RootWithCloudCheck(props: Props) { * Don't render Root component if current context is not cloud-console * and current user is not project owner */ - if (!isCloudConsole(globals) && globals.userRole !== 'owner') { - return null; + if (isCloudConsole(globals) && globals.userRole === 'owner') { + return ; } - return ; + return null; } export const RootWithoutCloudCheck = Root; diff --git a/console/src/features/OnboardingWizard/components/ConnectDBScreen/ConnectDBScreen.stories.tsx b/console/src/features/OnboardingWizard/components/ConnectDBScreen/ConnectDBScreen.stories.tsx index 09449304b23..48bae5bde0a 100644 --- a/console/src/features/OnboardingWizard/components/ConnectDBScreen/ConnectDBScreen.stories.tsx +++ b/console/src/features/OnboardingWizard/components/ConnectDBScreen/ConnectDBScreen.stories.tsx @@ -17,6 +17,7 @@ export const WithoutNeon: Story = () => ( dismissOnboarding={() => {}} dispatch={() => {}} hasNeonAccess={!true} + setStepperIndex={() => {}} /> ); @@ -24,14 +25,7 @@ WithoutNeon.play = async ({ canvasElement }) => { const canvas = within(canvasElement); // Expect element renders successfully - expect( - await canvas.findByText('Welcome to your new Hasura project!') - ).toBeVisible(); - expect( - await canvas.findByText( - "Let's get started by connecting your first database" - ) - ).toBeVisible(); + expect(canvas.getByText('Connect Your Database')).toBeVisible(); }; export const WithNeon: Story = () => ( @@ -40,28 +34,15 @@ export const WithNeon: Story = () => ( dismissOnboarding={() => {}} hasNeonAccess dispatch={() => {}} + setStepperIndex={() => {}} /> ); WithNeon.play = async ({ canvasElement }) => { const canvas = within(canvasElement); - // Expect element renders successfully - expect( - await canvas.findByText('Welcome to your new Hasura project!') - ).toBeVisible(); - expect( - await canvas.findByText( - "Let's get started by connecting your first database" - ) - ).toBeVisible(); - expect(await canvas.findByText('Connect Neon Database')).toBeVisible(); - - expect(await canvas.findByText('Need a new database?')).toBeVisible(); - - expect( - await canvas.findByText( - 'Hasura has partnered with Neon to help you seamlessly create your database with their serverless Postgres platform.' - ) - ).toBeVisible(); + // Expect element renders successfully, these texts are highly dynamic + // according to product needs, and doesn't make sense to keep a lot of + // "renders successfully" tests. + expect(canvas.getByText('Connect Neon Database')).toBeVisible(); }; diff --git a/console/src/features/OnboardingWizard/components/ConnectDBScreen/ConnectDBScreen.tsx b/console/src/features/OnboardingWizard/components/ConnectDBScreen/ConnectDBScreen.tsx index 49606a2ba9a..a22c0b098cb 100644 --- a/console/src/features/OnboardingWizard/components/ConnectDBScreen/ConnectDBScreen.tsx +++ b/console/src/features/OnboardingWizard/components/ConnectDBScreen/ConnectDBScreen.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { Dispatch } from '@/types'; import { Button } from '@/new-components/Button'; -import { OnboardingAnimation, OnboardingAnimationNavbar } from './components'; -import { NeonOnboarding } from './NeonOnboarding'; +import { OnboardingAnimation } from './components/OnboardingAnimation'; +import { NeonOnboarding } from './components/NeonOnboarding'; import _push from '../../../../components/Services/Data/push'; import { persistSkippedOnboarding, @@ -14,10 +14,17 @@ type ConnectDBScreenProps = { dismissOnboarding: VoidFunction; hasNeonAccess: boolean; dispatch: Dispatch; + setStepperIndex: (index: number) => void; }; export function ConnectDBScreen(props: ConnectDBScreenProps) { - const { proceed, dismissOnboarding, hasNeonAccess, dispatch } = props; + const { + proceed, + dismissOnboarding, + hasNeonAccess, + dispatch, + setStepperIndex, + } = props; const pushToConnectDBPage = () => { // TODO: Due to routing being slow on prod, but wizard closing instantaneously, this causes @@ -38,15 +45,7 @@ export function ConnectDBScreen(props: ConnectDBScreenProps) { return ( <> -

- Welcome to your new Hasura project! -

-

Let's get started by connecting your first database

- -
- - -
+
{hasNeonAccess ? ( @@ -54,6 +53,7 @@ export function ConnectDBScreen(props: ConnectDBScreenProps) { dispatch={dispatch} dismiss={dismissOnboarding} proceed={proceed} + setStepperIndex={setStepperIndex} /> ) : ( <> diff --git a/console/src/features/OnboardingWizard/components/ConnectDBScreen/components/HasuraOnboardingSVG.tsx b/console/src/features/OnboardingWizard/components/ConnectDBScreen/components/HasuraOnboardingSVG.tsx new file mode 100644 index 00000000000..47bb5f900b6 --- /dev/null +++ b/console/src/features/OnboardingWizard/components/ConnectDBScreen/components/HasuraOnboardingSVG.tsx @@ -0,0 +1,903 @@ +/* eslint react/no-unknown-property: 0 */ +import React from 'react'; + +type Props = { + className?: string; +}; + +export function HasuraOnboarding(props: Props) { + const { className } = props; + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/console/src/features/OnboardingWizard/components/ConnectDBScreen/NeonOnboarding.tsx b/console/src/features/OnboardingWizard/components/ConnectDBScreen/components/NeonOnboarding.tsx similarity index 72% rename from console/src/features/OnboardingWizard/components/ConnectDBScreen/NeonOnboarding.tsx rename to console/src/features/OnboardingWizard/components/ConnectDBScreen/components/NeonOnboarding.tsx index f70288cd5af..4d8d6d82dd0 100644 --- a/console/src/features/OnboardingWizard/components/ConnectDBScreen/NeonOnboarding.tsx +++ b/console/src/features/OnboardingWizard/components/ConnectDBScreen/components/NeonOnboarding.tsx @@ -2,24 +2,27 @@ import * as React from 'react'; import { Dispatch } from '@/types'; import { useNeonIntegration } from '@/components/Services/Data/DataSources/CreateDataSource/Neon/useNeonIntegration'; import { transformNeonIntegrationStatusToNeonBannerProps } from '@/components/Services/Data/DataSources/CreateDataSource/Neon/utils'; -import { NeonBanner } from '../NeonConnectBanner/NeonBanner'; -import _push from '../../../../components/Services/Data/push'; +import { reactQueryClient } from '@/lib/reactQuery'; +import { FETCH_NEON_PROJECTS_BY_PROJECTID_QUERYKEY } from '@/components/Services/Data/DataSources/CreateDataSource/Neon/components/NeonDashboardLink'; +import { NeonBanner } from '../../NeonConnectBanner/NeonBanner'; +import _push from '../../../../../components/Services/Data/push'; import { useInstallTemplate, usePrefetchNeonOnboardingTemplateData, useEmitOnboardingEvents, -} from '../../hooks'; -import { NEON_TEMPLATE_BASE_PATH } from '../../constants'; -import { persistSkippedOnboarding } from '../../utils'; +} from '../../../hooks'; +import { NEON_TEMPLATE_BASE_PATH } from '../../../constants'; +import { persistSkippedOnboarding } from '../../../utils'; export function NeonOnboarding(props: { dispatch: Dispatch; dismiss: VoidFunction; proceed: VoidFunction; + setStepperIndex: (index: number) => void; }) { const [installingTemplate, setInstallingTemplate] = React.useState(false); - const { dispatch, dismiss, proceed } = props; + const { dispatch, dismiss, proceed, setStepperIndex } = props; const onSkipHandler = () => { persistSkippedOnboarding(); @@ -49,6 +52,12 @@ export function NeonOnboarding(props: { const neonIntegrationStatus = useNeonIntegration( 'default', () => { + // on success, refetch queries to show neon onboarding link in connect database page, + // overriding the stale time + reactQueryClient.refetchQueries( + FETCH_NEON_PROJECTS_BY_PROJECTID_QUERYKEY + ); + setInstallingTemplate(true); install(); }, @@ -80,11 +89,11 @@ export function NeonOnboarding(props: { return (
- +
-
-
- Sources -
- -
-
-
- -
-
-
-
- -
REST Endpoints
-
-
-
- -
-
-
- -
Databases
-
-
-
- -
-
-
- -
GraphQL Services
-
-
-
-
+
+
+
- -
-
-
- - Connected -
-
- -
- -
-
-
- -
Hasura
-
-
-
-
Authentication
-
-
-
Permissions
-
- -
-
REST API
-
-
-
-
-
GraphQL API
-
-
-
Relationships
-
-
-
Caching
-
-
-
Observability
-
-
-
-
- -
-
- -
- - Powering -
-
-
- -
-
- Consumers -
- -
-
-
- -
-
-
-
- -
Apps
-
-
-
- -
-
-
- -
Data Platforms
-
-
-
- -
-
-
- -
Other Services
-
-
-
-
+
+ We recommend connecting a Postgres database to instantly explore your{' '} + Hasura GraphQL API
); diff --git a/console/src/features/OnboardingWizard/components/ConnectDBScreen/components/OnboardingAnimationNavbar.tsx b/console/src/features/OnboardingWizard/components/ConnectDBScreen/components/OnboardingAnimationNavbar.tsx deleted file mode 100644 index d4490ebd949..00000000000 --- a/console/src/features/OnboardingWizard/components/ConnectDBScreen/components/OnboardingAnimationNavbar.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import { CustomRightChevron } from './CustomRightChevron'; - -const commmonListItemStyle = - 'flex-shrink-0 w-10 h-10 flex items-center justify-center border-2 rounded-full'; - -export function OnboardingAnimationNavbar() { - return ( - - ); -} diff --git a/console/src/features/OnboardingWizard/components/ConnectDBScreen/components/index.ts b/console/src/features/OnboardingWizard/components/ConnectDBScreen/components/index.ts deleted file mode 100644 index 7ea0b7409d6..00000000000 --- a/console/src/features/OnboardingWizard/components/ConnectDBScreen/components/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { OnboardingAnimationNavbar } from './OnboardingAnimationNavbar'; -export { OnboardingAnimation } from './OnboardingAnimation'; diff --git a/console/src/features/OnboardingWizard/components/DialogContainer/DialogContainer.stories.tsx b/console/src/features/OnboardingWizard/components/DialogContainer/DialogContainer.stories.tsx new file mode 100644 index 00000000000..48e7b57c333 --- /dev/null +++ b/console/src/features/OnboardingWizard/components/DialogContainer/DialogContainer.stories.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { ComponentMeta, Story } from '@storybook/react'; +import { DialogContainer } from './DialogContainer'; +import { dialogHeader, familiaritySurveySubHeader } from '../../constants'; + +export default { + title: 'features/Onboarding Wizard/Dialog Container', + component: DialogContainer, +} as ComponentMeta; + +const ExampleChildren = () => { + return
Hello world!
; +}; + +export const Base: Story = () => ( + + + +); diff --git a/console/src/features/OnboardingWizard/components/DialogContainer/DialogContainer.tsx b/console/src/features/OnboardingWizard/components/DialogContainer/DialogContainer.tsx new file mode 100644 index 00000000000..f0e407418bd --- /dev/null +++ b/console/src/features/OnboardingWizard/components/DialogContainer/DialogContainer.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import * as Dialog from '@radix-ui/react-dialog'; +import { TopHeaderBar } from '../TopHeaderBar/TopHeaderBar'; +import { StepperNavbar } from '../StepperNavbar/StepperNavbar'; + +type DialogContainer = { + header: string; + subHeader?: string; + showStepper?: boolean; + activeIndex?: number; +}; + +export const DialogContainer: React.FC = props => { + const { activeIndex, showStepper, header, subHeader } = props; + return ( + // Radix dialog is being used for creating a layover component over the whole app. + // It does not make sense to extend common dialog component to fit this one-off use case. + // + // modal={false} is set to prevent focus issues when multiple modals are visible, + // for example survey modal and onboarding modal + + + +
+
+

+ {header} +

+ {subHeader &&

{subHeader}

} +
+ {showStepper && } + + {props.children} +
+
+
+ ); +}; diff --git a/console/src/features/OnboardingWizard/components/NeonConnectBanner/NeonBanner.stories.tsx b/console/src/features/OnboardingWizard/components/NeonConnectBanner/NeonBanner.stories.tsx index a3b39073064..917f89f152a 100644 --- a/console/src/features/OnboardingWizard/components/NeonConnectBanner/NeonBanner.stories.tsx +++ b/console/src/features/OnboardingWizard/components/NeonConnectBanner/NeonBanner.stories.tsx @@ -14,6 +14,7 @@ export const Base: Story = () => ( onClickConnect={() => window.alert('clicked connect button')} status={{ status: 'default' }} buttonText="Create a Neon Database" + setStepperIndex={() => {}} /> ); Base.play = async ({ canvasElement }) => { @@ -34,6 +35,7 @@ export const Creating: Story = () => ( onClickConnect={() => window.alert('clicked connect button')} status={{ status: 'loading' }} buttonText="Creating Neon Database" + setStepperIndex={() => {}} /> ); Creating.play = async ({ canvasElement }) => { @@ -60,6 +62,7 @@ export const Error: Story = () => ( }} buttonText="Try Again" icon="refresh" + setStepperIndex={() => {}} /> ); Error.play = async ({ canvasElement }) => { diff --git a/console/src/features/OnboardingWizard/components/NeonConnectBanner/NeonBanner.tsx b/console/src/features/OnboardingWizard/components/NeonConnectBanner/NeonBanner.tsx index 2e14216f93f..24ad9de7d50 100644 --- a/console/src/features/OnboardingWizard/components/NeonConnectBanner/NeonBanner.tsx +++ b/console/src/features/OnboardingWizard/components/NeonConnectBanner/NeonBanner.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { MdRefresh } from 'react-icons/md'; import { Button } from '@/new-components/Button'; import { IndicatorCard } from '@/new-components/IndicatorCard'; +import { HasuraLogoFull } from '@/new-components/HasuraLogo'; import { NeonIcon } from './NeonIcon'; const iconMap = { @@ -26,10 +27,11 @@ export type Props = { onClickConnect: VoidFunction; buttonText: string; icon?: keyof typeof iconMap; + setStepperIndex: (index: number) => void; }; export function NeonBanner(props: Props) { - const { status, onClickConnect, buttonText, icon } = props; + const { status, onClickConnect, buttonText, icon, setStepperIndex } = props; const isButtonDisabled = status.status === 'loading'; return ( @@ -37,12 +39,15 @@ export function NeonBanner(props: Props) {
- +
+ +
+
+ +
-
- Need a new database? Hasura has partnered with Neon to help - you seamlessly create your database with their serverless Postgres - platform. +
+ Need a new database? We've partnered with Neon to help you get + started.
@@ -56,12 +61,13 @@ export function NeonBanner(props: Props) { icon={icon ? iconMap[icon] : undefined} onClick={() => { if (!isButtonDisabled) { + setStepperIndex(2); onClickConnect(); } }} disabled={isButtonDisabled} > - {buttonText} +
{buttonText}
diff --git a/console/src/features/OnboardingWizard/components/QueryDialog/QueryDialog.stories.tsx b/console/src/features/OnboardingWizard/components/QueryDialog/QueryDialog.stories.tsx deleted file mode 100644 index e28cdf43bcd..00000000000 --- a/console/src/features/OnboardingWizard/components/QueryDialog/QueryDialog.stories.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import { ComponentMeta, Story } from '@storybook/react'; -import { userEvent, within } from '@storybook/testing-library'; -import { expect } from '@storybook/jest'; -import { QueryDialog } from './QueryDialog'; -import type { Props } from './QueryDialog'; - -export default { - title: 'features/Onboarding Wizard/Query Dialog', - component: QueryDialog, - argTypes: { - onRunHandler: { action: true }, - onSkipHandler: { action: true }, - }, -} as ComponentMeta; - -const defaultQuery = ` -# Lookup artist info, albums, tracks based on relations -# Filter for only 'ArtistId' with the ID of '22' - -query lookupArtist { - sample_Artist(where: {ArtistId: {_eq: 22}}) { - ArtistId - Name - Albums { - AlbumId - Title - Tracks { - TrackId - Name - } - } - } -} -`; - -export const Base: Story = args => ( - -); - -Base.args = { - title: 'πŸ‘‹ Welcome to Hasura!', - description: 'Get started learning Hasura with an example.', - query: defaultQuery, - schemaImage: - 'https://raw.githubusercontent.com/hasura/template-gallery/main/postgres/getting-started/diagram.png', -}; - -Base.play = async ({ args, canvasElement }) => { - const canvas = within(canvasElement); - const runButton = canvas.getByText('Run Query'); - const skipButton = canvas.getByText('Skip getting started tutorial'); - - // Expect element renders successfully - expect(canvas.getByText('πŸ‘‹ Welcome to Hasura!')).toBeVisible(); - expect( - canvas.getByText('Get started learning Hasura with an example.') - ).toBeVisible(); - // Expect button to be present in the dialog - expect(runButton).toBeInTheDocument(); - expect(runButton).not.toBeDisabled(); - expect(skipButton).toBeInTheDocument(); - expect(canvas.getByTestId('query-dialog-schema-image')).toBeVisible(); - expect(canvas.getByTestId('query-dialog-sample-query')).toBeVisible(); - await userEvent.click(runButton); - expect(args.onRunHandler).toBeCalledTimes(1); - await userEvent.click(skipButton); - expect(args.onSkipHandler).toBeCalledTimes(1); -}; diff --git a/console/src/features/OnboardingWizard/components/QueryDialog/QueryDialog.tsx b/console/src/features/OnboardingWizard/components/QueryDialog/QueryDialog.tsx deleted file mode 100644 index 5a6d50f3b30..00000000000 --- a/console/src/features/OnboardingWizard/components/QueryDialog/QueryDialog.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { Dialog } from '@/new-components/Dialog'; -import { Button } from '@/new-components/Button'; -import { FaPlayCircle } from 'react-icons/fa'; - -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 ( - - <> -
-
- We've created a `sample` schema to help you get started - using Hasura. It contains the sample structure of a music directory - with two tables β€˜Albums’ and β€˜Artists’ based on a foreign key - relationship. -
- getting-started -
-
- Give it a try with our example query: -
- -
-              {query}
-            
-
-
- -
-
- Skip getting started tutorial -
- -
- -
- ); -} diff --git a/console/src/features/OnboardingWizard/components/QueryScreen/QueryScreen.stories.tsx b/console/src/features/OnboardingWizard/components/QueryScreen/QueryScreen.stories.tsx new file mode 100644 index 00000000000..b861e92e81c --- /dev/null +++ b/console/src/features/OnboardingWizard/components/QueryScreen/QueryScreen.stories.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { ComponentMeta, Story } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; +import { QueryScreen } from './QueryScreen'; +import type { Props } from './QueryScreen'; + +const defaultQuery = ` +# +# An example query: +# Lookup all customers and their orders based on a foreign key relationship. +# β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β” +# β”‚ customer β”‚---->β”‚ order β”‚ +# β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”˜ +# + +query lookupCustomerOrder { + customer { + id + first_name + last_name + username + email + phone + orders { + id + order_date + product + purchase_price + discount_price + } + } +} +`; + +export default { + title: 'features/Onboarding Wizard/Query Screen', + component: QueryScreen, + argTypes: { + onRunHandler: { action: true }, + onSkipHandler: { action: true }, + }, +} as ComponentMeta; + +export const Base: Story = args => ( + +); + +Base.args = { + schemaImage: + 'https://raw.githubusercontent.com/hasura/template-gallery/main/postgres/getting-started/diagram.png', +}; + +Base.play = async ({ args, canvasElement }) => { + const canvas = within(canvasElement); + const runButton = canvas.getByText('Run a Sample Query'); + const skipButton = canvas.getByText('Skip, continue to Console'); + + // Expect element renders successfully + expect(canvas.getByText(`You're ready to go!`)).toBeVisible(); + + // Expect button to be present in the dialog + expect(runButton).toBeInTheDocument(); + expect(runButton).not.toBeDisabled(); + expect(skipButton).toBeInTheDocument(); + + await userEvent.click(runButton); + expect(args.onRunHandler).toBeCalledTimes(1); + await userEvent.click(skipButton); + expect(args.onSkipHandler).toBeCalledTimes(1); +}; diff --git a/console/src/features/OnboardingWizard/components/QueryScreen/QueryScreen.tsx b/console/src/features/OnboardingWizard/components/QueryScreen/QueryScreen.tsx new file mode 100644 index 00000000000..0454bf18e3d --- /dev/null +++ b/console/src/features/OnboardingWizard/components/QueryScreen/QueryScreen.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { Button } from '@/new-components/Button'; +import { FaPlayCircle } from 'react-icons/fa'; + +export interface Props { + schemaImage: string; + onRunHandler: () => void; + onSkipHandler: () => void; + query: string; +} + +export function QueryScreen(props: Props) { + const { schemaImage, onRunHandler, onSkipHandler, query } = props; + return ( + <> +
+
+
+ graphql-schema +
+
+
+ We've created a structure with two tables customer and{' '} + order connected through a foreign key relationship. +
+
+
+
+              
+ SAMPLE GRAPHQL QUERY +
+ {query} +
+
+
+
+ +
+
+
+
+
+
+ + πŸš€ + + You're ready to go! + Run your first sample query to get started. +
+
+
+ +
+
+
+
+ +
+ + ); +} diff --git a/console/src/features/OnboardingWizard/components/QueryDialog/TemplateSummary.tsx b/console/src/features/OnboardingWizard/components/QueryScreen/TemplateSummary.tsx similarity index 79% rename from console/src/features/OnboardingWizard/components/QueryDialog/TemplateSummary.tsx rename to console/src/features/OnboardingWizard/components/QueryScreen/TemplateSummary.tsx index dbe2a0c8350..e2be5cd1e02 100644 --- a/console/src/features/OnboardingWizard/components/QueryDialog/TemplateSummary.tsx +++ b/console/src/features/OnboardingWizard/components/QueryScreen/TemplateSummary.tsx @@ -7,7 +7,7 @@ import { templateSummaryRunQueryClickVariables, templateSummaryRunQuerySkipVariables, } from '../../constants'; -import { QueryDialog } from './QueryDialog'; +import { QueryScreen } from './QueryScreen'; import { fetchTemplateDataQueryFn, runQueryInGraphiQL, @@ -22,7 +22,31 @@ type Props = { }; const defaultQuery = ` -# Make a GraphQL query +# +# An example query: +# Lookup all customers and their orders based on a foreign key relationship. +# β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β” +# β”‚ customer β”‚---->β”‚ order β”‚ +# β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”˜ +# + +query lookupCustomerOrder { + customer { + id + first_name + last_name + username + email + phone + orders { + id + order_date + product + purchase_price + discount_price + } + } +} `; export function TemplateSummary(props: Props) { @@ -72,13 +96,11 @@ export function TemplateSummary(props: Props) { }; return ( - ); } diff --git a/console/src/features/OnboardingWizard/components/StepperNavbar/StepperNavbar.stories.tsx b/console/src/features/OnboardingWizard/components/StepperNavbar/StepperNavbar.stories.tsx new file mode 100644 index 00000000000..f454ab31ba4 --- /dev/null +++ b/console/src/features/OnboardingWizard/components/StepperNavbar/StepperNavbar.stories.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { ComponentMeta, Story } from '@storybook/react'; +import { StepperNavbar } from './StepperNavbar'; + +export default { + title: 'features/Onboarding Wizard/Stepper Navbar', + component: StepperNavbar, +} as ComponentMeta; + +export const Base: Story = () => ; diff --git a/console/src/features/OnboardingWizard/components/StepperNavbar/StepperNavbar.tsx b/console/src/features/OnboardingWizard/components/StepperNavbar/StepperNavbar.tsx new file mode 100644 index 00000000000..28a4510d34e --- /dev/null +++ b/console/src/features/OnboardingWizard/components/StepperNavbar/StepperNavbar.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import clsx from 'clsx'; +import { CustomRightChevron } from './components/CustomRightChevron'; + +const commmonListItemStyle = + 'flex-shrink-0 w-10 h-10 flex items-center justify-center border-2 rounded-full'; + +type StepperNavbarProps = { + /** + * step which is currently active, assumes 1-based indexing + */ + activeIndex?: number; +}; + +const steps = [ + { + step: '01', + text: 'Getting Started', + }, + { + step: '02', + text: 'Connect Database', + }, + { + step: '03', + text: 'Make Your First Query', + }, +]; + +export function StepperNavbar(props: StepperNavbarProps) { + const { activeIndex } = props; + const lastStep = steps.length - 1; + // for using 1-based indexing, if no activeIndex prop then set it as -1 + const currentActiveIndex = activeIndex ? activeIndex - 1 : -1; + + return ( + + ); +} diff --git a/console/src/features/OnboardingWizard/components/ConnectDBScreen/components/CustomRightChevron.tsx b/console/src/features/OnboardingWizard/components/StepperNavbar/components/CustomRightChevron.tsx similarity index 100% rename from console/src/features/OnboardingWizard/components/ConnectDBScreen/components/CustomRightChevron.tsx rename to console/src/features/OnboardingWizard/components/StepperNavbar/components/CustomRightChevron.tsx diff --git a/console/src/features/OnboardingWizard/components/index.ts b/console/src/features/OnboardingWizard/components/index.ts index fea2de6fb8c..fc3ac8981b5 100644 --- a/console/src/features/OnboardingWizard/components/index.ts +++ b/console/src/features/OnboardingWizard/components/index.ts @@ -1,3 +1,4 @@ -export { TopHeaderBar } from './TopHeaderBar/TopHeaderBar'; export { ConnectDBScreen } from './ConnectDBScreen/ConnectDBScreen'; -export { TemplateSummary } from './QueryDialog/TemplateSummary'; +export { TemplateSummary } from './QueryScreen/TemplateSummary'; +export { StepperNavbar } from './StepperNavbar/StepperNavbar'; +export { DialogContainer } from './DialogContainer/DialogContainer'; diff --git a/console/src/features/OnboardingWizard/constants.ts b/console/src/features/OnboardingWizard/constants.ts index 229557107fc..a5b3ffd9d53 100644 --- a/console/src/features/OnboardingWizard/constants.ts +++ b/console/src/features/OnboardingWizard/constants.ts @@ -114,3 +114,8 @@ export const getNeonOnboardingErrorVariables = (code: string) => { // A stale time of 5 minutes for use in useQuery hook export const staleTime = 5 * 60 * 1000; + +export const dialogHeader = 'Welcome to your new Hasura project!'; + +export const familiaritySurveySubHeader = + "We'd love to get to know you before you get started with your first API."; diff --git a/console/src/features/OnboardingWizard/hooks/useWizardState.ts b/console/src/features/OnboardingWizard/hooks/useWizardState.ts index 8f764f7c4ab..afb4b943353 100644 --- a/console/src/features/OnboardingWizard/hooks/useWizardState.ts +++ b/console/src/features/OnboardingWizard/hooks/useWizardState.ts @@ -1,9 +1,14 @@ import { useEffect, useState } from 'react'; import { GrowthExperimentsClient } from '@/features/GrowthExperiments'; +import { useFamiliaritySurveyData } from '@/features/Surveys'; import { experimentId } from '../constants'; -import { isExperimentActive, shouldShowOnboarding } from '../utils'; +import { getWizardState } from '../utils'; -type WizardState = 'landing-page' | 'template-summary' | 'hidden'; +export type WizardState = + | 'familiarity-survey' + | 'landing-page' + | 'template-summary' + | 'hidden'; export function useWizardState( growthExperimentsClient: GrowthExperimentsClient, @@ -12,26 +17,42 @@ export function useWizardState( const { getAllExperimentConfig } = growthExperimentsClient; const experimentData = getAllExperimentConfig(); + const { + showFamiliaritySurvey, + data: familiaritySurveyData, + onSkip: familiaritySurveyOnSkip, + onOptionClick: familiaritySurveyOnOptionClick, + } = useFamiliaritySurveyData(); + const [state, setState] = useState( - shouldShowOnboarding(experimentData, experimentId, hasNeonAccess) && - isExperimentActive(experimentData, experimentId) - ? 'landing-page' - : 'hidden' + getWizardState( + experimentData, + experimentId, + showFamiliaritySurvey, + hasNeonAccess + ) ); useEffect(() => { // this effect is only used to update the wizard state for initial async fetching of experiments config // it only takes care of "showing" the wizard, but not hiding it, hence the check for `hidden` // hiding wizard is taken care of by setting the wizard state directly to "hidden" - const wizardState = - shouldShowOnboarding(experimentData, experimentId, hasNeonAccess) && - isExperimentActive(experimentData, experimentId) - ? 'landing-page' - : 'hidden'; + const wizardState = getWizardState( + experimentData, + experimentId, + showFamiliaritySurvey, + hasNeonAccess + ); if (wizardState !== 'hidden') { setState(wizardState); } - }, [growthExperimentsClient.getAllExperimentConfig()]); + }, [experimentData, showFamiliaritySurvey, hasNeonAccess]); - return { state, setState }; + return { + state, + setState, + familiaritySurveyData, + familiaritySurveyOnSkip, + familiaritySurveyOnOptionClick, + }; } diff --git a/console/src/features/OnboardingWizard/utils.ts b/console/src/features/OnboardingWizard/utils.ts index 66d78f8510e..7055ab3820c 100644 --- a/console/src/features/OnboardingWizard/utils.ts +++ b/console/src/features/OnboardingWizard/utils.ts @@ -17,6 +17,7 @@ import { hasuraSourceCreationStartVariables, graphQlMutation, } from './constants'; +import { WizardState } from './hooks/useWizardState'; export function isExperimentActive( experimentsData: ExperimentConfig[], @@ -59,6 +60,22 @@ export function shouldShowOnboarding( return true; } +export function getWizardState( + experimentsData: ExperimentConfig[], + experimentId: string, + showFamiliaritySurvey: boolean, + hasNeonAccess: boolean +): WizardState { + if ( + shouldShowOnboarding(experimentsData, experimentId, hasNeonAccess) && + isExperimentActive(experimentsData, experimentId) + ) { + if (showFamiliaritySurvey) return 'familiarity-survey'; + return 'landing-page'; + } + return 'hidden'; +} + type ResponseDataOnMutation = { data: { trackExperimentsCohortActivity: { diff --git a/console/src/features/Surveys/HasuraFamiliaritySurvey/components/HasuraFamiliaritySurvey.tsx b/console/src/features/Surveys/HasuraFamiliaritySurvey/components/HasuraFamiliaritySurvey.tsx index aae5e91f2f0..86cbcfce7e2 100644 --- a/console/src/features/Surveys/HasuraFamiliaritySurvey/components/HasuraFamiliaritySurvey.tsx +++ b/console/src/features/Surveys/HasuraFamiliaritySurvey/components/HasuraFamiliaritySurvey.tsx @@ -14,13 +14,6 @@ export function HasuraFamiliaritySurvey(props: HasuraFamiliaritySurveyProps) { return ( <> -

- Welcome to your new Hasura project! -

-

- We'd love to get to know you before you get started with your first - API. -

{data.question}
diff --git a/console/src/new-components/HasuraLogo/HasuraLogoFull.stories.mdx b/console/src/new-components/HasuraLogo/HasuraLogoFull.stories.mdx index af701bde06c..b6cfabb1dfe 100644 --- a/console/src/new-components/HasuraLogo/HasuraLogoFull.stories.mdx +++ b/console/src/new-components/HasuraLogo/HasuraLogoFull.stories.mdx @@ -58,6 +58,9 @@ export const stories = { 'Variant - Mode primary': { mode: 'primary', }, + 'Variant - Mode brand': { + mode: 'brand', + }, 'Variant - Size sm': { size: 'sm', }, @@ -79,6 +82,7 @@ export const stories = { args={{ 'Variant - Mode default': stories['Variant - Mode default'], 'Variant - Mode primary': stories['Variant - Mode primary'], + 'Variant - Mode brand': stories['Variant - Mode brand'], }} > {TemplateStoriesFactory(Template).bind({})} diff --git a/console/src/new-components/HasuraLogo/HasuraLogoFull.tsx b/console/src/new-components/HasuraLogo/HasuraLogoFull.tsx index fe21554a108..1ce75d62229 100644 --- a/console/src/new-components/HasuraLogo/HasuraLogoFull.tsx +++ b/console/src/new-components/HasuraLogo/HasuraLogoFull.tsx @@ -1,6 +1,6 @@ import React from 'react'; -type LogoModes = 'default' | 'primary'; +type LogoModes = 'default' | 'primary' | 'brand'; type LogoSize = 'sm' | 'md' | 'lg'; type Props = { @@ -23,10 +23,15 @@ const logoSizing: Record = { const logoModesStyles: Record = { default: 'black', primary: 'white', + brand: '#1eb4d4', }; export function HasuraLogoFull(props: Props) { const { size = 'md', mode = 'default' } = props; + + const logoFill = logoModesStyles[mode]; + const textFill = mode === 'brand' ? 'black' : logoModesStyles[mode]; + return (