diff --git a/cypress/screenshots/Dialogs/DeleteConfirmationDialog.test.tsx/DeleteConfirmationDialog -- should display correct resource name in all labels (failed).png b/cypress/screenshots/Dialogs/DeleteConfirmationDialog.test.tsx/DeleteConfirmationDialog -- should display correct resource name in all labels (failed).png deleted file mode 100644 index c72623fe..00000000 Binary files a/cypress/screenshots/Dialogs/DeleteConfirmationDialog.test.tsx/DeleteConfirmationDialog -- should display correct resource name in all labels (failed).png and /dev/null differ diff --git a/public/locales/en.json b/public/locales/en.json index 8637500f..b65bb204 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -67,7 +67,7 @@ "menuDownload": "Download", "menuCopy": "Copy to clipboard" }, - "NoManagedControlPlaneBanner": { + "IllustratedBanner": { "titleMessage": "No ManagedControlPlane", "subtitleMessage": "Create a ManagedControlPlane to get started", "helpButton": "Help" @@ -75,6 +75,9 @@ "IntelligentBreadcrumbs": { "homeLabel": "Home" }, + "MCPContext": { + "errorMessage": "An unknown error occurred" + }, "NotInLuigiView": { "titleMessage": "Opened outside of Hyperspace Portal", "subtitleMessage": "Looks like this page is not opened inside of the Hyperspace Portal. Contact admins for help." diff --git a/src/components/ControlPlane/FluxList.tsx b/src/components/ControlPlane/FluxList.tsx index 91bc0a34..4897ea50 100644 --- a/src/components/ControlPlane/FluxList.tsx +++ b/src/components/ControlPlane/FluxList.tsx @@ -44,7 +44,7 @@ export default function FluxList() { if (repoErr || kustomizationErr) { return ( ); diff --git a/src/components/ControlPlane/ManagedResources.tsx b/src/components/ControlPlane/ManagedResources.tsx index c2fcbf05..0fb6d72b 100644 --- a/src/components/ControlPlane/ManagedResources.tsx +++ b/src/components/ControlPlane/ManagedResources.tsx @@ -109,7 +109,7 @@ export function ManagedResources() { <> {t('ManagedResources.header')} - {error && } + {error && } {!error && ( {t('Providers.headerProviders')} - {error && } + {error && } {!error && ( ; } if (error) { - return ; + return ; } return ( diff --git a/src/components/ControlPlanes/List/ControlPlaneListWorkspaceGridTile.tsx b/src/components/ControlPlanes/List/ControlPlaneListWorkspaceGridTile.tsx index 5ca9190e..5eee5d0d 100644 --- a/src/components/ControlPlanes/List/ControlPlaneListWorkspaceGridTile.tsx +++ b/src/components/ControlPlanes/List/ControlPlaneListWorkspaceGridTile.tsx @@ -10,7 +10,6 @@ import '@ui5/webcomponents-fiori/dist/illustrations/NoData.js'; import '@ui5/webcomponents-fiori/dist/illustrations/EmptyList.js'; import '@ui5/webcomponents-icons/dist/delete'; import { CopyButton } from '../../Shared/CopyButton.tsx'; -import { NoManagedControlPlaneBanner } from '../NoManagedControlPlaneBanner.tsx'; import { ControlPlaneCard } from '../ControlPlaneCard/ControlPlaneCard.tsx'; import { ListWorkspacesType, @@ -35,6 +34,9 @@ import IllustratedError from '../../Shared/IllustratedError.tsx'; import { APIError } from '../../../lib/api/error.ts'; import { useTranslation } from 'react-i18next'; import { YamlViewButton } from '../../Yaml/YamlViewButton.tsx'; +import { IllustratedBanner } from '../../Ui/IllustratedBanner/IllustratedBanner.tsx'; +import { useFrontendConfig } from '../../../context/FrontendConfigContext.tsx'; +import IllustrationMessageType from '@ui5/webcomponents-fiori/dist/types/IllustrationMessageType.js'; interface Props { projectName: string; @@ -62,6 +64,8 @@ export function ControlPlaneListWorkspaceGridTile({ const { trigger } = useApiResourceMutation( DeleteWorkspaceResource(projectNamespace, workspaceName), ); + + const { links } = useFrontendConfig(); const errorView = createErrorView(cpsError); function createErrorView(error: APIError) { @@ -72,7 +76,7 @@ export function ControlPlaneListWorkspaceGridTile({ title={t( 'ControlPlaneListWorkspaceGridTile.permissionErrorMessage', )} - subtitleText={t( + details={t( 'ControlPlaneListWorkspaceGridTile.permissionErrorMessageSubtitle', )} /> @@ -145,20 +149,26 @@ export function ControlPlaneListWorkspaceGridTile({ > {errorView ? ( errorView + ) : controlplanes?.length === 0 ? ( + ) : ( - {controlplanes?.length === 0 ? ( - - ) : ( - controlplanes?.map((cp) => ( - - )) - )} + {controlplanes?.map((cp) => ( + + ))} )} diff --git a/src/components/ControlPlanes/NoManagedControlPlaneBanner.tsx b/src/components/ControlPlanes/NoManagedControlPlaneBanner.tsx deleted file mode 100644 index 523063ef..00000000 --- a/src/components/ControlPlanes/NoManagedControlPlaneBanner.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Button, FlexBox, IllustratedMessage } from '@ui5/webcomponents-react'; - -import '@ui5/webcomponents-fiori/dist/illustrations/NoData.js'; -import '@ui5/webcomponents-fiori/dist/illustrations/EmptyList.js'; -import '@ui5/webcomponents-icons/dist/delete'; -import IllustrationMessageDesign from '@ui5/webcomponents-fiori/dist/types/IllustrationMessageDesign.js'; -import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js'; -import { useFrontendConfig } from '../../context/FrontendConfigContext'; -import { useTranslation } from 'react-i18next'; - -export function NoManagedControlPlaneBanner() { - const { links } = useFrontendConfig(); - const { t } = useTranslation(); - - return ( - - - - - ); -} diff --git a/src/components/Projects/ProjectChooser.tsx b/src/components/Projects/ProjectChooser.tsx index 537d9391..a06b5f18 100644 --- a/src/components/Projects/ProjectChooser.tsx +++ b/src/components/Projects/ProjectChooser.tsx @@ -14,7 +14,7 @@ export default function ProjectChooser({ currentProjectName }: Props) { const navigate = useLuigiNavigate(); if (error) { - return ; + return ; } return ( diff --git a/src/components/Projects/ProjectsList.tsx b/src/components/Projects/ProjectsList.tsx index 9314fb33..2f14307b 100644 --- a/src/components/Projects/ProjectsList.tsx +++ b/src/components/Projects/ProjectsList.tsx @@ -77,7 +77,7 @@ export default function ProjectsList() { [], ); if (error) { - return ; + return ; } return ( diff --git a/src/components/Shared/IllustratedError.tsx b/src/components/Shared/IllustratedError.tsx index 2c41418b..701e7528 100644 --- a/src/components/Shared/IllustratedError.tsx +++ b/src/components/Shared/IllustratedError.tsx @@ -1,28 +1,21 @@ -import { IllustratedMessage } from '@ui5/webcomponents-react'; import '@ui5/webcomponents-fiori/dist/illustrations/SimpleError'; -import IllustrationMessageDesign from '@ui5/webcomponents-fiori/dist/types/IllustrationMessageDesign.js'; import { useTranslation } from 'react-i18next'; +import { IllustratedBanner } from '../Ui/IllustratedBanner/IllustratedBanner'; +import IllustrationMessageType from '@ui5/webcomponents-fiori/dist/types/IllustrationMessageType.js'; interface Props { title?: string; - subtitleText?: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - error?: any; + details?: string; } -export default function IllustratedError({ - title, - subtitleText, - error, -}: Props) { +export default function IllustratedError({ title, details }: Props) { const { t } = useTranslation(); return ( - ); } diff --git a/src/components/Ui/IllustratedBanner/IllustratedBanner.test.tsx b/src/components/Ui/IllustratedBanner/IllustratedBanner.test.tsx new file mode 100644 index 00000000..08f13371 --- /dev/null +++ b/src/components/Ui/IllustratedBanner/IllustratedBanner.test.tsx @@ -0,0 +1,60 @@ +import IllustrationMessageType from '@ui5/webcomponents-fiori/dist/types/IllustrationMessageType.js'; +import { IllustratedBanner } from './IllustratedBanner'; +import '@ui5/webcomponents-fiori/dist/illustrations/AllIllustrations.js'; + +describe('', () => { + it('renders title and subtitle', () => { + cy.mount( + , + ); + + cy.contains('Test title').should('be.visible'); + cy.contains('Test subtitle').should('be.visible'); + }); + + it('renders help button with correct text and icon', () => { + cy.mount( + , + ); + + cy.get('ui5-button').contains('Need Help?').should('be.visible'); + cy.get('ui5-button').should( + 'have.attr', + 'icon', + 'sap-icon://question-mark', + ); + }); + + it('renders a link with correct attributes', () => { + cy.mount( + , + ); + + cy.get('a') + .should('have.attr', 'href', 'https://example.com') + .and('have.attr', 'target', '_blank') + .and('have.attr', 'rel', 'noreferrer'); + + cy.get('a').contains('Go'); + }); +}); diff --git a/src/components/Ui/IllustratedBanner/IllustratedBanner.tsx b/src/components/Ui/IllustratedBanner/IllustratedBanner.tsx new file mode 100644 index 00000000..2121a736 --- /dev/null +++ b/src/components/Ui/IllustratedBanner/IllustratedBanner.tsx @@ -0,0 +1,46 @@ +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 ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js'; +import '@ui5/webcomponents-fiori/dist/illustrations/AllIllustrations.js'; + +type InfoBannerProps = { + title: string; + subtitle: string; + illustrationName: IllustrationMessageType; // e.g. 'NoData', 'SimpleError', etc. + help?: { + link: string; + buttonText: string; + buttonIcon?: string; + }; +}; + +export const IllustratedBanner = ({ + title, + subtitle, + illustrationName, + help, +}: InfoBannerProps) => { + return ( + + + {help && ( + + + + )} + + ); +}; diff --git a/src/components/Yaml/YamlLoader.tsx b/src/components/Yaml/YamlLoader.tsx index 2b7c64d6..9107b113 100644 --- a/src/components/Yaml/YamlLoader.tsx +++ b/src/components/Yaml/YamlLoader.tsx @@ -21,7 +21,7 @@ export const YamlLoader: FC = ({ const { t } = useTranslation(); if (isLoading) return ; if (error) { - return ; + return ; } return ( diff --git a/src/context/AuthProviderOnboarding.tsx b/src/context/AuthProviderOnboarding.tsx index 570e3cbb..42e86670 100644 --- a/src/context/AuthProviderOnboarding.tsx +++ b/src/context/AuthProviderOnboarding.tsx @@ -1,9 +1,7 @@ import { ReactNode } from 'react'; -import { AuthProvider } from 'react-oidc-context'; +import { AuthProvider, AuthProviderProps } from 'react-oidc-context'; import { OIDCConfig, useFrontendConfig } from './FrontendConfigContext.tsx'; -import { WebStorageStateStore } from "oidc-client-ts"; -import { AuthProviderProps } from "react-oidc-context"; - +import { WebStorageStateStore } from 'oidc-client-ts'; interface AuthProviderOnboardingProps { children?: ReactNode; @@ -33,4 +31,4 @@ function buildAuthProviderConfig(oidcConfig: OIDCConfig) { }, }; return props; -} \ No newline at end of file +} diff --git a/src/context/FrontendConfigContext.tsx b/src/context/FrontendConfigContext.tsx index 3005076f..d10f4018 100644 --- a/src/context/FrontendConfigContext.tsx +++ b/src/context/FrontendConfigContext.tsx @@ -10,8 +10,6 @@ export enum Landscape { Local = 'LOCAL', } - - interface FrontendConfigContextType extends FrontendConfig { links: DocLinkCreator; } @@ -19,7 +17,9 @@ interface FrontendConfigContextType extends FrontendConfig { export const FrontendConfigContext = createContext(null); -const fetchPromise = fetch('/frontend-config.json').then((res) => res.json()).then((data) => validateAndCastFrontendConfig(data)); +const fetchPromise = fetch('/frontend-config.json') + .then((res) => res.json()) + .then((data) => validateAndCastFrontendConfig(data)); interface FrontendConfigProviderProps { children: ReactNode; @@ -72,4 +72,4 @@ function validateAndCastFrontendConfig(config: unknown): FrontendConfig { } catch (error) { throw new Error(`Invalid frontend config: ${error}`); } -} \ No newline at end of file +} diff --git a/src/lib/shared/McpContext.tsx b/src/lib/shared/McpContext.tsx index 39adf187..d6cd1431 100644 --- a/src/lib/shared/McpContext.tsx +++ b/src/lib/shared/McpContext.tsx @@ -8,7 +8,6 @@ import { import { ControlPlane as ManagedControlPlaneResource } from '../api/types/crate/controlPlanes.ts'; import { GetAuthPropsForContextName } from '../oidc/shared.ts'; import { AuthProvider, hasAuthParams, useAuth } from 'react-oidc-context'; -import IllustratedError from '../../components/Shared/IllustratedError.tsx'; import { ApiConfigContext, ApiConfigProvider, @@ -118,16 +117,12 @@ export function WithinManagedControlPlane({ }) { const mcp = useContext(McpContext); - try { - const authprops = GetAuthPropsForContextName(mcp.context, mcp.kubeconfig!); - return ( - <> - - {children} - - - ); - } catch (e) { - return ; - } + const authprops = GetAuthPropsForContextName(mcp.context, mcp.kubeconfig!); + return ( + <> + + {children} + + + ); } diff --git a/src/main.tsx b/src/main.tsx index 75f14724..5dbbee76 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -12,12 +12,22 @@ import { DarkModeSystemSwitcher } from './components/Core/DarkModeSystemSwitcher import '.././i18n.ts'; import './utils/i18n/timeAgo'; import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; -import IllustratedError from './components/Shared/IllustratedError.tsx'; import { AuthProviderOnboarding } from './context/AuthProviderOnboarding.tsx'; import { ApolloClientProvider } from './spaces/onboarding/services/ApolloClientProvider/ApolloClientProvider.tsx'; +import { IllustratedBanner } from './components/Ui/IllustratedBanner/IllustratedBanner.tsx'; +import { useTranslation } from 'react-i18next'; +import IllustrationMessageType from '@ui5/webcomponents-fiori/dist/types/IllustrationMessageType.js'; const ErrorFallback = ({ error }: FallbackProps) => { - return ; + const { t } = useTranslation(); + + return ( + + ); }; const rootElement = document.getElementById('root'); diff --git a/src/views/ControlPlanes/ControlPlaneView.tsx b/src/views/ControlPlanes/ControlPlaneView.tsx index 2aa03645..efe9c3c3 100644 --- a/src/views/ControlPlanes/ControlPlaneView.tsx +++ b/src/views/ControlPlanes/ControlPlaneView.tsx @@ -46,14 +46,14 @@ export default function ControlPlaneView() { } if (error) { - return ; + return ; } if ( !mcp?.status?.access?.key || !mcp?.status?.access?.name || !mcp?.status?.access?.namespace ) { - return ; + return ; } return (