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 }) => ; +const Action = ({ children, ...buttonProps }: React.PropsWithChildren) => ( + +); /** * A Popup primary action @@ -71,9 +74,8 @@ const Action = ({ children, ...buttonProps }) =>