diff --git a/web/jest.config.js b/web/jest.config.js index 4bc873f9a1..601503afbc 100644 --- a/web/jest.config.js +++ b/web/jest.config.js @@ -183,15 +183,16 @@ module.exports = { // A map from regular expressions to paths to transformers // transform: undefined, transform: { - "\\.jsx?$": "babel-jest", + "\\.m?jsx?$": "babel-jest", "\\.(css|svg)$": "jest-transform-stub" - } + }, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - // transformIgnorePatterns: [ - // "/node_modules/", - // "\\.pnp\\.[^\\/]+$" - // ], + transformIgnorePatterns: [ + // "/node_modules/", + // "\\.pnp\\.[^\\/]+$" + "/node_modules/(?!(react-teleporter)/)" + ], // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // unmockedModulePathPatterns: undefined, diff --git a/web/package-lock.json b/web/package-lock.json index 1e6e7f765b..0bea4232de 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -16,6 +16,7 @@ "react": "17.0.2", "react-dom": "17.0.2", "react-router-dom": "^6.3.0", + "react-teleporter": "^3.0.2", "regenerator-runtime": "^0.13.9" }, "devDependencies": { @@ -11171,6 +11172,19 @@ "react-dom": ">=16.8" } }, + "node_modules/react-teleporter": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/react-teleporter/-/react-teleporter-3.0.2.tgz", + "integrity": "sha512-6kxP/r01akC0NO/oWgz6bFJQFsDD0CSqKB6c+F3f2locfBUrDK+I8fic17W6idVL/Hv3ab72N2c3DyrL2+y4kQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/read": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/read/-/read-1.0.5.tgz", @@ -21509,6 +21523,12 @@ "react-router": "6.3.0" } }, + "react-teleporter": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/react-teleporter/-/react-teleporter-3.0.2.tgz", + "integrity": "sha512-6kxP/r01akC0NO/oWgz6bFJQFsDD0CSqKB6c+F3f2locfBUrDK+I8fic17W6idVL/Hv3ab72N2c3DyrL2+y4kQ==", + "requires": {} + }, "read": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/read/-/read-1.0.5.tgz", diff --git a/web/package.json b/web/package.json index fb5e26d4aa..7ecd344ece 100644 --- a/web/package.json +++ b/web/package.json @@ -68,6 +68,7 @@ "react": "17.0.2", "react-dom": "17.0.2", "react-router-dom": "^6.3.0", + "react-teleporter": "^3.0.2", "regenerator-runtime": "^0.13.9" } } diff --git a/web/src/App.jsx b/web/src/App.jsx index 461c959189..c4f471c314 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -23,14 +23,17 @@ import React, { useEffect, useReducer } from "react"; import { useInstallerClient } from "./context/installer"; import { Outlet } from "react-router-dom"; -import { PROBING, PROBED, INSTALLING, INSTALLED } from "./client/status"; - +import Layout, { Title, AdditionalInfo } from "./Layout"; +import About from "./About"; +import TargetIpsPopup from "./TargetIpsPopup"; import DBusError from "./DBusError"; import ProbingProgress from "./ProbingProgress"; import InstallationProgress from "./InstallationProgress"; import InstallationFinished from "./InstallationFinished"; import LoadingEnvironment from "./LoadingEnvironment"; +import { PROBING, PROBED, INSTALLING, INSTALLED } from "./client/status"; + const init = status => ({ loading: status === null, probing: status === PROBING, @@ -78,13 +81,26 @@ function App() { }); }, [client.monitor]); - if (state.dbusError) return ; - if (state.loading) return ; - if (state.probing) return ; - if (state.installing) return ; - if (state.finished) return ; + const Content = () => { + if (state.dbusError) return ; + if (state.loading) return ; + if (state.probing) return ; + if (state.installing) return ; + if (state.finished) return ; + + return ; + }; - return ; + return ( + + D-Installer + + + + + + + ); } export default App; diff --git a/web/src/DBusError.jsx b/web/src/DBusError.jsx index 6a01cf1cd9..e39d89cb83 100644 --- a/web/src/DBusError.jsx +++ b/web/src/DBusError.jsx @@ -22,11 +22,15 @@ import React from "react"; import { Button, Title, EmptyState, EmptyStateIcon, EmptyStateBody } from "@patternfly/react-core"; -import Layout from "./Layout"; +import { + Title as PageTitle, + PageIcon, + MainActions +} from "./Layout"; import Center from "./Center"; import { - EOS_ANNOUNCEMENT as SectionIcon, + EOS_ANNOUNCEMENT as Icon, EOS_ENDPOINTS_DISCONNECTED as DisconnectionIcon } from "eos-icons-react"; @@ -39,7 +43,11 @@ const ReloadAction = () => ( function DBusError() { return ( - + <> + D-Bus Error + + +
@@ -51,7 +59,7 @@ function DBusError() {
-
+ ); } diff --git a/web/src/InstallationFinished.jsx b/web/src/InstallationFinished.jsx index d71ee239ef..82acb9e821 100644 --- a/web/src/InstallationFinished.jsx +++ b/web/src/InstallationFinished.jsx @@ -30,7 +30,7 @@ import { EmptyStateSecondaryActions } from "@patternfly/react-core"; -import Layout from "./Layout"; +import { Title as SectionTitle, PageIcon, MainActions } from "./Layout"; import Center from "./Center"; import { useInstallerClient } from "./context/installer"; @@ -39,24 +39,20 @@ import { EOS_CHECK_CIRCLE as SectionIcon } from "eos-icons-react"; -const Actions = ({ onReboot }) => ( - <> - - -); - function InstallationFinished() { const client = useInstallerClient(); const onRebootAction = () => client.manager.rebootSystem(); return ( - } - > + <> + Installation Finished + + + + +
@@ -79,7 +75,7 @@ function InstallationFinished() {
-
+ ); } diff --git a/web/src/InstallationProgress.jsx b/web/src/InstallationProgress.jsx index 966df7063c..1ec495afc5 100644 --- a/web/src/InstallationProgress.jsx +++ b/web/src/InstallationProgress.jsx @@ -22,18 +22,18 @@ import React from "react"; import Center from "./Center"; -import Layout from "./Layout"; +import { Title, PageIcon } from "./Layout"; import ProgressReport from "./ProgressReport"; -import { EOS_DOWNLOADING as SectionIcon } from "eos-icons-react"; +import { EOS_DOWNLOADING as Icon } from "eos-icons-react"; function InstallationProgress() { return ( - -
- -
-
+ <> + Installing + +
+ ); } diff --git a/web/src/InstallationProgress.test.jsx b/web/src/InstallationProgress.test.jsx index c4b7e27f76..6a7b056b24 100644 --- a/web/src/InstallationProgress.test.jsx +++ b/web/src/InstallationProgress.test.jsx @@ -40,11 +40,4 @@ describe("InstallationProgress", () => { await screen.findByText("ProgressReport Mock"); }); - - it("does not show actions", async () => { - installerRender(); - - const button = screen.queryByRole("navigation", { name: /Installer Actions/i }); - expect(button).toBeNull(); - }); }); diff --git a/web/src/Layout.jsx b/web/src/Layout.jsx index a4e1812115..4e8acefa85 100644 --- a/web/src/Layout.jsx +++ b/web/src/Layout.jsx @@ -23,104 +23,139 @@ import React from "react"; import "./layout.scss"; import logo from "./assets/suse-horizontal-logo.svg"; +import { createTeleporter } from "react-teleporter"; -import About from "./About"; -import TargetIpsPopup from "./TargetIpsPopup"; +const PageTitle = createTeleporter(); +const HeaderActions = createTeleporter(); +const HeaderIcon = createTeleporter(); +const FooterActions = createTeleporter(); +const FooterInfoArea = createTeleporter(); /** * D-Installer main layout component. * - * It displays the content in a single vertical responsive column with sticky - * header and fixed footer. + * It displays the content in a single vertical responsive column with fixed + * header and footer. * * @example - * - * + * + * + * + * + * Dashboard + * + * + * + * + * + * + * * * @param {object} props - component props - * @param {React.ReactNode} [props.MenuIcon] - the icon for the application menu - * @param {string} [props.sectionTitle] - the section title in the header - * @param {React.ReactNode} [props.SectionIcon] - the section icon in the header - * @param {React.ReactNode} [props.FooterActions] - actions shown in the footer * @param {React.ReactNode} [props.children] - the section content * */ -function Layout({ MenuIcon, sectionTitle, SectionIcon, RightActions, FooterActions, children }) { +function Layout({ children }) { const responsiveWidthRules = "pf-u-w-66-on-lg pf-u-w-50-on-xl pf-u-w-33-on-2xl"; const className = `layout ${responsiveWidthRules}`; - // FIXME: by now, it is here only for illustrating a possible app/section menu - const renderHeaderLeftAction = () => { - // if (!SectionAction) - if (!MenuIcon) return null; - - return ( -
- -
- ); - }; - - const renderHeaderRightActions = () => { - // if (!SectionAction) - if (!RightActions) return null; - - return ( -
- -
- ); - }; - - const renderHeader = () => { - return ( + return ( +
- {renderHeaderLeftAction()} -

- {SectionIcon && } - {sectionTitle} + +

- {renderHeaderRightActions()} +
- ); - }; - const renderFooter = () => ( -
-
- Logo of SUSE - - +
{children}
+ +
+
+ Logo of SUSE + +
+
- { FooterActions && -
- -
}
); +} - return ( -
- {renderHeader()} +/** + * Component for setting the title shown at the header + * + * @example + * Partitioner + */ +const Title = PageTitle.Source; -
{children}
+/** + * Component for setting the icon shown at the header left + * + * @example + * import { PageIcon } from "dinstaller-layout"; + * import { FancyIcon } from "icons-package"; + * ... + * + */ +const PageIcon = HeaderIcon.Source; - {renderFooter()} -
- ); -} +/** + * Component for setting page actions shown on the header right + * + * @example + * import { PageActions } from "dinstaller-layout"; + * import { FancyButton } from "somewhere"; + * ... + * + * console.log("do something")} /> + * + */ +const PageActions = HeaderActions.Source; -export default Layout; +/** + * Component for setting the main actions shown on the footer right + * + * @example + * import { MainActions } from "dinstaller-layout"; + * import { FancyButton } from "somewhere"; + * ... + * + * console.log("do something")} /> + * + */ +const MainActions = FooterActions.Source; + +/** + * Component for setting the additional content shown at the footer + * + * @example + * import { AdditionaInfo } from "dinstaller-layout"; + * import { About, HostIp } from "somewhere"; + * + * ... + * + * + * console.log("show a pop-up with more information")} /> + * + * + */ +const AdditionalInfo = FooterInfoArea.Source; + +export { + Layout as default, + Title, + PageIcon, + PageActions, + MainActions, + AdditionalInfo +}; diff --git a/web/src/LoadingEnvironment.jsx b/web/src/LoadingEnvironment.jsx index 6e4180adf3..411ffb0ee5 100644 --- a/web/src/LoadingEnvironment.jsx +++ b/web/src/LoadingEnvironment.jsx @@ -22,23 +22,20 @@ import React from "react"; import { Title, EmptyState, EmptyStateIcon } from "@patternfly/react-core"; -import Layout from "./Layout"; import Center from "./Center"; import { EOS_THREE_DOTS_LOADING_ANIMATED as LoadingIcon } from "eos-icons-react"; function LoadingEnvironment({ text = "Loading installation environment, please wait." }) { return ( - -
- - - - { text } - - -
-
+
+ + + + { text } + + +
); } diff --git a/web/src/LuksActivationQuestion.jsx b/web/src/LuksActivationQuestion.jsx index 3e1fa3e357..040dd4eea9 100644 --- a/web/src/LuksActivationQuestion.jsx +++ b/web/src/LuksActivationQuestion.jsx @@ -47,7 +47,12 @@ export default function LuksActivationQuestion({ question, answerCallback }) { }; return ( - }> + } + > { renderAlert(question.attempt) } diff --git a/web/src/Overview.jsx b/web/src/Overview.jsx index 24d0ad3eb2..dadedfaf78 100644 --- a/web/src/Overview.jsx +++ b/web/src/Overview.jsx @@ -26,7 +26,7 @@ import { useNavigate } from "react-router-dom"; import { Button, Flex, FlexItem, Text } from "@patternfly/react-core"; -import Layout from "./Layout"; +import { Title, PageIcon, PageActions, MainActions } from "./Layout"; import Category from "./Category"; import LanguageSelector from "./LanguageSelector"; import Storage from "./Storage"; @@ -41,7 +41,7 @@ import { EOS_MODE_EDIT as ModeEditIcon } from "eos-icons-react"; -const RightActions = () => { +const ChangeProductButton = () => { const { products } = useSoftware(); const navigate = useNavigate(); @@ -119,14 +119,13 @@ function Overview() { }; return ( - + <> + {selectedProduct.name} + + + {renderCategories()} - + ); } diff --git a/web/src/ProbingProgress.jsx b/web/src/ProbingProgress.jsx index 626e926e5e..4de66ce64e 100644 --- a/web/src/ProbingProgress.jsx +++ b/web/src/ProbingProgress.jsx @@ -22,17 +22,17 @@ import React from "react"; import Center from "./Center"; -import Layout from "./Layout"; +import { Title, PageIcon } from "./Layout"; import ProgressReport from "./ProgressReport"; -import { EOS_MULTISTATE as SectionIcon } from "eos-icons-react"; +import { EOS_MULTISTATE as Icon } from "eos-icons-react"; const ProbingProgress = () => ( - -
- -
-
+ <> + Probing + +
+ ); export default ProbingProgress; diff --git a/web/src/ProbingProgress.test.jsx b/web/src/ProbingProgress.test.jsx index e813cf5558..f7d7f3a959 100644 --- a/web/src/ProbingProgress.test.jsx +++ b/web/src/ProbingProgress.test.jsx @@ -40,11 +40,4 @@ describe("ProbingProgress", () => { await screen.findByText("ProgressReport Mock"); }); - - it("does not show actions", async () => { - installerRender(); - - const button = screen.queryByRole("navigation", { name: /Installer Actions/i }); - expect(button).toBeNull(); - }); }); diff --git a/web/src/ProductSelectionPage.jsx b/web/src/ProductSelectionPage.jsx index 0f70a7e615..59dfc5bf6d 100644 --- a/web/src/ProductSelectionPage.jsx +++ b/web/src/ProductSelectionPage.jsx @@ -34,11 +34,9 @@ import { Radio } from "@patternfly/react-core"; -import { - EOS_PRODUCT_SUBSCRIPTIONS as SectionIcon, -} from "eos-icons-react"; +import { EOS_PRODUCT_SUBSCRIPTIONS as Icon } from "eos-icons-react"; -import Layout from "./Layout"; +import { Title, PageIcon, MainActions } from "./Layout"; function ProductSelectionPage() { const client = useInstallerClient(); @@ -66,14 +64,6 @@ function ProductSelectionPage() { .then(() => navigate("/")); }; - const SelectButton = () => { - return ( - - ); - }; - if (!products) return ( ); @@ -98,17 +88,21 @@ function ProductSelectionPage() { }; return ( - + <> + Product selection + + + + +
{buildOptions()}
-
+ ); } diff --git a/web/src/Questions.test.jsx b/web/src/Questions.test.jsx index 3437291ddb..365ff29fef 100644 --- a/web/src/Questions.test.jsx +++ b/web/src/Questions.test.jsx @@ -57,14 +57,14 @@ describe("Questions", () => { }); it("renders nothing", async () => { - const { container } = installerRender(); + const { container } = installerRender(, { usingLayout: false }); await waitFor(() => expect(container).toBeEmptyDOMElement()); }); }); describe("when a new question is added", () => { it("push it into the pending queue", async () => { - const { container } = installerRender(); + const { container } = installerRender(, { usingLayout: false }); expect(container).toBeEmptyDOMElement(); // Manually triggers the handler given for the onQuestionAdded signal @@ -80,7 +80,7 @@ describe("Questions", () => { }); it("removes it from the queue", async () => { - installerRender(); + installerRender(, { usingLayout: false }); await screen.findByText("A Generic question mock"); // Manually triggers the handler given for the onQuestionRemoved signal @@ -97,7 +97,7 @@ describe("Questions", () => { }); it("renders a GenericQuestion component", async () => { - installerRender(); + installerRender(, { usingLayout: false }); await screen.findByText("A Generic question mock"); }); @@ -109,7 +109,7 @@ describe("Questions", () => { }); it("renders a LuksActivationQuestion component", async () => { - installerRender(); + installerRender(, { usingLayout: false }); await screen.findByText("A LUKS activation question mock"); }); diff --git a/web/src/layout.scss b/web/src/layout.scss index dd4a77f130..449060dc4b 100644 --- a/web/src/layout.scss +++ b/web/src/layout.scss @@ -54,10 +54,7 @@ } } -.layout__header-left-action { -} - -.layout__header-right-actions { +.layout__header-section-actions { button { vertical-align: text-top; } @@ -79,7 +76,7 @@ font-size: 1.5rem; } -.layout__header-section-title-icon { +.layout__header-section-title-icon > svg { fill: white; // Sadly, we can't use font-size with EOS Icons block-size: 1em; diff --git a/web/src/test-utils.js b/web/src/test-utils.js index e989111920..5bc10fb893 100644 --- a/web/src/test-utils.js +++ b/web/src/test-utils.js @@ -24,23 +24,44 @@ import userEvent from "@testing-library/user-event"; import { render } from "@testing-library/react"; import { InstallerClientProvider } from "./context/installer"; +import Layout from "./Layout.jsx"; import { createClient } from "./client"; const InstallerProvider = ({ children }) => { const client = createClient(); return ( - {children} + + {children} + ); }; -const installerRender = (ui, options = {}) => ({ - user: userEvent.setup(), - ...render(ui, { wrapper: InstallerProvider, ...options }) -}); +const content = (ui, usingLayout) => { + if (!usingLayout) return ui; -const plainRender = (ui, options = {}) => ({ - user: userEvent.setup(), - ...render(ui, options) -}); + return {ui}; +}; + +const installerRender = (ui, options = { usingLayout: true }) => { + const { usingLayout, ...testingLibraryOptions } = options; + + return ( + { + user: userEvent.setup(), + ...render(content(ui, usingLayout), { wrapper: InstallerProvider, ...testingLibraryOptions }) + } + ); +}; + +const plainRender = (ui, options = { usingLayout: true }) => { + const { usingLayout, ...testingLibraryOptions } = options; + + return ( + { + user: userEvent.setup(), + ...render(content(ui, usingLayout), testingLibraryOptions) + } + ); +}; export { installerRender, plainRender };