Skip to content
Draft
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
43 changes: 43 additions & 0 deletions src/components/Onboarding/IndexRedirect.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";

import { IndexRedirect } from "./IndexRedirect";

vi.mock("@tanstack/react-router", () => ({
Navigate: ({ to }: { to: string }) => <div data-testid="navigate">{to}</div>,
}));

let onboarding = { isReady: true, isComplete: false, dismissed: false };
vi.mock("@/providers/OnboardingProvider/OnboardingProvider", () => ({
useOnboarding: () => onboarding,
}));

afterEach(cleanup);

const target = () => screen.queryByTestId("navigate");

describe("IndexRedirect", () => {
it("waits (no redirect) until onboarding state is ready", () => {
onboarding = { isReady: false, isComplete: false, dismissed: false };
render(<IndexRedirect />);
expect(target()).toBeNull();
});

it("redirects to /welcome while onboarding is active", () => {
onboarding = { isReady: true, isComplete: false, dismissed: false };
render(<IndexRedirect />);
expect(target()).toHaveTextContent("/welcome");
});

it("redirects to /dashboard once complete", () => {
onboarding = { isReady: true, isComplete: true, dismissed: false };
render(<IndexRedirect />);
expect(target()).toHaveTextContent("/dashboard");
});

it("redirects to /dashboard once dismissed", () => {
onboarding = { isReady: true, isComplete: false, dismissed: true };
render(<IndexRedirect />);
expect(target()).toHaveTextContent("/dashboard");
});
});
26 changes: 26 additions & 0 deletions src/components/Onboarding/IndexRedirect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Navigate } from "@tanstack/react-router";

import { BlockStack } from "@/components/ui/layout";
import { Spinner } from "@/components/ui/spinner";
import { useOnboarding } from "@/providers/OnboardingProvider/OnboardingProvider";
import { APP_ROUTES } from "@/routes/appRoutes";

export function IndexRedirect() {
const { isReady, isComplete, dismissed } = useOnboarding();

if (!isReady) {
return (
<BlockStack align="center" inlineAlign="center" className="h-full">
<Spinner />
</BlockStack>
);
}

const showOnboarding = !isComplete && !dismissed;
return (
<Navigate
replace
to={showOnboarding ? APP_ROUTES.WELCOME : APP_ROUTES.DASHBOARD}
/>
);
}
35 changes: 35 additions & 0 deletions src/components/Onboarding/OnboardingWelcome.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Link, Navigate } from "@tanstack/react-router";

import { OnboardingHero } from "@/components/Learn/OnboardingHero";
import { BlockStack } from "@/components/ui/layout";
import { useOnboarding } from "@/providers/OnboardingProvider/OnboardingProvider";
import { APP_ROUTES } from "@/routes/router";
import { tracking } from "@/utils/tracking";

export function OnboardingWelcome() {
const { isReady, isComplete, dismissed } = useOnboarding();

if (isReady && (isComplete || dismissed)) {
return <Navigate to={APP_ROUTES.DASHBOARD} replace />;
}

return (
<BlockStack
gap="4"
align="center"
inlineAlign="center"
className="h-full w-full"
>
<div className="w-full max-w-2xl">
<OnboardingHero />
</div>
<Link
to={APP_ROUTES.LEARN}
className="text-sm text-muted-foreground hover:text-foreground"
{...tracking("homepage.onboarding.learning_hub")}
>
Explore the Learning Hub →
</Link>
</BlockStack>
);
}
25 changes: 23 additions & 2 deletions src/routes/Dashboard/DashboardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { BlockStack, InlineStack } from "@/components/ui/layout";
import { Link as UILink } from "@/components/ui/link";
import { Text } from "@/components/ui/typography";
import { cn } from "@/lib/utils";
import { useOnboarding } from "@/providers/OnboardingProvider/OnboardingProvider";
import { APP_ROUTES } from "@/routes/appRoutes";
import {
ABOUT_URL,
Expand All @@ -28,7 +29,12 @@ interface SidebarItem {
}

const BASE_SIDEBAR_ITEMS: SidebarItem[] = [
{ to: "/", label: "My Dashboard", icon: "LayoutDashboard", exact: true },
{
to: APP_ROUTES.DASHBOARD,
label: "My Dashboard",
icon: "LayoutDashboard",
exact: true,
},
{ to: "/pipelines", label: "My Pipelines", icon: "GitBranch" },
{ to: "/runs", label: "All Runs", icon: "Play" },
{ to: "/components", label: "Components", icon: "Package" },
Expand All @@ -53,14 +59,29 @@ export function DashboardLayout() {
const requiresAuthorization = isAuthorizationRequired();
const isComponentSearchEnabled = useFlagValue("component-search-v2");

const sidebarItems = isComponentSearchEnabled
const { isComplete, dismissed, isResolved } = useOnboarding();
const showOnboarding = isResolved && !isComplete && !dismissed;

const baseItems = isComponentSearchEnabled
? BASE_SIDEBAR_ITEMS.map((item) =>
item.to === APP_ROUTES.DASHBOARD_COMPONENTS
? COMPONENT_SEARCH_ITEM
: item,
)
: BASE_SIDEBAR_ITEMS;

const sidebarItems: SidebarItem[] = showOnboarding
? [
{
to: APP_ROUTES.WELCOME,
label: "Get started",
icon: "Rocket",
exact: true,
},
...baseItems,
]
: baseItems;

return (
<div
className="flex w-full overflow-hidden"
Expand Down
3 changes: 2 additions & 1 deletion src/routes/appRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const TOUR_BASE_PATH = "/tour";

export const APP_ROUTES = {
HOME: "/",
DASHBOARD: "/",
DASHBOARD: "/dashboard",
WELCOME: "/welcome",
DASHBOARD_RUNS: "/runs",
DASHBOARD_PIPELINES: "/pipelines",
DASHBOARD_COMPONENTS: "/components",
Expand Down
16 changes: 16 additions & 0 deletions src/routes/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
redirect,
} from "@tanstack/react-router";

import { IndexRedirect } from "@/components/Onboarding/IndexRedirect";
import { OnboardingWelcome } from "@/components/Onboarding/OnboardingWelcome";
import { ErrorPage } from "@/components/shared/ErrorPage";
import { AuthorizationResultScreen as GitHubAuthorizationResultScreen } from "@/components/shared/GitHubAuth/AuthorizationResultScreen";
import { AuthorizationResultScreen as HuggingFaceAuthorizationResultScreen } from "@/components/shared/HuggingFaceAuth/AuthorizationResultScreen";
Expand Down Expand Up @@ -79,9 +81,21 @@ const dashboardRoute = createRoute({
const dashboardIndexRoute = createRoute({
getParentRoute: () => dashboardRoute,
path: "/",
component: IndexRedirect,
});

const dashboardHomeRoute = createRoute({
getParentRoute: () => dashboardRoute,
path: APP_ROUTES.DASHBOARD,
component: DashboardHomeView,
});

const welcomeRoute = createRoute({
getParentRoute: () => dashboardRoute,
path: APP_ROUTES.WELCOME,
component: OnboardingWelcome,
});

const dashboardRunsRoute = createRoute({
getParentRoute: () => dashboardRoute,
path: "/runs",
Expand Down Expand Up @@ -329,6 +343,8 @@ const artifactPreviewRoute = createRoute({

const dashboardRouteTree = dashboardRoute.addChildren([
dashboardIndexRoute,
dashboardHomeRoute,
welcomeRoute,
dashboardRunsRoute,
dashboardPipelinesRoute,
dashboardComponentsRoute,
Expand Down
22 changes: 14 additions & 8 deletions tests/e2e/navigation-tracking.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,25 @@ test.describe("Navigation tracking", () => {

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

const [initial, navigation] = events;

// First page_view from landing on /
expect(initial.actionType).toBe("page_view");
expect(initial.metadata).toMatchObject({
// Landing on / emits a page_view (/ then redirects to the welcome/dashboard
// route, which emits its own page_view — so we match by route, not position).
const landing = events.find(
(e: { metadata?: { to?: string } }) => e.metadata?.to === "/",
);
expect(landing).toBeTruthy();
expect(landing.metadata).toMatchObject({
to: "/",
route_pattern: expect.any(String),
});

// Second page_view from navigating to settings
expect(navigation.actionType).toBe("page_view");
// Navigating to settings emits a page_view.
const navigation = events.find(
(e: { metadata?: { to?: string } }) =>
typeof e.metadata?.to === "string" &&
e.metadata.to.includes("/settings"),
);
expect(navigation).toBeTruthy();
expect(navigation.metadata).toMatchObject({
from: "/",
to: expect.stringContaining("/settings"),
route_pattern: expect.any(String),
});
Expand Down
Loading