Skip to content

Commit 0118e2f

Browse files
authored
feat: add page titles (#802)
1 parent 21595e6 commit 0118e2f

File tree

2 files changed

+108
-0
lines changed

2 files changed

+108
-0
lines changed

src/hooks/usePageTitle.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { useEffect, useMemo } from "react";
2+
import { matchPath, useLocation } from "react-router-dom";
3+
import { routes } from "@/router/routes";
4+
5+
/**
6+
* Sets a descriptive document.title based on the current route.
7+
*/
8+
export function usePageTitle(): void {
9+
const location = useLocation();
10+
const pathname = location.pathname;
11+
12+
const baseTitle = useMemo(() => {
13+
// Helper to match exact route patterns (end=true for specificity).
14+
const is = (pattern: string) => matchPath({ path: pattern, end: true }, pathname) !== null;
15+
16+
// Public/standalone pages
17+
if (is(routes.login)) return "Login";
18+
if (is(routes.survey)) return "Survey";
19+
if (is(routes.onboarding)) return "Onboarding";
20+
if (is(routes.activateUser)) return "Activate Account";
21+
if (is(routes.activateServer)) return "Activate Server";
22+
if (is(routes.devices.verify)) return "Devices Verify";
23+
if (is(routes.upgrade)) return "Upgrade";
24+
25+
// Non-project scoped
26+
if (is(routes.home)) return "Overview";
27+
if (is(routes.projects.overview)) return "Projects";
28+
if (is(routes.stacks.overview)) return "Stacks";
29+
// Stack creation flow titles are placed immediately after Stacks overview
30+
if (is(routes.stacks.create.index)) return "Create Stack";
31+
if (is(routes.stacks.create.newInfra)) return "Create Stack: New Infrastructure";
32+
if (is(routes.stacks.create.manual)) return "Create Stack: Manual";
33+
if (is(routes.stacks.create.existingInfra)) return "Create Stack: Existing Infrastructure";
34+
if (is(routes.stacks.create.terraform)) return "Create Stack: Terraform";
35+
if (is(routes.components.overview)) return "Components";
36+
37+
// Non-project Settings: match specific routes first so they don't get shadowed by the generic prefix check.
38+
if (is(routes.settings.profile)) return "Profile Settings";
39+
if (is(routes.settings.members)) return "Members";
40+
if (is(routes.settings.apiTokens)) return "API Tokens";
41+
if (is(routes.settings.notifications)) return "Notifications";
42+
if (is(routes.settings.secrets.detail(":id"))) return "Secret";
43+
if (is(routes.settings.secrets.overview)) return "Secrets";
44+
if (is(routes.settings.connectors.detail.configuration(":connectorId")))
45+
return "Connector Configuration";
46+
if (is(routes.settings.connectors.detail.components(":connectorId")))
47+
return "Connector Components";
48+
if (is(routes.settings.connectors.detail.resources(":connectorId")))
49+
return "Connector Resources";
50+
if (is(routes.settings.connectors.create)) return "Create Connector";
51+
if (is(routes.settings.connectors.overview)) return "Connectors";
52+
if (is(routes.settings.service_accounts.detail(":id"))) return "Service Account";
53+
if (is(routes.settings.service_accounts.overview)) return "Service Accounts";
54+
if (is(routes.settings.general)) return "Settings";
55+
56+
// Settings (and subpages) fallback
57+
// Use a prefix check to cover nested settings paths that are not explicitly handled above.
58+
if (pathname.startsWith("/settings")) return "Settings";
59+
60+
// Components detail/edit/create
61+
if (is(routes.components.detail(":componentId"))) return "Component";
62+
if (is(routes.components.edit(":componentId"))) return "Component";
63+
if (is(routes.components.create)) return "Components";
64+
65+
// Project-scoped: Pipelines and detail sub-tabs
66+
if (is(routes.projects.pipelines.overview)) return "Pipelines";
67+
if (is(routes.projects.pipelines.detail.runs(":pipelineId"))) return "Pipeline";
68+
if (is(routes.projects.pipelines.detail.snapshots(":pipelineId"))) return "Pipeline Snapshots";
69+
if (is(routes.projects.pipelines.detail.deployments(":pipelineId")))
70+
return "Pipeline Deployments";
71+
72+
// Project-scoped: Snapshots
73+
if (is(routes.projects.snapshots.overview)) return "Snapshots";
74+
if (is(routes.projects.snapshots.create)) return "Create Snapshot";
75+
if (is(routes.projects.snapshots.detail.overview(":snapshotId"))) return "Snapshot";
76+
if (is(routes.projects.snapshots.detail.runs(":snapshotId"))) return "Snapshot Runs";
77+
78+
// Project-scoped: Runs
79+
if (is(routes.projects.runs.overview)) return "Pipeline Runs";
80+
if (is(routes.projects.runs.detail(":runId"))) return "Run";
81+
if (is(routes.projects.runs.createSnapshot(":runId"))) return "Create Snapshot";
82+
83+
// Project-scoped: Deployments
84+
if (is(routes.projects.deployments.overview)) return "Deployments";
85+
if (is(routes.projects.deployments.detail.overview(":deploymentId"))) return "Deployment";
86+
if (is(routes.projects.deployments.detail.runs(":deploymentId"))) return "Deployment Runs";
87+
if (is(routes.projects.deployments.detail.playground(":deploymentId")))
88+
return "Deployment Playground";
89+
90+
// Project-scoped: Other tabs
91+
if (is(routes.projects.models.overview)) return "Models";
92+
if (is(routes.projects.artifacts.overview)) return "Artifacts";
93+
94+
// Project-scoped settings
95+
if (is(routes.projects.settings.repositories.overview)) return "Repositories";
96+
if (is(routes.projects.settings.profile)) return "Profile Settings";
97+
98+
return undefined;
99+
}, [pathname]);
100+
101+
useEffect(() => {
102+
document.title = baseTitle ? `${baseTitle} - ZenML Dashboard` : "ZenML Dashboard";
103+
}, [baseTitle]);
104+
}

src/layouts/RootLayout.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ import { useServerInfo } from "@/data/server/info-query";
22
import { routes } from "@/router/routes";
33
import { useEffect } from "react";
44
import { Outlet, useNavigate } from "react-router-dom";
5+
import { usePageTitle } from "@/hooks/usePageTitle";
56

67
export function RootLayout() {
78
const navigate = useNavigate();
89
const { data } = useServerInfo({ throwOnError: true });
910

11+
// Set browser titles for both public and authenticated routes.
12+
usePageTitle();
13+
1014
useEffect(() => {
1115
if (data && data.active === false) {
1216
navigate(routes.activateServer + `?redirect=${routes.onboarding}`, { replace: true });

0 commit comments

Comments
 (0)