diff --git a/rust/agama-utils/src/api/software/config.rs b/rust/agama-utils/src/api/software/config.rs index f5cf5b5f34..961d9db3c2 100644 --- a/rust/agama-utils/src/api/software/config.rs +++ b/rust/agama-utils/src/api/software/config.rs @@ -19,57 +19,51 @@ // find current contact information at www.suse.com. //! Representation of the software settings -use std::collections::HashMap; - use merge::Merge; use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use std::collections::HashMap; use url::Url; /// User configuration for the localization of the target system. /// /// This configuration is provided by the user, so all the values are optional. +#[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Merge, utoipa::ToSchema)] #[schema(as = software::UserConfig)] #[serde(rename_all = "camelCase")] #[merge(strategy = merge::option::recurse)] pub struct Config { /// Product related configuration - #[serde(skip_serializing_if = "Option::is_none")] pub product: Option, /// Software related configuration - #[serde(skip_serializing_if = "Option::is_none")] pub software: Option, } /// Addon settings for registration +#[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct AddonConfig { pub id: String, /// Optional version of the addon, if not specified the version is found /// from the available addons - #[serde(skip_serializing_if = "Option::is_none")] pub version: Option, /// Free extensions do not require a registration code - #[serde(skip_serializing_if = "Option::is_none")] pub registration_code: Option, } /// Software settings for installation +#[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Merge, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] #[merge(strategy = merge::option::overwrite_none)] pub struct ProductConfig { /// ID of the product to install (e.g., "ALP", "Tumbleweed", etc.) - #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub registration_code: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub registration_email: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub registration_url: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub addons: Option>, } @@ -84,21 +78,18 @@ impl ProductConfig { } /// Software settings for installation +#[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Merge, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] #[merge(strategy = merge::option::overwrite_none)] pub struct SoftwareConfig { /// List of user selected patterns to install. - #[serde(skip_serializing_if = "Option::is_none")] pub patterns: Option, /// List of user selected packages to install. - #[serde(skip_serializing_if = "Option::is_none")] pub packages: Option>, /// List of user specified repositories to use on top of default ones. - #[serde(skip_serializing_if = "Option::is_none")] pub extra_repositories: Option>, /// Flag indicating if only hard requirements should be used by solver. - #[serde(skip_serializing_if = "Option::is_none")] pub only_required: Option, } @@ -118,11 +109,10 @@ impl Default for PatternsConfig { } } +#[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, utoipa::ToSchema)] pub struct PatternsMap { - #[serde(skip_serializing_if = "Option::is_none")] pub add: Option>, - #[serde(skip_serializing_if = "Option::is_none")] pub remove: Option>, } @@ -165,32 +155,27 @@ impl SoftwareConfig { } /// Parameters for creating new a repository +#[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct RepositoryConfig { /// repository alias. Has to be unique pub alias: String, /// repository name, if not specified the alias is used - #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, /// Repository url (raw format without expanded variables) pub url: String, /// product directory (currently not used, valid only for multiproduct DVDs) - #[serde(skip_serializing_if = "Option::is_none")] pub product_dir: Option, /// Whether the repository is enabled, if missing the repository is enabled - #[serde(skip_serializing_if = "Option::is_none")] pub enabled: Option, /// Repository priority, lower number means higher priority, the default priority is 99 - #[serde(skip_serializing_if = "Option::is_none")] pub priority: Option, /// Whenever repository can be unsigned. Default is false - #[serde(skip_serializing_if = "Option::is_none")] pub allow_unsigned: Option, /// List of fingerprints for GPG keys used for repository signing. If specified, /// the new list of fingerprints overrides the existing ones instead of merging /// with them. By default empty. - #[serde(skip_serializing_if = "Option::is_none")] pub gpg_fingerprints: Option>, } diff --git a/rust/agama-utils/src/api/software/system_info.rs b/rust/agama-utils/src/api/software/system_info.rs index 350f8ae330..c307a1cd3c 100644 --- a/rust/agama-utils/src/api/software/system_info.rs +++ b/rust/agama-utils/src/api/software/system_info.rs @@ -19,9 +19,11 @@ // find current contact information at www.suse.com. use serde::Serialize; +use serde_with::skip_serializing_none; /// Software-related information of the system where the installer /// is running. +#[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, utoipa::ToSchema)] pub struct SystemInfo { /// List of known patterns. @@ -66,6 +68,7 @@ pub struct Pattern { pub preselected: bool, } +#[skip_serializing_none] #[derive(Clone, Default, Debug, Serialize, utoipa::ToSchema)] pub struct RegistrationInfo { /// Registration code. diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index 891cf63297..a8ab12bcd6 100644 --- a/web/src/App.test.tsx +++ b/web/src/App.test.tsx @@ -30,10 +30,10 @@ import { useSystem } from "~/hooks/model/system"; import { Product } from "~/types/software"; import { PATHS } from "~/router"; import { PRODUCT } from "~/routes/paths"; -import type { Config } from "~/api"; -import type { Progress, Stage } from "~/model/status"; import App from "./App"; import { System } from "~/model/system/network"; +import type { Config } from "~/model/config"; +import type { Progress, Stage } from "~/model/status"; jest.mock("~/client"); diff --git a/web/src/App.tsx b/web/src/App.tsx index 676de66677..7680d04efc 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -26,7 +26,7 @@ import { useStatusChanges, useStatus } from "~/hooks/model/status"; import { useSystemChanges } from "~/hooks/model/system"; import { useProposalChanges } from "~/hooks/model/proposal"; import { useIssuesChanges } from "~/hooks/model/issue"; -import { useProduct } from "~/hooks/model/config"; +import { useProductInfo } from "~/hooks/model/config/product"; import { ROOT } from "~/routes/paths"; import { useQueryClient } from "@tanstack/react-query"; @@ -38,7 +38,7 @@ import { useQueryClient } from "@tanstack/react-query"; */ const Content = () => { const location = useLocation(); - const product = useProduct(); + const product = useProductInfo(); const { progresses, stage } = useStatus(); console.log("App Content component", { diff --git a/web/src/api.ts b/web/src/api.ts index 58e3487c96..9b93464e04 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -103,8 +103,4 @@ export { getStorageJobs, }; -export type { Response, System, Config, Proposal }; -export type * as system from "~/model/system"; -export type * as config from "~/model/config"; -export type * as proposal from "~/model/proposal"; -export type * as issue from "~/model/issue"; +export type { Response }; diff --git a/web/src/components/core/ConfirmPage.tsx b/web/src/components/core/ConfirmPage.tsx index 2d4216dc91..7b721d39ef 100644 --- a/web/src/components/core/ConfirmPage.tsx +++ b/web/src/components/core/ConfirmPage.tsx @@ -33,13 +33,13 @@ import NetworkDetailsItem from "~/components/network/NetworkDetailsItem"; import SoftwareDetailsItem from "~/components/software/SoftwareDetailsItem"; import PotentialDataLossAlert from "~/components/storage/PotentialDataLossAlert"; import { startInstallation } from "~/model/manager"; -import { useProduct } from "~/hooks/model/config"; +import { useProductInfo } from "~/hooks/model/config/product"; import { PRODUCT } from "~/routes/paths"; import { useDestructiveActions } from "~/hooks/use-destructive-actions"; import { _ } from "~/i18n"; export default function ConfirmPage() { - const product = useProduct(); + const product = useProductInfo(); const { actions } = useDestructiveActions(); const hasDestructiveActions = actions.length > 0; diff --git a/web/src/components/core/InstallationFinished.test.tsx b/web/src/components/core/InstallationFinished.test.tsx index 6e9b631e14..d80006259b 100644 --- a/web/src/components/core/InstallationFinished.test.tsx +++ b/web/src/components/core/InstallationFinished.test.tsx @@ -21,11 +21,10 @@ */ import React from "react"; - import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import InstallationFinished from "./InstallationFinished"; -import { Encryption } from "~/model/config/storage"; +import type { Storage } from "~/model/config"; jest.mock("~/queries/status", () => ({ ...jest.requireActual("~/queries/status"), @@ -39,12 +38,12 @@ type guidedEncryption = { pbkdFunction?: string; }; -let mockEncryption: undefined | Encryption | guidedEncryption; +let mockEncryption: undefined | Storage.Encryption | guidedEncryption; let mockType: storageConfigType; const mockStorageConfig = ( type: storageConfigType, - encryption: undefined | Encryption | guidedEncryption, + encryption: undefined | Storage.Encryption | guidedEncryption, ) => { const encryptionHash = {}; if (encryption !== undefined) encryptionHash["encryption"] = encryption; diff --git a/web/src/components/core/InstallerOptions.test.tsx b/web/src/components/core/InstallerOptions.test.tsx index 84ae904701..c8991ddf0a 100644 --- a/web/src/components/core/InstallerOptions.test.tsx +++ b/web/src/components/core/InstallerOptions.test.tsx @@ -24,7 +24,7 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { installerRender, mockRoutes } from "~/test-utils"; import { useSystem } from "~/hooks/model/system"; -import { useProduct } from "~/hooks/model/config"; +import { useProductInfo } from "~/hooks/model/config/product"; import { Product } from "~/types/software"; import { Keymap, Locale } from "~/model/system/l10n"; import { Progress, Stage } from "~/model/status"; @@ -88,7 +88,7 @@ jest.mock("~/hooks/api", () => ({ stage: mockStateFn(), progresses: mockProgressesFn(), }), - useProduct: (): ReturnType => mockSelectedProductFn(), + useProductInfo: (): ReturnType => mockSelectedProductFn(), })); jest.mock("~/context/installerL10n", () => ({ diff --git a/web/src/components/core/InstallerOptions.tsx b/web/src/components/core/InstallerOptions.tsx index 26ff8bcd3c..0b72b219a2 100644 --- a/web/src/components/core/InstallerOptions.tsx +++ b/web/src/components/core/InstallerOptions.tsx @@ -52,7 +52,7 @@ import { localConnection } from "~/utils"; import { _ } from "~/i18n"; import supportedLanguages from "~/languages.json"; import { PRODUCT, ROOT, L10N } from "~/routes/paths"; -import { useProduct } from "~/hooks/model/config"; +import { useProductInfo } from "~/hooks/model/config/product"; import { useSystem } from "~/hooks/model/system"; import { useStatus } from "~/hooks/model/status"; import { patchConfig } from "~/api"; @@ -556,7 +556,7 @@ export default function InstallerOptions({ } = useSystem(); const { language, keymap, changeLanguage, changeKeymap } = useInstallerL10n(); const { stage } = useStatus(); - const selectedProduct = useProduct(); + const selectedProduct = useProductInfo(); const initialFormState = { language, keymap, diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx index c46e7d91b0..e0521cf1ba 100644 --- a/web/src/components/layout/Header.tsx +++ b/web/src/components/layout/Header.tsx @@ -46,7 +46,7 @@ import { ChangeProductOption, InstallButton, InstallerOptions, SkipTo } from "~/ import ProgressStatusMonitor from "../core/ProgressStatusMonitor"; import { ROOT } from "~/routes/paths"; import { _ } from "~/i18n"; -import { useProduct } from "~/hooks/model/config"; +import { useProductInfo } from "~/hooks/model/config/product"; export type HeaderProps = { /** Whether the application sidebar should be mounted or not */ @@ -112,7 +112,7 @@ export default function Header({ isSidebarOpen, toggleSidebar, }: HeaderProps): React.ReactNode { - const product = useProduct(); + const product = useProductInfo(); const routeMatches = useMatches() as Route[]; const currentRoute = routeMatches.at(-1); // TODO: translate title diff --git a/web/src/components/layout/Sidebar.tsx b/web/src/components/layout/Sidebar.tsx index 85a8bc0ebe..2606fa674e 100644 --- a/web/src/components/layout/Sidebar.tsx +++ b/web/src/components/layout/Sidebar.tsx @@ -33,10 +33,10 @@ import { import { Icon } from "~/components/layout"; import { rootRoutes } from "~/router"; import { _ } from "~/i18n"; -import { useProduct } from "~/hooks/model/config"; +import { useProductInfo } from "~/hooks/model/config/product"; const MainNavigation = (): React.ReactNode => { - const product = useProduct(); + const product = useProductInfo(); const location = useLocation(); if (!product) { diff --git a/web/src/components/overview/OverviewPage.tsx b/web/src/components/overview/OverviewPage.tsx index 4f734f57af..90dfcc9fb8 100644 --- a/web/src/components/overview/OverviewPage.tsx +++ b/web/src/components/overview/OverviewPage.tsx @@ -24,14 +24,14 @@ import React from "react"; import { Navigate } from "react-router"; import { Content, Grid, GridItem } from "@patternfly/react-core"; import { Page } from "~/components/core"; -import { useProduct } from "~/hooks/model/config"; +import { useProductInfo } from "~/hooks/model/config/product"; import { PRODUCT } from "~/routes/paths"; import { _ } from "~/i18n"; import SystemInformationSection from "./SystemInformationSection"; import InstallationSummarySection from "./InstallationSummarySection"; export default function OverviewPage() { - const product = useProduct(); + const product = useProductInfo(); if (!product) { return ; diff --git a/web/src/components/product/ProductRegistrationAlert.test.tsx b/web/src/components/product/ProductRegistrationAlert.test.tsx index 99a7c5dea2..cfe81152b6 100644 --- a/web/src/components/product/ProductRegistrationAlert.test.tsx +++ b/web/src/components/product/ProductRegistrationAlert.test.tsx @@ -24,7 +24,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender, mockRoutes } from "~/test-utils"; import { useSystem } from "~/hooks/model/system"; -import { useProduct } from "~/hooks/model/config"; +import { useProductInfo } from "~/hooks/model/config/product"; import { useIssues } from "~/hooks/model/issue"; import { Issue } from "~/model/issue"; import { PRODUCT, REGISTRATION, ROOT } from "~/routes/paths"; @@ -65,7 +65,7 @@ jest.mock("~/hooks/api", () => ({ products: [tw, sle], network, }), - useProduct: (): ReturnType => mockSelectedProduct(), + useProductInfo: (): ReturnType => mockSelectedProduct(), useIssues: (): ReturnType => mockIssues(), })); diff --git a/web/src/components/product/ProductRegistrationAlert.tsx b/web/src/components/product/ProductRegistrationAlert.tsx index 6bd5e53ed9..d3aa5bfc81 100644 --- a/web/src/components/product/ProductRegistrationAlert.tsx +++ b/web/src/components/product/ProductRegistrationAlert.tsx @@ -28,7 +28,7 @@ import { REGISTRATION, SIDE_PATHS } from "~/routes/paths"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; import { useIssues } from "~/hooks/model/issue"; -import { useProduct } from "~/hooks/model/config"; +import { useProductInfo } from "~/hooks/model/config/product"; const LinkToRegistration = ({ text }: { text: string }) => { const location = useLocation(); @@ -44,7 +44,7 @@ const LinkToRegistration = ({ text }: { text: string }) => { export default function ProductRegistrationAlert() { const location = useLocation(); - const product = useProduct(); + const product = useProductInfo(); // FIXME: what scope reports these issues with the new API? const issues = useIssues("product"); const registrationRequired = issues?.find((i) => i.class === "missing_registration"); diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index ed2d9f7b0e..d18a0d1228 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -36,11 +36,11 @@ import { FormGroup, SelectList, SelectOption, - Title, + // Title, TextInput, List, ListItem, - Divider, + // Divider, } from "@patternfly/react-core"; import { Link, @@ -48,17 +48,20 @@ import { Page, SelectWrapper as Select, SubtleContent, + IssuesAlert, } from "~/components/core"; -import RegistrationExtension from "./RegistrationExtension"; +// import RegistrationExtension from "./RegistrationExtension"; import RegistrationCodeInput from "./RegistrationCodeInput"; -import { RegistrationParams } from "~/types/software"; import { HOSTNAME } from "~/routes/paths"; -import { useProduct, useRegistration, useRegisterMutation, useAddons } from "~/queries/software"; import { isEmpty } from "radashi"; import { mask } from "~/utils"; import { sprintf } from "sprintf-js"; import { _, N_ } from "~/i18n"; import { useProposal } from "~/hooks/model/proposal"; +import { useSystem } from "~/hooks/model/system/software"; +import { useProduct, useProductInfo } from "~/hooks/model/config/product"; +import { useIssues } from "~/hooks/model/issue"; +import { patchConfig } from "~/api"; const FORM_ID = "productRegistration"; const SERVER_LABEL = N_("Registration server"); @@ -68,8 +71,8 @@ const CUSTOM_SERVER_LABEL = N_("Custom"); const EXAMPLE_URL = "https://example.com"; const RegisteredProductSection = () => { - const { selectedProduct: product } = useProduct(); - const registration = useRegistration(); + const product = useProductInfo(); + const { registration } = useSystem(); const [showCode, setShowCode] = useState(false); const toggleCodeVisibility = () => setShowCode(!showCode); @@ -87,12 +90,12 @@ const RegisteredProductSection = () => { {registration.url} )} - {!isEmpty(registration.key) && ( + {!isEmpty(registration.code) && ( <> {_("Registration code")} - {showCode ? registration.key : mask(registration.key)} + {showCode ? registration.code : mask(registration.code)} @@ -271,7 +274,6 @@ function RegistrationEmail({ } const RegistrationFormSection = () => { - const { mutate: register } = useRegisterMutation(); const [server, setServer] = useState("default"); const [url, setUrl] = useState(""); const [key, setKey] = useState(""); @@ -280,12 +282,12 @@ const RegistrationFormSection = () => { const [provideEmail, setProvideEmail] = useState(false); const [requestError, setRequestError] = useState(null); const [errors, setErrors] = useState([]); - const [loading, setLoading] = useState(false); - const registration = useRegistration(); + const [loading] = useState(false); + const product = useProduct(); useEffect(() => { - if (registration) { - const { key, email, url } = registration; + if (product) { + const { registrationCode: key, registrationEmail: email, registrationUrl: url } = product; const server = isEmpty(url) ? "default" : "custom"; setServer(server); setKey(key); @@ -294,7 +296,7 @@ const RegistrationFormSection = () => { setProvideKey(!isEmpty(key)); setProvideEmail(!isEmpty(email)); } - }, [registration]); + }, [product]); const changeServer = (value: ServerOption) => { if (value !== "default") setProvideKey(!isEmpty(key)); @@ -311,11 +313,6 @@ const RegistrationFormSection = () => { setProvideEmail(value); }; - // FIXME: use the right type for AxiosResponse - const onRegisterError = ({ response }) => { - setRequestError(response.data.message); - }; - const submit = async (e: React.SyntheticEvent) => { e.preventDefault(); setRequestError(null); @@ -331,16 +328,14 @@ const RegistrationFormSection = () => { if (!isEmpty(errors)) return; - const data: RegistrationParams = { - url: isUrlRequired ? url : "", - key: isKeyRequired ? key : "", - email: provideEmail ? email : "", - }; - - setLoading(true); - - // @ts-expect-error - register(data, { onError: onRegisterError, onSettled: () => setLoading(false) }); + patchConfig({ + product: { + id: product.id, + registrationCode: isKeyRequired ? key : undefined, + registrationEmail: provideEmail ? email : undefined, + registrationUrl: isUrlRequired ? url : undefined, + }, + }); }; // TODO: adjust texts based of registration "type", mandatory or optional @@ -413,48 +408,52 @@ const HostnameAlert = () => { ); }; -const Extensions = () => { - const extensions = useAddons(); - if (extensions.length === 0) return null; - - const extensionComponents = extensions.map((ext) => ( - e.id === ext.id).length === 1} - /> - )); - - return ( - <> - - {_("Extensions")} - - - {extensionComponents} - - - - ); -}; +// const Extensions = () => { +// const { registration } = useSystem(); +// const extensions = registration?.addons; +// if (!extensions || extensions.length === 0) return null; + +// const extensionComponents = extensions.map((ext) => ( +// e.id === ext.id).length === 1} +// /> +// )); + +// return ( +// <> +// +// {_("Extensions")} +// +// +// {extensionComponents} +// +// +// +// ); +// }; export default function ProductRegistrationPage() { - const { selectedProduct: product } = useProduct(); - const { registered } = useRegistration(); + const product = useProductInfo(); + const { registration } = useSystem(); + const issues = useIssues("software"); + const showIssues = issues.find((i) => i.class === "software.register_system") !== undefined; // TODO: render something meaningful instead? "Product not registrable"? - if (!product.registration) return; + if (!product || !product.registration) return; return ( - + {_("Registration")} - {!registered && } - {!registered ? : } - {registered && } + {showIssues && } + {!registration && } + {!registration ? : } + {/* {registration && } */} ); diff --git a/web/src/components/product/ProductSelectionPage.test.tsx b/web/src/components/product/ProductSelectionPage.test.tsx index e154e6d7a0..6b21511030 100644 --- a/web/src/components/product/ProductSelectionPage.test.tsx +++ b/web/src/components/product/ProductSelectionPage.test.tsx @@ -24,7 +24,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender, mockNavigateFn } from "~/test-utils"; import { useSystem } from "~/hooks/model/system"; -import { useProduct } from "~/hooks/model/config"; +import { useProductInfo } from "~/hooks/model/config/product"; import { Product } from "~/types/software"; import ProductSelectionPage from "./ProductSelectionPage"; import { System } from "~/model/system/network"; @@ -80,7 +80,7 @@ jest.mock("~/hooks/model/system", () => ({ jest.mock("~/hooks/model/config", () => ({ ...jest.requireActual("~/hooks/model/config"), - useProduct: (): ReturnType => mockSelectedProduct(), + useProductInfo: (): ReturnType => mockSelectedProduct(), })); describe("ProductSelectionPage", () => { diff --git a/web/src/components/product/ProductSelectionPage.tsx b/web/src/components/product/ProductSelectionPage.tsx index c4de70c21a..dd88577cd2 100644 --- a/web/src/components/product/ProductSelectionPage.tsx +++ b/web/src/components/product/ProductSelectionPage.tsx @@ -46,7 +46,7 @@ import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; import agama from "~/agama"; import LicenseDialog from "./LicenseDialog"; -import { useProduct } from "~/hooks/model/config"; +import { useProductInfo } from "~/hooks/model/config/product"; import { useSystem } from "~/hooks/model/system"; import { patchConfig } from "~/api"; import { ROOT } from "~/routes/paths"; @@ -112,7 +112,7 @@ const BackLink = () => { function ProductSelectionPage() { const navigate = useNavigate(); const { products } = useSystem(); - const selectedProduct = useProduct(); + const selectedProduct = useProductInfo(); const [nextProduct, setNextProduct] = useState(selectedProduct); // FIXME: should not be accepted by default first selectedProduct is accepted // because it's a singleProduct iso. diff --git a/web/src/components/storage/ProposalTransactionalInfo.tsx b/web/src/components/storage/ProposalTransactionalInfo.tsx index 3dafbd4673..78f0b53b56 100644 --- a/web/src/components/storage/ProposalTransactionalInfo.tsx +++ b/web/src/components/storage/ProposalTransactionalInfo.tsx @@ -24,7 +24,7 @@ import React from "react"; import { Alert } from "@patternfly/react-core"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; -import { useProduct } from "~/hooks/model/config"; +import { useProductInfo } from "~/hooks/model/config/product"; import { useVolumeTemplates } from "~/hooks/model/system/storage"; import { isTransactionalSystem } from "~/components/storage/utils"; @@ -35,7 +35,7 @@ import { isTransactionalSystem } from "~/components/storage/utils"; * @param props */ export default function ProposalTransactionalInfo() { - const selectedProduct = useProduct(); + const selectedProduct = useProductInfo(); const volumes = useVolumeTemplates(); if (!isTransactionalSystem(volumes)) return; diff --git a/web/src/components/system/HostnamePage.test.tsx b/web/src/components/system/HostnamePage.test.tsx index ac13afbbf6..0e5fb825eb 100644 --- a/web/src/components/system/HostnamePage.test.tsx +++ b/web/src/components/system/HostnamePage.test.tsx @@ -23,26 +23,11 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; -import { Product } from "~/types/software"; import HostnamePage from "./HostnamePage"; let mockStaticHostname: string; const mockPatchConfig = jest.fn(); -const tw: Product = { - id: "Tumbleweed", - name: "openSUSE Tumbleweed", - registration: false, -}; - -const sle: Product = { - id: "sle", - name: "SLE", - registration: true, -}; - -let selectedProduct = tw; - jest.mock("~/components/product/ProductRegistrationAlert", () => () => (
ProductRegistrationAlert Mock
)); @@ -52,12 +37,11 @@ jest.mock("~/api", () => ({ patchConfig: (config) => mockPatchConfig(config), })); -jest.mock("~/hooks/model/config", () => ({ - ...jest.requireActual("~/hooks/model/config"), - useProduct: () => selectedProduct, - useConfig: () => ({ - product: selectedProduct.id, - }), +const system = jest.fn(); + +jest.mock("~/hooks/model/system", () => ({ + ...jest.requireActual("~/hooks/model/system"), + useSystem: () => system(), })); jest.mock("~/hooks/model/proposal", () => ({ @@ -81,7 +65,7 @@ jest.mock("~/hooks/model/proposal", () => ({ describe("HostnamePage", () => { beforeEach(() => { - selectedProduct = tw; + system.mockReturnValue({}); }); describe("when static hostname is set", () => { @@ -184,7 +168,7 @@ describe("HostnamePage", () => { }); }); - describe("when selected product is not registrable", () => { + describe("when selected product is not registered", () => { it("does not render an alert about registration", () => { installerRender(); expect(screen.queryByText("Info alert:")).toBeNull(); @@ -192,21 +176,27 @@ describe("HostnamePage", () => { }); }); - describe("when the selected product is registrable and registration code is not set", () => { + describe("when the product is not registered", () => { beforeEach(() => { - selectedProduct = sle; + system.mockReturnValue({ software: {} }); }); - xit("does not render an alert about registration", () => { + it("does not render an alert about registration", () => { installerRender(); expect(screen.queryByText("Info alert:")).toBeNull(); expect(screen.queryByText("Product is already registered")).toBeNull(); }); }); - describe("when the selected product is registrable and registration code is set", () => { + describe("when the selected product is registered", () => { beforeEach(() => { - selectedProduct = sle; + system.mockReturnValue({ + software: { + registration: { + code: "12345", + }, + }, + }); }); it("renders an alert to let user know that changes will not have effect in the registration", () => { diff --git a/web/src/components/system/HostnamePage.tsx b/web/src/components/system/HostnamePage.tsx index 5960f756be..97ac34bbb8 100644 --- a/web/src/components/system/HostnamePage.tsx +++ b/web/src/components/system/HostnamePage.tsx @@ -36,14 +36,12 @@ import { isEmpty } from "radashi"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; import { useProposal } from "~/hooks/model/proposal"; -import { useProduct } from "~/hooks/model/config"; +import { useSystem } from "~/hooks/model/system"; import { patchConfig } from "~/api"; export default function HostnamePage() { - const product = useProduct(); + const { software } = useSystem(); const { hostname: proposal } = useProposal(); - // FIXME: It should be fixed once the registration is adapted to API v2 - const registration = { registered: product.registration }; const { hostname: transientHostname, static: staticHostname } = proposal; const hasTransientHostname = isEmpty(staticHostname); const [success, setSuccess] = useState(null); @@ -83,7 +81,7 @@ export default function HostnamePage() { - {product.registration && registration.registered && ( + {software?.registration && ( {_( "Updating the hostname now or later will not change the currently registered hostname.", diff --git a/web/src/hooks/model/config.ts b/web/src/hooks/model/config.ts index 3b237d17d6..9197c7a004 100644 --- a/web/src/hooks/model/config.ts +++ b/web/src/hooks/model/config.ts @@ -20,11 +20,8 @@ * find current contact information at www.suse.com. */ -import { useCallback } from "react"; import { useSuspenseQuery } from "@tanstack/react-query"; import { getConfig, getExtendedConfig } from "~/api"; -import { useSystem } from "~/hooks/model/system"; -import type { system } from "~/api"; import type { Config } from "~/model/config"; const CONFIG_KEY = "config"; @@ -48,22 +45,6 @@ function useExtendedConfig(): Config | null { return useSuspenseQuery(extendedConfigQuery)?.data; } -// Returns the information of the current selected product from the list of products provided by -// the system. -function useProduct(): system.Product | null { - const products = useSystem()?.products; - const { data } = useSuspenseQuery({ - ...extendedConfigQuery, - select: useCallback( - (data: Config | null): system.Product | null => { - return products?.find((p) => p.id === data?.product?.id) || null; - }, - [products], - ), - }); - return data; -} - export { CONFIG_KEY, EXTENDED_CONFIG_KEY, @@ -71,6 +52,7 @@ export { extendedConfigQuery, useConfig, useExtendedConfig, - useProduct, }; +export * as network from "~/hooks/model/config/network"; +export * as product from "~/hooks/model/config/product"; export * as storage from "~/hooks/model/config/storage"; diff --git a/web/src/hooks/model/config/product.ts b/web/src/hooks/model/config/product.ts new file mode 100644 index 0000000000..4fc57a042f --- /dev/null +++ b/web/src/hooks/model/config/product.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ +import { useCallback } from "react"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { extendedConfigQuery } from "~/hooks/model/config"; +import { useSystem } from "~/hooks/model/system"; +import type { Config, Product } from "~/model/config"; +import type * as System from "~/model/system"; + +const selectProduct = (data: Config | null): Product.Config | null => data?.product; + +function useProduct(): Product.Config | null { + const { data } = useSuspenseQuery({ + ...extendedConfigQuery, + select: selectProduct, + }); + return data; +} + +// Returns the information of the current selected product from the list of products provided by +// the system. +function useProductInfo(): System.Product | null { + const products = useSystem()?.products; + const { data } = useSuspenseQuery({ + ...extendedConfigQuery, + select: useCallback( + (data: Config | null): System.Product | null => { + return products?.find((p) => p.id === data?.product?.id) || null; + }, + [products], + ), + }); + return data; +} + +export { useProduct, useProductInfo }; diff --git a/web/src/hooks/model/system.ts b/web/src/hooks/model/system.ts index 3dec0066c8..4fe6ab2dd2 100644 --- a/web/src/hooks/model/system.ts +++ b/web/src/hooks/model/system.ts @@ -32,7 +32,7 @@ const systemQuery = { }; function useSystem(): System | null { - return useSuspenseQuery(systemQuery)?.data; + return useSuspenseQuery(systemQuery).data; } function useSystemChanges() { diff --git a/web/src/model/config.ts b/web/src/model/config.ts index 0fdfa77bab..a36d3d9723 100644 --- a/web/src/model/config.ts +++ b/web/src/model/config.ts @@ -23,20 +23,17 @@ import type * as Hostname from "~/model/config/hostname"; import type * as L10n from "~/model/config/l10n"; import type * as Network from "~/model/config/network"; +import type * as Product from "~/model/config/product"; import type * as Software from "~/model/config/software"; -import type * as Storage from "~/model/config/storage"; +import type * as Storage from "~/openapi/config/storage"; type Config = { hostname?: Hostname.Config; l10n?: L10n.Config; network?: Network.Config; - product?: Product; + product?: Product.Config; storage?: Storage.Config; software?: Software.Config; }; -type Product = { - id?: string; -}; - export type { Config, Hostname, Product, L10n, Network, Storage }; diff --git a/web/src/model/config/product.ts b/web/src/model/config/product.ts new file mode 100644 index 0000000000..84a999c173 --- /dev/null +++ b/web/src/model/config/product.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +type Config = { + id?: string; + registrationCode?: string; + registrationEmail?: string; + registrationUrl?: string; + addons?: Addon[]; +}; + +type Addon = { + id: string; + version?: string; + registrationCode?: string; +}; + +export type { Config, Addon }; diff --git a/web/src/model/config/software.ts b/web/src/model/config/software.ts index 2f6b23064e..bc3b3d86e3 100644 --- a/web/src/model/config/software.ts +++ b/web/src/model/config/software.ts @@ -1,7 +1,23 @@ -/** - * This file was automatically generated by json-schema-to-typescript. - * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, - * and run json-schema-to-typescript to regenerate this file. +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. */ /** diff --git a/web/src/model/software.ts b/web/src/model/software.ts index 68c10e01f5..7b4f14a2e8 100644 --- a/web/src/model/software.ts +++ b/web/src/model/software.ts @@ -107,20 +107,6 @@ const updateConfig = (config: SoftwareConfig) => put("/api/software/config", con */ const probe = () => post("/api/software/probe"); -/** - * Request registration of selected product with given key - */ -const register = ({ key, email }: { key: string; email?: string }) => - post("/api/software/registration", { key, email }); - -/** - * Updates the URL for the registration - */ -const updateRegistrationUrl = (url: string) => - // Explicit content type is needed because the content is a string. The application/json type is - // automatically set only if the content is an object. - put("/api/software/registration/url", url, { headers: { "Content-Type": "application/json" } }); - /** * Request registration of the selected addon */ @@ -145,8 +131,6 @@ export { fetchRegistration, fetchRepositories, probe, - register, - updateRegistrationUrl, registerAddon, solveConflict, updateConfig, diff --git a/web/src/model/system/software.ts b/web/src/model/system/software.ts index c0cd8dd10a..8d4e1c42cc 100644 --- a/web/src/model/system/software.ts +++ b/web/src/model/system/software.ts @@ -24,6 +24,7 @@ type System = { addons: AddonInfo[]; patterns: Pattern[]; repositories: Repository[]; + registration?: RegistrationInfo; }; type Pattern = { @@ -53,16 +54,24 @@ type Repository = { loaded: boolean; }; +type RegistrationInfo = { + code?: string; + email?: string; + // FIXME: it should be mandatory. + url?: string; + addons: AddonInfo[]; +}; + type AddonInfo = { id: string; + status: string; version: string; label: string; available: boolean; free: boolean; recommended: boolean; description: string; - type: string; release: string; }; -export type { System, Pattern, Repository }; +export type { System, Pattern, RegistrationInfo, Repository }; diff --git a/web/src/model/config/storage.ts b/web/src/openapi/config/storage.ts similarity index 100% rename from web/src/model/config/storage.ts rename to web/src/openapi/config/storage.ts diff --git a/web/src/queries/software.ts b/web/src/queries/software.ts index 7173bbd06a..1b6942e544 100644 --- a/web/src/queries/software.ts +++ b/web/src/queries/software.ts @@ -56,14 +56,12 @@ import { fetchRegistration, fetchRepositories, probe, - register, registerAddon, - updateRegistrationUrl, solveConflict, updateConfig, } from "~/model/software"; import { QueryHookOptions } from "~/types/queries"; -import { probe as systemProbe, reprobe as systemReprobe } from "~/model/manager"; +import { probe as systemProbe } from "~/model/manager"; /** * Query to retrieve software configuration @@ -180,30 +178,6 @@ const useConfigMutation = () => { return useMutation(query); }; -/** - * Hook that builds a mutation for registering a product - * - * @note it would trigger a general probing as a side-effect when mutation - * includes a product. - */ -const useRegisterMutation = () => { - const queryClient = useQueryClient(); - - const query = { - mutationFn: async ({ url, key, email }: { url: string; key: string; email?: string }) => { - await updateRegistrationUrl(url).then(() => register({ key, email })); - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ["software", "registration"] }); - }, - onSuccess: async () => { - await systemReprobe(); - queryClient.invalidateQueries({ queryKey: ["storage"] }); - }, - }; - return useMutation(query); -}; - /** * Hook that builds a mutation for registering an addon * @@ -441,7 +415,6 @@ export { useSoftwareProposal, useSoftwareProposalChanges, useRegisterAddonMutation, - useRegisterMutation, useRegisteredAddons, useRegistration, useRepositories, diff --git a/web/src/types/software.ts b/web/src/types/software.ts index fa3c1af073..70bb34a011 100644 --- a/web/src/types/software.ts +++ b/web/src/types/software.ts @@ -122,12 +122,6 @@ type RegistrationInfo = { url: string; }; -type RegistrationParams = { - key: string; - email?: string; - url: string; -}; - type AddonInfo = { id: string; version: string; @@ -177,7 +171,6 @@ export type { Product, RegisteredAddonInfo, RegistrationInfo, - RegistrationParams, Repository, SoftwareConfig, SoftwareProposal,