Skip to content

Commit

Permalink
feat(web): refactor onboarding (#5849)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidsoderberg authored Jun 27, 2024
1 parent cf94a96 commit 891d475
Show file tree
Hide file tree
Showing 7 changed files with 38 additions and 135 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@ import { IconHelpOutline, IconOutlineArrowBack } from '@novu/novui/icons';
import { HStack } from '@novu/novui/jsx';
import { hstack } from '@novu/novui/patterns';
import { FC } from 'react';
import { useNavigate } from 'react-router-dom';
import { discordInviteUrl } from '../../../pages/quick-start/consts';
import { DocsButton } from '../../docs/DocsButton';
import { useLocation, useNavigate } from 'react-router-dom';
import { ROUTES } from '../../../constants/routes';
import { HEADER_NAV_HEIGHT } from '../constants';
import { BridgeMenuItems } from './v2/BridgeMenuItems';

export const LocalStudioHeader: FC = () => {
const navigate = useNavigate();
const { pathname } = useLocation();

if (pathname.startsWith(ROUTES.STUDIO_ONBOARDING)) {
return null;
}

return (
<Header
Expand Down
20 changes: 1 addition & 19 deletions apps/web/src/pages/studio-onboarding/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,23 @@ import { text } from '@novu/novui/recipes';
import { HStack, styled, VStack } from '@novu/novui/jsx';
import { Tooltip } from '@novu/design-system';
import { IconOutlineMenuBook } from '@novu/novui/icons';
import { useLocation, useNavigate } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import { css } from '@novu/novui/css';
import { When } from '../../../components/utils/When';
import { DocsButton } from '../../../components/docs/DocsButton';
import { Button } from '@novu/novui';
import { useSegment } from '../../../components/providers/SegmentProvider';
import { ROUTES } from '../../../constants/routes';

const Text = styled('a', text);

export const Footer = ({
canSkipSetup = true,
showLearnMore = true,
buttonText = 'Continue',
onClick,
loading = false,
tooltip = '',
disabled = false,
}: {
canSkipSetup?: boolean;
showLearnMore?: boolean;
buttonText?: string;
onClick?: () => void;
Expand All @@ -30,7 +27,6 @@ export const Footer = ({
disabled?: boolean;
}) => {
const segment = useSegment();
const navigate = useNavigate();
const { pathname } = useLocation();

return (
Expand Down Expand Up @@ -77,20 +73,6 @@ export const Footer = ({
</When>
</div>
<HStack gap="100">
<When truthy={canSkipSetup}>
<Button
disabled={loading}
onClick={() => {
segment.track('Skip setup button clicked - [Onboarding - Signup]', {
step: pathname,
});
navigate(ROUTES.STUDIO_FLOWS);
}}
variant="outline"
>
Skip setup
</Button>
</When>
<Tooltip label={tooltip} disabled={!tooltip}>
<Button loading={loading} onClick={onClick} disabled={disabled}>
{buttonText}
Expand Down
64 changes: 12 additions & 52 deletions apps/web/src/pages/studio-onboarding/components/SetupTimeline.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { CodeSnippet } from '../../get-started/components/CodeSnippet';
import { Loader, Timeline as MantineTimeline } from '@mantine/core';
import { useEffect, useMemo, useState } from 'react';
import { Input } from '@novu/design-system';
import { IconCheck, IconCancel } from '@novu/novui/icons';
import { IconCheck } from '@novu/novui/icons';
import { useQuery } from '@tanstack/react-query';
import { getApiKeys } from '../../../api/environment';
import { Text } from '@novu/novui';
Expand All @@ -17,29 +16,22 @@ const Icon = () => (
/>
);

export const SetupTimeline = ({
error,
url,
setUrl,
testResponse,
retest,
}: {
error: string;
url: string;
setUrl: (url: string) => void;
testResponse: { isLoading: boolean; data: { status: string } };
retest: () => void;
}) => {
export const SetupTimeline = ({ testResponse }: { testResponse: { isLoading: boolean; data: { status: string } } }) => {
const { data: apiKeys = [] } = useQuery<{ key: string }[]>(['getApiKeys'], getApiKeys);
const key = useMemo(() => apiKeys[0]?.key, [apiKeys]);
const [active, setActive] = useState(0);

useEffect(() => {
if (testResponse?.isLoading || testResponse?.data?.status !== 'ok') {
return;
}
setActive(3);
}, [testResponse?.data?.status, testResponse?.isLoading]);

function CheckStatusIcon() {
return (
<>
{testResponse?.isLoading ? <Loader size={16} color={'indigo'} /> : null}
{testResponse?.data?.status === 'ok' ? <IconCheck color="green" /> : null}
{url && !testResponse?.isLoading && testResponse?.data?.status !== 'ok' ? <IconCancel color="#f45c5c" /> : null}
{testResponse?.isLoading || testResponse?.data?.status !== 'ok' ? <Loader size={16} color="white" /> : <Icon />}
</>
);
}
Expand Down Expand Up @@ -76,46 +68,14 @@ export const SetupTimeline = ({
/>
</MantineTimeline.Item>
<MantineTimeline.Item
bullet={active >= 2 ? <Icon /> : 2}
lineVariant="dashed"
title="Start a localtunnel to your application"
active={active >= 2}
>
<CodeSnippet
command={'npx localtunnel --port=4000'}
onClick={() => {
setActive((old) => (old > 2 ? old : 2));
}}
/>
</MantineTimeline.Item>
<MantineTimeline.Item
bullet={active >= 3 ? <Icon /> : 3}
bullet={<CheckStatusIcon />}
lineVariant="dashed"
title="Connect to the endpoint"
active={active >= 3}
>
<Text variant="main" color="typography.text.secondary">
Enter the Novu Endpoint URL generated by the CLI.
{active < 3 ? 'Waiting for you to start the application' : 'Succefully connected to the Novu Endpoint'}
</Text>
<Input
rightSection={<CheckStatusIcon />}
type="url"
placeholder={'For Example: https://cold-dryers-leave.loca.lt/api/novu'}
onChange={(e) => {
setUrl(e.target.value);
}}
value={url}
error={
(url && error) || (url && !testResponse.isLoading && testResponse.data?.status !== 'ok') ? (
<>
Could not locate a valid Novu Endpoint.{' '}
<a style={{ textDecoration: 'underline', cursor: 'pointer' }} onClick={() => retest()}>
Try Again
</a>
</>
) : null
}
/>
</MantineTimeline.Item>
</Timeline>
);
Expand Down
36 changes: 12 additions & 24 deletions apps/web/src/pages/studio-onboarding/index.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,32 @@
import { css } from '@novu/novui/css';
import { Footer } from './components/Footer';
import { Header } from './components/Header';
import { useEffect, useState } from 'react';
import { useEnvironment } from '../../hooks/useEnvironment';
import { useEffect } from 'react';
import { Title, Text } from '@novu/novui';
import { VStack } from '@novu/novui/jsx';
import { SetupTimeline } from './components/SetupTimeline';
import { useSetupBridge } from './useSetupBridge';
import { useSegment } from '../../components/providers/SegmentProvider';
import { Wrapper } from './components/Wrapper';
import { ROUTES } from '../../constants/routes';
import { useNavigate } from 'react-router-dom';
import { useBridgeUrlTest } from './useUrlTest';

export const StudioOnboarding = () => {
const [url, setUrl] = useState('');
const [error, setError] = useState<string>('');
const { loading, setup, testEndpoint, testResponse } = useSetupBridge(url, setError);
const segment = useSegment();
const navigate = useNavigate();
const { runHealthCheck, isLoading, data } = useBridgeUrlTest();

useEffect(() => {
const delayDebounceFn = setTimeout(() => testEndpoint(url), 500);
const delayDebounceFn = setTimeout(() => runHealthCheck(), 500);

return () => clearTimeout(delayDebounceFn);
}, [url, testEndpoint]);
}, [runHealthCheck]);

useEffect(() => {
segment.track('Add endpoint step started - [Onboarding - Signup]');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

async function retest(continueSetup = false) {
const result = await testEndpoint(url);

if (continueSetup && result.data?.status === 'ok') {
await setup();
}
}

return (
<Wrapper>
<Header />
Expand All @@ -56,19 +48,15 @@ export const StudioOnboarding = () => {
To start sending your first workflows, you first need to connect Novu to your Bridge Endpoint. This setup
will create a sample Next.js project and pre-configured the @novu/framework client for you.
</Text>
<SetupTimeline error={error} url={url} setUrl={setUrl} testResponse={testResponse} retest={retest} />
<SetupTimeline testResponse={{ data, isLoading }} />
</div>
</VStack>
<Footer
disabled={loading || !url}
disabled={data?.status !== 'ok'}
onClick={async () => {
if (testResponse.data?.status === 'ok') {
setup();
} else {
retest(true);
}
navigate(ROUTES.STUDIO_ONBOARDING_PREVIEW);
}}
loading={loading}
loading={isLoading}
/>
</Wrapper>
);
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/pages/studio-onboarding/success.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ export const StudioOnboardingSuccess = () => {
segment.track('Workflows page accessed - [Onboarding - Signup]');
navigate(ROUTES.STUDIO_FLOWS);
}}
canSkipSetup={false}
buttonText="Explore workflows"
showLearnMore={false}
/>
Expand Down
34 changes: 0 additions & 34 deletions apps/web/src/pages/studio-onboarding/useSetupBridge.ts

This file was deleted.

10 changes: 6 additions & 4 deletions apps/web/src/pages/studio-onboarding/useUrlTest.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { useMutation } from '@tanstack/react-query';
import { useStudioState } from '../../studio/StudioStateProvider';

export function useBridgeUrlTest() {
const { bridgeURL } = useStudioState();

const {
isLoading,
mutateAsync: runHealthCheck,

data,
} = useMutation(async (url: string) => {
} = useMutation(async () => {
try {
new URL(url);
new URL(bridgeURL);
} catch (e) {
throw new Error('The provided URL is invalid');
}

try {
const response = await fetch(url + '?action=health-check', {
const response = await fetch(bridgeURL + '?action=health-check', {
headers: {
'Bypass-Tunnel-Reminder': 'true',
},
Expand Down

0 comments on commit 891d475

Please sign in to comment.