diff --git a/src/components/Onboarding/IndexRedirect.test.tsx b/src/components/Onboarding/IndexRedirect.test.tsx
new file mode 100644
index 000000000..17ce65d0e
--- /dev/null
+++ b/src/components/Onboarding/IndexRedirect.test.tsx
@@ -0,0 +1,37 @@
+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 }) =>
(key, {
...current,
[tourId]: { completedAt: new Date().toISOString(), completionCount },
diff --git a/src/routes/Dashboard/DashboardLayout.tsx b/src/routes/Dashboard/DashboardLayout.tsx
index 298b0d682..b96c2db15 100644
--- a/src/routes/Dashboard/DashboardLayout.tsx
+++ b/src/routes/Dashboard/DashboardLayout.tsx
@@ -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,
@@ -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" },
@@ -53,7 +59,9 @@ export function DashboardLayout() {
const requiresAuthorization = isAuthorizationRequired();
const isComponentSearchEnabled = useFlagValue("component-search-v2");
- const sidebarItems = isComponentSearchEnabled
+ const { shouldShowOnboarding } = useOnboarding();
+
+ const baseItems = isComponentSearchEnabled
? BASE_SIDEBAR_ITEMS.map((item) =>
item.to === APP_ROUTES.DASHBOARD_COMPONENTS
? COMPONENT_SEARCH_ITEM
@@ -61,6 +69,18 @@ export function DashboardLayout() {
)
: BASE_SIDEBAR_ITEMS;
+ const sidebarItems: SidebarItem[] = shouldShowOnboarding
+ ? [
+ {
+ to: APP_ROUTES.WELCOME,
+ label: "Get Started",
+ icon: "Rocket",
+ exact: true,
+ },
+ ...baseItems,
+ ]
+ : baseItems;
+
return (
", () => {
).toBeInTheDocument();
});
- test("renders the onboarding hero with progress", () => {
+ test("hides the onboarding hero when no backend is available", () => {
renderWithClient();
expect(
- screen.getByRole("heading", { level: 2, name: /welcome to tangle/i }),
- ).toBeInTheDocument();
+ screen.queryByRole("heading", { level: 2, name: /welcome to tangle/i }),
+ ).not.toBeInTheDocument();
expect(
- screen.getByRole("progressbar", { name: /onboarding progress/i }),
- ).toBeInTheDocument();
+ screen.queryByRole("progressbar", { name: /onboarding progress/i }),
+ ).not.toBeInTheDocument();
});
test("renders the docs quicklinks and full-docs link at the top", () => {
diff --git a/src/routes/Dashboard/Learn/LearnHomeView.tsx b/src/routes/Dashboard/Learn/LearnHomeView.tsx
index f74fff801..752a5b672 100644
--- a/src/routes/Dashboard/Learn/LearnHomeView.tsx
+++ b/src/routes/Dashboard/Learn/LearnHomeView.tsx
@@ -11,7 +11,7 @@ import { BlockStack } from "@/components/ui/layout";
import { useOnboarding } from "@/providers/OnboardingProvider/OnboardingProvider";
export function LearnHomeView() {
- const { dismissed } = useOnboarding();
+ const { dismissed, isOnboardingAvailable } = useOnboarding();
return (
@@ -26,7 +26,9 @@ export function LearnHomeView() {
-
+ {isOnboardingAvailable && (
+
+ )}
diff --git a/src/routes/appRoutes.ts b/src/routes/appRoutes.ts
index 231cb67fc..f278ca6a1 100644
--- a/src/routes/appRoutes.ts
+++ b/src/routes/appRoutes.ts
@@ -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",
diff --git a/src/routes/router.ts b/src/routes/router.ts
index f5b7d3ff6..94efa7673 100644
--- a/src/routes/router.ts
+++ b/src/routes/router.ts
@@ -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";
@@ -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",
@@ -329,6 +343,8 @@ const artifactPreviewRoute = createRoute({
const dashboardRouteTree = dashboardRoute.addChildren([
dashboardIndexRoute,
+ dashboardHomeRoute,
+ welcomeRoute,
dashboardRunsRoute,
dashboardPipelinesRoute,
dashboardComponentsRoute,
diff --git a/tests/e2e/navigation-tracking.spec.ts b/tests/e2e/navigation-tracking.spec.ts
index 90dbb376e..adc2aa0a0 100644
--- a/tests/e2e/navigation-tracking.spec.ts
+++ b/tests/e2e/navigation-tracking.spec.ts
@@ -5,12 +5,10 @@ test.describe("Navigation tracking", () => {
page,
}) => {
await page.addInitScript(() => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).__analyticsEvents = [];
window.addEventListener("tangle.analytics.track", (e) => {
const detail = (e as CustomEvent).detail;
if (detail.actionType === "page_view") {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).__analyticsEvents.push(detail);
}
});
@@ -24,26 +22,28 @@ test.describe("Navigation tracking", () => {
await page.getByRole("link", { name: "Settings" }).click();
await expect(page).toHaveURL(/\/settings/);
- const events = await page.evaluate(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- () => (window as any).__analyticsEvents,
- );
+ const events = await page.evaluate(() => (window as any).__analyticsEvents);
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({
- to: "/",
+ const landing = events.find(
+ (e: { metadata?: { to?: string } }) =>
+ e.metadata?.to === "/welcome" || e.metadata?.to === "/dashboard",
+ );
+ expect(landing).toBeTruthy();
+ expect(landing.metadata).toMatchObject({
+ to: expect.stringMatching(/^\/(welcome|dashboard)$/),
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),
});
@@ -51,12 +51,10 @@ test.describe("Navigation tracking", () => {
test("captures search params in page_view metadata", async ({ page }) => {
await page.addInitScript(() => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).__analyticsEvents = [];
window.addEventListener("tangle.analytics.track", (e) => {
const detail = (e as CustomEvent).detail;
if (detail.actionType === "page_view") {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).__analyticsEvents.push(detail);
}
});
@@ -67,10 +65,7 @@ test.describe("Navigation tracking", () => {
page.locator("[data-testid='app-menu-actions']"),
).toBeVisible();
- const events = await page.evaluate(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- () => (window as any).__analyticsEvents,
- );
+ const events = await page.evaluate(() => (window as any).__analyticsEvents);
expect(events.length).toBeGreaterThanOrEqual(1);