diff --git a/web/.eslintrc.json b/web/.eslintrc.json index 4b23faa524..76fbfa654e 100644 --- a/web/.eslintrc.json +++ b/web/.eslintrc.json @@ -21,14 +21,7 @@ }, "sourceType": "module" }, - "plugins": [ - "agama-i18n", - "flowtype", - "i18next", - "react", - "react-hooks", - "@typescript-eslint" - ], + "plugins": ["agama-i18n", "flowtype", "i18next", "react", "react-hooks", "@typescript-eslint"], "rules": { "agama-i18n/string-literals": "error", "i18next/no-literal-string": "error", @@ -69,19 +62,14 @@ "overrides": [ { // do not check translations in the testing or development files - "files": [ - "*.test.*", - "test-utils.js" - ], + "files": ["*.test.*", "test-utils.js"], "rules": { "i18next/no-literal-string": "off" } }, { // do not check translation arguments in the test, it checks some internals by passing variables - "files": [ - "i18n.test.js" - ], + "files": ["i18n.test.js"], "rules": { "agama-i18n/string-literals": "off" } diff --git a/web/.stylelintrc.json b/web/.stylelintrc.json index a7365de1cd..ad22c45498 100644 --- a/web/.stylelintrc.json +++ b/web/.stylelintrc.json @@ -1,7 +1,5 @@ { - "plugins": [ - "stylelint-prettier" - ], + "plugins": ["stylelint-prettier"], "extends": [ "stylelint-config-standard", "stylelint-config-standard-scss", diff --git a/web/package-lock.json b/web/package-lock.json index 4499acd7f6..a2deca342b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -37,6 +37,7 @@ "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.5.1", "@types/jest": "^29.5.12", + "@types/webpack-env": "^1.18.5", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "ajv": "^8.12.0", @@ -4967,6 +4968,13 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "node_modules/@types/webpack-env": { + "version": "1.18.5", + "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.5.tgz", + "integrity": "sha512-wz7kjjRRj8/Lty4B+Kr0LN6Ypc/3SymeCCGSbaXp2leH0ZVg/PriNiOwNj4bD4uphI7A8NXS4b6Gl373sfO5mA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", diff --git a/web/package.json b/web/package.json index 861ea8933a..b830def6a1 100644 --- a/web/package.json +++ b/web/package.json @@ -42,6 +42,7 @@ "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.5.1", "@types/jest": "^29.5.12", + "@types/webpack-env": "^1.18.5", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "ajv": "^8.12.0", diff --git a/web/src/client/network/model.test.js b/web/src/client/network/model.test.js index c25d998242..0ab12d642b 100644 --- a/web/src/client/network/model.test.js +++ b/web/src/client/network/model.test.js @@ -43,7 +43,7 @@ describe("createConnection", () => { it("merges given properties", () => { const addresses = [{ address: "192.168.0.1", prefix: 24 }]; - const connection = createConnection({ addresses, testing: 1 }); + const connection = createConnection({ addresses }); expect(connection.method4).toEqual("auto"); expect(connection.gateway4).toEqual(""); expect(connection.addresses).toEqual(addresses); diff --git a/web/src/client/storage.js b/web/src/client/storage.js index ab58fd31d7..16f20b3b71 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -43,7 +43,32 @@ const ZFCP_DISK_IFACE = "org.opensuse.Agama.Storage1.ZFCP.Disk"; /** @fixme Adapt code depending on D-Bus */ class DBusClient { - proxy() { + /** + * @param {string} service + * @param {string|undefined} address + */ + constructor(service, address) { + console.warn(`FIXME: Adapt code depending on D-Bus ${service} ${address}`); + } + + /** + * @param {string} iface + * @param {string} [path] + * @return {Promise} + */ + async proxy(iface, path) { + console.warn(`FIXME: Adapt code depending on D-Bus ${iface} ${path}`); + return Promise.resolve(undefined); + } + + /** + * @param {string|undefined} iface + * @param {string|undefined} path_namespace + * @param {object|undefined} options + * @return {Promise} + */ + async proxies(iface, path_namespace, options) { + console.warn(`FIXME: Adapt code depending on D-Bus ${iface} ${path_namespace} ${options}`); return Promise.resolve(undefined); } } @@ -1576,7 +1601,9 @@ class StorageBaseClient { this.staging = new DevicesManager(this.client, "result"); this.proposal = new ProposalManager(this.client, this.system); this.iscsi = new ISCSIManager(this.client); + // @ts-ignore this.dasd = new DASDManager(StorageBaseClient.SERVICE, client); + // @ts-ignore this.zfcp = new ZFCPManager(StorageBaseClient.SERVICE, client); } diff --git a/web/src/components/core/CardField.jsx b/web/src/components/core/CardField.jsx index 5695cb4c16..c11862e78e 100644 --- a/web/src/components/core/CardField.jsx +++ b/web/src/components/core/CardField.jsx @@ -43,11 +43,11 @@ import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; * @todo write documentation */ const CardField = ({ - label, - value, - description, - actions, - children = [], + label = undefined, + value = undefined, + description = undefined, + actions = undefined, + children, cardProps = {}, cardHeaderProps = {}, cardDescriptionProps = {}, @@ -58,12 +58,16 @@ const CardField = ({ - -

{label}

-
- - {value} - + {label && ( + +

{label}

+
+ )} + {value && ( + + {value} + + )}
diff --git a/web/src/components/core/Drawer.jsx b/web/src/components/core/Drawer.jsx index 504f09afa6..97b23dfee0 100644 --- a/web/src/components/core/Drawer.jsx +++ b/web/src/components/core/Drawer.jsx @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -// @ts-check +// FIXME: rewrite to .tsx import React, { forwardRef, useImperativeHandle, useState } from "react"; import { diff --git a/web/src/components/core/EmptyState.jsx b/web/src/components/core/EmptyState.jsx index e147766895..490226433a 100644 --- a/web/src/components/core/EmptyState.jsx +++ b/web/src/components/core/EmptyState.jsx @@ -41,10 +41,10 @@ import { Icon } from "~/components/layout"; * @param {object} props * @param {string} props.title * @param {IconName} props.icon - * @param {string} props.color + * @param {string} [props.color="color-100"] * @param {EmptyStateHeaderProps["headingLevel"]} [props.headingLevel="h4"] * @param {boolean} [props.noPadding=false] - * @param {React.ReactNode} props.children + * @param {React.ReactNode} [props.children] * @param {EmptyStateProps} [props.rest] * @todo write documentation */ @@ -57,6 +57,7 @@ export default function EmptyStateWrapper({ children, ...rest }) { + // @ts-ignore if (noPadding) rest.className = [rest.className, "no-padding"].join(" ").trim(); return ( @@ -64,12 +65,15 @@ export default function EmptyStateWrapper({ } /> - - {children} - + {children && ( + + {children} + + )} ); } diff --git a/web/src/components/core/LoginPage.jsx b/web/src/components/core/LoginPage.jsx index 1ff500b7c1..5ef18ed4e4 100644 --- a/web/src/components/core/LoginPage.jsx +++ b/web/src/components/core/LoginPage.jsx @@ -87,6 +87,7 @@ user privileges.", + {/** @ts-ignore */}

{rootExplanationStart} {rootUser} {rootExplanationEnd} diff --git a/web/src/components/core/Page.jsx b/web/src/components/core/Page.jsx index cdfae2514b..b71bcdbbcd 100644 --- a/web/src/components/core/Page.jsx +++ b/web/src/components/core/Page.jsx @@ -22,7 +22,7 @@ // @ts-check import React from "react"; -import { NavLink, Outlet, useNavigate, useMatches, useLocation } from "react-router-dom"; +import { NavLink, Outlet, useNavigate } from "react-router-dom"; import { Button, Card, @@ -85,6 +85,7 @@ const Action = ({ navigateTo, children, ...props }) => { /** * Simple action for navigating back + * @param {ActionProps & { text?: string }} props */ const CancelAction = ({ text = _("Cancel"), navigateTo }) => { const navigate = useNavigate(); @@ -186,11 +187,6 @@ const CardSection = ({ title, children, ...props }) => { * @param {React.ReactNode} [props.children] - The page content. */ const Page = () => { - const location = useLocation(); - const matches = useMatches(); - const currentRoute = matches.find((r) => r.pathname === location.pathname); - const titleFromRoute = currentRoute?.handle?.name; - return ( diff --git a/web/src/components/core/Section.jsx b/web/src/components/core/Section.jsx index 1d8c6c891d..8fc92c2098 100644 --- a/web/src/components/core/Section.jsx +++ b/web/src/components/core/Section.jsx @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -// @ts-check +// FIXME: Refactor or replace import React from "react"; import { Link } from "react-router-dom"; diff --git a/web/src/components/core/Section.test.jsx b/web/src/components/core/Section.test.jsx index 5d7c5cbb11..2104cac640 100644 --- a/web/src/components/core/Section.test.jsx +++ b/web/src/components/core/Section.test.jsx @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -// @ts-check +// FIXME: Refactor or replace import React from "react"; import { screen, within } from "@testing-library/react"; @@ -65,7 +65,6 @@ describe.skip("Section", () => { }); it("does not render an icon if not valid icon name is given", () => { - // @ts-expect-error: Creating the icon name dynamically is unlikely, but let's be safe. const { container } = plainRender(

, ); diff --git a/web/src/components/layout/Center.jsx b/web/src/components/layout/Center.jsx index 52bd24f759..a7df6e8a14 100644 --- a/web/src/components/layout/Center.jsx +++ b/web/src/components/layout/Center.jsx @@ -46,10 +46,9 @@ import React from "react"; * * @param {object} props * @param {React.ReactNode} props.children - * @param {React.HTMLAttributes} props.htmlProps */ -const Center = ({ children, ...htmlProps }) => ( -
+const Center = ({ children }) => ( +
{children}
); diff --git a/web/src/components/network/ConnectionsTable.jsx b/web/src/components/network/ConnectionsTable.jsx index 48b5b04210..e925e3350c 100644 --- a/web/src/components/network/ConnectionsTable.jsx +++ b/web/src/components/network/ConnectionsTable.jsx @@ -30,6 +30,7 @@ import { formatIp } from "~/client/network/utils"; import { _ } from "~/i18n"; /** + * @typedef {import("~/client/network/model").Device} Device * @typedef {import("~/client/network/model").Connection} Connection */ @@ -40,8 +41,8 @@ import { _ } from "~/i18n"; * * @param {object} props * @param {Connection[]} props.connections - Connections to be shown - * @param {function} props.onEdit - function to be called for editing a connection - * @param {function} props.onForget - function to be called for forgetting a connection + * @param {Device[]} props.devices - Connections to be shown + * @param {function} [props.onForget] - function to be called for forgetting a connection */ export default function ConnectionsTable({ connections, devices, onForget }) { const navigate = useNavigate(); diff --git a/web/src/components/network/NetworkPage.jsx b/web/src/components/network/NetworkPage.jsx index cdf974028b..dc99f2d296 100644 --- a/web/src/components/network/NetworkPage.jsx +++ b/web/src/components/network/NetworkPage.jsx @@ -38,6 +38,7 @@ import { sprintf } from "sprintf-js"; */ export default function NetworkPage() { const { network: client } = useInstallerClient(); + // @ts-ignore const { connections: initialConnections, devices: initialDevices, settings } = useLoaderData(); const [connections, setConnections] = useState(initialConnections); const [devices, setDevices] = useState(initialDevices); @@ -64,6 +65,7 @@ export default function NetworkPage() { NetworkEventTypes.DEVICE_ADDED, NetworkEventTypes.DEVICE_UPDATED, NetworkEventTypes.DEVICE_REMOVED, + // @ts-ignore ].includes(type) ) { setUpdateState(true); diff --git a/web/src/components/storage/BootSelection.jsx b/web/src/components/storage/BootSelection.jsx index 0241f51a32..61e68440e6 100644 --- a/web/src/components/storage/BootSelection.jsx +++ b/web/src/components/storage/BootSelection.jsx @@ -49,9 +49,19 @@ const BOOT_DISABLED_ID = "boot-disabled"; * Allows the user to select the boot configuration. */ export default function BootSelectionDialog() { + /** + * @typedef {object} BootSelectionState + * @property {boolean} load + * @property {string} [selectedOption] + * @property {boolean} [configureBoot] + * @property {StorageDevice} [bootDevice] + * @property {StorageDevice} [defaultBootDevice] + * @property {StorageDevice[]} [availableDevices] + */ const { cancellablePromise } = useCancellablePromise(); const { storage: client } = useInstallerClient(); - const [state, setState] = useState({}); + /** @type ReturnType> */ + const [state, setState] = useState({ load: false }); const navigate = useNavigate(); // FIXME: Repeated code, see DeviceSelection. Use a context/hook or whatever diff --git a/web/src/components/storage/DeviceSelection.jsx b/web/src/components/storage/DeviceSelection.jsx index 653ef377b7..d417bb6c18 100644 --- a/web/src/components/storage/DeviceSelection.jsx +++ b/web/src/components/storage/DeviceSelection.jsx @@ -47,8 +47,6 @@ import { compact, useCancellablePromise } from "~/utils"; import { useInstallerClient } from "~/context/installer"; /** - * @typedef {import ("~/client/storage").ProposalTarget} ProposalTarget - * @typedef {import ("~/client/storage").ProposalSettings} ProposalSettings * @typedef {import ("~/client/storage").StorageDevice} StorageDevice */ @@ -56,16 +54,24 @@ const SELECT_DISK_ID = "select-disk"; const CREATE_LVM_ID = "create-lvm"; const SELECT_DISK_PANEL_ID = "panel-for-disk-selection"; const CREATE_LVM_PANEL_ID = "panel-for-lvm-creation"; -const OPTIONS_NAME = "selection-mode"; /** * Allows the user to select a target device for installation. * @component */ export default function DeviceSelection() { - const [state, setState] = useState({}); + /** + * @typedef {object} DeviceSelectionState + * @property {boolean} load + * @property {string} [target] + * @property {StorageDevice} [targetDevice] + * @property {StorageDevice[]} [targetPVDevices] + * @property {StorageDevice[]} [availableDevices] + */ const navigate = useNavigate(); const { cancellablePromise } = useCancellablePromise(); + /** @type ReturnType> */ + const [state, setState] = useState({ load: false }); const isTargetDisk = state.target === "DISK"; const isTargetNewLvmVg = state.target === "NEW_LVM_VG"; diff --git a/web/src/components/storage/EncryptionSettingsDialog.jsx b/web/src/components/storage/EncryptionSettingsDialog.jsx index 0801c45298..efbeb39e40 100644 --- a/web/src/components/storage/EncryptionSettingsDialog.jsx +++ b/web/src/components/storage/EncryptionSettingsDialog.jsx @@ -21,7 +21,7 @@ // @ts-check -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useRef } from "react"; import { Checkbox, Form, Switch, Stack } from "@patternfly/react-core"; import { _ } from "~/i18n"; import { PasswordAndConfirmationInput, Popup } from "~/components/core"; @@ -78,6 +78,7 @@ export default function EncryptionSettingsDialog({ const [passwordsMatch, setPasswordsMatch] = useState(true); const [validSettings, setValidSettings] = useState(true); const [wasLoading, setWasLoading] = useState(isLoading); + const passwordRef = useRef(); const formId = "encryptionSettingsForm"; // reset the settings only after loading is finished @@ -131,6 +132,7 @@ export default function EncryptionSettingsDialog({ />
{ const props = { ...defaultProps, policy: deletePolicy, - actions: [{ device: 79, subvol: false, delete: true, text: "" }], + actions: [{ device: 79, subvol: false, delete: true, resize: false, text: "" }], }; installerRender(); diff --git a/web/src/components/storage/ProposalResultSection.test.jsx b/web/src/components/storage/ProposalResultSection.test.jsx index 0e3db28354..cc6839c1c9 100644 --- a/web/src/components/storage/ProposalResultSection.test.jsx +++ b/web/src/components/storage/ProposalResultSection.test.jsx @@ -81,7 +81,7 @@ describe.skip("ProposalResultSection", () => { // affected systems are rendered in the warning summary const props = { ...defaultProps, - actions: [{ device: 79, subvol: false, delete: true, text: "" }], + actions: [{ device: 79, subvol: false, delete: true, resize: false, text: "" }], }; plainRender(); diff --git a/web/src/components/storage/SpacePolicySelection.jsx b/web/src/components/storage/SpacePolicySelection.jsx index e5a7648206..602ad6d510 100644 --- a/web/src/components/storage/SpacePolicySelection.jsx +++ b/web/src/components/storage/SpacePolicySelection.jsx @@ -85,6 +85,7 @@ const SpacePolicyPicker = ({ currentPolicy, onChange = noop }) => { */ export default function SpacePolicySelection() { const [state, setState] = useState({ load: false, settings: {} }); + /** @type ReturnType> */ const [policy, setPolicy] = useState(); const [actions, setActions] = useState([]); const [expandedDevices, setExpandedDevices] = useState([]); @@ -154,6 +155,7 @@ export default function SpacePolicySelection() { const onSubmit = (e) => { e.preventDefault(); + // @ts-ignore client.proposal.calculate({ ...state.settings, spacePolicy: policy.id, diff --git a/web/src/components/storage/VolumeFields.jsx b/web/src/components/storage/VolumeFields.jsx index 66f4e1e995..f8047f93e5 100644 --- a/web/src/components/storage/VolumeFields.jsx +++ b/web/src/components/storage/VolumeFields.jsx @@ -102,7 +102,7 @@ const SizeUnitFormSelect = ({ units, ...formSelectProps }) => { {units.map((unit) => { // unit values are marked for translation in the utils.js file // eslint-disable-next-line agama-i18n/string-literals - return + return ; })} ); diff --git a/web/src/components/storage/test-data/full-result-example.js b/web/src/components/storage/test-data/full-result-example.js index f3587215e4..43419f309d 100644 --- a/web/src/components/storage/test-data/full-result-example.js +++ b/web/src/components/storage/test-data/full-result-example.js @@ -1181,101 +1181,118 @@ export const actions = [ text: "Delete partition /dev/md0p1 (2.00 GiB)", subvol: false, delete: true, + resize: false, }, { device: 72, text: "Delete RAID0 /dev/md0 (10.00 GiB)", subvol: false, delete: true, + resize: false, }, { device: 80, text: "Delete partition /dev/vdc3 (1.00 GiB)", subvol: false, delete: true, + resize: false, }, { device: 78, text: "Delete partition /dev/vdc1 (5.00 GiB)", subvol: false, delete: true, + resize: false, }, { device: 81, text: "Shrink partition /dev/vdc4 from 2.00 GiB to 1.50 GiB", subvol: false, delete: false, + resize: true, }, { device: 459, text: "Create partition /dev/vdc1 (8.00 MiB) as BIOS Boot Partition", subvol: false, delete: false, + resize: false, }, { device: 460, text: "Create partition /dev/vdc3 (1.50 GiB) for swap", subvol: false, delete: false, + resize: false, }, { device: 463, text: "Create partition /dev/vdc5 (17.50 GiB) for / with btrfs", subvol: false, delete: false, + resize: false, }, { device: 467, text: "Create subvolume @ on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, { device: 482, text: "Create subvolume @/boot/grub2/x86_64-efi on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, { device: 480, text: "Create subvolume @/boot/grub2/i386-pc on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, { device: 478, text: "Create subvolume @/var on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, { device: 476, text: "Create subvolume @/usr/local on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, { device: 474, text: "Create subvolume @/srv on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, { device: 472, text: "Create subvolume @/root on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, { device: 470, text: "Create subvolume @/opt on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, { device: 468, text: "Create subvolume @/home on /dev/vdc5 (17.50 GiB)", subvol: true, delete: false, + resize: false, }, ]; diff --git a/web/src/components/users/RootPasswordPopup.jsx b/web/src/components/users/RootPasswordPopup.jsx index aa3dc015d0..ea7cdd12b9 100644 --- a/web/src/components/users/RootPasswordPopup.jsx +++ b/web/src/components/users/RootPasswordPopup.jsx @@ -19,7 +19,9 @@ * find current contact information at www.suse.com. */ -import React, { useState } from "react"; +// @ts-check + +import React, { useState, useRef } from "react"; import { Form } from "@patternfly/react-core"; import { PasswordAndConfirmationInput, Popup } from "~/components/core"; @@ -42,6 +44,7 @@ export default function RootPasswordPopup({ title = _("Root password"), isOpen, const { users: client } = useInstallerClient(); const [password, setPassword] = useState(""); const [isValidPassword, setIsValidPassword] = useState(true); + const passwordRef = useRef(); const close = () => { setPassword(""); @@ -63,6 +66,7 @@ export default function RootPasswordPopup({ title = _("Root password"), isOpen,