From 4cf1626d45cba5465025ea4cfa44f8b884840850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 12 Dec 2024 19:03:48 +0000 Subject: [PATCH 1/6] fix(web): drop typedoc-plugin-external-module-map Because it fails after updating to TypeDoc 0.27.4 and it was introduced for a purpose that for now it is not currently being used. Thus, let's postpone its usage until really needed if it is updated to work with the lastest TypeDoc when such a moment arrive. See, * Agama commit in which it was added 5e09032a6d8 * Issue at upstream https://github.com/asgerjensen/typedoc-plugin-external-module-map/issues/27 --- web/package-lock.json | 31 ------------------------------- web/package.json | 1 - web/typedoc.json | 3 +-- 3 files changed, 1 insertion(+), 34 deletions(-) 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/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, From 643bc03e8c510825e75e2202dae20276461676ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 12 Dec 2024 19:16:05 +0000 Subject: [PATCH 2/6] fix(web): stop using Section in favor of Page.Section There were few core/Section leftover in storage namespace. Since core/Section is going to be removed, Page.Section has been used instead. --- .../storage/{ISCSIPage.jsx => ISCSIPage.tsx} | 19 ++++++++++++++++--- .../storage/iscsi/InitiatorSection.tsx | 8 ++++---- .../storage/iscsi/TargetsSection.tsx | 6 +++--- 3 files changed, 23 insertions(+), 10 deletions(-) rename web/src/components/storage/{ISCSIPage.jsx => ISCSIPage.tsx} (69%) 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/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 && ( )} -
+ ); } From 822be64e64239e8cb198d4bf1eb731995f275907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 12 Dec 2024 19:22:43 +0000 Subject: [PATCH 3/6] fix(web): delete core/Section component It was no longer needed and making TypeDoc to exit with an unexpected error. --- web/src/components/core/Section.jsx | 110 ------------ web/src/components/core/Section.test.jsx | 204 ----------------------- web/src/components/core/index.js | 1 - 3 files changed, 315 deletions(-) delete mode 100644 web/src/components/core/Section.jsx delete mode 100644 web/src/components/core/Section.test.jsx 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 ( -
-

- {headerIcon} - {headerText} -

- {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(
A settings summary
); - - 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/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"; From 34bc00089f5b4b11d46635abb47861667bbc6d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 13 Dec 2024 14:02:34 +0000 Subject: [PATCH 4/6] fix(web): migrate some components to TypeScript Those that were causing TypeDoc crashes, as described in commit 791857af613cf97c34a3985e1796d2cd5fa22244. --- ...r.test.jsx => ExpandableSelector.test.tsx} | 20 ++-- ...bleSelector.jsx => ExpandableSelector.tsx} | 102 +++++++++--------- ...dInput.test.jsx => PasswordInput.test.tsx} | 6 +- .../{PasswordInput.jsx => PasswordInput.tsx} | 15 ++- .../core/{Popup.test.jsx => Popup.test.tsx} | 19 ++-- .../components/core/{Popup.jsx => Popup.tsx} | 62 +++++------ ...{TreeTable.test.jsx => TreeTable.test.tsx} | 0 .../core/{TreeTable.jsx => TreeTable.tsx} | 67 ++++++------ .../storage/DeviceSelectorTable.tsx | 23 ++-- .../storage/ProposalResultTable.tsx | 3 +- .../components/storage/SpaceActionsTable.tsx | 19 ++-- .../storage/VolumeLocationSelectorTable.tsx | 23 +++- 12 files changed, 187 insertions(+), 172 deletions(-) rename web/src/components/core/{ExpandableSelector.test.jsx => ExpandableSelector.test.tsx} (96%) rename web/src/components/core/{ExpandableSelector.jsx => ExpandableSelector.tsx} (68%) rename web/src/components/core/{PasswordInput.test.jsx => PasswordInput.test.tsx} (94%) rename web/src/components/core/{PasswordInput.jsx => PasswordInput.tsx} (85%) rename web/src/components/core/{Popup.test.jsx => Popup.test.tsx} (92%) rename web/src/components/core/{Popup.jsx => Popup.tsx} (76%) rename web/src/components/core/{TreeTable.test.jsx => TreeTable.test.tsx} (100%) rename web/src/components/core/{TreeTable.jsx => TreeTable.tsx} (72%) 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..1061b994c1 100644 --- a/web/src/components/core/ExpandableSelector.test.jsx +++ b/web/src/components/core/ExpandableSelector.test.tsx @@ -24,8 +24,11 @@ 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; + +const sda: any = { sid: "59", isDrive: true, type: "disk", @@ -112,18 +115,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 +146,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..580c9fb4f3 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, @@ -35,8 +34,7 @@ import { } from "@patternfly/react-table"; /** - * @typedef {import("@patternfly/react-table").TableProps} TableProps - * @typedef {import("react").RefAttributes} HTMLTableProps + * {import("react").RefAttributes} HTMLTableProps */ /** @@ -47,26 +45,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 +106,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 +137,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 +151,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 +163,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 +172,7 @@ export default function ExpandableSelector({ } }; - const updateSelection = (item) => { + const updateSelection = (item: object) => { if (!isMultiple) { onSelectionChange([item]); return; @@ -182,11 +188,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 +218,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 }) => ; +const Action = ({ children, ...buttonProps }: React.PropsWithChildren) => ( + +); /** * A Popup primary action @@ -71,9 +74,8 @@ const Action = ({ children, ...buttonProps }) =>