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 = () => (
-
-
-

-
-
+
{children}
+
+
+
+

+
+
+
- { 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
+
+
+
+
+
-
+ >
);
}
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 };