Skip to content
Merged
69 changes: 63 additions & 6 deletions web/src/assets/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
--agm-t--color--jungle: #30ba78;
--agm-t--color--jungle--hover: #c0efde;
--agm-t--color--fog: #efefef;
--agm-t--color--midnight: #192072;
--agm-t--color--waterhole: #2453ff;

--agm-t--action--background--color--hover: #c0efde; // #eafaf4;
--agm-t--action--background--color--hover: #eafaf4;

--pf-t--global--background--color--secondary--default: var(--agm-t--color--fog);
--pf-t--global--background--color--action--plain--hover: var(
Expand All @@ -50,14 +52,36 @@
--pf-t--global--font--size--600: 1.75rem;
--pf-t--global--font--size--700: 2rem;
--pf-t--global--font--size--800: 2.25rem;
--pf-t--global--font--weight--100: 400;
--pf-t--global--font--weight--200: 500;
--pf-t--global--font--weight--300: 500;
--pf-t--global--font--weight--400: 700;
--pf-t--global--font--weight--100: 100;
--pf-t--global--font--weight--200: 600;
--pf-t--global--font--weight--300: 700;
--pf-t--global--font--weight--400: 800;
--pf-t--global--color--status--warning--200: #fe7c3f;
--pf-t--global--color--status--info--default: var(--agm-t--color--waterhole);
--pf-t--global--icon--color--status--info--default: var(--agm-t--color--waterhole);
--pf-t--global--breakpoint--lg: 64rem;
--pf-t--global--font--size--body--sm: var(--pf-t--global--font--size--sm);
}

// Temporary CSS rules written during migration to PFv6

// Reserve the sidebar space also for "lg" breakpoint
@media (width >= 64rem) {
.pf-v6-c-page.agm-full-layout {
--pf-v6-c-page__sidebar--Width: 12rem; // var(--pf-v6-c-page__sidebar--xl--Width);
grid-template-areas:
"header header"
"sidebar main ";
grid-template-columns: var(--pf-v6-c-page__sidebar--Width) 1fr;
}

.pf-v6-c-page__sidebar.pf-m-collapsed + .pf-v6-c-page__drawer {
--pf-v6-c-page__main-container--GridArea: var(
--pf-v6-c-page--masthead--main-container--GridArea
);
}
}

#productSelectionForm img {
max-inline-size: 100px;
}
Expand Down Expand Up @@ -95,6 +119,20 @@
}
}

.agm-issues-drawer-body {
padding: var(--pf-t--global--spacer--lg);

h4 a {
text-decoration: underline;
font-weight: var(--pf-t--global--font--weight--400);
}

ul li.pf-m-info,
ul li.pf-m-warning {
--pf-v6-c-notification-drawer__list-item--before--BackgroundColor: none;
}
}

// PatternFly overrides

.pf-v6-c-masthead__main {
Expand All @@ -105,13 +143,32 @@
--pf-v6-c-masthead__logo--Width: fit-content;
}

.pf-v6-c-alert {
--pf-v6-c-alert--m-info__title--Color: var(--agm-t--color--waterhole);
--pf-v6-c-alert__icon--FontSize: var(--pf-t--global--font--size--lg);
// --pf-v6-c-alert--BoxShadow: var(--pf-t--global--box-shadow--sm);
--pf-v6-c-alert--BoxShadow: none;
--pf-v6-c-alert--PaddingBlockStart: var(--pf-t--global--spacer--sm);
--pf-v6-c-alert--PaddingBlockEnd: var(--pf-t--global--spacer--sm);
--pf-v6-c-alert--PaddingInlineStart: var(--pf-t--global--spacer--md);
--pf-v6-c-alert--PaddingInlineEnd: var(--pf-t--global--spacer--md);

&:has(.pf-v6-c-alert__description) {
row-gap: var(--pf-t--global--spacer--sm);
}
}

.pf-v6-c-alert__title {
font-size: var(--pf-t--global--font--size--lg);
}

.pf-v6-c-menu {
--pf-v6-c-menu--BoxShadow: var(--pf-t--global--box-shadow--lg);
// background: #eafaf8;
}

.pf-v6-c-menu__item.pf-m-selected {
background: #eafaf4;
--pf-v6-c-menu__item-select-icon--Color: var(--agm-t--color--jungle);
}

.pf-v6-c-nav__link {
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/core/IssuesDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const IssuesDrawer = forwardRef(({ onClose }: { onClose: () => void }, ref) => {
return (
<NotificationDrawer ref={ref}>
<NotificationDrawerHeader title={_("Pre-installation checks")} onClose={onClose} />
<NotificationDrawerBody className="agama-issues-drawer-body">
<NotificationDrawerBody className="agm-issues-drawer-body">
<Stack hasGutter>
<p>
{_(
Expand Down
64 changes: 47 additions & 17 deletions web/src/components/core/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,22 @@

import React, { useState } from "react";
import { Navigate } from "react-router-dom";
import { ActionGroup, Bullseye, Button, Card, Form, FormGroup } from "@patternfly/react-core";
import { EmptyState, FormValidationError, Page, PasswordInput } from "~/components/core";
import {
Bullseye,
Button,
Content,
Divider,
Flex,
Form,
FormGroup,
Stack,
} from "@patternfly/react-core";
import { FormValidationError, Page, PasswordInput } from "~/components/core";
import { AuthErrors, useAuth } from "~/context/auth";
import { _ } from "~/i18n";
import shadowUtils from "@patternfly/react-styles/css/utilities/BoxShadow/box-shadow";
import alignmentUtils from "@patternfly/react-styles/css/utilities/Alignment/alignment";
import { Icon } from "../layout";
import { sprintf } from "sprintf-js";

/**
Expand Down Expand Up @@ -69,15 +81,35 @@ user privileges.",
return (
<Page.Content>
<Bullseye>
<Card component="section">
{/** @ts-ignore */}
<EmptyState title={sectionTitle} icon="lock" color="color-info-200" variant="xl">
<p>
{rootExplanationStart} <b>{rootUser}</b> {rootExplanationEnd}
</p>
<p>{_("Please, provide its password to log in to the system.")}</p>
<Page.Section
pfCardProps={{
isCompact: false,
isFullHeight: false,
className: shadowUtils.boxShadowMd,
}}
>
<Stack hasGutter>
<Content component="h1" className={alignmentUtils.textAlignCenter}>
<Flex
alignItems={{ default: "alignItemsCenter" }}
direction={{ default: "column" }}
gap={{ default: "gapSm" }}
>
<Icon name="lock" width="3rem" height="3rem" />
{sectionTitle}
</Flex>
</Content>
<div>
<Content component="p">
{rootExplanationStart} <b>{rootUser}</b> {rootExplanationEnd}
</Content>
<Content component="p">
{_("Please, provide its password to log in to the system.")}
</Content>
</div>
<Divider />
<Form id="login" onSubmit={login} aria-label={_("Login form")}>
<FormGroup fieldId="password">
<FormGroup fieldId="password" label={_("Password")}>
<PasswordInput
id="password"
name="password"
Expand All @@ -89,14 +121,12 @@ user privileges.",

{error && <FormValidationError message={errorMessage(loginError)} />}

<ActionGroup>
<Button type="submit" variant="primary">
{_("Log in")}
</Button>
</ActionGroup>
<Button type="submit" variant="primary">
{_("Log in")}
</Button>
</Form>
</EmptyState>
</Card>
</Stack>
</Page.Section>
</Bullseye>
</Page.Content>
);
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/core/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,8 @@ const Content = ({ children, ...pageSectionProps }: React.PropsWithChildren<Page

return (
<>
{mountRegistrationAlert && <ProductRegistrationAlert />}
<PageSection hasBodyWrapper={false} isFilled component="div" {...pageSectionProps}>
{mountRegistrationAlert && <ProductRegistrationAlert />}
{children}
</PageSection>
</>
Expand Down
6 changes: 6 additions & 0 deletions web/src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export type HeaderProps = {
showInstallerOptions?: boolean;
/** Callback to be triggered for toggling the IssuesDrawer visibility */
toggleIssuesDrawer?: () => void;
isSidebarOpen?: boolean;
toggleSidebar?: () => void;
};

const OptionsDropdown = ({ showInstallerOptions }) => {
Expand Down Expand Up @@ -123,6 +125,8 @@ export default function Header({
showSidebarToggle = true,
showProductName = true,
toggleIssuesDrawer,
isSidebarOpen,
toggleSidebar,
}: HeaderProps): React.ReactNode {
const location = useLocation();
const { selectedProduct } = useProduct();
Expand All @@ -143,6 +147,8 @@ export default function Header({
{showSidebarToggle && (
<MastheadToggle>
<PageToggleButton
isSidebarOpen={isSidebarOpen}
onSidebarToggle={toggleSidebar}
id="uncontrolled-nav-toggle"
variant="plain"
aria-label={_("Main navigation")}
Expand Down
44 changes: 40 additions & 4 deletions web/src/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,33 @@
* find current contact information at www.suse.com.
*/

import React, { Suspense, useState } from "react";
import React, { Suspense, useRef, useState } from "react";
import { Outlet, useLocation } from "react-router-dom";
import { Masthead, Page, PageProps } from "@patternfly/react-core";
import { Questions } from "~/components/questions";
import Header, { HeaderProps } from "~/components/layout/Header";
import { Loading, Sidebar } from "~/components/layout";
import { IssuesDrawer } from "~/components/core";
import { ROOT } from "~/routes/paths";
import { agamaWidthBreakpoints, getBreakpoint } from "~/utils";

export type LayoutProps = React.PropsWithChildren<{
className?: string;
mountHeader?: boolean;
mountSidebar?: boolean;
headerOptions?: HeaderProps;
}>;

const focusDrawer = (drawer: HTMLElement | null) => {
if (drawer === null) return;

const firstTabbableItem = drawer.querySelector("a, button") as
| HTMLAnchorElement
| HTMLButtonElement
| null;
firstTabbableItem?.focus();
};

/**
* Component for laying out the application content inside a PF/Page that might
* or might not mount a header and a sidebar depending on the given props.
Expand All @@ -46,28 +58,43 @@ const Layout = ({
mountSidebar = true,
headerOptions = {},
children,
...props
}: LayoutProps) => {
const drawerRef = useRef();
const location = useLocation();
const [issuesDrawerVisible, setIssuesDrawerVisible] = useState<boolean>(false);
const closeIssuesDrawer = () => setIssuesDrawerVisible(false);
const toggleIssuesDrawer = () => setIssuesDrawerVisible(!issuesDrawerVisible);
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(true);
const [windowSize, setWindowSize] = useState<number>();

const onPageResize = (_, { windowSize: newWindowSize }: { windowSize: number }) => {
if (newWindowSize === windowSize) return;
setWindowSize(newWindowSize);
mountSidebar && setIsSidebarOpen(newWindowSize >= agamaWidthBreakpoints.lg);
};

const pageProps: Omit<PageProps, keyof React.HTMLProps<HTMLDivElement>> = {
isManagedSidebar: true,
};

if (mountSidebar) pageProps.sidebar = <Sidebar />;
if (mountSidebar) {
pageProps.sidebar = <Sidebar isManagedSidebar={false} isSidebarOpen={isSidebarOpen} />;
pageProps.isManagedSidebar = false;
}
if (mountHeader) {
pageProps.masthead = (
<Header
isSidebarOpen={isSidebarOpen}
toggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)}
showSidebarToggle={mountSidebar}
toggleIssuesDrawer={toggleIssuesDrawer}
{...headerOptions}
/>
);
// notificationDrawer is open/close from the header, it does not make sense
// to mount it if there is no header.
pageProps.notificationDrawer = <IssuesDrawer onClose={closeIssuesDrawer} />;
pageProps.notificationDrawer = <IssuesDrawer onClose={closeIssuesDrawer} ref={drawerRef} />;
pageProps.isNotificationDrawerExpanded = issuesDrawerVisible;
} else {
// FIXME: render an empty Masthead instead of nothing, in order to have
Expand All @@ -78,7 +105,14 @@ const Layout = ({

return (
<>
<Page isContentFilled {...pageProps} className="agm-layout">
<Page
onPageResize={onPageResize}
getBreakpoint={getBreakpoint}
isContentFilled
{...pageProps}
{...props}
onNotificationDrawerExpand={() => focusDrawer(drawerRef.current)}
>
<Suspense fallback={<Loading />}>{children || <Outlet />}</Suspense>
</Page>
{location.pathname !== ROOT.login && <Questions />}
Expand All @@ -88,6 +122,7 @@ const Layout = ({

/** Default props for FullLayout */
const fullProps: LayoutProps = {
className: "agm-full-layout",
mountHeader: true,
mountSidebar: true,
headerOptions: {
Expand All @@ -104,6 +139,7 @@ const Full = (props: LayoutProps) => <Layout {...fullProps} {...props} />;

/** Default props for PlainLayout */
const plainProps: LayoutProps = {
className: "agm-plain-layout",
mountHeader: true,
mountSidebar: false,
headerOptions: {
Expand Down
14 changes: 11 additions & 3 deletions web/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@

import React from "react";
import { NavLink } from "react-router-dom";
import { Nav, NavItem, NavList, PageSidebar, PageSidebarBody, Stack } from "@patternfly/react-core";
import {
Nav,
NavItem,
NavList,
PageSidebar,
PageSidebarBody,
PageSidebarProps,
Stack,
} from "@patternfly/react-core";
import { Icon } from "~/components/layout";
import { ChangeProductLink } from "~/components/core";
import { rootRoutes } from "~/router";
Expand Down Expand Up @@ -62,9 +70,9 @@ const MainNavigation = (): React.ReactNode => {
);
};

export default function Sidebar(): React.ReactNode {
export default function Sidebar(props: PageSidebarProps): React.ReactNode {
return (
<PageSidebar id="agama-sidebar">
<PageSidebar id="agama-sidebar" {...props}>
<PageSidebarBody isFilled>
<MainNavigation />
</PageSidebarBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe("ProductRegistrationAlert", () => {
screen.getByRole("heading", {
name: /Warning alert:.*must be registered/,
});
const link = screen.getByRole("link", { name: "Register it now" });
const link = screen.getByRole("link");
expect(link).toHaveAttribute("href", REGISTRATION.root);
});

Expand Down
Loading