Skip to content

Commit 34e4d94

Browse files
committed
feat: rework breadcrumbs
1 parent ff13804 commit 34e4d94

File tree

10 files changed

+242
-145
lines changed

10 files changed

+242
-145
lines changed

public/locales/en.json

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,8 @@
103103
"subtitleMessage": "Sorry, we couldn’t find what you are looking for.<br />The link may be incorrect or the {{entityType}} might have been removed.",
104104
"navigateHome": "Back to Homepage"
105105
},
106-
"IntelligentBreadcrumbs": {
107-
"homeLabel": "Home",
108-
"projects": "Projects",
109-
"workspaces": "Workspaces",
110-
"mcps": "MCPs"
106+
"PathAwareBreadcrumbs": {
107+
"projectsLabel": "Projects"
111108
},
112109
"MCPContext": {
113110
"errorMessage": "An unknown error occurred"

src/Routes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const Routes = {
2+
Home: '/',
3+
Project: '/mcp/projects/:projectName',
4+
Mcp: '/mcp/projects/:projectName/workspaces/:workspaceName/mcps/:controlPlaneName',
5+
} as const;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Button, FlexBox, FlexBoxAlignItems, Menu, MenuItem } from '@ui5/webcomponents-react';
2+
3+
import { useTranslation } from 'react-i18next';
4+
import { FeedbackButton } from './FeedbackButton.tsx';
5+
import { BetaButton } from './BetaButton.tsx';
6+
import { useRef, useState } from 'react';
7+
import { useAuthOnboarding } from '../../spaces/onboarding/auth/AuthContextOnboarding.tsx';
8+
import { SearchParamToggleVisibility } from '../Helper/FeatureToggleExistance.tsx';
9+
import { PathAwareBreadcrumbs } from './PathAwareBreadcrumbs/PathAwareBreadcrumbs.tsx';
10+
11+
export function BreadcrumbFeedbackHeader() {
12+
return (
13+
<FlexBox alignItems={FlexBoxAlignItems.Center}>
14+
<PathAwareBreadcrumbs />
15+
<BetaButton />
16+
<FeedbackButton />
17+
<SearchParamToggleVisibility
18+
shouldBeVisible={(params) => {
19+
if (params === undefined) return false;
20+
if (params.get('showHeaderBar') === null) return false;
21+
return params?.get('showHeaderBar') === 'false';
22+
}}
23+
>
24+
<LogoutMenu />
25+
</SearchParamToggleVisibility>
26+
</FlexBox>
27+
);
28+
}
29+
30+
function LogoutMenu() {
31+
const auth = useAuthOnboarding();
32+
const { t } = useTranslation();
33+
34+
const buttonRef = useRef(null);
35+
const [menuIsOpen, setMenuIsOpen] = useState(false);
36+
return (
37+
<>
38+
<Button
39+
ref={buttonRef}
40+
icon="menu2"
41+
onClick={() => {
42+
setMenuIsOpen(true);
43+
}}
44+
/>
45+
<Menu
46+
opener={buttonRef.current}
47+
open={menuIsOpen}
48+
onClose={() => {
49+
setMenuIsOpen(false);
50+
}}
51+
>
52+
<MenuItem
53+
icon="log"
54+
text={t('ShellBar.signOutButton')}
55+
onClick={async () => {
56+
setMenuIsOpen(false);
57+
await auth.logout();
58+
}}
59+
/>
60+
</Menu>
61+
</>
62+
);
63+
}

src/components/Core/IntelligentBreadcrumbs.tsx

Lines changed: 0 additions & 128 deletions
This file was deleted.

src/components/Core/LandscapeLabel.tsx

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { PathAwareBreadcrumbs } from './PathAwareBreadcrumbs';
2+
3+
describe('PathAwareBreadcrumbs', () => {
4+
const navigateCalls = [];
5+
const fakeUseNavigate = () => (path: string) => navigateCalls.push(path);
6+
const fakeUseParams = () => ({
7+
projectName: 'my-project',
8+
workspaceName: 'my-workspace',
9+
controlPlaneName: 'my-control-plane',
10+
});
11+
12+
beforeEach(() => {
13+
navigateCalls.length = 0;
14+
});
15+
16+
it('renders breadcrumbs for all path parameters', () => {
17+
// @ts-ignore
18+
cy.mount(<PathAwareBreadcrumbs useNavigate={fakeUseNavigate} useParams={fakeUseParams} />);
19+
20+
// Check that all breadcrumbs are rendered
21+
cy.get("[data-testid='breadcrumb-item']").should('have.length', 4);
22+
cy.get("[data-testid='breadcrumb-item']").eq(0).should('contain', '[LOCAL] Projects');
23+
cy.get("[data-testid='breadcrumb-item']").eq(1).should('contain', 'my-project');
24+
cy.get("[data-testid='breadcrumb-item']").eq(2).should('contain', 'my-workspace');
25+
cy.get("[data-testid='breadcrumb-item']").eq(3).should('contain', 'my-control-plane');
26+
});
27+
28+
it('navigates when clicking breadcrumbs for all path parameters', () => {
29+
let lastNavigatedPath: string | null = null;
30+
const fakeUseNavigate = () => (path: string) => {
31+
lastNavigatedPath = path;
32+
};
33+
// @ts-ignore
34+
cy.mount(<PathAwareBreadcrumbs useNavigate={fakeUseNavigate} useParams={fakeUseParams} />);
35+
36+
// Navigate to '/'
37+
cy.contains('[LOCAL] Projects').click();
38+
cy.wrap(null).then(() => {
39+
expect(lastNavigatedPath).to.equal('/');
40+
});
41+
42+
// Click on 'my-project' > Navigate to 'my-project'
43+
cy.contains('my-project').click();
44+
cy.wrap(null).then(() => {
45+
expect(lastNavigatedPath).to.equal('/mcp/projects/my-project');
46+
});
47+
48+
// Click on 'my-workspace' > Navigate to 'my-project' since workspaces don’t expose a direct path
49+
cy.contains('my-workspace').click();
50+
cy.wrap(null).then(() => {
51+
expect(lastNavigatedPath).to.equal('/mcp/projects/my-project');
52+
});
53+
54+
// Click on 'my-control-plane' > Navigate to 'my-control-plane'
55+
cy.contains('my-control-plane').click();
56+
cy.wrap(null).then(() => {
57+
expect(lastNavigatedPath).to.equal('/mcp/projects/my-project/workspaces/my-workspace/mcps/my-control-plane');
58+
});
59+
});
60+
61+
it('renders only home breadcrumb when there are no path parameters', () => {
62+
const fakeUseParams = () => ({});
63+
64+
// @ts-ignore
65+
cy.mount(<PathAwareBreadcrumbs useNavigate={fakeUseNavigate} useParams={fakeUseParams} />);
66+
67+
cy.get("[data-testid='breadcrumb-item']").should('have.length', 1);
68+
});
69+
70+
it('handles partial route parameters', () => {
71+
const fakeUseParams = () => ({
72+
projectName: 'my-project',
73+
workspaceName: 'my-workspace',
74+
// No controlPlaneName
75+
});
76+
77+
// @ts-ignore
78+
cy.mount(<PathAwareBreadcrumbs useNavigate={fakeUseNavigate} useParams={fakeUseParams} />);
79+
80+
// Should show 3 breadcrumbs
81+
cy.get("[data-testid='breadcrumb-item']").should('have.length', 3);
82+
83+
// Verify data-target attributes
84+
cy.get("[data-testid='breadcrumb-item']").eq(0).should('contain', '[LOCAL] Projects');
85+
cy.get("[data-testid='breadcrumb-item']").eq(1).should('contain', 'my-project');
86+
cy.get("[data-testid='breadcrumb-item']").eq(2).should('contain', 'my-workspace');
87+
});
88+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Breadcrumbs } from '@ui5/webcomponents-react';
2+
import { BreadcrumbsItem } from '@ui5/webcomponents-react/wrappers';
3+
import { generatePath, useNavigate as _useNavgigate, useParams as _useParams } from 'react-router-dom';
4+
5+
import { useTranslation } from 'react-i18next';
6+
import { useFrontendConfig } from '../../../context/FrontendConfigContext.tsx';
7+
import { Routes } from '../../../Routes.ts';
8+
9+
export interface PathAwareBreadcrumbsProps {
10+
useNavigate?: typeof _useNavgigate;
11+
useParams?: typeof _useParams;
12+
}
13+
export function PathAwareBreadcrumbs({
14+
useNavigate = _useNavgigate,
15+
useParams = _useParams,
16+
}: PathAwareBreadcrumbsProps) {
17+
const { projectName, workspaceName, controlPlaneName } = useParams();
18+
const { t } = useTranslation();
19+
const frontendConfig = useFrontendConfig();
20+
const navigate = useNavigate();
21+
22+
const breadcrumbItems: { label: string; path: string }[] = [
23+
{
24+
label: `[${frontendConfig.landscape}] ${t('PathAwareBreadcrumbs.projectsLabel')}`,
25+
path: Routes.Home,
26+
},
27+
];
28+
29+
if (projectName) {
30+
breadcrumbItems.push({
31+
label: projectName,
32+
path: generatePath(Routes.Project, {
33+
projectName,
34+
}),
35+
});
36+
37+
if (workspaceName) {
38+
breadcrumbItems.push({
39+
label: workspaceName,
40+
// Navigate to the project route since workspaces don't provide a direct path
41+
path: generatePath(Routes.Project, {
42+
projectName,
43+
}),
44+
});
45+
46+
if (controlPlaneName) {
47+
breadcrumbItems.push({
48+
label: controlPlaneName,
49+
path: generatePath(Routes.Mcp, {
50+
projectName,
51+
workspaceName,
52+
controlPlaneName,
53+
}),
54+
});
55+
}
56+
}
57+
}
58+
59+
return (
60+
<Breadcrumbs
61+
design="NoCurrentPage"
62+
onItemClick={(event) => {
63+
event.preventDefault();
64+
const target = event.detail.item.dataset.target;
65+
66+
if (target) {
67+
navigate(target);
68+
}
69+
}}
70+
>
71+
{breadcrumbItems.map((item) => (
72+
<BreadcrumbsItem key={item.path} data-target={item.path} data-testid="breadcrumb-item">
73+
{item.label}
74+
</BreadcrumbsItem>
75+
))}
76+
</Breadcrumbs>
77+
);
78+
}

0 commit comments

Comments
 (0)