Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
13 changes: 11 additions & 2 deletions public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
"learnMore": "Learn more in our documentation",
"signInButton": "Sign in"
},
"Entities": {
"ManagedControlPlane": "Managed Control Plane",
"Project": "Project"
},
"ComponentList": {
"tableComponentHeader": "Name",
"tableVersionHeader": "Version"
Expand Down Expand Up @@ -87,6 +91,11 @@
"subtitleMessage": "Get started by creating your first Managed Control Plane.",
"helpButton": "Help"
},
"NotFoundBanner": {
"titleMessage": "{{entityType}} not found",
"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"
},
Expand Down Expand Up @@ -151,7 +160,7 @@
"EditMembers": {
"addButton": "Add"
},
"ControlPlaneListView": {
"ProjectsPage": {
"header": "Your instances of <span>ManagedControlPlane</span>",
"projectHeader": "Project:"
},
Expand All @@ -161,7 +170,7 @@
"deleteProject": "Delete project",
"deleteConfirmationDialog": "Project deleted"
},
"ControlPlaneView": {
"McpPage": {
"accessError": "Managed Control Plane does not have access information yet",
"componentsTitle": "Components",
"crossplaneTitle": "Crossplane",
Expand Down
9 changes: 5 additions & 4 deletions src/AppRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { HashRouter as Router, Navigate, Route } from 'react-router-dom';
import ControlPlaneView from './views/ControlPlanes/ControlPlaneView.tsx';
import ProjectListView from './views/ProjectList';
import ControlPlaneListView from './views/ControlPlanes/ControlPlaneListView.tsx';
import GlobalProviderOutlet from './components/Core/ApiConfigWrapper.tsx';
import { ShellBarComponent } from './components/Core/ShellBar.tsx';
import { SentryRoutes } from './mount.ts';
import ProjectPage from './spaces/onboarding/pages/ProjectPage.tsx';
import McpPage from './spaces/mcp/pages/McpPage.tsx';

function AppRouter() {
return (
Expand All @@ -14,13 +14,14 @@ function AppRouter() {
<SentryRoutes>
<Route path="/mcp" element={<GlobalProviderOutlet />}>
<Route path="projects" element={<ProjectListView />} />
<Route path="projects/:projectName" element={<ControlPlaneListView />} />
<Route path="projects/:projectName" element={<ProjectPage />} />
<Route
path="projects/:projectName/workspaces/:workspaceName/mcps/:controlPlaneName/context/:contextName"
element={<ControlPlaneView />}
element={<McpPage />}
/>
</Route>
<Route path="/" element={<Navigate to="/mcp/projects" />} />
<Route path="*" element={<Navigate to="/" />} />
</SentryRoutes>
</Router>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,31 @@
import { Button, FlexBox, IllustratedMessage } from '@ui5/webcomponents-react';
import IllustratedError from '../../Shared/IllustratedError.tsx';
import '@ui5/webcomponents-fiori/dist/illustrations/NoData.js';
import '@ui5/webcomponents-fiori/dist/illustrations/EmptyList.js';
import '@ui5/webcomponents-icons/dist/delete';
import Loading from '../../Shared/Loading.tsx';
import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
import { ControlPlaneListWorkspaceGridTile } from './ControlPlaneListWorkspaceGridTile.tsx';
import { useApiResource } from '../../../lib/api/useApiResource.ts';
import { ListWorkspaces } from '../../../lib/api/types/crate/listWorkspaces.ts';
import { ListWorkspacesType } from '../../../lib/api/types/crate/listWorkspaces.ts';
import { useLink } from '../../../lib/shared/useLink.ts';
import { useTranslation } from 'react-i18next';

interface Props {
projectName: string;
workspaces: ListWorkspacesType[];
}

export default function ControlPlaneListAllWorkspaces({ projectName }: Props) {
export default function ControlPlaneListAllWorkspaces({ projectName, workspaces }: Props) {
const { workspaceCreationGuide } = useLink();
const { data: allWorkspaces, error } = useApiResource(
ListWorkspaces(projectName),
);

const { t } = useTranslation();

if (!allWorkspaces) {
return <Loading />;
}
if (error) {
return <IllustratedError details={error.message} />;
}

return (
<>
{allWorkspaces.length === 0 ? (
{workspaces.length === 0 ? (
<FlexBox direction="Column" alignItems="Center">
<IllustratedMessage
name="EmptyList"
titleText={t('ControlPlaneListAllWorkspaces.emptyListTitleMessage')}
subtitleText={t(
'ControlPlaneListAllWorkspaces.emptyListSubtitleMessage',
)}
subtitleText={t('ControlPlaneListAllWorkspaces.emptyListSubtitleMessage')}
/>
<Button
design={ButtonDesign.Emphasized}
Expand All @@ -52,7 +38,7 @@ export default function ControlPlaneListAllWorkspaces({ projectName }: Props) {
</Button>
</FlexBox>
) : (
allWorkspaces.map((workspace) => (
workspaces.map((workspace) => (
<ControlPlaneListWorkspaceGridTile
key={`${projectName}-${workspace.metadata.name}`}
projectName={projectName}
Expand Down
30 changes: 0 additions & 30 deletions src/components/Core/DarkModeSystemSwitcher.tsx

This file was deleted.

21 changes: 21 additions & 0 deletions src/components/ThemeManager/ThemeManager.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, it, expect } from 'vitest';
import { resolveTheme } from './ThemeManager.tsx';

describe('ThemeManager', () => {
describe('resolveTheme()', () => {
it('returns theme coming from URL when it is truthy', () => {
expect(resolveTheme('sap_fiori_3', true)).toBe('sap_fiori_3');
expect(resolveTheme('custom_theme', false)).toBe('custom_theme');
});

it('falls back to dark default when URL theme is falsy and user prefers dark mode', () => {
expect(resolveTheme(null, true)).toBe('sap_horizon_dark');
expect(resolveTheme('', true)).toBe('sap_horizon_dark');
});

it('falls back to light default when URL theme is falsy and user prefers light mode', () => {
expect(resolveTheme(null, false)).toBe('sap_horizon');
expect(resolveTheme('', false)).toBe('sap_horizon');
});
});
});
25 changes: 25 additions & 0 deletions src/components/ThemeManager/ThemeManager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useEffect } from 'react';
import { setTheme } from '@ui5/webcomponents-base/dist/config/Theme.js';
import { useIsDarkModePreferred } from '../../hooks/useIsDarkModePreferred.ts';

const DEFAULT_THEME_LIGHT = 'sap_horizon';
const DEFAULT_THEME_DARK = 'sap_horizon_dark';

export function resolveTheme(themeFromUrl: string | null, isDarkModePreferred: boolean): string {
if (themeFromUrl) {
return themeFromUrl;
}
return isDarkModePreferred ? DEFAULT_THEME_DARK : DEFAULT_THEME_LIGHT;
}

export function ThemeManager() {
const isDarkModePreferred = useIsDarkModePreferred();
const themeFromUrl = new URL(window.location.href).searchParams.get('sap-theme');

useEffect(() => {
const resolvedTheme = resolveTheme(themeFromUrl, isDarkModePreferred);
void setTheme(resolvedTheme);
}, [isDarkModePreferred, themeFromUrl]);

return null;
}
18 changes: 13 additions & 5 deletions src/components/Ui/IllustratedBanner/IllustratedBanner.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ describe('<IllustratedBanner />', () => {
cy.contains('Test subtitle').should('be.visible');
});

it('renders subtitle nodes', () => {
cy.mount(
<IllustratedBanner
title="Test title"
subtitle={<button>Button as subtitle</button>}
illustrationName={IllustrationMessageType.NoData}
/>,
);

cy.get('button').contains('Button as subtitle').should('be.visible');
});

it('renders help button with correct text and icon', () => {
cy.mount(
<IllustratedBanner
Expand All @@ -30,11 +42,7 @@ describe('<IllustratedBanner />', () => {
);

cy.get('ui5-button').contains('Need Help?').should('be.visible');
cy.get('ui5-button').should(
'have.attr',
'icon',
'sap-icon://question-mark',
);
cy.get('ui5-button').should('have.attr', 'icon', 'sap-icon://question-mark');
});

it('renders a link with correct attributes', () => {
Expand Down
23 changes: 15 additions & 8 deletions src/components/Ui/IllustratedBanner/IllustratedBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,50 @@
import IllustrationMessageDesign from '@ui5/webcomponents-fiori/dist/types/IllustrationMessageDesign.js';
import IllustrationMessageType from '@ui5/webcomponents-fiori/dist/types/IllustrationMessageType.js';
import { FlexBox, IllustratedMessage, Button } from '@ui5/webcomponents-react';
import { FlexBox, IllustratedMessage, Button, UI5WCSlotsNode } from '@ui5/webcomponents-react';
import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js';
import '@ui5/webcomponents-fiori/dist/illustrations/AllIllustrations.js';
import { ReactElement } from 'react';

type InfoBannerProps = {
title: string;
subtitle: string;
subtitle: string | UI5WCSlotsNode;
illustrationName: IllustrationMessageType; // e.g. 'NoData', 'SimpleError', etc.
help?: {
link: string;
buttonText: string;
buttonIcon?: string;
};
button?: React.ReactElement;
button?: ReactElement;
};

export const IllustratedBanner = ({
title,
subtitle,
subtitle: subtitleProp,
illustrationName,
help,
button,
}: InfoBannerProps) => {
let subtitleText, subtitleNode;
if (typeof subtitleProp === 'string') {
subtitleText = subtitleProp;
} else {
subtitleNode = subtitleProp;
}

return (
<FlexBox direction="Column" alignItems="Center">
<IllustratedMessage
design={IllustrationMessageDesign.Scene}
name={illustrationName}
titleText={title}
subtitleText={subtitle}
subtitleText={subtitleText}
subtitle={subtitleNode}
/>
{help && (
<a href={help.link} target="_blank" rel="noreferrer">
<Button
design={ButtonDesign.Transparent}
icon={
help.buttonIcon ? help.buttonIcon : 'sap-icon://question-mark'
}
icon={help.buttonIcon ? help.buttonIcon : 'sap-icon://question-mark'}
>
{help.buttonText}
</Button>
Expand Down
18 changes: 18 additions & 0 deletions src/components/Ui/NotFoundBanner/NotFoundBanner.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import '@ui5/webcomponents-fiori/dist/illustrations/AllIllustrations.js';
import { NotFoundBanner } from './NotFoundBanner.tsx';
import { MemoryRouter } from 'react-router-dom';

describe('<NotFoundBanner />', () => {
it('renders title and subtitle interpolating the entityType', () => {
cy.mount(
<MemoryRouter>
<NotFoundBanner entityType="%entityType%" />
</MemoryRouter>,
);

cy.contains('%entityType% not found').should('be.visible');
cy.contains('Sorry, we couldn’t find what you are looking for').should('be.visible');

cy.get('ui5-button').contains('Back to Homepage');
});
});
9 changes: 9 additions & 0 deletions src/components/Ui/NotFoundBanner/NotFoundBanner.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.subtitleContainer {
display: flex;
flex-direction: column;
}

.button {
margin-inline: auto;
margin-block: 2rem;
}
32 changes: 32 additions & 0 deletions src/components/Ui/NotFoundBanner/NotFoundBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { IllustratedBanner } from '../IllustratedBanner/IllustratedBanner.tsx';
import IllustrationMessageType from '@ui5/webcomponents-fiori/dist/types/IllustrationMessageType.js';
import { Trans, useTranslation } from 'react-i18next';

import styles from './NotFoundBanner.module.css';
import { Button } from '@ui5/webcomponents-react';
import { useNavigate } from 'react-router-dom';

export interface NotFoundBannerProps {
entityType: string;
}
export function NotFoundBanner({ entityType }: NotFoundBannerProps) {
const { t } = useTranslation();
const navigate = useNavigate();

return (
<IllustratedBanner
illustrationName={IllustrationMessageType.PageNotFound}
title={t('NotFoundBanner.titleMessage', { entityType })}
subtitle={
<div className={styles.subtitleContainer}>
<span>
<Trans i18nKey="NotFoundBanner.subtitleMessage" values={{ entityType }} />
</span>
<Button className={styles.button} onClick={() => navigate('/')}>
{t('NotFoundBanner.navigateHome')}
</Button>
</div>
}
/>
);
}
Loading
Loading