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
This commit is contained in:
Abhijeet Khangarot 2022-10-10 15:17:20 +05:30 committed by hasura-bot
parent 8369cac3bd
commit 948ace94e7
36 changed files with 1622 additions and 537 deletions

View File

@ -1,29 +1,21 @@
import React from 'react';
import { Button } from '@/new-components/Button';
import { GrHeroku } from 'react-icons/gr';
export function HerokuBanner() {
return (
<div className="flex items-center justify-between border border-gray-300 border-l-4 border-l-[#430098] shadow-md rounded bg-white p-md">
<div className="w-[70%] flex items-center">
<div className="mr-2">
<GrHeroku size={30} color="#430098" />
<div className="flex items-center">
<GrHeroku size={15} className="mr-xs" color="#430098" />
<div className="text-sm text-gray-700">
Heroku free database integration support has been deprecated.
</div>
<div className="text-md text-gray-700">
Starting <b>November 28th, 2022,</b> free Heroku Dynos, free Heroku
Postgres, and free Heroku Data for Redis will no longer be available.
</div>
</div>
<div>
<a
href="https://help.heroku.com/RSBRUH58/removal-of-heroku-free-product-plans-faq"
className="hover:no-underline"
href="https://hasura.io/docs/latest/databases/connect-db/cloud-databases/heroku/"
className="ml-xs font-normal text-secondary italic text-sm"
target="_blank"
rel="noopener noreferrer"
>
<Button className="ml-auto">Know more</Button>
(Know More)
</a>
</div>
</div>
);
}

View File

@ -40,6 +40,9 @@ export function NeonBanner(props: Props) {
<span className="ml-xs font-semibold flex items-center text-sm py-0.5 px-1.5 text-indigo-600 bg-indigo-100 rounded">
Free
</span>
<span className="ml-xs font-semibold flex items-center text-sm py-0.5 px-1.5 text-indigo-600 bg-indigo-100 rounded">
Preview
</span>
</div>
<img
src="https://storage.googleapis.com/graphql-engine-cdn.hasura.io/cloud-console/assets/common/img/neon.jpg"
@ -51,9 +54,8 @@ export function NeonBanner(props: Props) {
</div>
<div className="flex justify-between items-center mb-sm">
<div className="w-[70%] text-md text-gray-700">
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.
</div>
<div>
<Button

View File

@ -0,0 +1,10 @@
import React from 'react';
import { ComponentMeta, Story } from '@storybook/react';
import { NeonDashboardLink } from './NeonDashboardLink';
export default {
title: 'features/Neon Integration/Neon Dashboard Link',
component: NeonDashboardLink,
} as ComponentMeta<typeof NeonDashboardLink>;
export const Base: Story = () => <NeonDashboardLink />;

View File

@ -0,0 +1,46 @@
import globals from '@/Globals';
import React from 'react';
import { NeonIcon } from './NeonIconSmall';
import { useShowNeonDashboardLink } from './utils';
type NeonDashboardLinkProps = {
className?: string;
};
export function NeonDashboardLink() {
const neonDashboardLink = `https://console.${globals.neonRootDomain}/app/projects`;
return (
<div className="flex items-center justify-start">
<div className="flex items-center">
<div className="mr-2">
<NeonIcon />
</div>
<div className="text-sm text-gray-700">
<a
className="hover:no-underline"
href={neonDashboardLink}
target="_blank"
rel="noopener noreferrer"
>
Neon Console
</a>
<span className="ml-1">- Manage your Neon databases</span>
</div>
</div>
</div>
);
}
export function NeonDashboardLinkWrapper(props: NeonDashboardLinkProps) {
const { className } = props;
const show = useShowNeonDashboardLink();
if (!show) return null;
return (
<div className={className}>
<NeonDashboardLink />
</div>
);
}

View File

@ -0,0 +1,37 @@
import React from 'react';
type NeonIconProps = {
className?: string;
};
export function NeonIcon(props: NeonIconProps) {
const { className } = props;
return (
<svg
width="13"
height="13"
viewBox="0 0 13 13"
fill="none"
className={className}
xmlns="http://www.w3.org/2000/svg"
>
<rect x="0.75" y="0.5" width="12" height="12" fill="url(#pattern0)" />
<defs>
<pattern
id="pattern0"
patternContentUnits="objectBoundingBox"
width="1"
height="1"
>
<use xlinkHref="#image0_445_24578" transform="scale(0.03125)" />
</pattern>
<image
id="image0_445_24578"
width="32"
height="32"
xlinkHref="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFW0lEQVRYhbWXUYhdVxWGv3+dM8lMZhITqK1Ea1Nq+xKhIIgPKibUBxNEFDMgiDVpIKDFKj5ZETwPgr4IrVDEakyIVKETRRDaR8cHn6rQImnSQjGoYJNQWpJpJnPv2ev34ZwzcydN0pmoCy5n33v22f9//rX+tfcVdiAl/3z5ftp4POQDge8KJYGpsIZxkISuu/Zj0c9Rdh8nUVpUxlZpoW25b+cLF3bvPPf88squH53acerVxk0IgPOvHMQ+HfJMkFSybw7Yg1x/n0Src5NwQW1LlDGRI9qVmt3bX9Z9d/yVNrcsJxw6Ofvsc+K1sw8QejHkmUo5CqiC9MbAJ3/rxhrG2aK2RWVMlDFlFOzecVZ77nixlJzaAixnlQ/WUek7wjOhHAWeClLVBt9aNyJDIgqRBblFLoQLdoKNRIBG4JkoerwWeaB7kKoiFfarAU+hLALJtioIzBAiUSSRUEUHCkkwzDNyAiYyqWir5XZuadvU5S+WrA6Co1/oQF11BefA7qV86vI9H/kJ/4fYP/oco/HWg5LBsuGuejKPFSbkAsDf/zjNHtpbLbhvI6iLwJ4/1Yv3NtdW2um5UGKLTiVRB6ZaX0ydM/bQov23JLC4EQJA430sAjLFCMnYwjJRKbXqZ3J9sv/nUQADvQJIsVrV6q5TpWxuTaPGTQxfJ8cbiXqwnDAVCdXmwBFuaPwNH9i6wt3ZqBlP3ntXApPttLPT5sAPv/n5nbGl/sHVq/6M/UY5+vahZ+/e9uHvN2pyIyTq9R0vN0K6i35itXXqd8B+EBIYvvePq3/7AHBkI8vE6uYxtNB4dxWGPD/y9qEDtvcDY9vFdhEeizh89Or8NxFuvK8+w3tv+lZRsX5XG9JwK4+f4Yw6EfjgsA4QkipMZYHNE4eXD32y0WI7y+zULRWo+iIMcpM1GDlYCoRtu2vfRTKR+s1XrnzhzpM6ee3mBPrc18q+IW3Ghtf5ug9DYLWS3z8V9TPDbKnf/tcR0FoT6hTYhBOo1oHbXrG91JFwAC3w6UeW5n888cANFGCiCDdFYDVsG0lv2TzWj8MmJCfi20eXDj1EcunGBLRGgk2lYIhVZbcvzeqUzHd7EmkDMkjHrfzUgLtGQNcpUN2OAqtFmLuu1LuOz53+odEfgBpITCLuAR0DsiMgAL9DgWqze8GEApJY1rUAmB5PfRXna5JqwDaW5C41toRsTxZh6Y5St6XAmhJyl8Of7vz1m4Yv2W4lVX2Z0IFL/XWtCIfD5H8Tnuh3x3xs6pezv/2L4Wt9PXjCLfQcHEF6fR207/DqRt4cuhQM8TRPt42bODF7+heGnxlXksa2W2AFCMRL9bAPaFAiSrfa+fP1Pjc3hFvijYrOLtVwuulorG/5DY0Blrbp0e3L/pDth3qSte1lQo+tbscaFOi78eK9R27aPukaDFV97UpptyL1wBbTTK/JIIzRghbKvOcPbl/215P8qJLXM/n5ydnT5+pQuSD8vqBIXS08+omLJ4mgdOmwK5KIAiSRY5RtNbq25crlpXOfnZu5hD1o7zIqy+vPkWskRsAT6xRyE3WQz4s8EsoiMoJ8QOEnh1OS+vQwdEkbMqm1zLidRkrblTsBdPbkrt+/tQo8SQKY93y1l70+wxntZa8bNamPXXzm/kq8JOVMOEdSVqG0KKvuWK0PFyhjVFpybOa2XtCuHf8iszJQK3j4+MzCr+Y9Xy1oYUMNRQAfv3jqYIRPB2WmB/SkNUXp/g3lQGBMjszc9CXtfM+/yQxsnjwxt/CtjYBORuAm/nznw88p88GQTwR+fQ28IIpCqaBIbiUXuv2lSGpH4Bdkvnw74AD/AfcZPcz7zebfAAAAAElFTkSuQmCC"
/>
</defs>
</svg>
);
}

View File

@ -0,0 +1,2 @@
export { NeonDashboardLinkWrapper as NeonDashboardLink } from './NeonDashboardLink';
export { FETCH_NEON_PROJECTS_BY_PROJECTID_QUERYKEY } from './utils';

View File

@ -0,0 +1,83 @@
import { useQuery } from 'react-query';
import { hasLuxFeatureAccess, isCloudConsole } from '@/utils/cloudConsole';
import globals from '@/Globals';
import { cloudDataServiceApiClient } from '@/hooks/cloudDataServiceApiClient';
import { isArray } from '@/components/Common/utils/jsUtils';
// TODO: These types should be replaced by autogenerated types, when we use typescript graphql codegen
type NeonProjectsByProjectIdResponseData = {
data: {
neon_db_integration: NeonDBIntegrationOptions[];
};
};
type NeonDBIntegrationOptions = {
env_var: string | null;
id: string;
neon_project_id: string;
};
const FETCH_NEON_PROJECTS_BY_PROJECTID = `
query fetchNeonProjectsByProjectId ($hasura_cloud_project_id: uuid) {
neon_db_integration(where: {hasura_cloud_project_id: {_eq: $hasura_cloud_project_id}}) {
env_var
id
neon_project_id
}
}
`;
const FETCH_NEON_PROJECTS_BY_PROJECTID_VARIABLES = {
hasura_cloud_project_id: globals.hasuraCloudProjectId,
};
export const FETCH_NEON_PROJECTS_BY_PROJECTID_QUERYKEY =
'FETCH_NEON_PROJECTS_BY_PROJECTID_QUERYKEY' as const;
const fetchNeonProjectsByProjectIdQueryFn = () => {
// cloud uses cookie-based auth, so does not require an admin secret
const headers = {
'content-type': 'application/json',
};
return cloudDataServiceApiClient<NeonProjectsByProjectIdResponseData>(
FETCH_NEON_PROJECTS_BY_PROJECTID,
FETCH_NEON_PROJECTS_BY_PROJECTID_VARIABLES,
headers
);
};
// A stale time of 5 minutes for use in useQuery hook
const staleTime = 5 * 60 * 1000;
export const useShowNeonDashboardLink = () => {
let hasNeonAccess = false;
if (
isCloudConsole(globals) &&
hasLuxFeatureAccess(globals, 'NeonDatabaseIntegration') &&
globals.neonOAuthClientId &&
globals.neonRootDomain
) {
hasNeonAccess = true;
}
const { data } = useQuery(
FETCH_NEON_PROJECTS_BY_PROJECTID_QUERYKEY,
fetchNeonProjectsByProjectIdQueryFn,
{
// don't run the query if user doesn't have neon access
enabled: hasNeonAccess,
staleTime,
}
);
if (
hasNeonAccess &&
data &&
isArray(data.data.neon_db_integration) &&
data.data.neon_db_integration.length > 0
) {
return true;
}
return false;
};

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import { Dispatch } from '@/types';
import { reactQueryClient } from '@/lib/reactQuery';
import { NeonBanner } from './components/Neon/NeonBanner';
import {
getNeonDBName,
@ -7,12 +8,18 @@ import {
} from './utils';
import { useNeonIntegration } from './useNeonIntegration';
import _push from '../../../push';
import { FETCH_NEON_PROJECTS_BY_PROJECTID_QUERYKEY } from './components/NeonDashboardLink';
// This component deals with Neon DB creation on connect DB page
export function Neon(props: { allDatabases: string[]; dispatch: Dispatch }) {
const { dispatch, allDatabases } = props;
// success callback
const pushToDatasource = (dataSourceName: string) => {
// 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);
dispatch(_push(`/data/${dataSourceName}`));
};
const pushToConnectDBPage = () => {

View File

@ -34,6 +34,7 @@ import { services } from '../../../../dataSources/services';
import CollapsibleToggle from './CollapsibleToggle';
import VPCBanner from '../../../Common/VPCBanner/VPCBanner';
import { useVPCBannerVisibility } from './utils';
import { NeonDashboardLink } from '../DataSources/CreateDataSource/Neon/components/NeonDashboardLink';
type DatabaseListItemProps = {
dataSource: DataSource;
@ -370,6 +371,8 @@ const ManageDatabase: React.FC<ManageDatabaseProps> = ({
<ManageAgents />
</div>
) : null}
<NeonDashboardLink className="mt-lg" />
</div>
</RightContainer>
);

View File

@ -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<number>(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 'landing-page': {
case 'familiarity-survey': {
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
<Dialog.Root modal={false} open>
<Dialog.Content className="fixed top-0 w-full h-full focus:outline-none bg-gray-50 overflow-hidden z-[100]">
<TopHeaderBar />
<div className="max-w-5xl p-md ml-auto mr-auto mt-xl">
{showFamiliaritySurvey ? (
<DialogContainer
header={dialogHeader}
subHeader={familiaritySurveySubHeader}
>
<HasuraFamiliaritySurvey
data={familiaritySurveyData}
onSkip={familiaritySurveyOnSkip}
onOptionClick={familiaritySurveyOnOptionClick}
/>
) : (
</DialogContainer>
);
}
case 'landing-page': {
return (
<DialogContainer
showStepper
activeIndex={stepperIndex}
header={dialogHeader}
>
<ConnectDBScreen
dismissOnboarding={dismiss}
proceed={transitionToTemplateSummary}
hasNeonAccess={hasNeonAccess}
dispatch={dispatch}
setStepperIndex={setStepperIndex}
/>
)}
</div>
</Dialog.Content>
</Dialog.Root>
</DialogContainer>
);
}
case 'template-summary': {
return (
<DialogContainer showStepper activeIndex={3} header={dialogHeader}>
<TemplateSummary
templateUrl={templateBaseUrl}
templateUrl={NEON_TEMPLATE_BASE_PATH}
dismiss={dismiss}
dispatch={dispatch}
/>
</DialogContainer>
);
}
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 <Root {...props} />;
}
return null;
}
export const RootWithoutCloudCheck = Root;

View File

@ -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();
};

View File

@ -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 (
<>
<h1 className="text-xl font-semibold text-cloud-darkest">
Welcome to your new Hasura project!
</h1>
<p>Let&apos;s get started by connecting your first database</p>
<div className="mt-5">
<OnboardingAnimationNavbar />
<OnboardingAnimation />
</div>
<div className="flex items-center justify-between">
{hasNeonAccess ? (
@ -54,6 +53,7 @@ export function ConnectDBScreen(props: ConnectDBScreenProps) {
dispatch={dispatch}
dismiss={dismissOnboarding}
proceed={proceed}
setStepperIndex={setStepperIndex}
/>
) : (
<>

File diff suppressed because one or more lines are too long

View File

@ -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 (
<div className="w-full">
<div className="w-full mb-sm">
<NeonBanner {...neonBannerProps} />
<NeonBanner {...neonBannerProps} setStepperIndex={setStepperIndex} />
</div>
<div className="flex justify-start items-center w-full">
<a
className={`w-auto text-secondary cursor-pointer text-sm hover:text-secondary-dark ${
className={`w-auto text-secondary text-sm hover:text-secondary-dark hover:no-underline ${
allowSkipping ? 'cursor-pointer' : 'cursor-not-allowed'
}`}
data-trackid="onboarding-skip-button"

View File

@ -1,168 +1,15 @@
import React from 'react';
import {
FaDatabase,
FaPlug,
FaCogs,
FaLink,
FaBolt,
FaCaretRight,
FaMobileAlt,
FaServer,
FaShareAlt,
} from 'react-icons/fa';
import { HasuraLogoIcon } from '@/new-components/HasuraLogo';
const commonStyles = {
consumerList:
'relative bg-white rounded border border-amber-500 shadow shadow-amber-500/50 overflow-hidden p-sm w-48',
featuresList:
'opacity-0 group flex items-center border-t border-gray-200 bg-gray-100 text-gray-400 text-sm px-sm',
};
import { HasuraOnboarding } from './HasuraOnboardingSVG';
export function OnboardingAnimation() {
return (
<div className="flex justify-center overflow-auto bg-gray-200 border border-gray-300 rounded-b p-md mb-md">
<div className="flex items-center relative pr-6">
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_500ms_forwards] opacity-0 absolute top-0 left-0 text-sm font-semibold text-muted uppercase tracking-wider">
Sources
</div>
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_2500ms_forwards] opacity-0 absolute h-full right-0 border-r border-gray-400" />
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_2500ms_forwards] opacity-0 absolute w-4 border-t border-gray-400 top-0 right-0" />
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_2500ms_forwards] opacity-0 absolute w-4 border-b border-gray-400 bottom-0 right-0" />
<div className="space-y-md">
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_1500ms_forwards] opacity-0 flex items-center justify-end">
<div className="relative bg-transparent rounded border border-gray-400 text-gray-400 overflow-hidden p-sm w-48">
<div className="flex items-center">
<FaCogs className="fill-current w-4 h-4 mr-1.5" />
<div className="font-semibold">REST Endpoints</div>
</div>
</div>
</div>
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_1000ms_forwards] opacity-0 flex items-center justify-end">
<div className="relative bg-white rounded shadow overflow-hidden border shadow p-sm w-48 hover:shadow-md">
<div className="flex items-center">
<FaDatabase className="fill-current w-4 h-4 mr-1.5" />
<div className="font-semibold">Databases</div>
</div>
</div>
</div>
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_2000ms_forwards] opacity-0 flex items-center justify-end">
<div className="relative bg-transparent rounded border border-gray-400 text-gray-400 overflow-hidden p-sm w-48">
<div className="flex items-center">
<FaPlug className="fill-current w-4 h-4 mr-1.5" />
<div className="font-semibold">GraphQL Services</div>
</div>
</div>
</div>
</div>
</div>
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_2500ms_forwards] opacity-0 flex items-center">
<div className="-mt-2.5 w-4 border-t border-gray-400" />
<div className="-mt-2.5 flex items-center px-xs text-sm text-muted">
<FaLink className="mr-1 w-3" />
Connected
</div>
<div className="-mt-2.5 w-4 border-t border-gray-400" />
<FaCaretRight className="-mt-2.5 w-2 mr-1 text-gray-400 h-full -ml-1.5" />
</div>
<div className="flex items-center">
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_3000ms_forwards] opacity-0 bg-white rounded shadow w-48 hover:shadow-md">
<div className="p-sm flex items-center">
<HasuraLogoIcon size="sm" />
<div className="font-semibold">Hasura</div>
</div>
<div className="relative">
<div
className={`animate-[onboardingWizardFadeIn_300ms_ease-out_3500ms_forwards] ${commonStyles.featuresList} py-1.5`}
>
<div>Authentication</div>
</div>
<div
className={`animate-[onboardingWizardFadeIn_300ms_ease-out_4000ms_forwards] ${commonStyles.featuresList} pt-1.5 pb-6`}
>
<div>Permissions</div>
</div>
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_4500ms_forwards] opacity-0 group flex items-center bg-gray-100 text-gray-400 text-sm px-sm pb-1.5 pt-6 ">
<div>REST API</div>
</div>
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_6500ms_forwards] -mt-[2rem] absolute top-1/2 -translate-y-1/2 font-semibold scale-105 w-48 opacity-0 group flex items-center text-sm px-sm rounded shadow border-gray-200 bg-white py-1.5">
<div className="absolute top-2.5 right-2.5 rounded-full w-3 h-3 bg-amber-500 animate-ping" />
<div className="absolute top-2.5 right-2.5 rounded-full w-3 h-3 bg-amber-500" />
<div>GraphQL API</div>
</div>
<div
className={`animate-[onboardingWizardFadeIn_300ms_ease-out_5000ms_forwards] ${commonStyles.featuresList} py-1.5`}
>
<div>Relationships</div>
</div>
<div
className={`animate-[onboardingWizardFadeIn_300ms_ease-out_5500ms_forwards] ${commonStyles.featuresList} py-1.5`}
>
<div>Caching</div>
</div>
<div
className={`animate-[onboardingWizardFadeIn_300ms_ease-out_6000ms_forwards] ${commonStyles.featuresList} py-1.5 rounded-b`}
>
<div>Observability</div>
</div>
</div>
</div>
</div>
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_7000ms_forwards] opacity-0 flex items-center">
<div className="-mt-2.5 w-2 ml-1.5 border-t border-gray-400" />
<FaCaretRight className="-mt-2.5 text-gray-400 w-2 h-full" />
<div className="-mt-2.5 flex items-center px-xs text-sm text-muted">
<FaBolt className="text-muted mr-1 w-3" />
Powering
</div>
<div className="-mt-2.5 w-4 border-t border-gray-400" />
</div>
<div className="flex items-center relative pl-6">
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_7500ms_forwards] opacity-0 absolute top-0 right-0 text-sm font-semibold text-muted uppercase tracking-wider">
Consumers
</div>
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_7000ms_forwards] opacity-0 absolute h-full left-0 border-r border-gray-400" />
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_7000ms_forwards] opacity-0 absolute w-4 border-t border-gray-400 top-0 left-0" />
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_7000ms_forwards] opacity-0 absolute w-4 border-b border-gray-400 bottom-0 left-0" />
<div className="space-y-md">
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_8000ms_forwards] opacity-0 flex items-center justify-end">
<div className={commonStyles.consumerList}>
<div className="flex items-center">
<FaMobileAlt className="fill-current w-4 h-full mr-1.5" />
<div className="font-semibold">Apps</div>
</div>
</div>
</div>
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_8500ms_forwards] opacity-0 flex items-center justify-end">
<div className={commonStyles.consumerList}>
<div className="flex items-center">
<FaServer className="fill-current w-4 h-full mr-1.5" />
<div className="font-semibold">Data Platforms</div>
</div>
</div>
</div>
<div className="animate-[onboardingWizardFadeIn_300ms_ease-out_9000ms_forwards] opacity-0 flex items-center justify-end">
<div className={commonStyles.consumerList}>
<div className="flex items-center">
<FaShareAlt className="fill-current w-4 h-full mr-1.5" />
<div className="font-semibold">Other Services</div>
</div>
</div>
</div>
<div className="flex flex-col justify-center items-center overflow-auto bg-gray-200 border border-gray-300 rounded-b p-md mb-md">
<div>
<HasuraOnboarding />
</div>
<div className="font-normal text-sm text-gray-600 mt-md">
We recommend connecting a Postgres database to instantly explore your{' '}
<b>Hasura GraphQL API</b>
</div>
</div>
);

View File

@ -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 (
<nav>
<ol className="border-t border-l border-r border-gray-300 rounded-t divide-y mb-0 divide-gray-300 md:flex md:divide-y-0 bg-white">
<li className="relative flex-grow md:flex">
<div className="group flex items-center w-full">
<span className="px-md py-sm flex items-center">
<span
className={`${commmonListItemStyle} border-amber-500 bg-[#f9c548] font-semibold`}
>
<span className="text-sm text-gray-800">01</span>
</span>
<span className="ml-sm text-gray-900">Getting Started</span>
</span>
</div>
<div
className="hidden md:block absolute top-0 right-0 h-full w-5"
aria-hidden="true"
>
<CustomRightChevron className="h-full w-full text-gray-300" />
</div>
</li>
<li className="relative flex-grow md:flex">
<div className="px-6 py-4 flex items-center" aria-current="step">
<span
className={`${commmonListItemStyle} border-gray-300 group-hover:border-gray-400`}
>
<span className="text-sm font-semibold text-gray-500 group-hover:text-gray-900">
02
</span>
</span>
<span className="ml-sm text-gray-900">Connect Database</span>
</div>
<div
className="hidden md:block absolute top-0 right-0 h-full w-5"
aria-hidden="true"
>
<CustomRightChevron className="h-full w-full text-gray-300" />
</div>
</li>
<li className="relative flex-grow md:flex">
<div className="group flex items-center">
<span className="px-6 py-4 flex items-center">
<span
className={`${commmonListItemStyle} border-gray-300 group-hover:border-gray-400`}
>
<span className="text-sm font-semibold text-gray-500 group-hover:text-gray-900">
03
</span>
</span>
<span className="ml-sm text-gray-900">Make Your First Query</span>
</span>
</div>
</li>
</ol>
</nav>
);
}

View File

@ -1,2 +0,0 @@
export { OnboardingAnimationNavbar } from './OnboardingAnimationNavbar';
export { OnboardingAnimation } from './OnboardingAnimation';

View File

@ -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<typeof DialogContainer>;
const ExampleChildren = () => {
return <div>Hello world!</div>;
};
export const Base: Story = () => (
<DialogContainer
header={dialogHeader}
subHeader={familiaritySurveySubHeader}
showStepper
activeIndex={1}
>
<ExampleChildren />
</DialogContainer>
);

View File

@ -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<DialogContainer> = 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
<Dialog.Root modal={false} open>
<Dialog.Content className="fixed top-0 w-full h-full focus:outline-none bg-gray-50 overflow-y-scroll z-[100]">
<TopHeaderBar />
<div className="max-w-5xl p-md ml-auto mr-auto mt-xl">
<div className="mb-5">
<h1 className="text-xl font-semibold text-cloud-darkest">
{header}
</h1>
{subHeader && <p>{subHeader}</p>}
</div>
{showStepper && <StepperNavbar activeIndex={activeIndex} />}
{props.children}
</div>
</Dialog.Content>
</Dialog.Root>
);
};

View File

@ -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 }) => {

View File

@ -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) {
<div className="flex items-center">
<div className="flex w-3/4 items-center">
<div className="mr-sm">
<div className="flex items-center">
<HasuraLogoFull mode="brand" size="sm" />
<div className="font-bold mx-xs">+</div>
<NeonIcon />
</div>
<div className="text-lg text-gray-700 ml-sm">
<b>Need a new database?</b> Hasura has partnered with Neon to help
you seamlessly create your database with their serverless Postgres
platform.
</div>
<div className="text-md text-gray-700 ml-xs">
Need a new database? We&apos;ve partnered with Neon to help you get
started.
</div>
</div>
<div className="flex w-1/4 justify-end">
@ -56,12 +61,13 @@ export function NeonBanner(props: Props) {
icon={icon ? iconMap[icon] : undefined}
onClick={() => {
if (!isButtonDisabled) {
setStepperIndex(2);
onClickConnect();
}
}}
disabled={isButtonDisabled}
>
{buttonText}
<div className="text-black font-semibold text-md">{buttonText}</div>
</Button>
</div>
</div>

View File

@ -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<typeof QueryDialog>;
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<Props> = args => (
<QueryDialog
title={args.title}
description={args.description}
query={args.query}
schemaImage={args.schemaImage}
onRunHandler={args.onRunHandler}
onSkipHandler={args.onSkipHandler}
/>
);
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);
};

View File

@ -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 (
<Dialog hasBackdrop title={title} description={description} size="md">
<>
<div className="mx-4 my-2">
<div className="text-md text-gray-700 mb-2">
We&apos;ve created a <b>`sample`</b> 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.
</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">
<div
data-trackid="query-dialog-skip-button"
className="cursor-pointer text-secondary text-sm hover:text-secondary-dark"
onClick={onSkipHandler}
>
Skip getting started tutorial
</div>
<Button
mode="primary"
data-trackid="query-dialog-get-started-button"
onClick={onRunHandler}
>
Run Query <FaPlayCircle />
</Button>
</div>
</>
</Dialog>
);
}

View File

@ -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<typeof QueryScreen>;
export const Base: Story<Props> = args => (
<QueryScreen
schemaImage={args.schemaImage}
onRunHandler={args.onRunHandler}
onSkipHandler={args.onSkipHandler}
query={defaultQuery}
/>
);
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);
};

View File

@ -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 (
<>
<div className="flex flex-col items-center justify-center overflow-auto bg-gray-200 border border-gray-300 rounded-b p-md mb-md">
<div>
<div>
<img className="mb-4" src={schemaImage} alt="graphql-schema" />
</div>
<div className="flex justify-center mb-4">
<div className="text-sm text-gray-700">
We&apos;ve created a structure with two tables <b>customer</b> and{' '}
<b>order</b> connected through a foreign key relationship.
</div>
</div>
<div className="w-full" data-testid="query-dialog-sample-query">
<pre className="border border-gray-300 bg-gray-100 text-muted font-mono text-sm px-4 py-4">
<div className="font-semibold text-gray-600 text-xs mb-sm">
SAMPLE GRAPHQL QUERY
</div>
{query}
</pre>
</div>
</div>
</div>
<div className="w-full">
<div className="w-full mb-sm">
<div className="border border-gray-300 border-l-4 border-l-[#297393] shadow-md rounded bg-white p-md">
<div className="flex items-center">
<div className="flex w-3/4 items-center">
<div className="text-lg text-gray-700 ml-sm">
<span className="mr-xs" role="img" aria-label="rocket">
🚀
</span>
<b className="mr-sm">You&apos;re ready to go!</b>
Run your first sample query to get started.
</div>
</div>
<div className="flex w-1/4 justify-end">
<Button
mode="primary"
data-trackid="query-screen-get-started-button"
onClick={onRunHandler}
>
Run a Sample Query <FaPlayCircle />
</Button>
</div>
</div>
</div>
</div>
<div className="flex justify-start items-center w-full">
<a
className="w-auto text-secondary cursor-pointer text-sm hover:text-secondary-dark"
data-trackid="onboarding-skip-button"
onClick={() => {
onSkipHandler();
}}
>
Skip, continue to Console
</a>
</div>
</div>
</>
);
}

View File

@ -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 (
<QueryDialog
title="Welcome to Hasura"
description="Get started learning Hasura with an example."
query={sampleQuery}
<QueryScreen
schemaImage={schemaImagePath}
onRunHandler={onRunHandler}
onSkipHandler={onSkipHandler}
query={sampleQuery}
/>
);
}

View File

@ -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<typeof StepperNavbar>;
export const Base: Story = () => <StepperNavbar activeIndex={1} />;

View File

@ -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 (
<nav>
<ol className="border-t border-l border-r border-gray-300 rounded-t divide-y mb-0 divide-gray-300 md:flex md:divide-y-0 bg-white">
{steps.map((stepDetails, index) => (
<li className="relative flex-grow md:flex">
<div className="group flex items-center w-full">
<span className="px-md py-sm flex items-center">
<span
className={clsx(
`font-semibold text-sm`,
commmonListItemStyle,
index === currentActiveIndex
? `bg-[#f9c548] border-amber-500 text-gray-800`
: `border-gray-300 text-gray-500`
)}
>
{stepDetails.step}
</span>
<span className="ml-sm text-gray-900">{stepDetails.text}</span>
</span>
</div>
<div
className="md:block absolute top-0 right-0 h-full w-5"
aria-hidden="true"
>
{index !== lastStep && (
<CustomRightChevron className="h-full w-full text-gray-300" />
)}
</div>
</li>
))}
</ol>
</nav>
);
}

View File

@ -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';

View File

@ -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.";

View File

@ -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<WizardState>(
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,
};
}

View File

@ -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: {

View File

@ -14,13 +14,6 @@ export function HasuraFamiliaritySurvey(props: HasuraFamiliaritySurveyProps) {
return (
<>
<h1 className="text-xl font-semibold text-cloud-darkest">
Welcome to your new Hasura project!
</h1>
<p>
We&apos;d love to get to know you before you get started with your first
API.
</p>
<div className="mt-lg">
<div className="font-bold text-gray-600 mb-xs">{data.question}</div>
<div className="flex justify-center">

View File

@ -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({})}

View File

@ -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<LogoSize, string> = {
const logoModesStyles: Record<LogoModes, string> = {
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 (
<svg
className={logoSizing[size]}
@ -37,35 +42,35 @@ export function HasuraLogoFull(props: Props) {
<g clipPath="url(#clip0_5273_22707)">
<path
d="M81.771 28.7984C84.2262 21.3329 82.7493 6.43933 77.9917 0.940859C77.3682 0.219597 76.208 0.322635 75.6899 1.11883L69.8299 10.1018C68.3814 11.9003 65.7727 12.3125 63.8061 11.0573C57.4471 6.99198 49.8511 4.63147 41.6889 4.63147C33.5267 4.63147 25.9304 6.99198 19.5714 11.0573C17.6148 12.3125 15.006 11.8909 13.5481 10.1018L7.68787 1.11883C7.16996 0.322635 6.00938 0.228964 5.38597 0.940859C0.628701 6.44866 -0.848352 21.3422 1.60701 28.7984C2.42227 31.2807 2.64287 33.9035 2.16331 36.4513C1.68374 38.9804 1.20418 42.0341 1.20418 44.1603C1.20418 65.9951 19.3316 83.689 41.6793 83.689C64.0365 83.689 82.1542 65.9858 82.1542 44.1603C82.1542 42.0341 81.6749 38.9804 81.1955 36.4513C80.7254 33.9035 80.9553 31.2807 81.771 28.7984ZM41.6793 74.8658C24.3959 74.8658 10.3447 61.1334 10.3447 44.2541C10.3447 43.7014 10.3638 43.1581 10.3926 42.6148C11.016 31.1121 18.1711 21.2861 28.2899 16.6026C32.347 14.7104 36.8933 13.6613 41.6889 13.6613C46.4845 13.6613 51.0212 14.7104 55.088 16.6119C65.2066 21.2954 72.3616 31.1308 72.9851 42.6242C73.014 43.1675 73.0331 43.7201 73.0331 44.2634C73.0233 61.1334 58.9628 74.8658 41.6793 74.8658Z"
fill={logoModesStyles[mode]}
fill={logoFill}
/>
<path
d="M55.2591 56.0378L47.2505 42.4741L40.3832 31.1681C40.2201 30.8965 39.9228 30.7372 39.6063 30.7372H33.0459C32.7197 30.7372 32.4224 30.9058 32.2594 31.1869C32.0963 31.4585 32.1059 31.7957 32.269 32.0673L38.839 42.8488L30.0246 55.9816C29.8424 56.2533 29.8328 56.5996 29.9863 56.8806C30.1397 57.1615 30.4466 57.3398 30.7823 57.3398H37.3907C37.6976 57.3398 37.9854 57.1898 38.1484 56.9367L42.9153 49.668L47.1929 56.9089C47.356 57.1806 47.6533 57.3491 47.9699 57.3491H54.4823C54.8082 57.3491 55.1057 57.1806 55.2591 56.9089C55.4224 56.6466 55.4224 56.3095 55.2591 56.0378Z"
fill={logoModesStyles[mode]}
fill={logoFill}
/>
<path
d="M119.484 21.5306H127.838V66.2673H119.484V47.2056H110.046V66.2771H101.692V21.5306H110.046V40.9578H119.484V21.5306Z"
fill={logoModesStyles[mode]}
fill={textFill}
/>
<path
d="M153.61 66.276L151.864 56.9842H141.842L140.24 66.276H131.886L141.103 21.5391H152.353L162.012 66.276H153.61ZM142.935 50.811H150.704L146.714 29.398L142.935 50.811Z"
fill={logoModesStyles[mode]}
fill={textFill}
/>
<path
d="M180.619 58.1171V48.5442C180.619 47.7855 180.476 47.2797 180.188 47.0174C179.9 46.7551 179.363 46.624 178.586 46.624H172.707C167.719 46.624 165.226 44.2635 165.226 39.5331V28.555C165.226 23.8714 167.834 21.5391 173.061 21.5391H181.051C186.278 21.5391 188.887 23.8808 188.887 28.555V34.7934H180.466V29.6884C180.466 28.9296 180.322 28.4238 180.035 28.1615C179.747 27.8993 179.21 27.7682 178.433 27.7682H175.671C174.846 27.7682 174.289 27.8993 174.002 28.1615C173.714 28.4238 173.57 28.9296 173.57 29.6884V38.6901C173.57 39.4488 173.714 39.9546 174.002 40.2169C174.289 40.4792 174.846 40.6103 175.671 40.6103H181.406C186.489 40.6103 189.031 42.924 189.031 47.5607V59.2603C189.031 63.9436 186.394 66.276 181.118 66.276H173.273C167.998 66.276 165.36 63.9343 165.36 59.2603V53.0872H173.704V58.1171C173.704 58.8758 173.848 59.382 174.136 59.6443C174.424 59.9062 174.98 60.0376 175.805 60.0376H178.567C179.344 60.0376 179.871 59.9062 180.169 59.6443C180.466 59.382 180.619 58.8758 180.619 58.1171Z"
fill={logoModesStyles[mode]}
fill={textFill}
/>
<path
d="M211.32 21.5306H219.664V59.2516C219.664 63.9349 217.027 66.2673 211.752 66.2673H202.899C197.624 66.2673 194.986 63.9256 194.986 59.2516V21.5306H203.34V58.1182C203.34 58.8769 203.484 59.3826 203.772 59.6449C204.059 59.9073 204.597 60.0382 205.374 60.0382H209.219C210.044 60.0382 210.601 59.9073 210.888 59.6449C211.176 59.3826 211.32 58.8769 211.32 58.1182V21.5306Z"
fill={logoModesStyles[mode]}
fill={textFill}
/>
<path
d="M234.914 48.8355V66.2771H226.569V21.5306H243.412C248.687 21.5306 251.324 23.8723 251.324 28.5465V41.8102C251.324 45.6882 249.56 47.955 246.021 48.62L253.646 66.2771H244.64L237.667 48.8355H234.914ZM234.914 27.769V42.8031H240.937C241.714 42.8031 242.242 42.672 242.539 42.4097C242.827 42.1474 242.97 41.6416 242.97 40.8829V29.6893C242.97 28.9305 242.827 28.4247 242.539 28.1624C242.251 27.9001 241.714 27.769 240.937 27.769H234.914Z"
fill={logoModesStyles[mode]}
fill={textFill}
/>
<path
d="M276.589 66.276L274.843 56.9842H264.82L263.218 66.276H254.874L264.091 21.5391H275.342L285 66.276H276.589ZM265.923 50.811H273.692L269.702 29.398L265.923 50.811Z"
fill={logoModesStyles[mode]}
fill={textFill}
/>
</g>
<defs>