diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust.yml index c5a0dd0bc1..10cb94ec29 100644 --- a/.github/workflows/ci-rust.yml +++ b/.github/workflows/ci-rust.yml @@ -114,7 +114,6 @@ jobs: continue-on-error: true uses: coverallsapp/github-action@v2 # ignore errors in this step - continue-on-error: true with: base-path: ./rust format: cobertura @@ -129,7 +128,6 @@ jobs: continue-on-error: true uses: coverallsapp/github-action@v2 # ignore errors in this step - continue-on-error: true with: parallel-finished: true carryforward: "service,web" diff --git a/live/src/config.sh b/live/src/config.sh index 73ad4ddefd..fa76ccd53d 100644 --- a/live/src/config.sh +++ b/live/src/config.sh @@ -195,7 +195,7 @@ mkdir -p /etc/agama.d # insists on running systemd as PID 1 :-/ ls -1 -d /usr/lib/locale/*.utf8 | sed -e "s#/usr/lib/locale/##" -e "s#utf8#UTF-8#" >/etc/agama.d/locales -# delete translations and unusupported languages (makes ISO about 22MiB smaller) +# delete translations and unsupported languages (makes ISO about 22MiB smaller) # build list of ignore options for "ls" with supported languages like "-I cs* -I de* -I es* ..." readarray -t IGNORE_OPTS < <(ls /usr/share/agama/web_ui/po.*.js.gz | sed -e "s#/usr/share/agama/web_ui/po\.\(.*\)\.js\.gz#-I\n\\1*#") # additionally keep the en_US translations diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index 45769fd109..74844f67fb 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -20,7 +20,7 @@ use crate::dbus::{get_optional_property, get_property}; use crate::error::ServiceError; -use crate::software::model::AddonParams; +use crate::software::model::{AddonParams, AddonProperties}; use crate::software::proxies::SoftwareProductProxy; use serde::Serialize; use std::collections::HashMap; @@ -146,6 +146,28 @@ impl<'a> ProductClient<'a> { Ok(addons) } + // details of available addons + pub async fn available_addons(&self) -> Result, ServiceError> { + self.registration_proxy + .available_addons() + .await? + .into_iter() + .map(|hash| { + Ok(AddonProperties { + id: get_property(&hash, "id")?, + version: get_property(&hash, "version")?, + label: get_property(&hash, "label")?, + available: get_property(&hash, "available")?, + free: get_property(&hash, "free")?, + recommended: get_property(&hash, "recommended")?, + description: get_property(&hash, "description")?, + release: get_property(&hash, "release")?, + r#type: get_property(&hash, "type")?, + }) + }) + .collect() + } + /// register product pub async fn register(&self, code: &str, email: &str) -> Result<(u32, String), ServiceError> { let mut options: HashMap<&str, &zbus::zvariant::Value> = HashMap::new(); diff --git a/rust/agama-lib/src/product/proxies.rs b/rust/agama-lib/src/product/proxies.rs index 14a2cb41e9..d351684ecc 100644 --- a/rust/agama-lib/src/product/proxies.rs +++ b/rust/agama-lib/src/product/proxies.rs @@ -75,4 +75,10 @@ pub trait Registration { /// registered addons property, list of tuples (name, version, reg_code)) #[zbus(property)] fn registered_addons(&self) -> zbus::Result>; + + /// available addons property, a hash with string key + #[zbus(property)] + fn available_addons( + &self, + ) -> zbus::Result>>; } diff --git a/rust/agama-lib/src/software/model/registration.rs b/rust/agama-lib/src/software/model/registration.rs index d4adf2537e..3673cecbb5 100644 --- a/rust/agama-lib/src/software/model/registration.rs +++ b/rust/agama-lib/src/software/model/registration.rs @@ -41,6 +41,30 @@ pub struct AddonParams { pub registration_code: Option, } +/// Addon registration +#[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct AddonProperties { + /// Addon identifier + pub id: String, + /// Version of the addon + pub version: String, + /// User visible name + pub label: String, + /// Whether the addon is mirrored on the RMT server, on SCC it is always `true` + pub available: bool, + /// Whether a registration code is required for registering the addon + pub free: bool, + /// Whether the addon is recommended for the users + pub recommended: bool, + /// Short description of the addon (translated) + pub description: String, + /// Type of the addon, like "extension" or "module" + pub r#type: String, + /// Release status of the addon, e.g. "beta" + pub release: String, +} + /// Information about registration configuration (product, patterns, etc.). #[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index 95bab4dfa5..5d0e8f37a9 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -38,7 +38,7 @@ use agama_lib::{ product::{proxies::RegistrationProxy, Product, ProductClient}, software::{ model::{ - AddonParams, License, LicenseContent, LicensesRepo, RegistrationError, + AddonParams, AddonProperties, License, LicenseContent, LicensesRepo, RegistrationError, RegistrationInfo, RegistrationParams, Repository, ResolvableParams, SoftwareConfig, }, proxies::{Software1Proxy, SoftwareProductProxy}, @@ -218,6 +218,7 @@ pub async fn software_service(dbus: zbus::Connection) -> Result), + (status = 400, description = "The D-Bus service could not perform the action") + ) +)] +async fn get_available_addons( + State(state): State>, +) -> Result>, Error> { + let result = state.product.available_addons().await?; + + Ok(Json(result)) +} + /// Register an addon /// /// * `state`: service state. @@ -484,7 +505,7 @@ pub struct SoftwareProposal { /// Space required for installation. It is returned as a formatted string which includes /// a number and a unit (e.g., "GiB"). size: String, - /// Patterns selection. It is respresented as a hash map where the key is the pattern's name + /// Patterns selection. It is represented as a hash map where the key is the pattern's name /// and the value why the pattern is selected. patterns: HashMap, } diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb index ae1176b762..b44eb5cb7e 100644 --- a/service/lib/agama/dbus/software/product.rb +++ b/service/lib/agama/dbus/software/product.rb @@ -151,6 +151,27 @@ def registered_addons end end + # list of available addons + # + # @return [Array>] List of addons + def available_addons + addons = backend.registration.available_addons || [] + + addons.map do |a| + { + "id" => a.identifier, + "version" => a.version, + "label" => a.friendly_name, + "available" => a.available, # boolean + "free" => a.free, # boolean + "recommended" => a.recommended, # boolean + "description" => a.description, + "type" => a.product_type, # "extension" + "release" => a.release_stage # "beta" + } + end + end + # Tries to register with the given registration code. # # @note Software is not automatically probed after registering the product. The reason is @@ -275,6 +296,8 @@ def deregister dbus_reader(:registered_addons, "a(sss)") + dbus_reader(:available_addons, "aa{sv}") + dbus_method(:Register, "in reg_code:s, in options:a{sv}, out result:(us)") do |*args| [register(args[0], email: args[1]["Email"])] end diff --git a/service/lib/agama/registration.rb b/service/lib/agama/registration.rb index 58fce1e108..67421149e9 100644 --- a/service/lib/agama/registration.rb +++ b/service/lib/agama/registration.rb @@ -200,6 +200,20 @@ def finish end end + # Get the available addons for the specified base product. + # + # @note The result is bound to the registration code used for the base product, the result + # might be different for different codes. E.g. the Alpha/Beta extensions might or might not + # be included in the list. + def available_addons + return @available_addons if @available_addons + + @available_addons = SUSE::Connect::YaST.show_product(base_target_product, + connect_params).extensions + @logger.info "Available addons: #{available_addons.inspect}" + @available_addons + end + # Callbacks to be called when registration changes (e.g., a different product is selected). def on_change(&block) @on_change_callbacks ||= [] @@ -330,20 +344,6 @@ def repository_data(repo) data end - # Get the available addons for the specified base product. - # - # @note The result is bound to the registration code used for the base product, the result - # might be different for different codes. E.g. the Alpha/Beta extensions might or might not - # be included in the list. - def available_addons - return @available_addons if @available_addons - - @available_addons = SUSE::Connect::YaST.show_product(base_target_product, - connect_params).extensions - @logger.info "Available addons: #{available_addons.inspect}" - @available_addons - end - # Find the version for the specified addon, if none if multiple addons with the same name # are found an exception is thrown. # diff --git a/web/src/api/software.ts b/web/src/api/software.ts index b190c398d2..e83768d6f6 100644 --- a/web/src/api/software.ts +++ b/web/src/api/software.ts @@ -21,14 +21,16 @@ */ import { + AddonInfo, + License, + LicenseContent, Pattern, Product, - SoftwareConfig, + RegisteredAddonInfo, RegistrationInfo, Repository, + SoftwareConfig, SoftwareProposal, - License, - LicenseContent, } from "~/types/software"; import { get, post, put } from "~/api/http"; @@ -63,6 +65,17 @@ const fetchLicense = (id: string, lang: string = "en"): Promise */ const fetchRegistration = (): Promise => get("/api/software/registration"); +/** + * Returns list of available addons + */ +const fetchAddons = (): Promise => get("/api/software/registration/addons/available"); + +/** + * Returns list of already registered addons + */ +const fetchRegisteredAddons = (): Promise => + get("/api/software/registration/addons/registered"); + /** * Returns the list of patterns for the selected product */ @@ -91,16 +104,25 @@ const probe = () => post("/api/software/probe"); const register = ({ key, email }: { key: string; email?: string }) => post("/api/software/registration", { key, email }); +/** + * Request registration of the selected addon + */ +const registerAddon = (addon: RegisteredAddonInfo) => + post("/api/software/registration/addons/register", addon); + export { + fetchAddons, fetchConfig, + fetchLicense, + fetchLicenses, fetchPatterns, - fetchProposal, fetchProducts, - fetchLicenses, - fetchLicense, + fetchProposal, + fetchRegisteredAddons, fetchRegistration, fetchRepositories, - updateConfig, probe, register, + registerAddon, + updateConfig, }; diff --git a/web/src/components/product/ProductRegistrationPage.test.tsx b/web/src/components/product/ProductRegistrationPage.test.tsx index d24622f0bb..e78f39216d 100644 --- a/web/src/components/product/ProductRegistrationPage.test.tsx +++ b/web/src/components/product/ProductRegistrationPage.test.tsx @@ -24,8 +24,8 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import ProductRegistrationPage from "./ProductRegistrationPage"; -import { Product, RegistrationInfo } from "~/types/software"; -import { useProduct, useRegistration } from "~/queries/software"; +import { AddonInfo, Product, RegisteredAddonInfo, RegistrationInfo } from "~/types/software"; +import { useAddons, useProduct, useRegisteredAddons, useRegistration } from "~/queries/software"; const tw: Product = { id: "Tumbleweed", @@ -42,6 +42,8 @@ const sle: Product = { let selectedProduct: Product; let staticHostnameMock: string; let registrationInfoMock: RegistrationInfo; +let addonInfoMock: AddonInfo[] = []; +let registeredAddonInfoMock: RegisteredAddonInfo[] = []; const registerMutationMock = jest.fn(); jest.mock("~/components/product/ProductRegistrationAlert", () => () => ( @@ -52,6 +54,8 @@ jest.mock("~/queries/software", () => ({ ...jest.requireActual("~/queries/software"), useRegisterMutation: () => ({ mutate: registerMutationMock }), useRegistration: (): ReturnType => registrationInfoMock, + useAddons: (): ReturnType => addonInfoMock, + useRegisteredAddons: (): ReturnType => registeredAddonInfoMock, useProduct: (): ReturnType => { return { products: [tw, sle], @@ -198,6 +202,19 @@ describe("ProductRegistrationPage", () => { beforeEach(() => { selectedProduct = sle; registrationInfoMock = { key: "INTERNAL-USE-ONLY-1234-5678", email: "example@company.test" }; + addonInfoMock = [ + { + id: "sle-ha", + version: "16.0", + label: "SUSE Linux Enterprise High Availability Extension 16.0 x86_64 (BETA)", + available: true, + free: false, + recommended: false, + description: "SUSE Linux High Availability Extension provides ...", + type: "extension", + release: "beta", + }, + ]; }); it("does not render a custom alert about hostname", () => { @@ -221,5 +238,52 @@ describe("ProductRegistrationPage", () => { expect(screen.queryByText("INTERNAL-USE-ONLY-1234-5678")).toBeNull(); screen.getByText(/\*?5678/); }); + + it("renders available extensions", async () => { + const { container } = installerRender(); + + // description is displayed + screen.getByText(addonInfoMock[0].description); + // label without "BETA" + screen.getByText("SUSE Linux Enterprise High Availability Extension 16.0 x86_64"); + + // registration input field is displayed + const addonRegCode = container.querySelector('[id="input-reg-code-sle-ha-16.0"]'); + expect(addonRegCode).not.toBeNull(); + + // submit button is displayed + const addonRegButton = container.querySelector('[id="register-button-sle-ha-16.0"]'); + expect(addonRegButton).not.toBeNull(); + }); + + describe("when the extension is registered", () => { + beforeEach(() => { + registeredAddonInfoMock = [ + { + id: "sle-ha", + version: "16.0", + registrationCode: "INTERNAL-USE-ONLY-1234-ad42", + }, + ]; + }); + + it("renders registration information with code partially hidden", async () => { + const { user } = installerRender(); + + // the second "Show" button, the first one belongs to the base product registration code + const visibilityCodeToggler = screen.getAllByRole("button", { name: "Show" })[1]; + expect(visibilityCodeToggler).not.toBeNull(); + + // only the end of the code is displayed + screen.getByText(/\*+ad42/); + // not the full code + expect(screen.queryByText(registeredAddonInfoMock[0].registrationCode)).toBeNull(); + + // after pressing the "Show" button + await user.click(visibilityCodeToggler); + // the full code is visible + screen.getByText(registeredAddonInfoMock[0].registrationCode); + }); + }); }); }); diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index c40aad4edc..ddb4bf44a6 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -34,19 +34,21 @@ import { Flex, Form, FormGroup, + Title, TextInput, } from "@patternfly/react-core"; -import { Link, Page, PasswordInput } from "~/components/core"; +import { Link, Page } from "~/components/core"; import { RegistrationInfo } from "~/types/software"; import { HOSTNAME } from "~/routes/paths"; -import { useProduct, useRegistration, useRegisterMutation } from "~/queries/software"; +import { useProduct, useRegistration, useRegisterMutation, useAddons } from "~/queries/software"; import { useHostname } from "~/queries/system"; import { isEmpty, mask } from "~/utils"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; +import RegistrationExtension from "./RegistrationExtension"; +import RegistrationCodeInput from "./RegistrationCodeInput"; const FORM_ID = "productRegistration"; -const KEY_LABEL = _("Registration code"); const EMAIL_LABEL = "Email"; const RegisteredProductSection = () => { @@ -62,7 +64,8 @@ const RegisteredProductSection = () => { - {KEY_LABEL} + {/* TRANSLATORS: input field label */} + {_("Registration code")} {showCode ? registration.key : mask(registration.key)} @@ -120,9 +123,9 @@ const RegistrationFormSection = () => { return (
{error && } - - - setKey(v)} /> + {/* // TRANSLATORS: input field label */} + + setKey(v)} /> @@ -174,10 +177,31 @@ 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} + + ); +}; + export default function ProductRegistrationPage() { const { selectedProduct: product } = useProduct(); const { key } = useRegistration(); - const isUnregistered = isEmpty(key); + // FIXME: this needs to be fixed for RMT which allows registering with empty key + const isRegistered = !isEmpty(key); // TODO: render something meaningful instead? "Product not registrable"? if (!product.registration) return; @@ -189,8 +213,9 @@ export default function ProductRegistrationPage() { - {isUnregistered && } - {isUnregistered ? : } + {!isRegistered && } + {!isRegistered ? : } + {isRegistered && } ); diff --git a/web/src/components/product/RegistrationCodeInput.tsx b/web/src/components/product/RegistrationCodeInput.tsx new file mode 100644 index 0000000000..f1c5373a5f --- /dev/null +++ b/web/src/components/product/RegistrationCodeInput.tsx @@ -0,0 +1,33 @@ +/* + * 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 React from "react"; +import { TextInputProps } from "@patternfly/react-core"; + +import { PasswordInput } from "~/components/core"; + +// the registration code might be quite long, make the password field wider +export default function RegistrationCodeInput({ + ...props +}: TextInputProps & { inputRef?: React.Ref }) { + return ; +} diff --git a/web/src/components/product/RegistrationExtension.tsx b/web/src/components/product/RegistrationExtension.tsx new file mode 100644 index 0000000000..94b1c409f3 --- /dev/null +++ b/web/src/components/product/RegistrationExtension.tsx @@ -0,0 +1,189 @@ +/* + * 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 React, { useState } from "react"; +import { + ActionGroup, + Alert, + Button, + Content, + Form, + FormGroup, + Label, + Title, + Stack, +} from "@patternfly/react-core"; +import { AddonInfo, RegisteredAddonInfo } from "~/types/software"; +import { useRegisteredAddons, useRegisterAddonMutation } from "~/queries/software"; +import { mask } from "~/utils"; +import { _ } from "~/i18n"; +import RegistrationCodeInput from "./RegistrationCodeInput"; + +/** + * Display registered status of the extension. + * + * @param param0 + * @returns + */ +const RegisteredExtensionStatus = ({ registrationCode }: { registrationCode: string }) => { + const [showCode, setShowCode] = useState(false); + + // TRANSLATORS: %s will be replaced by the registration key. + const [msg1, msg2] = _("The extension has been registered with key %s.").split("%s"); + + return ( + + {msg1} + {showCode ? registrationCode : mask(registrationCode)} + {msg2}{" "} + + + ); +}; + +/** + * Display an extension from the registration server. + * + * @param extension The extension to display + * @returns React component + */ +export default function RegistrationExtension({ + extension, + isUnique, +}: { + extension: AddonInfo; + isUnique: boolean; +}) { + const { mutate: registerAddon } = useRegisterAddonMutation(); + const registeredExtensions = useRegisteredAddons(); + const [regCode, setRegCode] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const onRegisterError = ({ response }) => { + setError(response.data.message); + }; + + const registrationData = registeredExtensions.find( + (e) => e.id === extension.id && (e.version === extension.version || e.version === null), + ); + + const isRegistered = !!registrationData; + + const submit = async (e: React.SyntheticEvent | undefined) => { + e?.preventDefault(); + setLoading(true); + + const data: RegisteredAddonInfo = { + id: extension.id, + registrationCode: regCode, + // omit the version if only one version of the extension exists + version: isUnique ? null : extension.version, + }; + + registerAddon(data, { + // @ts-expect-error + onError: onRegisterError, + onSuccess: () => setError(null), + onSettled: () => setLoading(false), + }); + }; + + return ( + + {/* remove the "(BETA)" suffix, we display a Beta label instead */} + + {extension.label.replace(/\s*\(beta\)$/i, "")}{" "} + {extension.release === "beta" && ( + <Label color="blue" isCompact> + {/* TRANSLATORS: Beta version label */} + {_("Beta")} + </Label> + )} + {extension.recommended && ( + <Label color="orange" isCompact> + {/* TRANSLATORS: Label for recommended extensions */} + {_("Recommended")} + </Label> + )} + + {extension.description} + {error && } + + {isRegistered && ( + + )} + {!isRegistered && extension.available && !extension.free && ( + + {/* // TRANSLATORS: input field label */} + + setRegCode(v)} + /> + + + + + + )} + {!isRegistered && extension.available && extension.free && ( + // for free extensions display just the button without any form + + )} + + {!isRegistered && !extension.available && ( + // TRANSLATORS: warning title, the extension is not available on the server and cannot be registered + + {_( + // TRANSLATORS: warning message, the extension is not available on the server and cannot be registered + "This extension is not available on the server. Ask the server administrator to mirror the extension.", + )} + + )} + + + ); +} diff --git a/web/src/queries/software.ts b/web/src/queries/software.ts index 71d3aa8c66..d4a5410e64 100644 --- a/web/src/queries/software.ts +++ b/web/src/queries/software.ts @@ -31,10 +31,12 @@ import { } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; import { + AddonInfo, License, Pattern, PatternsSelection, Product, + RegisteredAddonInfo, RegistrationInfo, Repository, SelectedBy, @@ -42,15 +44,18 @@ import { SoftwareProposal, } from "~/types/software"; import { + fetchAddons, fetchConfig, fetchLicenses, fetchPatterns, fetchProducts, fetchProposal, + fetchRegisteredAddons, fetchRegistration, fetchRepositories, probe, register, + registerAddon, updateConfig, } from "~/api/software"; import { QueryHookOptions } from "~/types/queries"; @@ -106,6 +111,22 @@ const registrationQuery = () => ({ queryFn: fetchRegistration, }); +/** + * Query to retrieve available addons info + */ +const addonsQuery = () => ({ + queryKey: ["software", "registration", "addons"], + queryFn: fetchAddons, +}); + +/** + * Query to retrieve registered addons info + */ +const registeredAddonsQuery = () => ({ + queryKey: ["software", "registration", "addons", "registered"], + queryFn: fetchRegisteredAddons, +}); + /** * Query to retrieve available patterns */ @@ -166,6 +187,22 @@ const useRegisterMutation = () => { return useMutation(query); }; +/** + * Hook that builds a mutation for registering an addon + * + */ +const useRegisterAddonMutation = () => { + const queryClient = useQueryClient(); + + const query = { + mutationFn: registerAddon, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: registeredAddonsQuery().queryKey }); + }, + }; + return useMutation(query); +}; + /** * Hook that builds a mutation for reloading repositories */ @@ -262,6 +299,22 @@ const useRegistration = (): RegistrationInfo => { return registration; }; +/** + * Returns details about the available addons + */ +const useAddons = (): AddonInfo[] => { + const { data: addons } = useSuspenseQuery(addonsQuery()); + return addons; +}; + +/** + * Returns list of registered addons + */ +const useRegisteredAddons = (): RegisteredAddonInfo[] => { + const { data: addons } = useSuspenseQuery(registeredAddonsQuery()); + return addons; +}; + /** * Returns repository info */ @@ -318,15 +371,18 @@ export { configQuery, productsQuery, selectedProductQuery, + useAddons, useConfigMutation, + useLicenses, usePatterns, useProduct, - useLicenses, useProductChanges, useProposal, useProposalChanges, - useRegistration, + useRegisterAddonMutation, useRegisterMutation, + useRegisteredAddons, + useRegistration, useRepositories, useRepositoryMutation, }; diff --git a/web/src/types/software.ts b/web/src/types/software.ts index e5563c1fd9..2cf757fc63 100644 --- a/web/src/types/software.ts +++ b/web/src/types/software.ts @@ -111,15 +111,35 @@ type RegistrationInfo = { email?: string; }; +type AddonInfo = { + id: string; + version: string; + label: string; + available: boolean; + free: boolean; + recommended: boolean; + description: string; + type: string; + release: string; +}; + +type RegisteredAddonInfo = { + id: string; + version: string | null; + registrationCode: string; +}; + export { SelectedBy }; export type { + AddonInfo, + License, + LicenseContent, Pattern, PatternsSelection, Product, - License, - LicenseContent, - SoftwareConfig, + RegisteredAddonInfo, RegistrationInfo, Repository, + SoftwareConfig, SoftwareProposal, };