diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml
index 2862c6f339..69794c4444 100644
--- a/.github/workflows/github-pages.yml
+++ b/.github/workflows/github-pages.yml
@@ -52,7 +52,7 @@ jobs:
run: cd web && npm ci
- name: Build Web UI documentation
- run: cd web && npm run typedoc:client && mv typedoc.out/ ../doc/dist/web-ui
+ run: cd web && npm run typedoc && mv typedoc.out/ ../doc/dist/web-ui
- name: Setup Pages
uses: actions/configure-pages@v3
diff --git a/web/package-lock.json b/web/package-lock.json
index 3d3ad4045e..24a40993f2 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -93,7 +93,6 @@
"ts-loader": "^9.5.1",
"tsconfig-paths-webpack-plugin": "^4.0.0",
"typedoc": "^0.27.4",
- "typedoc-plugin-external-module-map": "^2.1.0",
"typedoc-plugin-merge-modules": "^6.0.0",
"typedoc-plugin-missing-exports": "^3.0.0",
"typescript": "^5.7.2",
@@ -17658,36 +17657,6 @@
"typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x"
}
},
- "node_modules/typedoc-plugin-external-module-map": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/typedoc-plugin-external-module-map/-/typedoc-plugin-external-module-map-2.1.0.tgz",
- "integrity": "sha512-xw5nwrlNsfOLWcjUW6JhG55doxjLseH9UQwn3apsXhIeank5Ni2S6ffxeKavtCr8eDIyddal6QNwCraKa8xp4A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "^20.14.14"
- },
- "peerDependencies": {
- "typedoc": ">=0.26 <2.0"
- }
- },
- "node_modules/typedoc-plugin-external-module-map/node_modules/@types/node": {
- "version": "20.17.10",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz",
- "integrity": "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": "~6.19.2"
- }
- },
- "node_modules/typedoc-plugin-external-module-map/node_modules/undici-types": {
- "version": "6.19.8",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
- "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/typedoc-plugin-merge-modules": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/typedoc-plugin-merge-modules/-/typedoc-plugin-merge-modules-6.1.0.tgz",
diff --git a/web/package.json b/web/package.json
index 4a5ff44728..4bac7b46d0 100644
--- a/web/package.json
+++ b/web/package.json
@@ -96,7 +96,6 @@
"ts-loader": "^9.5.1",
"tsconfig-paths-webpack-plugin": "^4.0.0",
"typedoc": "^0.27.4",
- "typedoc-plugin-external-module-map": "^2.1.0",
"typedoc-plugin-merge-modules": "^6.0.0",
"typedoc-plugin-missing-exports": "^3.0.0",
"typescript": "^5.7.2",
diff --git a/web/src/components/core/ExpandableSelector.test.jsx b/web/src/components/core/ExpandableSelector.test.tsx
similarity index 96%
rename from web/src/components/core/ExpandableSelector.test.jsx
rename to web/src/components/core/ExpandableSelector.test.tsx
index bc1f776d71..9f1f9e6815 100644
--- a/web/src/components/core/ExpandableSelector.test.jsx
+++ b/web/src/components/core/ExpandableSelector.test.tsx
@@ -24,8 +24,13 @@ import React from "react";
import { screen, within } from "@testing-library/react";
import { plainRender } from "~/test-utils";
import { ExpandableSelector } from "~/components/core";
+import { ExpandableSelectorColumn } from "./ExpandableSelector";
-const sda = {
+let consoleErrorSpy: jest.SpyInstance;
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+const sda: any = {
sid: "59",
isDrive: true,
type: "disk",
@@ -112,18 +117,20 @@ const vg = {
lvs: [lv1],
};
-const columns = [
- { name: "Device", value: (item) => item.name },
+const columns: ExpandableSelectorColumn[] = [
+ // FIXME: do not use any but the right types once storage part is rewritten.
+ // Or even better, write a test not coupled to storage
+ { name: "Device", value: (item: any) => item.name },
{
name: "Content",
- value: (item) => {
+ value: (item: any) => {
if (item.isDrive) return item.systems.map((s, i) =>
{s}
);
if (item.type === "vg") return `${item.lvs.length} logical volume(s)`;
return item.content;
},
},
- { name: "Size", value: (item) => item.size },
+ { name: "Size", value: (item: any) => item.size },
];
const onChangeFn = jest.fn();
@@ -141,11 +148,12 @@ const commonProps = {
describe("ExpandableSelector", () => {
beforeAll(() => {
- jest.spyOn(console, "error").mockImplementation();
+ consoleErrorSpy = jest.spyOn(console, "error");
+ consoleErrorSpy.mockImplementation();
});
afterAll(() => {
- console.error.mockRestore();
+ consoleErrorSpy.mockRestore();
});
beforeEach(() => {
diff --git a/web/src/components/core/ExpandableSelector.jsx b/web/src/components/core/ExpandableSelector.tsx
similarity index 68%
rename from web/src/components/core/ExpandableSelector.jsx
rename to web/src/components/core/ExpandableSelector.tsx
index 37860fa459..b8c04f58fa 100644
--- a/web/src/components/core/ExpandableSelector.jsx
+++ b/web/src/components/core/ExpandableSelector.tsx
@@ -20,11 +20,10 @@
* find current contact information at www.suse.com.
*/
-// @ts-check
-
import React, { useState } from "react";
import {
Table,
+ TableProps,
Thead,
Tr,
Th,
@@ -34,10 +33,7 @@ import {
RowSelectVariant,
} from "@patternfly/react-table";
-/**
- * @typedef {import("@patternfly/react-table").TableProps} TableProps
- * @typedef {import("react").RefAttributes} HTMLTableProps
- */
+/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* An object for sharing data across nested maps
@@ -47,26 +43,48 @@ import {
* places, as it is the case of the rowIndex prop here.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions#passing_arguments
- *
- * @typedef {object} SharedData
- * @property {number} rowIndex - The current row index, to be incremented each time a table row is generated.
*/
-/**
- * @typedef {object} ExpandableSelectorColumn
- * @property {string} name - The column header text.
- * @property {(object) => React.ReactNode} value - A function receiving
- * the item to work with and returning the column value.
- * @property {string} [classNames] - space-separated list of additional CSS class names.
- */
+type SharedData = {
+ rowIndex: number;
+};
+
+export type ExpandableSelectorColumn = {
+ /** The column header text */
+ name: string;
+ /** A function receiving the item to work with and returns the column value */
+ value: (item: object) => React.ReactNode;
+ /** Space-separated list of additional CSS class names */
+ classNames?: string;
+};
+
+export type ExpandableSelectorProps = {
+ /** Collection of objects defining columns. */
+ columns?: ExpandableSelectorColumn[];
+ /** Whether multiple selection is allowed. */
+ isMultiple?: boolean;
+ /** Collection of items to be rendered. */
+ items?: object[];
+ /** The key for retrieving the item id. */
+ itemIdKey?: string;
+ /** Lookup method to retrieve children from given item. */
+ itemChildren?: (item: object) => object[];
+ /** Whether an item will be selectable or not. */
+ itemSelectable?: (item: object) => boolean;
+ /** Callback to add additional CSS class names to item row. */
+ itemClassNames?: (item: object) => string | undefined;
+ /** Collection of selected items. */
+ itemsSelected?: object[];
+ /** Ids of initially expanded items. */
+ initialExpandedKeys?: any[];
+ /** Callback to be triggered when selection changes. */
+ onSelectionChange?: (selection: object[]) => void;
+} & TableProps;
/**
* Internal component for building the table header
- *
- * @param {object} props
- * @param {ExpandableSelectorColumn[]} props.columns
*/
-const TableHeader = ({ columns }) => (
+const TableHeader = ({ columns }: { columns: ExpandableSelectorColumn[] }) => (
@@ -86,14 +104,14 @@ const TableHeader = ({ columns }) => (
* It logs information to console.error if given value does not match
* expectations.
*
- * @param {*} selection - The value to check.
- * @param {boolean} allowMultiple - Whether the returned collection can have
+ * @param selection - The value to check.
+ * @param allowMultiple - Whether the returned collection can have
* more than one item
- * @return {Array} Empty array if given value is not valid. The first element if
+ * @return Empty array if given value is not valid. The first element if
* it is a collection with more than one but selector does not allow multiple.
* The original value otherwise.
*/
-const sanitizeSelection = (selection, allowMultiple) => {
+const sanitizeSelection = (selection: any[], allowMultiple: boolean): any[] => {
if (!Array.isArray(selection)) {
console.error("`itemSelected` prop must be an array. Ignoring given value", selection);
return [];
@@ -117,20 +135,6 @@ const sanitizeSelection = (selection, allowMultiple) => {
*
* @note It only accepts one nesting level.
*
- * @typedef {object} ExpandableSelectorBaseProps
- * @property {ExpandableSelectorColumn[]} [columns=[]] - Collection of objects defining columns.
- * @property {boolean} [isMultiple=false] - Whether multiple selection is allowed.
- * @property {object[]} [items=[]] - Collection of items to be rendered.
- * @property {string} [itemIdKey="id"] - The key for retrieving the item id.
- * @property {(item: object) => Array} [itemChildren=() => []] - Lookup method to retrieve children from given item.
- * @property {(item: object) => boolean} [itemSelectable=() => true] - Whether an item will be selectable or not.
- * @property {(item: object) => (string|undefined)} [itemClassNames=() => ""] - Callback that allows adding additional CSS class names to item row.
- * @property {object[]} [itemsSelected=[]] - Collection of selected items.
- * @property {any[]} [initialExpandedKeys=[]] - Ids of initially expanded items.
- * @property {(selection: Array) => void} [onSelectionChange=noop] - Callback to be triggered when selection changes.
- *
- * @typedef {ExpandableSelectorBaseProps & TableProps & HTMLTableProps} ExpandableSelectorProps
- *
* @param {ExpandableSelectorProps} props
*/
export default function ExpandableSelector({
@@ -145,10 +149,10 @@ export default function ExpandableSelector({
initialExpandedKeys = [],
onSelectionChange,
...tableProps
-}) {
+}: ExpandableSelectorProps) {
const [expandedItemsKeys, setExpandedItemsKeys] = useState(initialExpandedKeys);
const selection = sanitizeSelection(itemsSelected, isMultiple);
- const isItemSelected = (item) => {
+ const isItemSelected = (item: object) => {
const selected = selection.find((selectionItem) => {
return (
Object.hasOwn(selectionItem, itemIdKey) && selectionItem[itemIdKey] === item[itemIdKey]
@@ -157,8 +161,8 @@ export default function ExpandableSelector({
return selected !== undefined || selection.includes(item);
};
- const isItemExpanded = (key) => expandedItemsKeys.includes(key);
- const toggleExpanded = (key) => {
+ const isItemExpanded = (key: string | number) => expandedItemsKeys.includes(key);
+ const toggleExpanded = (key: string | number) => {
if (isItemExpanded(key)) {
setExpandedItemsKeys(expandedItemsKeys.filter((k) => k !== key));
} else {
@@ -166,7 +170,7 @@ export default function ExpandableSelector({
}
};
- const updateSelection = (item) => {
+ const updateSelection = (item: object) => {
if (!isMultiple) {
onSelectionChange([item]);
return;
@@ -182,11 +186,11 @@ export default function ExpandableSelector({
/**
* Render method for building the markup for an item child
*
- * @param {object} item - The child to be rendered
- * @param {boolean} isExpanded - Whether the child should be shown or not
- * @param {SharedData} sharedData - An object holding shared data
+ * @param item - The child to be rendered
+ * @param isExpanded - Whether the child should be shown or not
+ * @param sharedData - An object holding shared data
*/
- const renderItemChild = (item, isExpanded, sharedData) => {
+ const renderItemChild = (item: object, isExpanded: boolean, sharedData: SharedData) => {
const rowIndex = sharedData.rowIndex++;
const selectProps = {
@@ -212,10 +216,10 @@ export default function ExpandableSelector({
/**
* Render method for building the markup for item
*
- * @param {object} item - The item to be rendered
- * @param {SharedData} sharedData - An object holding shared data
+ * @param item - The item to be rendered
+ * @param sharedData - An object holding shared data
*/
- const renderItem = (item, sharedData) => {
+ const renderItem = (item: object, sharedData: SharedData) => {
const itemKey = item[itemIdKey];
const rowIndex = sharedData.rowIndex++;
const children = itemChildren(item);
diff --git a/web/src/components/core/PasswordInput.test.jsx b/web/src/components/core/PasswordInput.test.tsx
similarity index 94%
rename from web/src/components/core/PasswordInput.test.jsx
rename to web/src/components/core/PasswordInput.test.tsx
index a0e872da6f..8437498107 100644
--- a/web/src/components/core/PasswordInput.test.jsx
+++ b/web/src/components/core/PasswordInput.test.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -24,7 +24,7 @@ import React, { useState } from "react";
import { screen } from "@testing-library/react";
import { plainRender } from "~/test-utils";
import userEvent from "@testing-library/user-event";
-import PasswordInput from "./PasswordInput";
+import PasswordInput, { PasswordInputProps } from "./PasswordInput";
import { _ } from "~/i18n";
describe("PasswordInput Component", () => {
@@ -58,7 +58,7 @@ describe("PasswordInput Component", () => {
// Using a controlled component for testing the rendered result instead of testing if
// the given onChange callback is called. The former is more aligned with the
// React Testing Library principles, https://testing-library.com/docs/guiding-principles/
- const PasswordInputTest = (props) => {
+ const PasswordInputTest = (props: PasswordInputProps) => {
const [password, setPassword] = useState(null);
return (
diff --git a/web/src/components/core/PasswordInput.jsx b/web/src/components/core/PasswordInput.tsx
similarity index 85%
rename from web/src/components/core/PasswordInput.jsx
rename to web/src/components/core/PasswordInput.tsx
index 4f31fa24c5..ee373c1def 100644
--- a/web/src/components/core/PasswordInput.jsx
+++ b/web/src/components/core/PasswordInput.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -20,29 +20,26 @@
* find current contact information at www.suse.com.
*/
-// @ts-check
-
import React, { useState } from "react";
-import { Button, InputGroup, TextInput } from "@patternfly/react-core";
+import { Button, InputGroup, TextInput, TextInputProps } from "@patternfly/react-core";
import { _ } from "~/i18n";
import { Icon } from "~/components/layout";
/**
- * @typedef {import("@patternfly/react-core").TextInputProps} TextInputProps
- *
* Props matching the {@link https://www.patternfly.org/components/forms/text-input PF/TextInput},
* except `type` that will be forced to 'password'.
- * @typedef {Omit & { inputRef?: React.Ref }} PasswordInputProps
*/
+export type PasswordInputProps = Omit & {
+ inputRef?: React.Ref;
+};
/**
* Renders a password input field and a toggle button that can be used to reveal
* and hide the password
* @component
*
- * @param {PasswordInputProps} props
*/
-export default function PasswordInput({ id, inputRef, ...props }) {
+export default function PasswordInput({ id, inputRef, ...props }: PasswordInputProps) {
const [showPassword, setShowPassword] = useState(false);
const visibilityIconName = showPassword ? "visibility_off" : "visibility";
diff --git a/web/src/components/core/Popup.test.jsx b/web/src/components/core/Popup.test.tsx
similarity index 92%
rename from web/src/components/core/Popup.test.jsx
rename to web/src/components/core/Popup.test.tsx
index 8bb003d8ec..4b29e1c098 100644
--- a/web/src/components/core/Popup.test.jsx
+++ b/web/src/components/core/Popup.test.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022] SUSE LLC
+ * Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -26,14 +26,15 @@ import { screen, within } from "@testing-library/react";
import { installerRender } from "~/test-utils";
import { Popup } from "~/components/core";
+import { PopupProps } from "./Popup";
-let isOpen;
-let isLoading;
+let isOpen: boolean;
+let isLoading: boolean;
const confirmFn = jest.fn();
const cancelFn = jest.fn();
const loadingText = "Loading text";
-const TestingPopup = (props) => {
+const TestingPopup = (props: PopupProps) => {
const [isMounted, setIsMounted] = useState(true);
if (!isMounted) return null;
@@ -64,7 +65,7 @@ describe("Popup", () => {
});
it("renders nothing", async () => {
- installerRender( );
+ installerRender(Testing );
const dialog = screen.queryByRole("dialog");
expect(dialog).toBeNull();
@@ -78,7 +79,7 @@ describe("Popup", () => {
});
it("renders the popup content inside a PF/Modal", async () => {
- installerRender( );
+ installerRender(Testing );
const dialog = await screen.findByRole("dialog");
expect(dialog.classList.contains("pf-v5-c-modal-box")).toBe(true);
@@ -87,7 +88,7 @@ describe("Popup", () => {
});
it("does not display a progress message", async () => {
- installerRender( );
+ installerRender(Testing );
const dialog = await screen.findByRole("dialog");
@@ -95,7 +96,7 @@ describe("Popup", () => {
});
it("renders the popup actions inside a PF/Modal footer", async () => {
- installerRender( );
+ installerRender(Testing );
const dialog = await screen.findByRole("dialog");
// NOTE: Sadly, PF Modal/ModalFooter does not have a footer or navigation role.
@@ -115,7 +116,7 @@ describe("Popup", () => {
});
it("displays progress message instead of the content", async () => {
- installerRender( );
+ installerRender(Testing );
const dialog = await screen.findByRole("dialog");
diff --git a/web/src/components/core/Popup.jsx b/web/src/components/core/Popup.tsx
similarity index 76%
rename from web/src/components/core/Popup.jsx
rename to web/src/components/core/Popup.tsx
index b11964c047..9135bfcb6c 100644
--- a/web/src/components/core/Popup.jsx
+++ b/web/src/components/core/Popup.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022] SUSE LLC
+ * Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -20,19 +20,24 @@
* find current contact information at www.suse.com.
*/
-// @ts-check
-
import React from "react";
-import { Button, Modal } from "@patternfly/react-core";
+import { Button, ButtonProps, Modal, ModalProps } from "@patternfly/react-core";
import { Loading } from "~/components/layout";
import { _ } from "~/i18n";
import { partition } from "~/utils";
-/**
- * @typedef {import("@patternfly/react-core").ModalProps} ModalProps
- * @typedef {import("@patternfly/react-core").ButtonProps} ButtonProps
- * @typedef {Omit} ButtonWithoutVariantProps
- */
+type ButtonWithoutVariantProps = Omit;
+type PredefinedAction = React.PropsWithChildren;
+export type PopupProps = {
+ /** The block/height size for the dialog. Default is "auto". */
+ blockSize?: "auto" | "small" | "medium" | "large";
+ /** The inline/width size for the dialog. Default is "medium". */
+ inlineSize?: "auto" | "small" | "medium" | "large";
+ /** Whether it should display a loading indicator instead of the requested content. */
+ isLoading?: boolean;
+ /** Text displayed when `isLoading` is set to `true` */
+ loadingText?: string;
+} & Omit;
/**
* Wrapper component for holding Popup actions
@@ -41,20 +46,18 @@ import { partition } from "~/utils";
* Popup.Action or PF/Button
*
* @see Popup examples.
- *
- * @param {object} props
- * @param {React.ReactNode} [props.children] - a collection of Action components
*/
-const Actions = ({ children }) => <>{children}>;
+const Actions = ({ children }: React.PropsWithChildren) => <>{children}>;
/**
* A convenient component representing a Popup action
*
* Built on top of {@link https://www.patternfly.org/components/button PF/Button}
*
- * @param {ButtonProps} props
*/
-const Action = ({ children, ...buttonProps }) => {children} ;
+const Action = ({ children, ...buttonProps }: React.PropsWithChildren) => (
+ {children}
+);
/**
* A Popup primary action
@@ -71,9 +74,8 @@ const Action = ({ children, ...buttonProps }) => {child
* Upload
*
*
- * @param {ButtonWithoutVariantProps} props
*/
-const PrimaryAction = ({ children, ...actionProps }) => (
+const PrimaryAction = ({ children, ...actionProps }: PredefinedAction) => (
{children}
@@ -88,9 +90,8 @@ const PrimaryAction = ({ children, ...actionProps }) => (
* @example Using it with a custom text
* Accept
*
- * @param {ButtonWithoutVariantProps} props
*/
-const Confirm = ({ children = _("Confirm"), ...actionProps }) => (
+const Confirm = ({ children = _("Confirm"), ...actionProps }: PredefinedAction) => (
{children}
@@ -110,10 +111,8 @@ const Confirm = ({ children = _("Confirm"), ...actionProps }) => (
*
* Dismiss
*
- *
- * @param {ButtonWithoutVariantProps} props
*/
-const SecondaryAction = ({ children, ...actionProps }) => (
+const SecondaryAction = ({ children, ...actionProps }: PredefinedAction) => (
{children}
@@ -127,10 +126,8 @@ const SecondaryAction = ({ children, ...actionProps }) => (
*
* @example Using it with a custom text
* Dismiss
- *
- * @param {ButtonWithoutVariantProps} props
*/
-const Cancel = ({ children = _("Cancel"), ...actionProps }) => (
+const Cancel = ({ children = _("Cancel"), ...actionProps }: PredefinedAction) => (
{children}
@@ -150,10 +147,8 @@ const Cancel = ({ children = _("Cancel"), ...actionProps }) => (
*
* Do not set
*
- *
- * @param {ButtonWithoutVariantProps} props
*/
-const AncillaryAction = ({ children, ...actionsProps }) => (
+const AncillaryAction = ({ children, ...actionsProps }: PredefinedAction) => (
{children}
@@ -194,15 +189,6 @@ const AncillaryAction = ({ children, ...actionsProps }) => (
*
*
*
- *
- * @typedef {object} PopupBaseProps
- * @property {"auto" | "small" | "medium" | "large"} [blockSize="auto"] - The block/height size for the dialog. Default is "auto".
- * @property {"auto" | "small" | "medium" | "large"} [inlineSize="medium"] - The inline/width size for the dialog. Default is "medium".
- * @property {boolean} [isLoading=false] - Whether the data is loading, if yes it displays a loading indicator instead of the requested content
- * @property {string} [loadingText="Loading data..."] - Text displayed when `isLoading` is set to `true`
- * @typedef {Omit & PopupBaseProps} PopupProps
- *
- * @param {PopupProps} props
*/
const Popup = ({
isOpen = false,
@@ -215,7 +201,7 @@ const Popup = ({
className = "",
children,
...props
-}) => {
+}: PopupProps) => {
const [actions, content] = partition(
React.Children.toArray(children),
(child) => child.type === Actions,
diff --git a/web/src/components/core/Section.jsx b/web/src/components/core/Section.jsx
deleted file mode 100644
index 233d570b75..0000000000
--- a/web/src/components/core/Section.jsx
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (c) [2022-2024] SUSE LLC
- *
- * All Rights Reserved.
- *
- * This program is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
- * more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, contact SUSE LLC.
- *
- * To contact SUSE LLC about this file by physical or electronic mail, you may
- * find current contact information at www.suse.com.
- */
-
-// FIXME: Refactor or replace
-
-import React from "react";
-import { Link } from "react-router-dom";
-import { PageSection, Stack } from "@patternfly/react-core";
-import { Icon } from "~/components/layout";
-/**
- * @typedef {import("~/components/layout/Icon").IconName} IconName
- */
-
-/**
- * Renders children into an HTML section
- * @component
- *
- * @example Simple usage
- *
- *
- * @example A section without title
- *
- *
- * @example A section that allows navigating to a page
- *
- *
- * @typedef { Object } SectionProps
- * @property {IconName} [icon] - Name of the section icon. Not rendered if title is not provided.
- * @property {string} [title] - The section title. If not given, aria-label must be provided.
- * @property {string|React.ReactElement} [description] - A section description. Use only if really needed.
- * @property {string} [name] - The section name. Used to build the header id.
- * @property {string} [path] - Path where the section links to.
- * when user clicks on the title, used for opening a dialog.
- * @property {boolean} [loading] - Whether the section is busy loading its content or not.
- * @property {string} [className] - Class name for section html tag.
- * @property {string} [id] - Id of the section ("software", "product", "storage", "storage-actions", ...)
- * @property {import("~/types/issues").ValidationError[]} [props.errors] - Validation errors to be shown before the title.
- * @property {React.ReactNode} [children] - The section content.
- * @property {string} [aria-label] - aria-label attribute, required if title if not given
- *
- * @param { SectionProps } props
- */
-export default function Section({
- icon,
- title,
- description,
- name,
- path,
- loading,
- className,
- children,
- "aria-label": ariaLabel,
-}) {
- const headerId = `${name || crypto.randomUUID()}-section-header`;
-
- if (!title && !ariaLabel) {
- console.error("The Section component must have either, a 'title' or an 'aria-label'");
- }
-
- const Header = () => {
- if (!title?.trim()) return;
-
- const iconName = loading ? "loading" : icon;
- const headerIcon = iconName ? : null;
- const headerText = !path?.trim() ? title : {title};
- const renderDescription = React.isValidElement(description) || description?.length > 0;
-
- return (
-
-
- {renderDescription && {description}
}
-
- );
- };
-
- return (
-
-
- {children}
-
- );
-}
diff --git a/web/src/components/core/Section.test.jsx b/web/src/components/core/Section.test.jsx
deleted file mode 100644
index ba7a2e80e5..0000000000
--- a/web/src/components/core/Section.test.jsx
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * Copyright (c) [2022-2023] SUSE LLC
- *
- * All Rights Reserved.
- *
- * This program is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License as published by the Free
- * Software Foundation; either version 2 of the License, or (at your option)
- * any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
- * more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, contact SUSE LLC.
- *
- * To contact SUSE LLC about this file by physical or electronic mail, you may
- * find current contact information at www.suse.com.
- */
-
-// FIXME: Refactor or replace
-
-import React from "react";
-import { screen, within } from "@testing-library/react";
-import { plainRender, installerRender } from "~/test-utils";
-import { Section } from "~/components/core";
-
-let consoleErrorSpy;
-
-describe.skip("Section", () => {
- beforeAll(() => {
- consoleErrorSpy = jest.spyOn(console, "error");
- consoleErrorSpy.mockImplementation();
- });
-
- afterAll(() => {
- consoleErrorSpy.mockRestore();
- });
-
- describe("when title is given", () => {
- it("renders the section header", () => {
- plainRender();
- screen.getByRole("banner");
- });
-
- it("renders given title as section heading", () => {
- plainRender();
- const header = screen.getByRole("banner");
- within(header).getByRole("heading", { name: "Settings" });
- });
-
- it("renders an icon if valid icon name is given", () => {
- const { container } = plainRender();
- const icon = container.querySelector("svg");
- expect(icon).toHaveAttribute("data-icon-name", "settings");
- });
-
- it("does not render an icon if icon name not given", () => {
- const { container } = plainRender();
- const icon = container.querySelector("svg");
- expect(icon).toBeNull();
- // Check that component was not mounted with 'undefined'
- expect(console.error).not.toHaveBeenCalled();
- });
-
- it("does not render an icon if not valid icon name is given", () => {
- const { container } = plainRender(
- ,
- );
- const icon = container.querySelector("svg");
- expect(icon).toBeNull();
- });
-
- it("renders given description as part of the header", () => {
- plainRender(
- ,
- );
- const header = screen.getByRole("banner");
- within(header).getByText(/Short explanation/);
- });
- });
-
- describe("when title is not given", () => {
- it("does not render the section header", async () => {
- plainRender();
- const header = await screen.queryByRole("banner");
- expect(header).not.toBeInTheDocument();
- });
-
- it("does not render a section heading", async () => {
- plainRender();
- const heading = await screen.queryByRole("heading");
- expect(heading).not.toBeInTheDocument();
- });
-
- it("does not render the section icon", () => {
- const { container } = plainRender();
- const icon = container.querySelector("svg");
- expect(icon).toBeNull();
- });
- });
-
- describe("when aria-label is given", () => {
- it("sets aria-label attribute", () => {
- plainRender();
- const section = screen.getByRole("region", { name: "User settings" });
- expect(section).toHaveAttribute("aria-label", "User settings");
- });
-
- it("does not set aria-labelledby", () => {
- plainRender();
- const section = screen.getByRole("region", { name: "User settings" });
- expect(section).not.toHaveAttribute("aria-labelledby");
- });
- });
-
- describe("when aria-label is not given", () => {
- it("sets aria-labelledby if title is provided", () => {
- plainRender();
- const section = screen.getByRole("region", { name: "Settings" });
- expect(section).toHaveAttribute("aria-labelledby");
- });
-
- it("does not set aria-label", () => {
- plainRender();
- const section = screen.getByRole("region", { name: "Settings" });
- expect(section).not.toHaveAttribute("aria-label");
- });
- });
-
- it("sets predictable header id if name is given", () => {
- plainRender();
- const section = screen.getByRole("heading", { name: "Settings" });
- expect(section).toHaveAttribute("id", "settings-section-header");
- });
-
- it("sets partially random header id if name is not given", () => {
- plainRender();
- const section = screen.getByRole("heading", { name: "Settings" });
- expect(section).toHaveAttribute("id", expect.stringContaining("section-header"));
- });
-
- it("renders a polite live region", () => {
- plainRender();
-
- const section = screen.getByRole("region", { name: "Settings" });
- expect(section).toHaveAttribute("aria-live", "polite");
- });
-
- it("renders given errors", () => {
- plainRender(
- ,
- );
-
- screen.getByText("Something went wrong");
- });
-
- it("renders given content", () => {
- plainRender();
-
- screen.getByText("A settings summary");
- });
-
- it("does not set aria-busy", () => {
- plainRender();
-
- screen.getByRole("region", { name: "Settings", busy: false });
- });
-
- describe("when set as loading", () => {
- it("sets aria-busy", () => {
- plainRender();
-
- screen.getByRole("region", { busy: true });
- });
-
- it("renders the loading icon if title was given", () => {
- const { container } = plainRender();
- const icon = container.querySelector("svg");
- expect(icon).toHaveAttribute("data-icon-name", "loading");
- });
-
- it("does not render the loading icon if title was not given", () => {
- const { container } = plainRender();
- const icon = container.querySelector("svg");
- expect(icon).toBeNull();
- });
- });
-
- describe("when path is given", () => {
- it("renders a link for navigating to it", async () => {
- installerRender();
- const heading = screen.getByRole("heading", { name: "Settings" });
- const link = within(heading).getByRole("link", { name: "Settings" });
- expect(link).toHaveAttribute("href", "/settings");
- });
- });
-});
diff --git a/web/src/components/core/TreeTable.test.jsx b/web/src/components/core/TreeTable.test.tsx
similarity index 100%
rename from web/src/components/core/TreeTable.test.jsx
rename to web/src/components/core/TreeTable.test.tsx
diff --git a/web/src/components/core/TreeTable.jsx b/web/src/components/core/TreeTable.tsx
similarity index 72%
rename from web/src/components/core/TreeTable.jsx
rename to web/src/components/core/TreeTable.tsx
index 94b3931113..645772606f 100644
--- a/web/src/components/core/TreeTable.jsx
+++ b/web/src/components/core/TreeTable.tsx
@@ -20,41 +20,37 @@
* find current contact information at www.suse.com.
*/
-// @ts-check
-
import React, { useEffect, useState } from "react";
-import { Table, Thead, Tr, Th, Tbody, Td, TreeRowWrapper } from "@patternfly/react-table";
-
-/**
- * @typedef {import("@patternfly/react-table").TableProps} TableProps
- */
-
-/**
- * @typedef {object} TreeTableColumn
- * @property {string} name
- * @property {(object) => React.ReactNode} value
- * @property {string} [classNames]
- */
-
-/**
- * @typedef {object} TreeTableBaseProps
- * @property {TreeTableColumn[]} columns=[]
- * @property {object[]} items=[]
- * @property {object[]} [expandedItems=[]]
- * @property {(any) => array} [itemChildren]
- * @property {(any) => string} [rowClassNames]
- */
+import {
+ Table,
+ TableProps,
+ Thead,
+ Tr,
+ Th,
+ Tbody,
+ Td,
+ TdProps,
+ TreeRowWrapper,
+} from "@patternfly/react-table";
+
+export type TreeTableColumn = {
+ name: string;
+ value: (item: object) => React.ReactNode;
+ classNames?: string;
+};
+
+type TreeTableProps = {
+ columns: TreeTableColumn[];
+ items: object[];
+ expandedItems?: object[];
+ /* eslint-disable @typescript-eslint/no-explicit-any */
+ itemChildren?: (item: any) => any[];
+ rowClassNames?: (item: object) => string;
+} & Omit;
/**
* Table built on top of PF/Table
* @component
- *
- * FIXME: omitting `ref` here to avoid a TypeScript error but keep component as
- * typed as possible. Further investigation is needed.
- *
- * @typedef {TreeTableBaseProps & Omit} TreeTableProps
- *
- * @param {TreeTableProps} props
*/
export default function TreeTable({
columns = [],
@@ -63,16 +59,16 @@ export default function TreeTable({
expandedItems = [],
rowClassNames = () => "",
...tableProps
-}) {
+}: TreeTableProps) {
const [expanded, setExpanded] = useState(expandedItems);
useEffect(() => {
setExpanded(expandedItems);
}, [expandedItems, setExpanded]);
- const isExpanded = (item) => expanded.includes(item);
+ const isExpanded = (item: object) => expanded.includes(item);
- const toggle = (item) => {
+ const toggle = (item: object) => {
if (isExpanded(item)) {
setExpanded(expanded.filter((d) => d !== item));
} else {
@@ -80,9 +76,9 @@ export default function TreeTable({
}
};
- const renderColumns = (item, treeRow) => {
+ const renderColumns = (item: object, treeRow: TdProps["treeRow"]) => {
return columns.map((c, cIdx) => {
- const props = {
+ const props: TdProps = {
dataLabel: c.name,
className: c.classNames,
};
@@ -97,7 +93,7 @@ export default function TreeTable({
});
};
- const renderRows = (items, level, hidden = false) => {
+ const renderRows = (items: object[], level: number, hidden = false) => {
if (items?.length <= 0) return;
return items.map((item, itemIdx) => {
diff --git a/web/src/components/core/index.js b/web/src/components/core/index.js
index 7ce2bff886..0345080fd4 100644
--- a/web/src/components/core/index.js
+++ b/web/src/components/core/index.js
@@ -22,7 +22,6 @@
export { default as ChangeProductLink } from "./ChangeProductLink";
export { default as Description } from "./Description";
-export { default as Section } from "./Section";
export { default as FormLabel } from "./FormLabel";
export { default as FormReadOnlyField } from "./FormReadOnlyField";
export { default as FormValidationError } from "./FormValidationError";
diff --git a/web/src/components/storage/DeviceSelectorTable.tsx b/web/src/components/storage/DeviceSelectorTable.tsx
index 6af674935d..982f0459db 100644
--- a/web/src/components/storage/DeviceSelectorTable.tsx
+++ b/web/src/components/storage/DeviceSelectorTable.tsx
@@ -20,8 +20,6 @@
* find current contact information at www.suse.com.
*/
-// @ts-check
-
import React from "react";
import {
DeviceName,
@@ -37,6 +35,7 @@ import { sprintf } from "sprintf-js";
import { deviceBaseName } from "~/components/storage/utils";
import { PartitionSlot, StorageDevice } from "~/types/storage";
import { ExpandableSelectorColumn, ExpandableSelectorProps } from "../core/ExpandableSelector";
+import { DeviceInfo as DeviceInfoType } from "~/api/storage/types";
/**
* @component
@@ -185,16 +184,22 @@ const DeviceExtendedDetails = ({ item }: { item: PartitionSlot | StorageDevice }
};
const columns: ExpandableSelectorColumn[] = [
- { name: _("Device"), value: (item) => },
- { name: _("Details"), value: (item) => },
- { name: _("Size"), value: (item) => , classNames: "sizes-column" },
+ { name: _("Device"), value: (item: PartitionSlot | StorageDevice) => },
+ {
+ name: _("Details"),
+ value: (item: PartitionSlot | StorageDevice) => ,
+ },
+ {
+ name: _("Size"),
+ value: (item: PartitionSlot | StorageDevice) => ,
+ classNames: "sizes-column",
+ },
];
-type DeviceSelectorTableBaseProps = {
+type DeviceSelectorTableProps = {
devices: StorageDevice[];
selectedDevices: StorageDevice[];
-};
-type DeviceSelectorTableProps = DeviceSelectorTableBaseProps & ExpandableSelectorProps;
+} & ExpandableSelectorProps;
/**
* Table for selecting the installation device.
@@ -210,7 +215,7 @@ export default function DeviceSelectorTable({
columns={columns}
items={devices}
itemIdKey="sid"
- itemClassNames={(device) => {
+ itemClassNames={(device: DeviceInfoType) => {
if (!device.sid) {
return "dimmed-row";
}
diff --git a/web/src/components/storage/ISCSIPage.jsx b/web/src/components/storage/ISCSIPage.tsx
similarity index 69%
rename from web/src/components/storage/ISCSIPage.jsx
rename to web/src/components/storage/ISCSIPage.tsx
index aaa54cca9d..401c03d84a 100644
--- a/web/src/components/storage/ISCSIPage.jsx
+++ b/web/src/components/storage/ISCSIPage.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -20,15 +20,28 @@
* find current contact information at www.suse.com.
*/
+import { Grid, GridItem } from "@patternfly/react-core";
import React from "react";
import { Page } from "~/components/core";
import { InitiatorSection, TargetsSection } from "~/components/storage/iscsi";
+import { _ } from "~/i18n";
export default function ISCSIPage() {
return (
-
-
+
+ {_("iSCSI")}
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/web/src/components/storage/ProposalResultTable.tsx b/web/src/components/storage/ProposalResultTable.tsx
index 3fb150f6cd..71a95c9ee2 100644
--- a/web/src/components/storage/ProposalResultTable.tsx
+++ b/web/src/components/storage/ProposalResultTable.tsx
@@ -35,6 +35,7 @@ import { sprintf } from "sprintf-js";
import { deviceChildren, deviceSize } from "~/components/storage/utils";
import { PartitionSlot, StorageDevice } from "~/types/storage";
import { TreeTableColumn } from "~/components/core/TreeTable";
+import { DeviceInfo } from "~/api/storage/types";
type TableItem = StorageDevice | PartitionSlot;
@@ -151,7 +152,7 @@ export default function ProposalResultTable({ devicesManager }: ProposalResultTa
items={devices}
expandedItems={devices}
itemChildren={deviceChildren}
- rowClassNames={(item) => {
+ rowClassNames={(item: DeviceInfo) => {
if (!item.sid) return "dimmed-row";
}}
className="proposal-result"
diff --git a/web/src/components/storage/SpaceActionsTable.tsx b/web/src/components/storage/SpaceActionsTable.tsx
index cb5936bbad..974be606e2 100644
--- a/web/src/components/storage/SpaceActionsTable.tsx
+++ b/web/src/components/storage/SpaceActionsTable.tsx
@@ -20,8 +20,6 @@
* find current contact information at www.suse.com.
*/
-// @ts-check
-
import React from "react";
import {
Button,
@@ -47,6 +45,7 @@ import { TreeTable } from "~/components/core";
import { Icon } from "~/components/layout";
import { PartitionSlot, SpaceAction, StorageDevice } from "~/types/storage";
import { TreeTableColumn } from "~/components/core/TreeTable";
+import { DeviceInfo as DeviceInfoType } from "~/api/storage/types";
/**
* Info about the device.
@@ -204,12 +203,18 @@ export default function SpaceActionsTable({
onActionChange,
}: SpaceActionsTableProps) {
const columns: TreeTableColumn[] = [
- { name: _("Device"), value: (item) => },
- { name: _("Details"), value: (item) => },
- { name: _("Size"), value: (item) => },
+ {
+ name: _("Device"),
+ value: (item: PartitionSlot | StorageDevice) => ,
+ },
+ {
+ name: _("Details"),
+ value: (item: PartitionSlot | StorageDevice) => ,
+ },
+ { name: _("Size"), value: (item: PartitionSlot | StorageDevice) => },
{
name: _("Action"),
- value: (item) => (
+ value: (item: PartitionSlot | StorageDevice) => (
),
},
@@ -222,7 +227,7 @@ export default function SpaceActionsTable({
aria-label={_("Actions to find space")}
expandedItems={expandedDevices}
itemChildren={deviceChildren}
- rowClassNames={(item) => {
+ rowClassNames={(item: DeviceInfoType) => {
if (!item.sid) return "dimmed-row";
}}
className="devices-table"
diff --git a/web/src/components/storage/VolumeLocationSelectorTable.tsx b/web/src/components/storage/VolumeLocationSelectorTable.tsx
index 5d043ecaee..78f063901a 100644
--- a/web/src/components/storage/VolumeLocationSelectorTable.tsx
+++ b/web/src/components/storage/VolumeLocationSelectorTable.tsx
@@ -35,6 +35,7 @@ import {
ExpandableSelectorProps,
} from "~/components/core/ExpandableSelector";
import { PartitionSlot, StorageDevice, Volume } from "~/types/storage";
+import { DeviceInfo } from "~/api/storage/types";
/**
* Returns what (volumes, installation device) is using a device.
@@ -93,13 +94,25 @@ export default function VolumeLocationSelectorTable({
...props
}: VolumeLocationSelectorTableProps) {
const columns: ExpandableSelectorColumn[] = [
- { name: _("Device"), value: (item) => },
- { name: _("Details"), value: (item) => },
+ {
+ name: _("Device"),
+ value: (item: PartitionSlot | StorageDevice) => ,
+ },
+ {
+ name: _("Details"),
+ value: (item: PartitionSlot | StorageDevice) => ,
+ },
{
name: _("Usage"),
- value: (item) => ,
+ value: (item: PartitionSlot | StorageDevice) => (
+
+ ),
+ },
+ {
+ name: _("Size"),
+ value: (item: PartitionSlot | StorageDevice) => ,
+ classNames: "sizes-column",
},
- { name: _("Size"), value: (item) => , classNames: "sizes-column" },
];
return (
@@ -107,7 +120,7 @@ export default function VolumeLocationSelectorTable({
columns={columns}
items={devices}
itemIdKey="sid"
- itemClassNames={(device) => {
+ itemClassNames={(device: DeviceInfo) => {
if (!device.sid) {
return "dimmed-row";
}
diff --git a/web/src/components/storage/iscsi/InitiatorSection.tsx b/web/src/components/storage/iscsi/InitiatorSection.tsx
index 9b371324f7..a4c4eca0b0 100644
--- a/web/src/components/storage/iscsi/InitiatorSection.tsx
+++ b/web/src/components/storage/iscsi/InitiatorSection.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2023] SUSE LLC
+ * Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
@@ -23,7 +23,7 @@
import React from "react";
import { _ } from "~/i18n";
-import { Section } from "~/components/core";
+import { Page } from "~/components/core";
import { InitiatorPresenter } from "~/components/storage/iscsi";
import { useInitiator, useInitiatorChanges } from "~/queries/storage/iscsi";
@@ -33,8 +33,8 @@ export default function InitiatorSection() {
return (
// TRANSLATORS: iSCSI initiator section name
-
+
);
}
diff --git a/web/src/components/storage/iscsi/TargetsSection.tsx b/web/src/components/storage/iscsi/TargetsSection.tsx
index 368b437df2..ed429d1fa5 100644
--- a/web/src/components/storage/iscsi/TargetsSection.tsx
+++ b/web/src/components/storage/iscsi/TargetsSection.tsx
@@ -22,7 +22,7 @@
import React, { useState } from "react";
import { Button, Toolbar, ToolbarItem, ToolbarContent, Stack } from "@patternfly/react-core";
-import { Section } from "~/components/core";
+import { Page } from "~/components/core";
import { NodesPresenter, DiscoverForm } from "~/components/storage/iscsi";
import { _ } from "~/i18n";
import { useNodes, useNodesChanges } from "~/queries/storage/iscsi";
@@ -83,11 +83,11 @@ export default function TargetsSection() {
return (
// TRANSLATORS: iSCSI targets section title
-
+
{isDiscoverFormOpen && (
)}
-
+
);
}
diff --git a/web/typedoc.json b/web/typedoc.json
index 7d0b6cc273..2897d5c9a2 100644
--- a/web/typedoc.json
+++ b/web/typedoc.json
@@ -4,10 +4,9 @@
"entryPointStrategy": "expand",
"exclude": ["./src/lib"],
"excludeNotDocumented": true,
- "plugin": ["typedoc-plugin-external-module-map", "typedoc-plugin-merge-modules"],
+ "plugin": ["typedoc-plugin-merge-modules"],
"excludeReferences": true,
"mergeModulesMergeMode": "module-category",
- "external-modulemap": [".*/src/(client)/", ".*/src/components/([\\w\\-_]+)/"],
"sort": ["alphabetical", "visibility"],
"navigation": {
"includeCategories": true,