Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions studio/src/components/layout/onboarding-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
export interface OnboardingLayoutProps {
children?: React.ReactNode;
}
import { Logo } from '../logo';
import { Card, CardContent } from '../ui/card';
import { Stepper } from '../onboarding/stepper';
import { ONBOARDING_STEPS } from '../onboarding/onboarding-steps';
import { useOnboarding } from '@/hooks/use-onboarding';

export const OnboardingLayout = ({ children, title }: { children?: React.ReactNode; title?: string }) => {
const { currentStep } = useOnboarding();

export const OnboardingLayout = ({ children }: OnboardingLayoutProps) => {
return (
<div className="flex min-h-screen w-full flex-col items-center justify-center bg-background font-sans antialiased">
<main className="w-full max-w-lg px-4">{children}</main>
<div className="flex min-h-screen w-full flex-col bg-background font-sans antialiased">
<header className="mx-auto flex w-full max-w-2xl items-center gap-3 py-6">
<Logo width={32} height={32} />
{title && <h1 className="text-lg font-semibold tracking-tight">{title}</h1>}
<Stepper steps={ONBOARDING_STEPS} currentStep={(currentStep ?? 1) - 1} className="ml-auto" />
</header>
<main className="w-full flex-1 px-6 pb-4 pt-12">
<Card className="mx-auto w-full max-w-2xl">
<CardContent className="flex min-h-[788px] flex-col p-6">{children}</CardContent>
</Card>
</main>
</div>
);
};
3 changes: 3 additions & 0 deletions studio/src/components/onboarding/onboarding-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const OnboardingContainer = ({ children }: { children: React.ReactNode }) => {
return <div className="flex flex-1 flex-col items-center gap-4 text-center">{children}</div>;
};
71 changes: 71 additions & 0 deletions studio/src/components/onboarding/onboarding-navigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ArrowLeftIcon, ArrowRightIcon, InfoCircledIcon } from '@radix-ui/react-icons';
import { Link } from '../ui/link';
import { Button } from '../ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';

export const OnboardingNavigation = ({
backHref,
forward,
forwardLabel = 'Next',
onSkip,
}: {
backHref?: string;
forward: { href: string } | { onClick: () => void; isLoading?: boolean; disabled?: boolean };
forwardLabel?: string;
onSkip: () => void;
}) => {
return (
<div className="mt-auto flex w-full justify-between pt-8">
<div className="flex items-center gap-1">
<Button asChild variant="outline" onClick={onSkip}>
<Link href="/">Skip</Link>
</Button>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<button
type="button"
aria-label="Open onboarding tooltip"
className="rounded-sm border-0 bg-transparent p-0 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
<InfoCircledIcon className="ml-2 size-3.5 text-muted-foreground" />
</button>
</TooltipTrigger>
<TooltipContent>You can always get back to this wizard from the application. Safe to skip.</TooltipContent>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</Tooltip>
</div>
<div className="flex gap-2">
{backHref ? (
<Button className="group" asChild variant="outline">
<Link href={backHref}>
<ArrowLeftIcon className="mr-2 transition-transform group-hover:-translate-x-1" />
Back
</Link>
</Button>
) : (
<Button variant="outline" disabled>
<ArrowLeftIcon className="mr-2" />
Back
</Button>
)}
{'href' in forward ? (
<Button className="group" asChild>
<Link href={forward.href}>
{forwardLabel}
<ArrowRightIcon className="ml-2 transition-transform group-hover:translate-x-1" />
</Link>
</Button>
) : (
<Button
className="group"
onClick={forward.onClick}
isLoading={forward.isLoading}
disabled={forward.isLoading || forward.disabled}
>
{forwardLabel}
<ArrowRightIcon className="ml-2 transition-transform group-hover:translate-x-1" />
</Button>
)}
</div>
</div>
);
};
2 changes: 2 additions & 0 deletions studio/src/components/onboarding/onboarding-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { useSessionStorage } from '@/hooks/use-session-storage';
type Onboarding = {
finishedAt?: Date;
federatedGraphsCount: number;
slack: boolean;
email: boolean;
};

export interface OnboardingState {
Expand Down
10 changes: 10 additions & 0 deletions studio/src/components/onboarding/onboarding-steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface StepperStep {
number: number;
label: string;
}

export const ONBOARDING_STEPS: StepperStep[] = [
{ number: 1, label: 'Get started with WunderGraph' },
{ number: 2, label: 'Create your first graph' },
{ number: 3, label: 'Run your services' },
];
150 changes: 123 additions & 27 deletions studio/src/components/onboarding/step-1.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,50 @@
import { useEffect } from 'react';
import { Link } from '../ui/link';
import { Button } from '../ui/button';
import { useOnboarding } from '@/hooks/use-onboarding';
import { OnboardingContainer } from './onboarding-container';
import { OnboardingNavigation } from './onboarding-navigation';
import { useMutation } from '@connectrpc/connect-query';
import { createOnboarding } from '@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery';
import { useRouter } from 'next/router';
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
import { useToast } from '../ui/use-toast';
import { SubmitHandler, useZodForm } from '@/hooks/use-form';
import { Controller } from 'react-hook-form';
import { z } from 'zod';
import { Form } from '../ui/form';
import { Checkbox } from '../ui/checkbox';
import { TrafficAnimation } from './traffic-animation';

const onboardingSchema = z.object({
channels: z.object({
slack: z.boolean(),
email: z.boolean(),
}),
});

type OnboardingFormValues = z.infer<typeof onboardingSchema>;

const WhyListItem = ({ title, text }: { title: string; text: string }) => (
<li className="flex gap-2">
<span className="mt-2 size-1.5 shrink-0 rounded-full bg-muted-foreground/60" />
<div className="flex flex-col">
<span className="text-sm font-medium">{title}</span>
<span className="text-sm text-muted-foreground">{text}</span>
</div>
</li>
);

export const Step1 = () => {
const router = useRouter();
const { toast } = useToast();
const { setStep, setSkipped, setOnboarding } = useOnboarding();
const { setStep, setSkipped, setOnboarding, onboarding } = useOnboarding();

const form = useZodForm<OnboardingFormValues>({
mode: 'onChange',
schema: onboardingSchema,
defaultValues: {
channels: onboarding,
},
Comment thread
comatory marked this conversation as resolved.
});

const { mutate, isPending } = useMutation(createOnboarding, {
onSuccess: (d) => {
Expand All @@ -23,9 +56,12 @@ export const Step1 = () => {
return;
}

const formValues = form.getValues();
setOnboarding({
federatedGraphsCount: d.federatedGraphsCount,
finishedAt: d.finishedAt ? new Date(d.finishedAt) : undefined,
slack: formValues.channels.slack,
email: formValues.channels.email,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
router.push('/onboarding/2');
},
Expand All @@ -37,36 +73,96 @@ export const Step1 = () => {
},
});

const onSubmit: SubmitHandler<OnboardingFormValues> = (data) => {
mutate(data.channels);
};

useEffect(() => {
setStep(1);
}, [setStep]);

return (
<div className="flex flex-col items-center gap-4 text-center">
<h2 className="text-2xl font-semibold tracking-tight">Step 1</h2>
<div className="flex w-full justify-between">
<Button asChild variant="secondary" onClick={setSkipped}>
<Link href="/">Skip</Link>
</Button>
<div className="flex">
<Button className="mr-2" asChild disabled>
<Link href="#">Back</Link>
</Button>
<Button
onClick={() => {
// TODO: replace with real values in form
mutate({
slack: true,
email: false,
});
}}
isLoading={isPending}
disabled={isPending}
>
Next
</Button>
<OnboardingContainer>
<div className="flex w-full flex-col gap-8 text-left">
<div className="space-y-2">
<p className="text-sm text-muted-foreground">
In ~<span className="font-medium text-foreground">3 minutes</span> you will have a federated GraphQL graph
running locally and serving live traffic into Cosmo Cloud platform.
</p>
</div>

<TrafficAnimation />

<div className="space-y-3">
<p className="text-sm font-semibold">What you will do</p>
<ul className="flex flex-col gap-3">
<WhyListItem
title="Create your first graph"
text="See how the products and reviews subgraphs compose into one supergraph, giving your client a single endpoint to resolve the data it needs."
/>
<WhyListItem
title="Run your services"
text="Run the same router stack you would run in production, locally."
/>
<WhyListItem title="Send a query" text="Watch real request metrics flow through the router." />
</ul>
</div>

<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-full" aria-busy={isPending}>
<div className="rounded-md border border-dashed p-4">
<p className="text-sm font-medium">If you get stuck, how can we reach you?</p>
<div className="mt-3 flex flex-col gap-3">
<Controller
control={form.control}
name="channels.slack"
render={({ field }) => (
<label className="flex items-start gap-3">
<Checkbox
checked={field.value}
disabled={isPending}
onCheckedChange={(checked) => field.onChange(checked === true)}
/>
<div className="flex flex-col gap-y-1">
<span className="text-sm font-medium leading-none">Slack</span>
<span className="text-[0.8rem] text-muted-foreground">
We automatically create a Slack channel for you.
</span>
</div>
</label>
)}
/>
<Controller
control={form.control}
name="channels.email"
render={({ field }) => (
<label className="flex items-start gap-3">
<Checkbox
checked={field.value}
disabled={isPending}
onCheckedChange={(checked) => field.onChange(checked === true)}
/>
<div className="flex flex-col gap-y-1">
<span className="text-sm font-medium leading-none">Email</span>
<span className="text-[0.8rem] text-muted-foreground">Receive updates via email.</span>
</div>
</label>
)}
/>
</div>
</div>
</form>
</Form>
</div>
</div>

<OnboardingNavigation
onSkip={setSkipped}
forwardLabel="Start the tour"
forward={{
onClick: form.handleSubmit(onSubmit),
isLoading: isPending,
}}
/>
</OnboardingContainer>
);
};
22 changes: 5 additions & 17 deletions studio/src/components/onboarding/step-2.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { Link } from '../ui/link';
import { Button } from '../ui/button';
import { useOnboarding } from '@/hooks/use-onboarding';
import { OnboardingContainer } from './onboarding-container';
import { OnboardingNavigation } from './onboarding-navigation';

export const Step2 = () => {
const { setStep, setSkipped } = useOnboarding();
Expand All @@ -11,21 +11,9 @@ export const Step2 = () => {
}, [setStep]);

return (
<div className="flex flex-col items-center gap-4 text-center">
<OnboardingContainer>
<h2 className="text-2xl font-semibold tracking-tight">Step 2</h2>
<div className="flex w-full justify-between">
<Button asChild variant="secondary" onClick={setSkipped}>
<Link href="/">Skip</Link>
</Button>
<div className="flex">
<Button className="mr-2" asChild>
<Link href="/onboarding/1">Back</Link>
</Button>
<Button asChild>
<Link href="/onboarding/3">Next</Link>
</Button>
</div>
</div>
</div>
<OnboardingNavigation onSkip={setSkipped} backHref="/onboarding/1" forward={{ href: '/onboarding/3' }} />
</OnboardingContainer>
);
};
Loading
Loading