Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
7 changes: 2 additions & 5 deletions public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,8 @@
"subtitleMessage": "Sorry, we couldn’t find what you are looking for.<br />The link may be incorrect or the {{entityType}} might have been removed.",
"navigateHome": "Back to Homepage"
},
"IntelligentBreadcrumbs": {
"homeLabel": "Home",
"projects": "Projects",
"workspaces": "Workspaces",
"mcps": "MCPs"
"PathAwareBreadcrumbs": {
"projectsLabel": "Projects"
},
"MCPContext": {
"errorMessage": "An unknown error occurred"
Expand Down
5 changes: 5 additions & 0 deletions src/Routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const Routes = {
Home: '/',
Project: '/mcp/projects/:projectName',
Mcp: '/mcp/projects/:projectName/workspaces/:workspaceName/mcps/:controlPlaneName',
} as const;
63 changes: 63 additions & 0 deletions src/components/Core/BreadcrumbFeedbackHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Button, FlexBox, FlexBoxAlignItems, Menu, MenuItem } from '@ui5/webcomponents-react';

import { useTranslation } from 'react-i18next';
import { FeedbackButton } from './FeedbackButton.tsx';
import { BetaButton } from './BetaButton.tsx';
import { useRef, useState } from 'react';
import { useAuthOnboarding } from '../../spaces/onboarding/auth/AuthContextOnboarding.tsx';
import { SearchParamToggleVisibility } from '../Helper/FeatureToggleExistance.tsx';
import { PathAwareBreadcrumbs } from './PathAwareBreadcrumbs/PathAwareBreadcrumbs.tsx';

export function BreadcrumbFeedbackHeader() {
return (
<FlexBox alignItems={FlexBoxAlignItems.Center}>
<PathAwareBreadcrumbs />
<BetaButton />
<FeedbackButton />
<SearchParamToggleVisibility
shouldBeVisible={(params) => {
if (params === undefined) return false;
if (params.get('showHeaderBar') === null) return false;
return params?.get('showHeaderBar') === 'false';
}}
>
<LogoutMenu />
</SearchParamToggleVisibility>
</FlexBox>
);
}

function LogoutMenu() {
const auth = useAuthOnboarding();
const { t } = useTranslation();

const buttonRef = useRef(null);
const [menuIsOpen, setMenuIsOpen] = useState(false);
return (
<>
<Button
ref={buttonRef}
icon="menu2"
onClick={() => {
setMenuIsOpen(true);
}}
/>
<Menu
opener={buttonRef.current}
open={menuIsOpen}
onClose={() => {
setMenuIsOpen(false);
}}
>
<MenuItem
icon="log"
text={t('ShellBar.signOutButton')}
onClick={async () => {
setMenuIsOpen(false);
await auth.logout();
}}
/>
</Menu>
</>
);
}
128 changes: 0 additions & 128 deletions src/components/Core/IntelligentBreadcrumbs.tsx

This file was deleted.

6 changes: 0 additions & 6 deletions src/components/Core/LandscapeLabel.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { PathAwareBreadcrumbs } from './PathAwareBreadcrumbs';
import { useNavigate, useParams } from 'react-router-dom';

describe('PathAwareBreadcrumbs', () => {
let lastNavigatedPath = '';
const fakeUseNavigate = (() => (path: string) => {
lastNavigatedPath = path;
}) as typeof useNavigate;
const fakeUseParams = (() => ({
projectName: 'my-project',
workspaceName: 'my-workspace',
controlPlaneName: 'my-control-plane',
})) as typeof useParams;

beforeEach(() => {
lastNavigatedPath = '';
});

it('renders breadcrumbs for all path parameters', () => {
cy.mount(<PathAwareBreadcrumbs useNavigate={fakeUseNavigate} useParams={fakeUseParams} />);

// Check that all breadcrumbs are rendered
cy.get("[data-testid='breadcrumb-item']").should('have.length', 4);
cy.get("[data-testid='breadcrumb-item']").eq(0).should('contain', '[LOCAL] Projects');
cy.get("[data-testid='breadcrumb-item']").eq(1).should('contain', 'my-project');
cy.get("[data-testid='breadcrumb-item']").eq(2).should('contain', 'my-workspace');
cy.get("[data-testid='breadcrumb-item']").eq(3).should('contain', 'my-control-plane');
});

it('navigates when clicking breadcrumbs for all path parameters', () => {
cy.mount(<PathAwareBreadcrumbs useNavigate={fakeUseNavigate} useParams={fakeUseParams} />);

// Navigate to '/'
cy.contains('[LOCAL] Projects').click();
cy.wrap(null).then(() => {
expect(lastNavigatedPath).to.equal('/');
});

// Click on 'my-project' > Navigate to 'my-project'
cy.contains('my-project').click();
cy.wrap(null).then(() => {
expect(lastNavigatedPath).to.equal('/mcp/projects/my-project');
});

// Click on 'my-workspace' > Navigate to 'my-project' since workspaces don’t expose a direct path
cy.contains('my-workspace').click();
cy.wrap(null).then(() => {
expect(lastNavigatedPath).to.equal('/mcp/projects/my-project');
});

// Click on 'my-control-plane' > Navigate to 'my-control-plane'
cy.contains('my-control-plane').click();
cy.wrap(null).then(() => {
expect(lastNavigatedPath).to.equal('/mcp/projects/my-project/workspaces/my-workspace/mcps/my-control-plane');
});
});

it('renders only home breadcrumb when there are no path parameters', () => {
const fakeUseParams = (() => ({})) as typeof useParams;

cy.mount(<PathAwareBreadcrumbs useNavigate={fakeUseNavigate} useParams={fakeUseParams} />);

cy.get("[data-testid='breadcrumb-item']").should('have.length', 1);
});

it('handles partial route parameters', () => {
const fakeUseParams = (() => ({
projectName: 'my-project',
workspaceName: 'my-workspace',
// No controlPlaneName
})) as typeof useParams;

cy.mount(<PathAwareBreadcrumbs useNavigate={fakeUseNavigate} useParams={fakeUseParams} />);

// Should show 3 breadcrumbs
cy.get("[data-testid='breadcrumb-item']").should('have.length', 3);

// Verify data-target attributes
cy.get("[data-testid='breadcrumb-item']").eq(0).should('contain', '[LOCAL] Projects');
cy.get("[data-testid='breadcrumb-item']").eq(1).should('contain', 'my-project');
cy.get("[data-testid='breadcrumb-item']").eq(2).should('contain', 'my-workspace');
});
});
78 changes: 78 additions & 0 deletions src/components/Core/PathAwareBreadcrumbs/PathAwareBreadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Breadcrumbs } from '@ui5/webcomponents-react';
import { BreadcrumbsItem } from '@ui5/webcomponents-react/wrappers';
import { generatePath, useNavigate as _useNavigate, useParams as _useParams } from 'react-router-dom';

import { useTranslation } from 'react-i18next';
import { useFrontendConfig } from '../../../context/FrontendConfigContext.tsx';
import { Routes } from '../../../Routes.ts';

export interface PathAwareBreadcrumbsProps {
useNavigate?: typeof _useNavigate;
useParams?: typeof _useParams;
}
export function PathAwareBreadcrumbs({
useNavigate = _useNavigate,
useParams = _useParams,
}: PathAwareBreadcrumbsProps) {
const { projectName, workspaceName, controlPlaneName } = useParams();
const { t } = useTranslation();
const frontendConfig = useFrontendConfig();
const navigate = useNavigate();

const breadcrumbItems: { label: string; path: string }[] = [
{
label: `[${frontendConfig.landscape}] ${t('PathAwareBreadcrumbs.projectsLabel')}`,
path: Routes.Home,
},
];

if (projectName) {
breadcrumbItems.push({
label: projectName,
path: generatePath(Routes.Project, {
projectName,
}),
});

if (workspaceName) {
breadcrumbItems.push({
label: workspaceName,
// Navigate to the project route since workspaces don't provide a direct path
path: generatePath(Routes.Project, {
projectName,
}),
});

if (controlPlaneName) {
breadcrumbItems.push({
label: controlPlaneName,
path: generatePath(Routes.Mcp, {
projectName,
workspaceName,
controlPlaneName,
}),
});
}
}
}

return (
<Breadcrumbs
design="NoCurrentPage"
onItemClick={(event) => {
event.preventDefault();
const target = event.detail.item.dataset.target;

if (target) {
navigate(target);
}
}}
>
{breadcrumbItems.map((item) => (
<BreadcrumbsItem key={item.path} data-target={item.path} data-testid="breadcrumb-item">
{item.label}
</BreadcrumbsItem>
))}
</Breadcrumbs>
);
}
Loading
Loading