Skip to content

Commit 4833c25

Browse files
committed
feat: Onboarding Welcome Page
1 parent 5f5d39b commit 4833c25

7 files changed

Lines changed: 159 additions & 11 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { cleanup, render, screen } from "@testing-library/react";
2+
import { afterEach, describe, expect, it, vi } from "vitest";
3+
4+
import { IndexRedirect } from "./IndexRedirect";
5+
6+
vi.mock("@tanstack/react-router", () => ({
7+
Navigate: ({ to }: { to: string }) => <div data-testid="navigate">{to}</div>,
8+
}));
9+
10+
let onboarding = { isReady: true, isComplete: false, dismissed: false };
11+
vi.mock("@/providers/OnboardingProvider/OnboardingProvider", () => ({
12+
useOnboarding: () => onboarding,
13+
}));
14+
15+
afterEach(cleanup);
16+
17+
const target = () => screen.queryByTestId("navigate");
18+
19+
describe("IndexRedirect", () => {
20+
it("waits (no redirect) until onboarding state is ready", () => {
21+
onboarding = { isReady: false, isComplete: false, dismissed: false };
22+
render(<IndexRedirect />);
23+
expect(target()).toBeNull();
24+
});
25+
26+
it("redirects to /welcome while onboarding is active", () => {
27+
onboarding = { isReady: true, isComplete: false, dismissed: false };
28+
render(<IndexRedirect />);
29+
expect(target()).toHaveTextContent("/welcome");
30+
});
31+
32+
it("redirects to /dashboard once complete", () => {
33+
onboarding = { isReady: true, isComplete: true, dismissed: false };
34+
render(<IndexRedirect />);
35+
expect(target()).toHaveTextContent("/dashboard");
36+
});
37+
38+
it("redirects to /dashboard once dismissed", () => {
39+
onboarding = { isReady: true, isComplete: false, dismissed: true };
40+
render(<IndexRedirect />);
41+
expect(target()).toHaveTextContent("/dashboard");
42+
});
43+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Navigate } from "@tanstack/react-router";
2+
3+
import { BlockStack } from "@/components/ui/layout";
4+
import { Spinner } from "@/components/ui/spinner";
5+
import { useOnboarding } from "@/providers/OnboardingProvider/OnboardingProvider";
6+
import { APP_ROUTES } from "@/routes/appRoutes";
7+
8+
export function IndexRedirect() {
9+
const { isReady, isComplete, dismissed } = useOnboarding();
10+
11+
if (!isReady) {
12+
return (
13+
<BlockStack align="center" inlineAlign="center" className="h-full">
14+
<Spinner />
15+
</BlockStack>
16+
);
17+
}
18+
19+
const showOnboarding = !isComplete && !dismissed;
20+
return (
21+
<Navigate
22+
replace
23+
to={showOnboarding ? APP_ROUTES.WELCOME : APP_ROUTES.DASHBOARD}
24+
/>
25+
);
26+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Link, Navigate } from "@tanstack/react-router";
2+
3+
import { OnboardingHero } from "@/components/Learn/OnboardingHero";
4+
import { BlockStack } from "@/components/ui/layout";
5+
import { useOnboarding } from "@/providers/OnboardingProvider/OnboardingProvider";
6+
import { APP_ROUTES } from "@/routes/router";
7+
import { tracking } from "@/utils/tracking";
8+
9+
export function OnboardingWelcome() {
10+
const { isReady, isComplete, dismissed } = useOnboarding();
11+
12+
if (isReady && (isComplete || dismissed)) {
13+
return <Navigate to={APP_ROUTES.DASHBOARD} replace />;
14+
}
15+
16+
return (
17+
<BlockStack
18+
gap="4"
19+
align="center"
20+
inlineAlign="center"
21+
className="h-full w-full"
22+
>
23+
<div className="w-full max-w-2xl">
24+
<OnboardingHero />
25+
</div>
26+
<Link
27+
to={APP_ROUTES.LEARN}
28+
className="text-sm text-muted-foreground hover:text-foreground"
29+
{...tracking("homepage.onboarding.learning_hub")}
30+
>
31+
Explore the Learning Hub →
32+
</Link>
33+
</BlockStack>
34+
);
35+
}

src/routes/Dashboard/DashboardLayout.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { BlockStack, InlineStack } from "@/components/ui/layout";
99
import { Link as UILink } from "@/components/ui/link";
1010
import { Text } from "@/components/ui/typography";
1111
import { cn } from "@/lib/utils";
12+
import { useOnboarding } from "@/providers/OnboardingProvider/OnboardingProvider";
1213
import { APP_ROUTES } from "@/routes/appRoutes";
1314
import {
1415
ABOUT_URL,
@@ -28,7 +29,12 @@ interface SidebarItem {
2829
}
2930

3031
const BASE_SIDEBAR_ITEMS: SidebarItem[] = [
31-
{ to: "/", label: "My Dashboard", icon: "LayoutDashboard", exact: true },
32+
{
33+
to: APP_ROUTES.DASHBOARD,
34+
label: "My Dashboard",
35+
icon: "LayoutDashboard",
36+
exact: true,
37+
},
3238
{ to: "/pipelines", label: "My Pipelines", icon: "GitBranch" },
3339
{ to: "/runs", label: "All Runs", icon: "Play" },
3440
{ to: "/components", label: "Components", icon: "Package" },
@@ -53,14 +59,29 @@ export function DashboardLayout() {
5359
const requiresAuthorization = isAuthorizationRequired();
5460
const isComponentSearchEnabled = useFlagValue("component-search-v2");
5561

56-
const sidebarItems = isComponentSearchEnabled
62+
const { isComplete, dismissed, isResolved } = useOnboarding();
63+
const showOnboarding = isResolved && !isComplete && !dismissed;
64+
65+
const baseItems = isComponentSearchEnabled
5766
? BASE_SIDEBAR_ITEMS.map((item) =>
5867
item.to === APP_ROUTES.DASHBOARD_COMPONENTS
5968
? COMPONENT_SEARCH_ITEM
6069
: item,
6170
)
6271
: BASE_SIDEBAR_ITEMS;
6372

73+
const sidebarItems: SidebarItem[] = showOnboarding
74+
? [
75+
{
76+
to: APP_ROUTES.WELCOME,
77+
label: "Get started",
78+
icon: "Rocket",
79+
exact: true,
80+
},
81+
...baseItems,
82+
]
83+
: baseItems;
84+
6485
return (
6586
<div
6687
className="flex w-full overflow-hidden"

src/routes/appRoutes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ const TOUR_BASE_PATH = "/tour";
99

1010
export const APP_ROUTES = {
1111
HOME: "/",
12-
DASHBOARD: "/",
12+
DASHBOARD: "/dashboard",
13+
WELCOME: "/welcome",
1314
DASHBOARD_RUNS: "/runs",
1415
DASHBOARD_PIPELINES: "/pipelines",
1516
DASHBOARD_COMPONENTS: "/components",

src/routes/router.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
redirect,
88
} from "@tanstack/react-router";
99

10+
import { IndexRedirect } from "@/components/Onboarding/IndexRedirect";
11+
import { OnboardingWelcome } from "@/components/Onboarding/OnboardingWelcome";
1012
import { ErrorPage } from "@/components/shared/ErrorPage";
1113
import { AuthorizationResultScreen as GitHubAuthorizationResultScreen } from "@/components/shared/GitHubAuth/AuthorizationResultScreen";
1214
import { AuthorizationResultScreen as HuggingFaceAuthorizationResultScreen } from "@/components/shared/HuggingFaceAuth/AuthorizationResultScreen";
@@ -79,9 +81,21 @@ const dashboardRoute = createRoute({
7981
const dashboardIndexRoute = createRoute({
8082
getParentRoute: () => dashboardRoute,
8183
path: "/",
84+
component: IndexRedirect,
85+
});
86+
87+
const dashboardHomeRoute = createRoute({
88+
getParentRoute: () => dashboardRoute,
89+
path: APP_ROUTES.DASHBOARD,
8290
component: DashboardHomeView,
8391
});
8492

93+
const welcomeRoute = createRoute({
94+
getParentRoute: () => dashboardRoute,
95+
path: APP_ROUTES.WELCOME,
96+
component: OnboardingWelcome,
97+
});
98+
8599
const dashboardRunsRoute = createRoute({
86100
getParentRoute: () => dashboardRoute,
87101
path: "/runs",
@@ -329,6 +343,8 @@ const artifactPreviewRoute = createRoute({
329343

330344
const dashboardRouteTree = dashboardRoute.addChildren([
331345
dashboardIndexRoute,
346+
dashboardHomeRoute,
347+
welcomeRoute,
332348
dashboardRunsRoute,
333349
dashboardPipelinesRoute,
334350
dashboardComponentsRoute,

tests/e2e/navigation-tracking.spec.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,25 @@ test.describe("Navigation tracking", () => {
3131

3232
expect(events.length).toBeGreaterThanOrEqual(2);
3333

34-
const [initial, navigation] = events;
35-
36-
// First page_view from landing on /
37-
expect(initial.actionType).toBe("page_view");
38-
expect(initial.metadata).toMatchObject({
34+
// Landing on / emits a page_view (/ then redirects to the welcome/dashboard
35+
// route, which emits its own page_view — so we match by route, not position).
36+
const landing = events.find(
37+
(e: { metadata?: { to?: string } }) => e.metadata?.to === "/",
38+
);
39+
expect(landing).toBeTruthy();
40+
expect(landing.metadata).toMatchObject({
3941
to: "/",
4042
route_pattern: expect.any(String),
4143
});
4244

43-
// Second page_view from navigating to settings
44-
expect(navigation.actionType).toBe("page_view");
45+
// Navigating to settings emits a page_view.
46+
const navigation = events.find(
47+
(e: { metadata?: { to?: string } }) =>
48+
typeof e.metadata?.to === "string" &&
49+
e.metadata.to.includes("/settings"),
50+
);
51+
expect(navigation).toBeTruthy();
4552
expect(navigation.metadata).toMatchObject({
46-
from: "/",
4753
to: expect.stringContaining("/settings"),
4854
route_pattern: expect.any(String),
4955
});

0 commit comments

Comments
 (0)