From 4a9e47a81aa1b39338a2e1a34a2fe983bc32f3f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Tue, 14 Mar 2023 15:39:53 +0000 Subject: [PATCH 01/19] [service] Add missing attribure to D-Bus --- service/lib/dinstaller/dbus/storage/manager.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/service/lib/dinstaller/dbus/storage/manager.rb b/service/lib/dinstaller/dbus/storage/manager.rb index 5b7416b02a..8562b00977 100644 --- a/service/lib/dinstaller/dbus/storage/manager.rb +++ b/service/lib/dinstaller/dbus/storage/manager.rb @@ -161,6 +161,13 @@ def initiator_name=(value) backend.iscsi.initiator.name = value end + # Whether the initiator name was set via iBFT + # + # @return [Boolean] + def ibft + backend.iscsi.initiator.ibft_name? + end + # Performs an iSCSI discovery # # @param address [String] IP address of the iSCSI server @@ -199,6 +206,8 @@ def iscsi_delete(path) dbus_interface ISCSI_INITIATOR_INTERFACE do dbus_accessor :initiator_name, "s" + dbus_reader :ibft, "b", dbus_name: "IBFT" + dbus_method :Discover, "in address:s, in port:u, in options:a{sv}, out result:u" do |address, port, options| busy_while { iscsi_discover(address, port, options) } From 31bfe2e4e0476b1d44c11e1a9f7a7413e1679509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Tue, 14 Mar 2023 15:40:30 +0000 Subject: [PATCH 02/19] [web] Add missing features for storage client --- web/src/client/storage.js | 77 +++++++++++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/web/src/client/storage.js b/web/src/client/storage.js index c0887c71a1..03eba0994b 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -260,6 +260,11 @@ class ISCSIManager { this.proxies = {}; } + async getInitiatorIbft() { + const proxy = await this.iscsiInitiatorProxy(); + return proxy.IBFT; + } + /** * Gets the iSCSI initiator name * @@ -296,23 +301,8 @@ class ISCSIManager { * @property {string} startup */ async getNodes() { - const buildNode = (iscsiProxy) => { - const id = path => path.split("/").slice(-1)[0]; - - return { - id: id(iscsiProxy.path), - target: iscsiProxy.Target, - address: iscsiProxy.Address, - port: iscsiProxy.Port, - interface: iscsiProxy.Interface, - ibft: iscsiProxy.IBFT, - connected: iscsiProxy.Connected, - startup: iscsiProxy.Startup - }; - }; - const proxy = await this.iscsiNodesProxy(); - return Object.values(proxy).map(p => buildNode(p)); + return Object.values(proxy).map(this.buildNode); } /** @@ -342,6 +332,19 @@ class ISCSIManager { return proxy.Discover(address, port, auth); } + /** + * Sets the statup status of the connection + * + * @param {ISCSINode} node + * @param {String} startup + */ + async setStartup(node, startup) { + const path = this.nodePath(node); + + const proxy = await this.client.proxy(ISCSI_NODE_IFACE, path); + proxy.Startup = startup; + } + /** * Deletes the given iSCSI node * @@ -401,6 +404,48 @@ class ISCSIManager { return proxy.Logout(); } + onInitiatorChanged(handler) { + return this.client.onObjectChanged(STORAGE_OBJECT, ISCSI_INITIATOR_IFACE, (changes) => { + const data = { + name: changes.InitiatorName?.v, + ibft: changes.IBFT?.v + }; + + const filtered = Object.entries(data).filter(([, v]) => v !== undefined); + return handler(Object.fromEntries(filtered)); + }); + } + + async onNodeAdded(handler) { + const proxy = await this.iscsiNodesProxy(); + proxy.addEventListener("added", (_, proxy) => handler(this.buildNode(proxy))); + } + + async onNodeChanged(handler) { + const proxy = await this.iscsiNodesProxy(); + proxy.addEventListener("changed", (_, proxy) => handler(this.buildNode(proxy))); + } + + async onNodeRemoved(handler) { + const proxy = await this.iscsiNodesProxy(); + proxy.addEventListener("removed", (_, proxy) => handler(this.buildNode(proxy))); + } + + buildNode(proxy) { + const id = path => path.split("/").slice(-1)[0]; + + return { + id: id(proxy.path), + target: proxy.Target, + address: proxy.Address, + port: proxy.Port, + interface: proxy.Interface, + ibft: proxy.IBFT, + connected: proxy.Connected, + startup: proxy.Startup + }; + } + /** * @private * Proxy for org.opensuse.DInstaller.Storage1.ISCSI.Initiator iface From 302ded88c4a5233bc84995809eaa645de5699578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Tue, 14 Mar 2023 15:45:43 +0000 Subject: [PATCH 03/19] [web] Add hook for local storage --- web/src/utils.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/web/src/utils.js b/web/src/utils.js index 87a041b44f..d8e719c7e3 100644 --- a/web/src/utils.js +++ b/web/src/utils.js @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2023] SUSE LLC * * All Rights Reserved. * @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -import { useEffect, useRef, useCallback } from "react"; +import { useEffect, useRef, useCallback, useState } from "react"; /** * Returns an empty function useful to be used as a default callback. @@ -143,9 +143,29 @@ function useCancellablePromise() { return { cancellablePromise }; } +/** Hook for using local storage + * + * @see {@link https://www.robinwieruch.de/react-uselocalstorage-hook/} + * + * @param {String} storageKey + * @param {*} fallbackState + */ +const useLocalStorage = (storageKey, fallbackState) => { + const [value, setValue] = useState( + JSON.parse(localStorage.getItem(storageKey)) ?? fallbackState + ); + + useEffect(() => { + localStorage.setItem(storageKey, JSON.stringify(value)); + }, [value, storageKey]); + + return [value, setValue]; +}; + export { noop, partition, classNames, useCancellablePromise, + useLocalStorage }; From 2c98d6c4b1bab3602e5a7aa6d397e49365a729c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Tue, 14 Mar 2023 15:49:07 +0000 Subject: [PATCH 04/19] [web] Add iSCSI page --- web/src/assets/styles/blocks.scss | 4 + .../assets/styles/patternfly-overrides.scss | 11 ++ web/src/components/layout/Icon.jsx | 1 + web/src/components/storage/ISCSIPage.jsx | 45 +++++ .../storage/ProposalTargetSection.jsx | 43 +++-- web/src/components/storage/index.js | 1 + .../components/storage/iscsi/AuthFields.jsx | 157 +++++++++++++++++ .../components/storage/iscsi/DiscoverForm.jsx | 153 ++++++++++++++++ .../components/storage/iscsi/EditNodeForm.jsx | 70 ++++++++ .../storage/iscsi/InitiatorForm.jsx | 66 +++++++ .../storage/iscsi/InitiatorPresenter.jsx | 122 +++++++++++++ .../storage/iscsi/InitiatorSection.jsx | 55 ++++++ .../components/storage/iscsi/LoginForm.jsx | 101 +++++++++++ .../storage/iscsi/NodesPresenter.jsx | 156 +++++++++++++++++ .../storage/iscsi/TargetsSection.jsx | 163 ++++++++++++++++++ web/src/components/storage/iscsi/index.js | 37 ++++ web/src/index.js | 3 +- 17 files changed, 1173 insertions(+), 15 deletions(-) create mode 100644 web/src/components/storage/ISCSIPage.jsx create mode 100644 web/src/components/storage/iscsi/AuthFields.jsx create mode 100644 web/src/components/storage/iscsi/DiscoverForm.jsx create mode 100644 web/src/components/storage/iscsi/EditNodeForm.jsx create mode 100644 web/src/components/storage/iscsi/InitiatorForm.jsx create mode 100644 web/src/components/storage/iscsi/InitiatorPresenter.jsx create mode 100644 web/src/components/storage/iscsi/InitiatorSection.jsx create mode 100644 web/src/components/storage/iscsi/LoginForm.jsx create mode 100644 web/src/components/storage/iscsi/NodesPresenter.jsx create mode 100644 web/src/components/storage/iscsi/TargetsSection.jsx create mode 100644 web/src/components/storage/iscsi/index.js diff --git a/web/src/assets/styles/blocks.scss b/web/src/assets/styles/blocks.scss index 1ebd036c82..82e4ca5fd2 100644 --- a/web/src/assets/styles/blocks.scss +++ b/web/src/assets/styles/blocks.scss @@ -10,6 +10,10 @@ section { gap: var(--spacer-small); } +section:not(:last-child) { + margin-block-end: var(--spacer-large); +} + section > svg { grid-area: icon; } diff --git a/web/src/assets/styles/patternfly-overrides.scss b/web/src/assets/styles/patternfly-overrides.scss index 70647cd675..aa65a16d4a 100644 --- a/web/src/assets/styles/patternfly-overrides.scss +++ b/web/src/assets/styles/patternfly-overrides.scss @@ -110,3 +110,14 @@ table td > .pf-c-empty-state { .pf-c-data-list { --pf-c-data-list--BorderTopWidth: 2px; } + +// Toolbar content aligned to the right (alignment prop seems to be ignored) +.pf-c-toolbar__content-section { + justify-content: flex-end; +} + +section .pf-c-toolbar { + --stack-gutter: 0; + --pf-c-toolbar--PaddingBottom: 0; + --pf-c-toolbar--PaddingTop: 0; +} diff --git a/web/src/components/layout/Icon.jsx b/web/src/components/layout/Icon.jsx index 8d1a843cc4..66f9677ce1 100644 --- a/web/src/components/layout/Icon.jsx +++ b/web/src/components/layout/Icon.jsx @@ -23,6 +23,7 @@ import React from 'react'; // NOTE: "@icons" is an alias to use a shorter path to real icons location. // Check the tsconfig.json file to see its value. +import Add from "@icons/add.svg?component"; import Apps from "@icons/apps.svg?component"; import Badge from "@icons/badge.svg?component"; import CheckCircle from "@icons/check_circle.svg?component"; diff --git a/web/src/components/storage/ISCSIPage.jsx b/web/src/components/storage/ISCSIPage.jsx new file mode 100644 index 0000000000..5be687a21c --- /dev/null +++ b/web/src/components/storage/ISCSIPage.jsx @@ -0,0 +1,45 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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. + */ + +import React from "react"; +import { useNavigate } from "react-router-dom"; +import { Button } from "@patternfly/react-core"; + +import { Title as PageTitle, MainActions } from "~/components/layout"; +import { InitiatorSection, TargetsSection } from "~/components/storage/iscsi"; + +export default function ISCSIPage() { + const navigate = useNavigate(); + + return ( + <> + Storage iSCSI + + + + + + + + ); +} diff --git a/web/src/components/storage/ProposalTargetSection.jsx b/web/src/components/storage/ProposalTargetSection.jsx index 847b21e1a1..ea3baae84a 100644 --- a/web/src/components/storage/ProposalTargetSection.jsx +++ b/web/src/components/storage/ProposalTargetSection.jsx @@ -20,12 +20,15 @@ */ import React, { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Button } from "@patternfly/react-core"; import { If, Popup, Section } from "~/components/core"; import { ProposalSummary, ProposalTargetForm } from "~/components/storage"; export default function ProposalTargetSection({ proposal, calculateProposal }) { const [isOpen, setIsOpen] = useState(false); + const navigate = useNavigate(); const onTargetChange = ({ candidateDevices }) => { setIsOpen(false); @@ -33,27 +36,39 @@ export default function ProposalTargetSection({ proposal, calculateProposal }) { }; const openDeviceSelector = () => setIsOpen(true); + const navigateToISCSIPage = () => navigate("/storage/iscsi"); const { availableDevices = [] } = proposal; - const renderSelector = availableDevices.length > 0; - const Content = () => ( - <> - - - - - Accept - setIsOpen(false)} autoFocus /> - - - - ); + const Content = () => { + return ( + <> + + + + + Accept + setIsOpen(false)} autoFocus /> + + + + ); + }; + + const NoDevicesContent = () => { + return ( +
+
No devices found
+
Please, configure iSCSI targets in order to find available devices for installation.
+ +
+ ); + }; return (
- } else="No available devices" /> + } else={} />
); } diff --git a/web/src/components/storage/index.js b/web/src/components/storage/index.js index b01be2c1d5..e3c69a4a11 100644 --- a/web/src/components/storage/index.js +++ b/web/src/components/storage/index.js @@ -28,3 +28,4 @@ export { default as ProposalSettingsForm } from "./ProposalSettingsForm"; export { default as DeviceSelector } from "./DeviceSelector"; export { default as ProposalActions } from "./ProposalActions"; export { default as ProposalSummary } from "./ProposalSummary"; +export { default as ISCSIPage } from "./ISCSIPage"; diff --git a/web/src/components/storage/iscsi/AuthFields.jsx b/web/src/components/storage/iscsi/AuthFields.jsx new file mode 100644 index 0000000000..0620ef4ec9 --- /dev/null +++ b/web/src/components/storage/iscsi/AuthFields.jsx @@ -0,0 +1,157 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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. + */ + +import React, { useEffect } from "react"; +import { FormGroup, TextInput } from "@patternfly/react-core"; + +import { Fieldset } from "~/components/core"; +import { Icon } from "~/components/layout"; + +export default function AuthFields({ data, onChange, onValidate }) { + const onUsernameChange = v => onChange("username", v); + const onPasswordChange = v => onChange("password", v); + const onReverseUsernameChange = v => onChange("reverseUsername", v); + const onReversePasswordChange = v => onChange("reversePassword", v); + + const isValidText = v => v.length > 0; + const isValidUsername = () => isValidText(data.username); + const isValidPassword = () => isValidText(data.password); + const isValidReverseUsername = () => isValidText(data.reverseUsername); + const isValidReversePassword = () => isValidText(data.reversePassword); + const isValidAuth = () => isValidUsername() && isValidPassword(); + + const showUsernameError = () => isValidPassword() ? !isValidUsername() : false; + const showPasswordError = () => isValidUsername() ? !isValidPassword() : false; + const showReverseUsernameError = () => { + return (isValidAuth() && isValidReversePassword()) ? !isValidReverseUsername() : false; + }; + const showReversePasswordError = () => { + return (isValidAuth() && isValidReverseUsername()) ? !isValidReversePassword() : false; + }; + + useEffect(() => { + onValidate( + !showUsernameError() && + !showPasswordError() && + !showReverseUsernameError() && + !showReversePasswordError() + ); + }); + + const ByInitiatorAuthLegend = () => { + const Info = () => { + return ( +

+ + Only available if authentication by target is provided +

+ ); + }; + + return ( + <> + Authentication by inititator + { !isValidAuth() && } + + ); + }; + + return ( + <> +
+ + + + + + +
+
+ + + + + + +
+ + ); +} diff --git a/web/src/components/storage/iscsi/DiscoverForm.jsx b/web/src/components/storage/iscsi/DiscoverForm.jsx new file mode 100644 index 0000000000..70f53962aa --- /dev/null +++ b/web/src/components/storage/iscsi/DiscoverForm.jsx @@ -0,0 +1,153 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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. + */ + +import React, { useEffect, useState } from "react"; +import { + Alert, + Form, FormGroup, + TextInput, +} from "@patternfly/react-core"; + +import { Popup } from "~/components/core"; +import { AuthFields } from "~/components/storage/iscsi"; +import { useLocalStorage } from "~/utils"; +import { isValidIp } from "~/client/network/utils"; + +const defaultData = { + address: "", + port: "3260", + username: "", + password: "", + reverseUsername: "", + reversePassword: "" +}; + +export default function DiscoverForm({ client, onSuccess, onCancel }) { + const [savedData, setSavedData] = useLocalStorage("dinstaller-iscsi-discovery", {}); + const [data, setData] = useState(defaultData); + const [isLoading, setIsLoading] = useState(false); + const [isFailed, setIsFailed] = useState(false); + const [isValidAuth, setIsValidAuth] = useState(true); + + useEffect(() => { + setData(savedData); + }, [setData, savedData]); + + const updateData = (key, value) => setData({ ...data, [key]: value }); + const onAddressChange = v => updateData("address", v); + const onPortChange = v => updateData("port", v); + + const onSubmit = async (event) => { + event.preventDefault(); + + setIsLoading(true); + setSavedData(data); + + const { username, password, reverseUsername, reversePassword } = data; + const result = await client.iscsi.discover(data.address, parseInt(data.port), { + username, password, reverseUsername, reversePassword + }); + + if (result === 0) { + onSuccess(); + return; + } + + setIsFailed(true); + setIsLoading(false); + }; + + const isValidAddress = () => isValidIp(data.address); + const isValidPort = () => Number.isInteger(parseInt(data.port)); + const isValidForm = () => { + return ( + isValidAddress() && + isValidPort() && + isValidAuth + ); + }; + + const showAddressError = () => data.address.length > 0 && !isValidAddress(data.address); + const showPortError = () => data.port.length > 0 && !isValidPort(data.port); + + const id = "iscsiDiscover"; + const isDisabled = isLoading || !isValidForm(); + + return ( + +
+ { isFailed && + +

Make sure you provide the correct values

+
} + + + + + + + setIsValidAuth(v)} + /> + + + + + +
+ ); +} diff --git a/web/src/components/storage/iscsi/EditNodeForm.jsx b/web/src/components/storage/iscsi/EditNodeForm.jsx new file mode 100644 index 0000000000..4a858d2783 --- /dev/null +++ b/web/src/components/storage/iscsi/EditNodeForm.jsx @@ -0,0 +1,70 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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. + */ + +import React, { useState } from "react"; +import { + Form, FormGroup, FormSelect, FormSelectOption +} from "@patternfly/react-core"; + +import { Popup } from "~/components/core"; +import { NodeStartupOptions } from "~/components/storage/iscsi"; + +export default function EditNodeForm({ node, client, onSuccess, onCancel }) { + const [data, setData] = useState({ startup: node.startup }); + + const onStartupChange = v => setData({ ...data, startup: v }); + + const onSubmit = async (event) => { + event.preventDefault(); + await client.iscsi.setStartup(node, data.startup); + onSuccess(); + }; + + const startupFormOptions = Object.values(NodeStartupOptions).map((option, i) => ( + + )); + + const id = "iscsiEditNode"; + + return ( + +
+ + + {startupFormOptions} + + +
+ + + + +
+ ); +} diff --git a/web/src/components/storage/iscsi/InitiatorForm.jsx b/web/src/components/storage/iscsi/InitiatorForm.jsx new file mode 100644 index 0000000000..ad0d04a8e3 --- /dev/null +++ b/web/src/components/storage/iscsi/InitiatorForm.jsx @@ -0,0 +1,66 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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. + */ + +import React, { useState } from "react"; +import { Form, FormGroup, TextInput } from "@patternfly/react-core"; + +import { Popup } from "~/components/core"; + +export default function InitiatorForm({ initiator, client, onSuccess, onCancel }) { + const [data, setData] = useState({ ...initiator }); + + const onNameChange = name => setData({ ...data, name }); + + const onSubmit = async (event) => { + event.preventDefault(); + await client.iscsi.setInitiatorName(data.name); + onSuccess(); + }; + + const id = "editIscsiInitiator"; + const isDisabled = data.name === ""; + + return ( + +
+ + + +
+ + + + +
+ ); +} diff --git a/web/src/components/storage/iscsi/InitiatorPresenter.jsx b/web/src/components/storage/iscsi/InitiatorPresenter.jsx new file mode 100644 index 0000000000..257256522f --- /dev/null +++ b/web/src/components/storage/iscsi/InitiatorPresenter.jsx @@ -0,0 +1,122 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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. + */ + +import React, { useEffect, useState } from "react"; +import { DropdownToggle, Skeleton } from "@patternfly/react-core"; +import { TableComposable, Thead, Tr, Th, Tbody, Td, ActionsColumn } from '@patternfly/react-table'; + +import { Icon } from '~/components/layout'; +import { InitiatorForm } from "~/components/storage/iscsi"; + +const RowActions = ({ actions, id, ...props }) => { + const actionsToggle = (props) => ( + + + + ); + + return ( + + ); +}; + +export default function InitiatorPresenter({ initiator, client }) { + const [data, setData] = useState(); + const [isFormOpen, setIsFormOpen] = useState(false); + + useEffect(() => { + if (initiator !== undefined) setData({ ...initiator }); + }, [setData, initiator]); + + const openForm = () => setIsFormOpen(true); + const closeForm = () => setIsFormOpen(false); + + const onSuccess = () => { + setData(undefined); + closeForm(); + }; + + const initiatorActions = () => { + const actions = { + edit: { title: "Edit", onClick: openForm } + }; + + return [actions.edit]; + }; + + const Content = () => { + if (data === undefined) { + return ( + + + + + + ); + } + + return ( + + {data.name} + {data.ibft ? "Yes" : "No"} + {data.offloadCard || "None"} + + + + + ); + }; + + return ( + <> + + + + Name + iBFT + Offload card + + + + + + + + { isFormOpen && + } + + ); +} diff --git a/web/src/components/storage/iscsi/InitiatorSection.jsx b/web/src/components/storage/iscsi/InitiatorSection.jsx new file mode 100644 index 0000000000..96bd4652bc --- /dev/null +++ b/web/src/components/storage/iscsi/InitiatorSection.jsx @@ -0,0 +1,55 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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. + */ + +import React, { useEffect, useState } from "react"; + +import { Section } from "~/components/core"; +import { InitiatorPresenter } from "~/components/storage/iscsi"; +import { useInstallerClient } from "~/context/installer"; +import { useCancellablePromise } from "~/utils"; + +export default function InitiatorSection() { + const { storage: client } = useInstallerClient(); + const { cancellablePromise } = useCancellablePromise(); + const [initiator, setInitiator] = useState(); + + useEffect(() => { + const loadInitiator = async () => { + setInitiator(undefined); + const name = await cancellablePromise(client.iscsi.getInitiatorName()); + const ibft = await cancellablePromise(client.iscsi.getInitiatorIbft()); + setInitiator({ name, ibft, offloadCard: "" }); + }; + + loadInitiator().catch(console.error); + + return client.iscsi.onInitiatorChanged(loadInitiator); + }, [cancellablePromise, client.iscsi]); + + return ( +
+ +
+ ); +} diff --git a/web/src/components/storage/iscsi/LoginForm.jsx b/web/src/components/storage/iscsi/LoginForm.jsx new file mode 100644 index 0000000000..361b28e477 --- /dev/null +++ b/web/src/components/storage/iscsi/LoginForm.jsx @@ -0,0 +1,101 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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. + */ + +import React, { useState } from "react"; +import { + Alert, + Form, FormGroup, FormSelect, FormSelectOption +} from "@patternfly/react-core"; + +import { Popup } from "~/components/core"; +import { AuthFields, NodeStartupOptions } from "~/components/storage/iscsi"; + +export default function LoginForm({ node, client, onSuccess, onCancel }) { + const [data, setData] = useState({ + username: "", + password: "", + reverseUsername: "", + reversePassword: "", + startup: "onboot" + }); + const [isLoading, setIsLoading] = useState(false); + const [isFailed, setIsFailed] = useState(false); + const [isValidAuth, setIsValidAuth] = useState(true); + + const updateData = (key, value) => setData({ ...data, [key]: value }); + const onStartupChange = v => updateData("startup", v); + + const onSubmit = async (event) => { + setIsLoading(true); + event.preventDefault(); + + const result = await client.iscsi.login(node, data); + + if (result === 0) { + onSuccess(); + return; + } + + setIsFailed(true); + setIsLoading(false); + }; + + const startupFormOptions = Object.values(NodeStartupOptions).map((option, i) => ( + + )); + + const id = "iscsiLogin"; + const isDisabled = isLoading || !isValidAuth; + + return ( + +
+ { isFailed && + +

Make sure you provide the correct values

+
} + + + {startupFormOptions} + + + setIsValidAuth(v)} + /> + + + + + +
+ ); +} diff --git a/web/src/components/storage/iscsi/NodesPresenter.jsx b/web/src/components/storage/iscsi/NodesPresenter.jsx new file mode 100644 index 0000000000..fcdd761e5c --- /dev/null +++ b/web/src/components/storage/iscsi/NodesPresenter.jsx @@ -0,0 +1,156 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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. + */ + +import React, { useState } from "react"; +import { DropdownToggle } from "@patternfly/react-core"; +import { TableComposable, Thead, Tr, Th, Tbody, Td, ActionsColumn } from '@patternfly/react-table'; + +import { Icon } from '~/components/layout'; +import { EditNodeForm, LoginForm, NodeStartupOptions } from "~/components/storage/iscsi"; + +const RowActions = ({ actions, id, ...props }) => { + const actionsToggle = (props) => ( + + + + ); + + return ( + + ); +}; + +export default function NodesPresenter ({ nodes, client }) { + const [currentNode, setCurrentNode] = useState(); + const [isEditFormOpen, setIsEditFormOpen] = useState(false); + const [isLoginFormOpen, setIsLoginFormOpen] = useState(false); + + const openLoginForm = (node) => { + setCurrentNode(node); + setIsLoginFormOpen(true); + }; + + const closeLoginForm = () => setIsLoginFormOpen(false); + + const openEditForm = (node) => { + setCurrentNode(node); + setIsEditFormOpen(true); + }; + + const closeEditForm = () => setIsEditFormOpen(false); + + const nodeStatus = (node) => { + if (!node.connected) return "Disconnected"; + + const startup = Object.values(NodeStartupOptions).find(o => o.value === node.startup); + return `Connected (${startup.label})`; + }; + + const nodeActions = (node) => { + const actions = { + edit: { + title: "Edit", + onClick: () => openEditForm(node) + }, + delete: { + title: "Delete", + onClick: () => client.iscsi.delete(node), + className: "danger-action" + }, + login: { + title: "Login", + onClick: () => openLoginForm(node) + }, + logout: { + title: "Logout", + onClick: () => client.iscsi.logout(node) + } + }; + + if (node.connected) + return [actions.edit, actions.logout]; + else + return [actions.login, actions.delete]; + }; + + const NodeRow = ({ node }) => { + return ( + + {node.target} + {node.address + ":" + node.port} + {node.interface} + {node.ibft ? "Yes" : "No"} + {nodeStatus(node)} + + + + + ); + }; + + const Content = () => { + return nodes.map(n => ); + }; + + return ( + <> + + + + Name + Portal + Interface + iBFT + Status + + + + + + + + { isLoginFormOpen && + } + { isEditFormOpen && + } + + ); +} diff --git a/web/src/components/storage/iscsi/TargetsSection.jsx b/web/src/components/storage/iscsi/TargetsSection.jsx new file mode 100644 index 0000000000..37cd9977d9 --- /dev/null +++ b/web/src/components/storage/iscsi/TargetsSection.jsx @@ -0,0 +1,163 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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. + */ + +import React, { useEffect, useReducer } from "react"; +import { + Button, + Toolbar, ToolbarItem, ToolbarContent, + Skeleton +} from "@patternfly/react-core"; + +import { Section } from "~/components/core"; +import { NodesPresenter, DiscoverForm } from "~/components/storage/iscsi"; +import { useInstallerClient } from "~/context/installer"; +import { useCancellablePromise } from "~/utils"; + +const reducer = (state, action) => { + switch (action.type) { + case "SET_NODES": { + return { ...state, nodes: action.payload.nodes }; + } + + case "ADD_NODE": { + if (state.nodes.find(n => n.id === action.payload.node.id)) return state; + + const nodes = [...state.nodes]; + nodes.push(action.payload.node); + return { ...state, nodes }; + } + + case "UPDATE_NODE": { + const index = state.nodes.findIndex(n => n.id === action.payload.node.id); + if (index === -1) return state; + + const nodes = [...state.nodes]; + nodes[index] = action.payload.node; + return { ...state, nodes }; + } + + case "REMOVE_NODE": { + const nodes = state.nodes.filter(n => n.id !== action.payload.node.id); + return { ...state, nodes }; + } + + case "OPEN_DISCOVER_FORM": { + return { ...state, isDiscoverFormOpen: true }; + } + + case "CLOSE_DISCOVER_FORM": { + return { ...state, isDiscoverFormOpen: false }; + } + + case "START_LOADING": { + return { ...state, isLoading: true }; + } + + case "STOP_LOADING": { + return { ...state, isLoading: false }; + } + + default: { + return state; + } + } +}; + +const initialState = { + nodes: [], + isDiscoverFormOpen: false, + isLoading: true +}; + +export default function TargetsSection() { + const { storage: client } = useInstallerClient(); + const { cancellablePromise } = useCancellablePromise(); + const [state, dispatch] = useReducer(reducer, initialState); + + useEffect(() => { + const loadNodes = async () => { + dispatch({ type: "START_LOADING" }); + const nodes = await cancellablePromise(client.iscsi.getNodes()); + dispatch({ type: "SET_NODES", payload: { nodes } }); + dispatch({ type: "STOP_LOADING" }); + }; + + loadNodes().catch(console.error); + }, [cancellablePromise, client.iscsi]); + + useEffect(() => { + const action = (type, node) => dispatch({ type, payload: { node } }); + + client.iscsi.onNodeAdded(n => action("ADD_NODE", n)); + client.iscsi.onNodeChanged(n => action("UPDATE_NODE", n)); + client.iscsi.onNodeRemoved(n => action("REMOVE_NODE", n)); + }, [client.iscsi]); + + const openDiscoverForm = () => { + dispatch({ type: "OPEN_DISCOVER_FORM" }); + }; + + const closeDiscoverForm = () => { + dispatch({ type: "CLOSE_DISCOVER_FORM" }); + }; + + const SectionContent = () => { + if (state.isLoading) return ; + + if (state.nodes.length === 0) { + return ( +
+
No iSCSI targets found
+
Please, perform an iSCSI discovery in order to find available iSCSI targets.
+ +
+ ); + } + + return ( + <> + + + + + + + + + + ); + }; + + return ( +
+ + { state.isDiscoverFormOpen && + } +
+ ); +} diff --git a/web/src/components/storage/iscsi/index.js b/web/src/components/storage/iscsi/index.js new file mode 100644 index 0000000000..1e27bf7d70 --- /dev/null +++ b/web/src/components/storage/iscsi/index.js @@ -0,0 +1,37 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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. + */ + +const NodeStartupOptions = Object.freeze({ + MANUAL: { label: "Manual", value: "manual" }, + ONBOOT: { label: "On boot", value: "onboot" }, + AUTOMATIC: { label: "Automatic", value: "automatic" } +}); + +export { default as InitiatorSection } from "./InitiatorSection"; +export { default as InitiatorPresenter } from "./InitiatorPresenter"; +export { default as InitiatorForm } from "./InitiatorForm"; +export { default as TargetsSection } from "./TargetsSection"; +export { default as NodesPresenter } from "./NodesPresenter"; +export { default as DiscoverForm } from "./DiscoverForm"; +export { default as EditNodeForm } from "./EditNodeForm"; +export { default as LoginForm } from "./LoginForm"; +export { default as AuthFields } from "./AuthFields"; +export { NodeStartupOptions }; diff --git a/web/src/index.js b/web/src/index.js index 674d23ca9c..29f24b1cc3 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -36,7 +36,7 @@ import App from "~/App"; import Main from "~/Main"; import { Overview } from "~/components/overview"; import { ProductSelectionPage } from "~/components/software"; -import { ProposalPage as StoragePage } from "~/components/storage"; +import { ProposalPage as StoragePage, ISCSIPage } from "~/components/storage"; import { UsersPage } from "~/components/users"; import { L10nPage } from "~/components/l10n"; import { NetworkPage } from "~/components/network"; @@ -58,6 +58,7 @@ root.render( } /> } /> } /> + } /> } /> From b4789aa1abfb60c721c44ea83f72c16ebb9fe9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Tue, 14 Mar 2023 15:49:36 +0000 Subject: [PATCH 05/19] [web] Sidebar option for iSCSI page --- web/src/components/storage/ProposalPage.jsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/web/src/components/storage/ProposalPage.jsx b/web/src/components/storage/ProposalPage.jsx index 206f5de105..911c5e66b1 100644 --- a/web/src/components/storage/ProposalPage.jsx +++ b/web/src/components/storage/ProposalPage.jsx @@ -21,11 +21,12 @@ import React, { useReducer, useEffect } from "react"; import { Alert } from "@patternfly/react-core"; +import { Link } from "react-router-dom"; import { useInstallerClient } from "~/context/installer"; import { useCancellablePromise } from "~/utils"; import { Icon } from "~/components/layout"; -import { Page, SectionSkeleton } from "~/components/core"; +import { Page, PageOptions, SectionSkeleton } from "~/components/core"; import { ProposalTargetSection, ProposalSettingsSection, @@ -113,8 +114,14 @@ export default function ProposalPage() { }; return ( - + + + + + Configure iSCSI + + ); } From 8eb9956a5f97e60985381a8db663e4cb06154b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 16 Mar 2023 11:27:55 +0000 Subject: [PATCH 06/19] [service] Emit properties changed signal --- service/lib/dinstaller/dbus/storage/manager.rb | 9 +++++++-- service/lib/dinstaller/storage/iscsi/manager.rb | 11 ++++++++++- service/test/dinstaller/dbus/storage/manager_test.rb | 4 +++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/service/lib/dinstaller/dbus/storage/manager.rb b/service/lib/dinstaller/dbus/storage/manager.rb index 8562b00977..5d5a9e5cf9 100644 --- a/service/lib/dinstaller/dbus/storage/manager.rb +++ b/service/lib/dinstaller/dbus/storage/manager.rb @@ -232,18 +232,23 @@ def proposal def register_proposal_callbacks proposal.on_calculate do export_proposal - properties_changed + proposal_properties_changed update_validation end end def register_iscsi_callbacks + backend.iscsi.on_activate do + properties = interfaces_and_properties[ISCSI_INITIATOR_INTERFACE] + dbus_properties_changed(ISCSI_INITIATOR_INTERFACE, properties, []) + end + backend.iscsi.on_probe do refresh_iscsi_nodes end end - def properties_changed + def proposal_properties_changed properties = interfaces_and_properties[PROPOSAL_CALCULATOR_INTERFACE] dbus_properties_changed(PROPOSAL_CALCULATOR_INTERFACE, properties, []) end diff --git a/service/lib/dinstaller/storage/iscsi/manager.rb b/service/lib/dinstaller/storage/iscsi/manager.rb index d281f9506d..15097068bc 100644 --- a/service/lib/dinstaller/storage/iscsi/manager.rb +++ b/service/lib/dinstaller/storage/iscsi/manager.rb @@ -49,6 +49,7 @@ def initialize(logger: nil) @logger = logger || ::Logger.new($stdout) @initiator = ISCSI::Initiator.new + @on_activate_callbacks = [] @on_probe_callbacks = [] end @@ -71,7 +72,8 @@ def activate Yast::IscsiClientLib.getConfig Yast::IscsiClientLib.autoLogOn - sleep(sl) + + @on_activate_callbacks.each(&:call) end # Probes iSCSI @@ -171,6 +173,13 @@ def update(node, startup:) end end + # Registers a callback to be called after performing iSCSI actvation + # + # @param block [Proc] + def on_activate(&block) + @on_activate_callbacks << block + end + # Registers a callback to be called when the nodes are probed # # @param block [Proc] diff --git a/service/test/dinstaller/dbus/storage/manager_test.rb b/service/test/dinstaller/dbus/storage/manager_test.rb index d63abeb292..858bb69b9c 100644 --- a/service/test/dinstaller/dbus/storage/manager_test.rb +++ b/service/test/dinstaller/dbus/storage/manager_test.rb @@ -50,7 +50,9 @@ let(:settings) { nil } - let(:iscsi) { instance_double(DInstaller::Storage::ISCSI::Manager, on_probe: nil) } + let(:iscsi) do + instance_double(DInstaller::Storage::ISCSI::Manager, on_activate: nil, on_probe: nil) + end before do allow(Yast::Arch).to receive(:s390).and_return false From bac07259e63e0bc50d8491ffb33ea3e385e3e63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 16 Mar 2023 11:29:03 +0000 Subject: [PATCH 07/19] [web] Add method to set up the storage client --- web/src/client/storage.js | 107 ++++++++++++++------------------------ 1 file changed, 38 insertions(+), 69 deletions(-) diff --git a/web/src/client/storage.js b/web/src/client/storage.js index 03eba0994b..d2369da82c 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -20,16 +20,18 @@ */ // @ts-check +// cspell:ignore startup import DBusClient from "./dbus"; import { WithStatus, WithValidation } from "./mixins"; -const PROPOSAL_CALCULATOR_IFACE = "org.opensuse.DInstaller.Storage1.Proposal.Calculator"; -const ISCSI_NODE_IFACE = "org.opensuse.DInstaller.Storage1.ISCSI.Node"; +const SERVICE = "org.opensuse.DInstaller.Storage"; +const STORAGE_OBJECT = "/org/opensuse/DInstaller/Storage1"; const ISCSI_NODES_NAMESPACE = "/org/opensuse/DInstaller/Storage1/iscsi_nodes"; +const PROPOSAL_CALCULATOR_IFACE = "org.opensuse.DInstaller.Storage1.Proposal.Calculator"; const ISCSI_INITIATOR_IFACE = "org.opensuse.DInstaller.Storage1.ISCSI.Initiator"; const PROPOSAL_IFACE = "org.opensuse.DInstaller.Storage1.Proposal"; -const STORAGE_OBJECT = "/org/opensuse/DInstaller/Storage1"; +const ISCSI_NODE_IFACE = "org.opensuse.DInstaller.Storage1.ISCSI.Node"; /** * Removes properties with undefined value @@ -62,6 +64,12 @@ class ProposalManager { this.proxies = {}; } + async setUp() { + const proposalCalculator = await this.client.proxy(PROPOSAL_CALCULATOR_IFACE, STORAGE_OBJECT); + + this.proxies = { proposalCalculator }; + } + /** * Gets data associated to the proposal * @@ -95,8 +103,7 @@ class ProposalManager { }; }; - const proxy = await this.proposalCalculatorProxy(); - return proxy.AvailableDevices.map(buildDevice); + return this.proxies.proposalCalculator.AvailableDevices.map(buildDevice); } /** @@ -214,21 +221,7 @@ class ProposalManager { Volumes: { t: "aa{sv}", v: volumes?.map(dbusVolume) } }); - const proxy = await this.proposalCalculatorProxy(); - return proxy.Calculate(settings); - } - - /** - * @private - * Proxy for org.opensuse.DInstaller.Storage1.Proposal.Calculator iface - * - * @returns {Promise} - */ - async proposalCalculatorProxy() { - if (!this.proxies.proposalCalculator) - this.proxies.proposalCalculator = await this.client.proxy(PROPOSAL_CALCULATOR_IFACE, STORAGE_OBJECT); - - return this.proxies.proposalCalculator; + return this.proxies.proposalCalculator.Calculate(settings); } /** @@ -260,9 +253,15 @@ class ISCSIManager { this.proxies = {}; } + async setUp() { + const iscsiInitiator = await this.client.proxy(ISCSI_INITIATOR_IFACE, STORAGE_OBJECT); + const iscsiNodes = await this.client.proxies(ISCSI_NODE_IFACE, ISCSI_NODES_NAMESPACE); + + this.proxies = { iscsiInitiator, iscsiNodes }; + } + async getInitiatorIbft() { - const proxy = await this.iscsiInitiatorProxy(); - return proxy.IBFT; + return this.proxies.iscsiInitiator.IBFT; } /** @@ -271,8 +270,7 @@ class ISCSIManager { * @returns {Promise} */ async getInitiatorName() { - const proxy = await this.iscsiInitiatorProxy(); - return proxy.InitiatorName; + return this.proxies.iscsiInitiator.InitiatorName; } /** @@ -281,8 +279,7 @@ class ISCSIManager { * @param {string} value */ async setInitiatorName(value) { - const proxy = await this.iscsiInitiatorProxy(); - proxy.InitiatorName = value; + this.proxies.iscsiInitiator.InitiatorName = value; } /** @@ -301,8 +298,7 @@ class ISCSIManager { * @property {string} startup */ async getNodes() { - const proxy = await this.iscsiNodesProxy(); - return Object.values(proxy).map(this.buildNode); + return Object.values(this.proxies.iscsiNodes).map(this.buildNode); } /** @@ -328,8 +324,7 @@ class ISCSIManager { ReversePassword: { t: "s", v: options.reversePassword } }); - const proxy = await this.iscsiInitiatorProxy(); - return proxy.Discover(address, port, auth); + return this.proxies.iscsiInitiator.Discover(address, port, auth); } /** @@ -355,8 +350,7 @@ class ISCSIManager { async delete(node) { const path = this.nodePath(node); - const proxy = await this.iscsiInitiatorProxy(); - return proxy.Delete(path); + return this.proxies.iscsiInitiator.Delete(path); } /** @@ -417,18 +411,15 @@ class ISCSIManager { } async onNodeAdded(handler) { - const proxy = await this.iscsiNodesProxy(); - proxy.addEventListener("added", (_, proxy) => handler(this.buildNode(proxy))); + this.proxies.iscsiNodes.addEventListener("added", (_, proxy) => handler(this.buildNode(proxy))); } async onNodeChanged(handler) { - const proxy = await this.iscsiNodesProxy(); - proxy.addEventListener("changed", (_, proxy) => handler(this.buildNode(proxy))); + this.proxies.iscsiNodes.addEventListener("changed", (_, proxy) => handler(this.buildNode(proxy))); } async onNodeRemoved(handler) { - const proxy = await this.iscsiNodesProxy(); - proxy.addEventListener("removed", (_, proxy) => handler(this.buildNode(proxy))); + this.proxies.iscsiNodes.addEventListener("removed", (_, proxy) => handler(this.buildNode(proxy))); } buildNode(proxy) { @@ -446,34 +437,6 @@ class ISCSIManager { }; } - /** - * @private - * Proxy for org.opensuse.DInstaller.Storage1.ISCSI.Initiator iface - * - * @returns {Promise} - */ - async iscsiInitiatorProxy() { - if (!this.proxies.iscsiInitiator) - this.proxies.iscsiInitiator = await this.client.proxy(ISCSI_INITIATOR_IFACE, STORAGE_OBJECT); - - return this.proxies.iscsiInitiator; - } - - /** - * @private - * Proxy for objects implementing org.opensuse.DInstaller.Storage1.ISCSI.Node iface - * - * @note The ISCSI nodes are dynamically exported. - * - * @returns {Promise} - */ - async iscsiNodesProxy() { - if (!this.proxies.iscsiNodes) - this.proxies.iscsiNodes = await this.client.proxies(ISCSI_NODE_IFACE, ISCSI_NODES_NAMESPACE); - - return this.proxies.iscsiNodes; - } - /** * @private * Builds the D-Bus path for the given iSCSI node @@ -492,16 +455,22 @@ class ISCSIManager { * @ignore */ class StorageBaseClient { - static SERVICE = "org.opensuse.DInstaller.Storage"; - /** * @param {string|undefined} address - D-Bus address; if it is undefined, it uses the system bus. */ constructor(address = undefined) { - this.client = new DBusClient(StorageBaseClient.SERVICE, address); + this.client = new DBusClient(SERVICE, address); this.proposal = new ProposalManager(this.client); this.iscsi = new ISCSIManager(this.client); } + + /** + * Initializes the proxies + */ + async setUp() { + await this.proposal.setUp(); + await this.iscsi.setUp(); + } } /** From 1b81b9dbdb84ee1739e457fe99a02ff065c55fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 16 Mar 2023 11:30:27 +0000 Subject: [PATCH 08/19] [web] Move nodes option to a separate file --- .../storage/iscsi/NodeStartupOptions.js | 30 +++++++++++++++++++ web/src/components/storage/iscsi/index.js | 8 +---- 2 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 web/src/components/storage/iscsi/NodeStartupOptions.js diff --git a/web/src/components/storage/iscsi/NodeStartupOptions.js b/web/src/components/storage/iscsi/NodeStartupOptions.js new file mode 100644 index 0000000000..a27fd1c9e7 --- /dev/null +++ b/web/src/components/storage/iscsi/NodeStartupOptions.js @@ -0,0 +1,30 @@ +/* + * Copyright (c) [2023] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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. + */ + +// cspell:ignore onboot + +const NodeStartupOptions = Object.freeze({ + MANUAL: { label: "Manual", value: "manual" }, + ONBOOT: { label: "On boot", value: "onboot" }, + AUTOMATIC: { label: "Automatic", value: "automatic" } +}); + +export default NodeStartupOptions; diff --git a/web/src/components/storage/iscsi/index.js b/web/src/components/storage/iscsi/index.js index 1e27bf7d70..561badb9e8 100644 --- a/web/src/components/storage/iscsi/index.js +++ b/web/src/components/storage/iscsi/index.js @@ -19,12 +19,6 @@ * find current contact information at www.suse.com. */ -const NodeStartupOptions = Object.freeze({ - MANUAL: { label: "Manual", value: "manual" }, - ONBOOT: { label: "On boot", value: "onboot" }, - AUTOMATIC: { label: "Automatic", value: "automatic" } -}); - export { default as InitiatorSection } from "./InitiatorSection"; export { default as InitiatorPresenter } from "./InitiatorPresenter"; export { default as InitiatorForm } from "./InitiatorForm"; @@ -34,4 +28,4 @@ export { default as DiscoverForm } from "./DiscoverForm"; export { default as EditNodeForm } from "./EditNodeForm"; export { default as LoginForm } from "./LoginForm"; export { default as AuthFields } from "./AuthFields"; -export { NodeStartupOptions }; +export { default as NodeStartupOptions } from "./NodeStartupOptions"; From 00abb2701f79f4752f108568195a6b00c423b90f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 16 Mar 2023 11:34:59 +0000 Subject: [PATCH 09/19] [web] Set up storage client when needed --- web/src/components/overview/StorageSection.jsx | 15 ++++++++++++--- .../components/storage/iscsi/InitiatorSection.jsx | 9 ++++++++- .../components/storage/iscsi/TargetsSection.jsx | 15 ++++++++++++--- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/web/src/components/overview/StorageSection.jsx b/web/src/components/overview/StorageSection.jsx index 0379e71372..1833318fa3 100644 --- a/web/src/components/overview/StorageSection.jsx +++ b/web/src/components/overview/StorageSection.jsx @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -import React, { useReducer, useEffect } from "react"; +import React, { useReducer, useState, useEffect } from "react"; import { useCancellablePromise } from "~/utils"; import { useInstallerClient } from "~/context/installer"; import { BUSY } from "~/client/status"; @@ -56,8 +56,15 @@ export default function StorageSection({ showErrors }) { const client = useInstallerClient(); const { cancellablePromise } = useCancellablePromise(); const [state, dispatch] = useReducer(reducer, initialState); + const [initialized, setInitialized] = useState(false); useEffect(() => { + client.storage.setUp().then(() => setInitialized(true)); + }, [client.storage]); + + useEffect(() => { + if (!initialized) return; + const updateStatus = (status) => { dispatch({ type: "UPDATE_STATUS", payload: { status } }); }; @@ -65,9 +72,11 @@ export default function StorageSection({ showErrors }) { cancellablePromise(client.storage.getStatus()).then(updateStatus); return client.storage.onStatusChange(updateStatus); - }, [client.storage, cancellablePromise]); + }, [client.storage, cancellablePromise, initialized]); useEffect(() => { + if (!initialized) return; + const updateProposal = async () => { const proposal = await cancellablePromise(client.storage.proposal.getData()); const errors = await cancellablePromise(client.storage.getValidationErrors()); @@ -76,7 +85,7 @@ export default function StorageSection({ showErrors }) { }; updateProposal(); - }, [client.storage, cancellablePromise, state.busy]); + }, [client.storage, cancellablePromise, state.busy, initialized]); const errors = showErrors ? state.errors : []; diff --git a/web/src/components/storage/iscsi/InitiatorSection.jsx b/web/src/components/storage/iscsi/InitiatorSection.jsx index 96bd4652bc..5f0fc42947 100644 --- a/web/src/components/storage/iscsi/InitiatorSection.jsx +++ b/web/src/components/storage/iscsi/InitiatorSection.jsx @@ -30,8 +30,15 @@ export default function InitiatorSection() { const { storage: client } = useInstallerClient(); const { cancellablePromise } = useCancellablePromise(); const [initiator, setInitiator] = useState(); + const [initialized, setInitialized] = useState(false); useEffect(() => { + client.setUp().then(() => setInitialized(true)); + }, [client]); + + useEffect(() => { + if (!initialized) return; + const loadInitiator = async () => { setInitiator(undefined); const name = await cancellablePromise(client.iscsi.getInitiatorName()); @@ -42,7 +49,7 @@ export default function InitiatorSection() { loadInitiator().catch(console.error); return client.iscsi.onInitiatorChanged(loadInitiator); - }, [cancellablePromise, client.iscsi]); + }, [cancellablePromise, client, initialized]); return (
diff --git a/web/src/components/storage/iscsi/TargetsSection.jsx b/web/src/components/storage/iscsi/TargetsSection.jsx index 37cd9977d9..12c8b75772 100644 --- a/web/src/components/storage/iscsi/TargetsSection.jsx +++ b/web/src/components/storage/iscsi/TargetsSection.jsx @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -import React, { useEffect, useReducer } from "react"; +import React, { useEffect, useReducer, useState } from "react"; import { Button, Toolbar, ToolbarItem, ToolbarContent, @@ -91,8 +91,15 @@ export default function TargetsSection() { const { storage: client } = useInstallerClient(); const { cancellablePromise } = useCancellablePromise(); const [state, dispatch] = useReducer(reducer, initialState); + const [initialized, setInitialized] = useState(false); useEffect(() => { + client.setUp().then(() => setInitialized(true)); + }, [client]); + + useEffect(() => { + if (!initialized) return; + const loadNodes = async () => { dispatch({ type: "START_LOADING" }); const nodes = await cancellablePromise(client.iscsi.getNodes()); @@ -101,15 +108,17 @@ export default function TargetsSection() { }; loadNodes().catch(console.error); - }, [cancellablePromise, client.iscsi]); + }, [cancellablePromise, client.iscsi, initialized]); useEffect(() => { + if (!initialized) return; + const action = (type, node) => dispatch({ type, payload: { node } }); client.iscsi.onNodeAdded(n => action("ADD_NODE", n)); client.iscsi.onNodeChanged(n => action("UPDATE_NODE", n)); client.iscsi.onNodeRemoved(n => action("REMOVE_NODE", n)); - }, [client.iscsi]); + }, [client.iscsi, initialized]); const openDiscoverForm = () => { dispatch({ type: "OPEN_DISCOVER_FORM" }); From af80899ecec23422df1ca8bd46dea795ffe1a6fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 16 Mar 2023 11:36:19 +0000 Subject: [PATCH 10/19] [web] Do not pass client prop to forms --- .../components/storage/iscsi/DiscoverForm.jsx | 16 +++++--------- .../components/storage/iscsi/EditNodeForm.jsx | 5 ++--- .../storage/iscsi/InitiatorForm.jsx | 5 ++--- .../storage/iscsi/InitiatorPresenter.jsx | 22 +++++++++---------- .../components/storage/iscsi/LoginForm.jsx | 15 ++++++------- .../storage/iscsi/NodesPresenter.jsx | 18 +++++++++++---- .../storage/iscsi/TargetsSection.jsx | 14 ++++++++++-- 7 files changed, 53 insertions(+), 42 deletions(-) diff --git a/web/src/components/storage/iscsi/DiscoverForm.jsx b/web/src/components/storage/iscsi/DiscoverForm.jsx index 70f53962aa..baeaf5f9b3 100644 --- a/web/src/components/storage/iscsi/DiscoverForm.jsx +++ b/web/src/components/storage/iscsi/DiscoverForm.jsx @@ -40,7 +40,7 @@ const defaultData = { reversePassword: "" }; -export default function DiscoverForm({ client, onSuccess, onCancel }) { +export default function DiscoverForm({ onSubmit: onSubmitProp, onCancel }) { const [savedData, setSavedData] = useLocalStorage("dinstaller-iscsi-discovery", {}); const [data, setData] = useState(defaultData); const [isLoading, setIsLoading] = useState(false); @@ -61,18 +61,12 @@ export default function DiscoverForm({ client, onSuccess, onCancel }) { setIsLoading(true); setSavedData(data); - const { username, password, reverseUsername, reversePassword } = data; - const result = await client.iscsi.discover(data.address, parseInt(data.port), { - username, password, reverseUsername, reversePassword - }); + const result = await onSubmitProp(data); - if (result === 0) { - onSuccess(); - return; + if (result !== 0) { + setIsFailed(true); + setIsLoading(false); } - - setIsFailed(true); - setIsLoading(false); }; const isValidAddress = () => isValidIp(data.address); diff --git a/web/src/components/storage/iscsi/EditNodeForm.jsx b/web/src/components/storage/iscsi/EditNodeForm.jsx index 4a858d2783..2ca8ec0f83 100644 --- a/web/src/components/storage/iscsi/EditNodeForm.jsx +++ b/web/src/components/storage/iscsi/EditNodeForm.jsx @@ -27,15 +27,14 @@ import { import { Popup } from "~/components/core"; import { NodeStartupOptions } from "~/components/storage/iscsi"; -export default function EditNodeForm({ node, client, onSuccess, onCancel }) { +export default function EditNodeForm({ node, onSubmit: onSubmitProp, onCancel }) { const [data, setData] = useState({ startup: node.startup }); const onStartupChange = v => setData({ ...data, startup: v }); const onSubmit = async (event) => { event.preventDefault(); - await client.iscsi.setStartup(node, data.startup); - onSuccess(); + await onSubmitProp(data); }; const startupFormOptions = Object.values(NodeStartupOptions).map((option, i) => ( diff --git a/web/src/components/storage/iscsi/InitiatorForm.jsx b/web/src/components/storage/iscsi/InitiatorForm.jsx index ad0d04a8e3..a8257f4df0 100644 --- a/web/src/components/storage/iscsi/InitiatorForm.jsx +++ b/web/src/components/storage/iscsi/InitiatorForm.jsx @@ -24,15 +24,14 @@ import { Form, FormGroup, TextInput } from "@patternfly/react-core"; import { Popup } from "~/components/core"; -export default function InitiatorForm({ initiator, client, onSuccess, onCancel }) { +export default function InitiatorForm({ initiator, onSubmit: onSubmitProp, onCancel }) { const [data, setData] = useState({ ...initiator }); const onNameChange = name => setData({ ...data, name }); const onSubmit = async (event) => { event.preventDefault(); - await client.iscsi.setInitiatorName(data.name); - onSuccess(); + onSubmitProp(data); }; const id = "editIscsiInitiator"; diff --git a/web/src/components/storage/iscsi/InitiatorPresenter.jsx b/web/src/components/storage/iscsi/InitiatorPresenter.jsx index 257256522f..7c5ed9094a 100644 --- a/web/src/components/storage/iscsi/InitiatorPresenter.jsx +++ b/web/src/components/storage/iscsi/InitiatorPresenter.jsx @@ -49,18 +49,19 @@ const RowActions = ({ actions, id, ...props }) => { }; export default function InitiatorPresenter({ initiator, client }) { - const [data, setData] = useState(); + const [isLoading, setIsLoading] = useState(true); const [isFormOpen, setIsFormOpen] = useState(false); useEffect(() => { - if (initiator !== undefined) setData({ ...initiator }); - }, [setData, initiator]); + setIsLoading(initiator === undefined); + }, [initiator]); const openForm = () => setIsFormOpen(true); const closeForm = () => setIsFormOpen(false); + const submitForm = async (data) => { + await client.iscsi.setInitiatorName(data.name); - const onSuccess = () => { - setData(undefined); + setIsLoading(true); closeForm(); }; @@ -73,7 +74,7 @@ export default function InitiatorPresenter({ initiator, client }) { }; const Content = () => { - if (data === undefined) { + if (isLoading) { return ( @@ -85,9 +86,9 @@ export default function InitiatorPresenter({ initiator, client }) { return ( - {data.name} - {data.ibft ? "Yes" : "No"} - {data.offloadCard || "None"} + {initiator.name} + {initiator.ibft ? "Yes" : "No"} + {initiator.offloadCard || "None"} @@ -113,8 +114,7 @@ export default function InitiatorPresenter({ initiator, client }) { { isFormOpen && } diff --git a/web/src/components/storage/iscsi/LoginForm.jsx b/web/src/components/storage/iscsi/LoginForm.jsx index 361b28e477..f4c4621d25 100644 --- a/web/src/components/storage/iscsi/LoginForm.jsx +++ b/web/src/components/storage/iscsi/LoginForm.jsx @@ -19,6 +19,8 @@ * find current contact information at www.suse.com. */ +// cspell:ignore onboot + import React, { useState } from "react"; import { Alert, @@ -28,7 +30,7 @@ import { import { Popup } from "~/components/core"; import { AuthFields, NodeStartupOptions } from "~/components/storage/iscsi"; -export default function LoginForm({ node, client, onSuccess, onCancel }) { +export default function LoginForm({ node, onSubmit: onSubmitProp, onCancel }) { const [data, setData] = useState({ username: "", password: "", @@ -47,15 +49,12 @@ export default function LoginForm({ node, client, onSuccess, onCancel }) { setIsLoading(true); event.preventDefault(); - const result = await client.iscsi.login(node, data); + const result = await onSubmitProp(data); - if (result === 0) { - onSuccess(); - return; + if (result !== 0) { + setIsFailed(true); + setIsLoading(false); } - - setIsFailed(true); - setIsLoading(false); }; const startupFormOptions = Object.values(NodeStartupOptions).map((option, i) => ( diff --git a/web/src/components/storage/iscsi/NodesPresenter.jsx b/web/src/components/storage/iscsi/NodesPresenter.jsx index fcdd761e5c..4382782ff1 100644 --- a/web/src/components/storage/iscsi/NodesPresenter.jsx +++ b/web/src/components/storage/iscsi/NodesPresenter.jsx @@ -60,6 +60,13 @@ export default function NodesPresenter ({ nodes, client }) { const closeLoginForm = () => setIsLoginFormOpen(false); + const submitLoginForm = async (options) => { + const result = await client.iscsi.login(currentNode, options); + if (result === 0) closeLoginForm(); + + return result; + }; + const openEditForm = (node) => { setCurrentNode(node); setIsEditFormOpen(true); @@ -67,6 +74,11 @@ export default function NodesPresenter ({ nodes, client }) { const closeEditForm = () => setIsEditFormOpen(false); + const submitEditForm = async (data) => { + await client.iscsi.setStartup(currentNode, data.startup); + closeEditForm(); + }; + const nodeStatus = (node) => { if (!node.connected) return "Disconnected"; @@ -140,15 +152,13 @@ export default function NodesPresenter ({ nodes, client }) { { isLoginFormOpen && } { isEditFormOpen && } diff --git a/web/src/components/storage/iscsi/TargetsSection.jsx b/web/src/components/storage/iscsi/TargetsSection.jsx index 12c8b75772..9b7651484f 100644 --- a/web/src/components/storage/iscsi/TargetsSection.jsx +++ b/web/src/components/storage/iscsi/TargetsSection.jsx @@ -128,6 +128,17 @@ export default function TargetsSection() { dispatch({ type: "CLOSE_DISCOVER_FORM" }); }; + const submitDiscoverForm = async (data) => { + const { username, password, reverseUsername, reversePassword } = data; + const result = await client.iscsi.discover(data.address, parseInt(data.port), { + username, password, reverseUsername, reversePassword + }); + + if (result === 0) closeDiscoverForm(); + + return result; + }; + const SectionContent = () => { if (state.isLoading) return ; @@ -163,8 +174,7 @@ export default function TargetsSection() { { state.isDiscoverFormOpen && }
From 12fbb7b69589379957a1229d55d30944060f0a31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 16 Mar 2023 11:37:22 +0000 Subject: [PATCH 11/19] [web] Fix typos --- web/src/client/storage.js | 2 +- web/src/components/storage/iscsi/AuthFields.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/client/storage.js b/web/src/client/storage.js index d2369da82c..2244312467 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -328,7 +328,7 @@ class ISCSIManager { } /** - * Sets the statup status of the connection + * Sets the startup status of the connection * * @param {ISCSINode} node * @param {String} startup diff --git a/web/src/components/storage/iscsi/AuthFields.jsx b/web/src/components/storage/iscsi/AuthFields.jsx index 0620ef4ec9..71be608a16 100644 --- a/web/src/components/storage/iscsi/AuthFields.jsx +++ b/web/src/components/storage/iscsi/AuthFields.jsx @@ -72,7 +72,7 @@ export default function AuthFields({ data, onChange, onValidate }) { return ( <> - Authentication by inititator + Authentication by initiator { !isValidAuth() && } ); From 4e858cdb55c3b692841864da5f92c52fff22204a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 16 Mar 2023 11:37:38 +0000 Subject: [PATCH 12/19] [web] Use SectionSkeleton --- web/src/components/storage/iscsi/TargetsSection.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/src/components/storage/iscsi/TargetsSection.jsx b/web/src/components/storage/iscsi/TargetsSection.jsx index 9b7651484f..5e886b1751 100644 --- a/web/src/components/storage/iscsi/TargetsSection.jsx +++ b/web/src/components/storage/iscsi/TargetsSection.jsx @@ -23,10 +23,9 @@ import React, { useEffect, useReducer, useState } from "react"; import { Button, Toolbar, ToolbarItem, ToolbarContent, - Skeleton } from "@patternfly/react-core"; -import { Section } from "~/components/core"; +import { Section, SectionSkeleton } from "~/components/core"; import { NodesPresenter, DiscoverForm } from "~/components/storage/iscsi"; import { useInstallerClient } from "~/context/installer"; import { useCancellablePromise } from "~/utils"; @@ -140,7 +139,7 @@ export default function TargetsSection() { }; const SectionContent = () => { - if (state.isLoading) return ; + if (state.isLoading) return ; if (state.nodes.length === 0) { return ( From 30f8cfbff17e7e55c020d4af3c1c1c180f1bcdb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 16 Mar 2023 11:38:11 +0000 Subject: [PATCH 13/19] [web] Leftover --- web/src/components/layout/Icon.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/components/layout/Icon.jsx b/web/src/components/layout/Icon.jsx index 66f9677ce1..8d1a843cc4 100644 --- a/web/src/components/layout/Icon.jsx +++ b/web/src/components/layout/Icon.jsx @@ -23,7 +23,6 @@ import React from 'react'; // NOTE: "@icons" is an alias to use a shorter path to real icons location. // Check the tsconfig.json file to see its value. -import Add from "@icons/add.svg?component"; import Apps from "@icons/apps.svg?component"; import Badge from "@icons/badge.svg?component"; import CheckCircle from "@icons/check_circle.svg?component"; From daff64cc0b4b515bb977683411b27b14dfaac15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 16 Mar 2023 12:56:00 +0000 Subject: [PATCH 14/19] [web] Fix tests --- web/src/client/storage.test.js | 33 ++++++++++++------- .../overview/StorageSection.test.jsx | 3 +- .../storage/ProposalTargetSection.test.jsx | 8 ++++- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index 9c32743680..78f1bce9fe 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -168,9 +168,10 @@ describe("#proposal", () => { }; describe("#getData", () => { - beforeEach(() => { + beforeEach(async () => { contexts.withAvailableDevices(); contexts.withProposal(); + await client.setUp(); }); it("returns the available devices and the proposal result", async () => { @@ -181,8 +182,9 @@ describe("#proposal", () => { }); describe("#getAvailableDevices", () => { - beforeEach(() => { + beforeEach(async () => { contexts.withAvailableDevices(); + await client.setUp(); }); it("returns the list of available devices", async () => { @@ -193,8 +195,9 @@ describe("#proposal", () => { describe("#getResult", () => { describe("if there is no proposal yet", () => { - beforeEach(() => { + beforeEach(async () => { contexts.withoutProposal(); + await client.setUp(); }); it("returns undefined", async () => { @@ -204,8 +207,9 @@ describe("#proposal", () => { }); describe("if there is a proposal", () => { - beforeEach(() => { + beforeEach(async () => { contexts.withProposal(); + await client.setUp(); }); it("returns the proposal settings and actions", async () => { @@ -216,10 +220,11 @@ describe("#proposal", () => { }); describe("#calculate", () => { - beforeEach(() => { + beforeEach(async () => { cockpitProxies.proposalCalculator = { Calculate: jest.fn() }; + await client.setUp(); }); it("calculates a default proposal when no settings are given", async () => { @@ -278,10 +283,11 @@ describe("#proposal", () => { describe("#iscsi", () => { describe("#getInitiatorName", () => { - beforeEach(() => { + beforeEach(async () => { cockpitProxies.iscsiInitiator = { InitiatorName: "iqn.1996-04.com.suse:01:351e6d6249" }; + await client.setUp(); }); it("returns the current initiator name", async () => { @@ -291,10 +297,11 @@ describe("#iscsi", () => { }); describe("#setInitiatorName", () => { - beforeEach(() => { + beforeEach(async () => { cockpitProxies.iscsiInitiator = { InitiatorName: "iqn.1996-04.com.suse:01:351e6d6249" }; + await client.setUp(); }); it("sets the given initiator name", async () => { @@ -306,8 +313,9 @@ describe("#iscsi", () => { describe("#getNodes", () => { describe("if there is no exported iSCSI nodes yet", () => { - beforeEach(() => { + beforeEach(async () => { contexts.withoutISCSINodes(); + await client.setUp(); }); it("returns an empty list", async () => { @@ -317,8 +325,9 @@ describe("#iscsi", () => { }); describe("if there are exported iSCSI nodes", () => { - beforeEach(() => { + beforeEach(async () => { contexts.withISCSINodes(); + await client.setUp(); }); it("returns a list with the exported iSCSI nodes", async () => { @@ -349,10 +358,11 @@ describe("#iscsi", () => { }); describe("#discover", () => { - beforeEach(() => { + beforeEach(async () => { cockpitProxies.iscsiInitiator = { Discover: jest.fn() }; + await client.setUp(); }); it("performs an iSCSI discovery with the given options", async () => { @@ -373,10 +383,11 @@ describe("#iscsi", () => { }); describe("#Delete", () => { - beforeEach(() => { + beforeEach(async () => { cockpitProxies.iscsiInitiator = { Delete: jest.fn() }; + await client.setUp(); }); it("deletes the given iSCSI node", async () => { diff --git a/web/src/components/overview/StorageSection.test.jsx b/web/src/components/overview/StorageSection.test.jsx index b66d7c738b..bd7c387c83 100644 --- a/web/src/components/overview/StorageSection.test.jsx +++ b/web/src/components/overview/StorageSection.test.jsx @@ -50,7 +50,8 @@ beforeEach(() => { proposal: { getData: jest.fn().mockResolvedValue(proposal) }, getStatus: jest.fn().mockResolvedValue(status), getValidationErrors: jest.fn().mockResolvedValue(errors), - onStatusChange: onStatusChangeFn + onStatusChange: onStatusChangeFn, + setUp: jest.fn().mockResolvedValue(null) }, }; }); diff --git a/web/src/components/storage/ProposalTargetSection.test.jsx b/web/src/components/storage/ProposalTargetSection.test.jsx index c48ad4c29b..88c21cc0c7 100644 --- a/web/src/components/storage/ProposalTargetSection.test.jsx +++ b/web/src/components/storage/ProposalTargetSection.test.jsx @@ -109,7 +109,13 @@ describe("when there are no candidate devices available", () => { it("renders an informative message", () => { installerRender(); - screen.getByText("No available devices"); + screen.getByText("No devices found"); + }); + + it("allows configuring iSCSI devices", () => { + installerRender(); + + screen.getByRole("button", { name: "Configure iSCSI" }); }); it("does not allow configuring the candidate devices", () => { From 204c24111bf5748f8a04e3663262c12623b1e096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 16 Mar 2023 14:21:18 +0000 Subject: [PATCH 15/19] [web] Add words to cspell --- web/cspell.json | 2 ++ web/src/client/storage.js | 1 - web/src/client/storage.test.js | 1 - web/src/components/storage/iscsi/LoginForm.jsx | 2 -- web/src/components/storage/iscsi/NodeStartupOptions.js | 2 -- 5 files changed, 2 insertions(+), 6 deletions(-) diff --git a/web/cspell.json b/web/cspell.json index 41fca75c3e..df288e3e51 100644 --- a/web/cspell.json +++ b/web/cspell.json @@ -37,12 +37,14 @@ "localdomain", "luks", "mgmt", + "onboot", "partitioner", "patternfly", "rfkill", "screenreader", "ssid", "ssids", + "startup", "subprogress", "subvol", "subvolume", diff --git a/web/src/client/storage.js b/web/src/client/storage.js index 2244312467..d26f9bc7ba 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -20,7 +20,6 @@ */ // @ts-check -// cspell:ignore startup import DBusClient from "./dbus"; import { WithStatus, WithValidation } from "./mixins"; diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index 78f1bce9fe..28418faa1d 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -20,7 +20,6 @@ */ // @ts-check -// cspell:ignore onboot import DBusClient from "./dbus"; import { StorageClient } from "./storage"; diff --git a/web/src/components/storage/iscsi/LoginForm.jsx b/web/src/components/storage/iscsi/LoginForm.jsx index f4c4621d25..ce322e1104 100644 --- a/web/src/components/storage/iscsi/LoginForm.jsx +++ b/web/src/components/storage/iscsi/LoginForm.jsx @@ -19,8 +19,6 @@ * find current contact information at www.suse.com. */ -// cspell:ignore onboot - import React, { useState } from "react"; import { Alert, diff --git a/web/src/components/storage/iscsi/NodeStartupOptions.js b/web/src/components/storage/iscsi/NodeStartupOptions.js index a27fd1c9e7..fdfec899ac 100644 --- a/web/src/components/storage/iscsi/NodeStartupOptions.js +++ b/web/src/components/storage/iscsi/NodeStartupOptions.js @@ -19,8 +19,6 @@ * find current contact information at www.suse.com. */ -// cspell:ignore onboot - const NodeStartupOptions = Object.freeze({ MANUAL: { label: "Manual", value: "manual" }, ONBOOT: { label: "On boot", value: "onboot" }, From 0ecf6e32d3c32ec72fcb9085bb1bfdf1b7a79082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 16 Mar 2023 14:22:53 +0000 Subject: [PATCH 16/19] [web] Rename proxies --- web/src/client/storage.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/web/src/client/storage.js b/web/src/client/storage.js index d26f9bc7ba..0b2beb30a3 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -253,14 +253,14 @@ class ISCSIManager { } async setUp() { - const iscsiInitiator = await this.client.proxy(ISCSI_INITIATOR_IFACE, STORAGE_OBJECT); - const iscsiNodes = await this.client.proxies(ISCSI_NODE_IFACE, ISCSI_NODES_NAMESPACE); + const initiator = await this.client.proxy(ISCSI_INITIATOR_IFACE, STORAGE_OBJECT); + const nodes = await this.client.proxies(ISCSI_NODE_IFACE, ISCSI_NODES_NAMESPACE); - this.proxies = { iscsiInitiator, iscsiNodes }; + this.proxies = { initiator, nodes }; } async getInitiatorIbft() { - return this.proxies.iscsiInitiator.IBFT; + return this.proxies.initiator.IBFT; } /** @@ -269,7 +269,7 @@ class ISCSIManager { * @returns {Promise} */ async getInitiatorName() { - return this.proxies.iscsiInitiator.InitiatorName; + return this.proxies.initiator.InitiatorName; } /** @@ -278,7 +278,7 @@ class ISCSIManager { * @param {string} value */ async setInitiatorName(value) { - this.proxies.iscsiInitiator.InitiatorName = value; + this.proxies.initiator.InitiatorName = value; } /** @@ -297,7 +297,7 @@ class ISCSIManager { * @property {string} startup */ async getNodes() { - return Object.values(this.proxies.iscsiNodes).map(this.buildNode); + return Object.values(this.proxies.nodes).map(this.buildNode); } /** @@ -323,7 +323,7 @@ class ISCSIManager { ReversePassword: { t: "s", v: options.reversePassword } }); - return this.proxies.iscsiInitiator.Discover(address, port, auth); + return this.proxies.initiator.Discover(address, port, auth); } /** @@ -349,7 +349,7 @@ class ISCSIManager { async delete(node) { const path = this.nodePath(node); - return this.proxies.iscsiInitiator.Delete(path); + return this.proxies.initiator.Delete(path); } /** @@ -410,15 +410,15 @@ class ISCSIManager { } async onNodeAdded(handler) { - this.proxies.iscsiNodes.addEventListener("added", (_, proxy) => handler(this.buildNode(proxy))); + this.proxies.nodes.addEventListener("added", (_, proxy) => handler(this.buildNode(proxy))); } async onNodeChanged(handler) { - this.proxies.iscsiNodes.addEventListener("changed", (_, proxy) => handler(this.buildNode(proxy))); + this.proxies.nodes.addEventListener("changed", (_, proxy) => handler(this.buildNode(proxy))); } async onNodeRemoved(handler) { - this.proxies.iscsiNodes.addEventListener("removed", (_, proxy) => handler(this.buildNode(proxy))); + this.proxies.nodes.addEventListener("removed", (_, proxy) => handler(this.buildNode(proxy))); } buildNode(proxy) { From 823e5b7f64f61be8f78166e86805f12a24831fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 16 Mar 2023 14:23:16 +0000 Subject: [PATCH 17/19] [web] Improve auth tip --- .../components/storage/iscsi/AuthFields.jsx | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/web/src/components/storage/iscsi/AuthFields.jsx b/web/src/components/storage/iscsi/AuthFields.jsx index 71be608a16..6b43c84127 100644 --- a/web/src/components/storage/iscsi/AuthFields.jsx +++ b/web/src/components/storage/iscsi/AuthFields.jsx @@ -56,25 +56,18 @@ export default function AuthFields({ data, onChange, onValidate }) { ); }); - const ByInitiatorAuthLegend = () => { - const Info = () => { - return ( -

- - Only available if authentication by target is provided -

- ); - }; + const ByInitiatorAuthTip = () => { + if (isValidAuth()) return null; return ( - <> - Authentication by initiator - { !isValidAuth() && } - +

+ + Only available if authentication by target is provided +

); }; @@ -115,7 +108,8 @@ export default function AuthFields({ data, onChange, onValidate }) { /> -
+
+ Date: Thu, 16 Mar 2023 14:23:35 +0000 Subject: [PATCH 18/19] [web] Reorder routes --- web/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/index.js b/web/src/index.js index 29f24b1cc3..cc6650de24 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -56,9 +56,9 @@ root.render( } /> } /> } /> + } /> } /> } /> - } /> } /> From fed9904a26233f4078202780a38d8f0e79ac4de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 16 Mar 2023 14:23:51 +0000 Subject: [PATCH 19/19] [web] Fix typo --- service/lib/dinstaller/storage/iscsi/manager.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/lib/dinstaller/storage/iscsi/manager.rb b/service/lib/dinstaller/storage/iscsi/manager.rb index 15097068bc..05409c77fa 100644 --- a/service/lib/dinstaller/storage/iscsi/manager.rb +++ b/service/lib/dinstaller/storage/iscsi/manager.rb @@ -173,7 +173,7 @@ def update(node, startup:) end end - # Registers a callback to be called after performing iSCSI actvation + # Registers a callback to be called after performing iSCSI activation # # @param block [Proc] def on_activate(&block)