Skip to content

Commit eb4ee2b

Browse files
from feature/PRD-109-breadcrumbs (#605)
* breadcrumbs routing * tests * hover link * fixes PR - env * added tooltip
1 parent 9b310a4 commit eb4ee2b

File tree

14 files changed

+308
-15
lines changed

14 files changed

+308
-15
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
VITE_API_BASE_URL=
22
VITE_FRONTEND_VERSION=
3-
VITE_FEATURE_OS_KEY=
3+
VITE_FEATURE_OS_KEY=

src/app/pipelines/[namespace]/page.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import { useParams } from "react-router-dom";
22
import { Header } from "./Header";
33
import { PipelineRunsTable } from "./RunsTable";
4+
import { useEffect } from "react";
5+
import { useBreadcrumbsContext } from "@/layouts/AuthenticatedLayout/BreadcrumbsContext";
46

57
export default function PipelineNamespacePage() {
68
const { namespace } = useParams() as { namespace: string };
9+
const { setCurrentBreadcrumbData } = useBreadcrumbsContext();
10+
11+
useEffect(() => {
12+
namespace &&
13+
setCurrentBreadcrumbData({ segment: "pipeline_detail", data: { name: namespace } });
14+
}, [namespace]);
715

816
return (
917
<div>

src/app/pipelines/page.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ import { Button, DataTable, Skeleton } from "@zenml-io/react-component-library";
66
import { getPipelineColumns } from "./columns";
77
import { usePipelineOverviewSearchParams } from "./service";
88
import Pagination from "@/components/Pagination";
9+
import { useBreadcrumbsContext } from "@/layouts/AuthenticatedLayout/BreadcrumbsContext";
10+
import { useEffect } from "react";
911

1012
export default function PipelinesPage() {
13+
const { setCurrentBreadcrumbData } = useBreadcrumbsContext();
1114
const queryParams = usePipelineOverviewSearchParams();
1215

1316
const { data, refetch } = useAllPipelineNamespaces(
@@ -20,6 +23,10 @@ export default function PipelinesPage() {
2023
{ throwOnError: true }
2124
);
2225

26+
useEffect(() => {
27+
setCurrentBreadcrumbData({ segment: "pipelines", data: null });
28+
}, []);
29+
2330
return (
2431
<div>
2532
<PageHeader>

src/app/runs/[id]/Header.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@ import RunIcon from "@/assets/icons/terminal.svg?react";
22
import { ExecutionStatusIcon, getExecutionStatusColor } from "@/components/ExecutionStatus";
33
import { PageHeader } from "@/components/PageHeader";
44
import { usePipelineRun } from "@/data/pipeline-runs/pipeline-run-detail-query";
5+
import { useBreadcrumbsContext } from "@/layouts/AuthenticatedLayout/BreadcrumbsContext";
56
import { Skeleton } from "@zenml-io/react-component-library";
7+
import { useEffect } from "react";
68
import { useParams } from "react-router-dom";
79

810
export function RunsDetailHeader() {
911
const { runId } = useParams() as { runId: string };
12+
const { setCurrentBreadcrumbData } = useBreadcrumbsContext();
1013

1114
const { data, isSuccess } = usePipelineRun({ runId }, { throwOnError: true });
1215

16+
useEffect(() => {
17+
data && setCurrentBreadcrumbData({ segment: "runs", data: data });
18+
}, [data]);
19+
1320
return (
1421
<PageHeader>
1522
<div className="flex items-center gap-1">

src/app/runs/[id]/_Tabs/Overview/Details.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CopyButton } from "@/components/CopyButton";
44
import { DisplayDate } from "@/components/DisplayDate";
55
import { ExecutionStatusIcon, getExecutionStatusTagColor } from "@/components/ExecutionStatus";
66
import { Key, Value } from "@/components/KeyValue";
7+
78
import { usePipelineRun } from "@/data/pipeline-runs/pipeline-run-detail-query";
89
import {
910
CollapsibleContent,
@@ -13,15 +14,28 @@ import {
1314
Skeleton,
1415
Tag
1516
} from "@zenml-io/react-component-library";
16-
import { useState } from "react";
17-
import { useParams } from "react-router-dom";
17+
import { useEffect, useState } from "react";
18+
import { useParams, useSearchParams, useNavigate } from "react-router-dom";
1819

1920
export function Details() {
2021
const { runId } = useParams() as { runId: string };
2122
const [open, setOpen] = useState(true);
23+
const [searchParams] = useSearchParams();
24+
const navigate = useNavigate();
2225

2326
const { data, isError, isPending } = usePipelineRun({ runId: runId }, { throwOnError: true });
2427

28+
useEffect(() => {
29+
// To set current tab in URL
30+
const tabParam = searchParams.get("tab");
31+
if (!tabParam) {
32+
const newUrl = new URL(window.location.href);
33+
newUrl.searchParams.set("tab", "overview");
34+
const newPath = `${newUrl.pathname}${newUrl.search}`;
35+
navigate(newPath, { replace: true });
36+
}
37+
}, [searchParams, navigate]);
38+
2539
if (isError) return null;
2640
if (isPending) return <Skeleton className="h-[200px] w-full" />;
2741

src/assets/icons/slash-divider.svg

Lines changed: 4 additions & 0 deletions
Loading

src/assets/icons/tool-02.svg

Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import Divider from "@/assets/icons/slash-divider.svg?react";
2+
import { useEffect, useState } from "react";
3+
import { Link, useLocation, useSearchParams } from "react-router-dom";
4+
import {
5+
matchSegmentWithPages,
6+
matchSegmentWithRequest,
7+
matchSegmentWithTab,
8+
matchSegmentWithURL
9+
} from "./SegmentsBreadcrumbs";
10+
import { useBreadcrumbsContext } from "@/layouts/AuthenticatedLayout/BreadcrumbsContext";
11+
import { formatIdToTitleCase, transformToEllipsis } from "@/lib/strings";
12+
import {
13+
Tooltip,
14+
TooltipContent,
15+
TooltipProvider,
16+
TooltipTrigger
17+
} from "@zenml-io/react-component-library";
18+
19+
type BreadcrumbData = { [key: string]: { id?: string; name?: string } };
20+
21+
export function Breadcrumbs() {
22+
const { currentBreadcrumbData: data } = useBreadcrumbsContext();
23+
const [currentData, setCurrentData] = useState<BreadcrumbData | null>(null);
24+
const [searchParams] = useSearchParams();
25+
const { pathname } = useLocation();
26+
27+
useEffect(() => {
28+
let matchedData: BreadcrumbData = {};
29+
30+
const pathSegments = pathname.split("/").filter((segment: string) => segment !== "");
31+
const segmentsToCheck: string[] = ["pipelines", "runs"];
32+
const mainPaths = segmentsToCheck.some((segment) => pathSegments.includes(segment));
33+
34+
if (!mainPaths) {
35+
const currentSegment =
36+
pathSegments.length === 0
37+
? "overview"
38+
: pathSegments.includes("settings")
39+
? pathSegments[1]
40+
: pathSegments[0];
41+
42+
matchedData = matchSegmentWithPages(currentSegment);
43+
setCurrentData(matchedData);
44+
} else {
45+
if (data && data.segment) {
46+
const tabParam = searchParams.get("tab");
47+
matchedData = matchSegmentWithRequest(data) as BreadcrumbData;
48+
49+
const newMatchedData = {
50+
...matchedData,
51+
...(tabParam && { tab: { id: tabParam, name: tabParam } })
52+
};
53+
setCurrentData(newMatchedData);
54+
}
55+
}
56+
}, [data, searchParams, pathname]);
57+
58+
const totalEntries = currentData ? Object.entries(currentData).length : 0;
59+
60+
return (
61+
<div className="flex">
62+
{currentData &&
63+
Object.entries(currentData).map(([segment, value], index: number) => {
64+
const isLastOne = index === totalEntries - 1;
65+
66+
return (
67+
<div className="flex items-center" key={index}>
68+
{index !== 0 && <Divider className="h-4 w-4 flex-shrink-0 fill-neutral-200" />}
69+
{segment === "tab" ? (
70+
<div className="align-center ml-1 flex items-center">
71+
<div>{matchSegmentWithTab(value?.name as string)}</div>
72+
<span
73+
className={`
74+
${isLastOne ? "pointer-events-none text-theme-text-primary" : "text-theme-text-secondary"}
75+
ml-1 flex items-center text-text-md font-semibold capitalize`}
76+
>
77+
{formatIdToTitleCase(value?.name as string)}
78+
</span>
79+
</div>
80+
) : (
81+
<Link
82+
className={`${isLastOne || segment === "settings" ? "pointer-events-none" : ""}
83+
${isLastOne ? "font-semibold text-theme-text-primary" : "text-theme-text-secondary"}
84+
rounded-sm p-0.5 px-1 text-text-md capitalize hover:text-purple-900 hover:underline`}
85+
to={matchSegmentWithURL(segment, value?.id as string)}
86+
>
87+
{typeof value?.name === "string" ? (
88+
value?.name.length > 20 ? (
89+
<TooltipProvider>
90+
<Tooltip>
91+
<TooltipTrigger className="hover:text-theme-text-brand hover:underline">
92+
{transformToEllipsis(value?.name, 20)}
93+
</TooltipTrigger>
94+
<TooltipContent>{value?.name}</TooltipContent>
95+
</Tooltip>
96+
</TooltipProvider>
97+
) : (
98+
value?.name
99+
)
100+
) : (
101+
value?.name
102+
)}
103+
</Link>
104+
)}
105+
</div>
106+
);
107+
})}
108+
</div>
109+
);
110+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import Info from "@/assets/icons/info.svg?react";
2+
import Tools from "@/assets/icons/tool-02.svg?react";
3+
import { routes } from "@/router/routes";
4+
5+
export const matchSegmentWithRequest = ({ segment, data }: { segment: string; data?: any }) => {
6+
const routeMap: { [key: string]: { [key: string]: { id?: string | null; name?: string } } } = {
7+
// Pipelines
8+
pipelines: {
9+
pipelines: { id: data?.body?.pipeline?.id, name: "pipelines" }
10+
},
11+
pipeline_detail: {
12+
pipelines: { id: data?.body?.pipeline?.id, name: "pipelines" },
13+
pipeline_detail: { id: data?.name, name: data?.name }
14+
},
15+
runs: {
16+
pipelines: { id: data?.body?.pipeline?.id, name: "pipelines" },
17+
pipeline_detail: {
18+
id: data?.body?.pipeline?.name,
19+
name: data?.body?.pipeline?.name
20+
},
21+
runs: { id: data?.id, name: data?.name }
22+
}
23+
};
24+
25+
return routeMap[segment];
26+
};
27+
28+
export const matchSegmentWithPages = (segment: string): any => {
29+
const generateRouteMap = (segments: string[], withSettings: boolean = false) => {
30+
return segments.reduce(
31+
(acc, name) => {
32+
acc[name] = withSettings
33+
? { settings: { name: "settings" }, [name]: { name } }
34+
: { [name]: { name } };
35+
return acc;
36+
},
37+
{} as { [key: string]: any }
38+
);
39+
};
40+
41+
const routeMap = {
42+
...generateRouteMap(["onboarding", "overview", "stacks", "models", "artifacts"]),
43+
...generateRouteMap(
44+
[
45+
"general",
46+
"members",
47+
"roles",
48+
"updates",
49+
"repositories",
50+
"connectors",
51+
"secrets",
52+
"notifications",
53+
"profile"
54+
],
55+
true
56+
)
57+
};
58+
59+
return routeMap[segment];
60+
};
61+
62+
export const matchSegmentWithURL = (segment: string, id: string) => {
63+
const routeMap: { [key: string]: string } = {
64+
// Pipelines
65+
pipelines: routes.pipelines.overview,
66+
pipeline_detail: routes.pipelines.namespace(id),
67+
runs: routes.runs.detail(id)
68+
};
69+
70+
return routeMap[segment] || "#";
71+
};
72+
73+
export const matchSegmentWithTab = (segment: string) => {
74+
const routeMap: { [key: string]: JSX.Element } = {
75+
overview: <Info className="h-5 w-5 fill-theme-text-tertiary" />,
76+
configuration: <Tools className="h-5 w-5 fill-theme-text-tertiary" />
77+
};
78+
79+
return routeMap[segment] || <Info className="h-5 w-5 fill-theme-text-tertiary" />;
80+
};

src/layouts/AuthenticatedLayout/AuthenticatedHeader.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import ZenMLIcon from "@/assets/icons/zenml-icon.svg?react";
22
import { routes } from "@/router/routes";
33
import { Link } from "react-router-dom";
44
import { UserDropdown } from "./UserDropdown";
5+
import { Breadcrumbs } from "@/components/breadcrumbs/Breadcrumbs";
56

67
export function AuthenticatedHeader() {
78
return (
@@ -14,6 +15,7 @@ export function AuthenticatedHeader() {
1415
>
1516
<ZenMLIcon className="h-6 w-6 fill-theme-text-brand" />
1617
</Link>
18+
<Breadcrumbs />
1719
<div className="ml-auto pl-3 pr-4">
1820
<UserDropdown />
1921
</div>

0 commit comments

Comments
 (0)