From 8f0b9c82e971a9888216d29de53ecab217a2178c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Mon, 31 Mar 2025 10:43:18 +0200 Subject: [PATCH 01/32] The first draft --- .../product/ProductRegistrationPage.tsx | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index c40aad4edc..62b866d579 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -27,6 +27,11 @@ import { Button, Checkbox, Content, + DataList, + DataListCell, + DataListItem, + DataListItemCells, + DataListItemRow, DescriptionList, DescriptionListDescription, DescriptionListGroup, @@ -34,6 +39,8 @@ import { Flex, Form, FormGroup, + Label, + Stack, TextInput, } from "@patternfly/react-core"; import { Link, Page, PasswordInput } from "~/components/core"; @@ -174,6 +181,81 @@ const HostnameAlert = () => { ); }; +const Extensions = () => { + // just some testing data + // TODO: read from backend + const extensions = [ + // the current HA extension data + { + identifier: "sle-ha", + version: "16.0", + arch: "x86_64", + isBase: false, + friendlyName: "SUSE Linux Enterprise High Availability Extension 16.0 x86_64 (BETA)", + productLine: "", + available: true, + free: false, + recommended: false, + description: + "SUSE Linux High Availability Extension provides mature, industry-leading open-source high-availability clustering technologies that are easy to set up and use. It can be deployed in physical and/or virtual environments, and can cluster physical servers, virtual servers, or any combination of the two to suit the needs of your business.", + productType: "extension", + shortName: "SLEHA16", + name: "SUSE Linux Enterprise High Availability Extension", + releaseStage: "beta", + }, + ]; + + const extensionComponents = extensions.map((ext) => ( + + + + +
+ {/* remove the "(BETA)" suffix, we display a Beta label instead */} + {ext.friendlyName.replace(/\s*\(beta\)$/i, "")}{" "} + {ext.releaseStage === "beta" && ( + + )} + {ext.free && ( + + )} +
+
{ext.description}
+
+ {!ext.free && ( + + + + )} + + + + +
+
+ , + ]} + /> +
+
+ )); + + return ( + <> + {_("Extensions")} + {extensionComponents} + + ); +}; + export default function ProductRegistrationPage() { const { selectedProduct: product } = useProduct(); const { key } = useRegistration(); @@ -191,6 +273,8 @@ export default function ProductRegistrationPage() { {isUnregistered && } {isUnregistered ? : } + {/* TODO: display only when registered */} + ); From 7c29a902d46cd4c543b1fd3ca503f007cb396cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Thu, 3 Apr 2025 17:49:31 +0200 Subject: [PATCH 02/32] HTTP API for available addons --- live/src/config.sh | 8 ++++- rust/agama-lib/src/product/client.rs | 23 +++++++++++++- rust/agama-lib/src/product/proxies.rs | 4 +++ .../src/software/model/registration.rs | 17 +++++++++++ rust/agama-server/src/software/web.rs | 30 +++++++++++++++++-- service/lib/agama/dbus/software/product.rb | 23 ++++++++++++++ service/lib/agama/registration.rb | 28 ++++++++--------- 7 files changed, 114 insertions(+), 19 deletions(-) diff --git a/live/src/config.sh b/live/src/config.sh index 8313be0562..c1a90a9c3c 100644 --- a/live/src/config.sh +++ b/live/src/config.sh @@ -12,6 +12,12 @@ echo "Configure image: [$kiwi_iname]..." # setup baseproduct link suseSetupProduct +# save the build data +mkdir -p /var/log/build +cat << EOF > /var/log/build/info +Build date: $(LC_ALL=C date -u "+%F %T %Z") +EOF + # enable the corresponding repository DISTRO=$(grep "^NAME" /etc/os-release | cut -f2 -d\= | tr -d '"' | tr " " "_") REPO="/etc/zypp/repos.d/agama-${DISTRO}.repo" @@ -163,7 +169,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..175f1b8ac2 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,27 @@ impl<'a> ProductClient<'a> { Ok(addons) } + pub async fn available_addons(&self) -> Result, ServiceError> { + let addons: Vec = self + .registration_proxy + .available_addons() + .await? + .into_iter() + .map(|hash| AddonProperties { + id: hash.get("name").unwrap_or(&zbus::zvariant::Value::new("")).try_into().unwrap_or_default(), + version: hash.get("version").unwrap_or(&zbus::zvariant::Value::new("")).try_into().unwrap_or_default(), + label: hash.get("label").unwrap_or(&zbus::zvariant::Value::new("")).try_into().unwrap_or_default(), + available: hash.get("available").unwrap_or(&zbus::zvariant::Value::new(true)).try_into().unwrap_or(true), + free: hash.get("free").unwrap_or(&zbus::zvariant::Value::new(false)).try_into().unwrap_or(false), + recommended: hash.get("recommended").unwrap_or(&zbus::zvariant::Value::new(false)).try_into().unwrap_or(false), + description: hash.get("description").unwrap_or(&zbus::zvariant::Value::new("")).try_into().unwrap_or_default(), + release: hash.get("release").unwrap_or(&zbus::zvariant::Value::new("")).try_into().unwrap_or_default(), + r#type: hash.get("type").unwrap_or(&zbus::zvariant::Value::new("")).try_into().unwrap_or_default(), + }) + .collect(); + Ok(addons) + } + /// 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..842780f785 100644 --- a/rust/agama-lib/src/product/proxies.rs +++ b/rust/agama-lib/src/product/proxies.rs @@ -75,4 +75,8 @@ 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..a82ef529e5 100644 --- a/rust/agama-lib/src/software/model/registration.rs +++ b/rust/agama-lib/src/software/model/registration.rs @@ -41,6 +41,23 @@ 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, + pub version: String, + pub label: String, + pub available: bool, + pub free: bool, + pub recommended: bool, + pub description: String, + // "type" is a keyword, use a raw identifier for that + pub r#type: String, + 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..ec5fbceee4 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -38,8 +38,8 @@ use agama_lib::{ product::{proxies::RegistrationProxy, Product, ProductClient}, software::{ model::{ - AddonParams, License, LicenseContent, LicensesRepo, RegistrationError, - RegistrationInfo, RegistrationParams, Repository, ResolvableParams, SoftwareConfig, + AddonParams, AddonProperties, License, LicenseContent, LicensesRepo, RegistrationError, + RegistrationInfo, RegistrationParams, Repository, ResolvableParams, SoftwareConfig }, proxies::{Software1Proxy, SoftwareProductProxy}, Pattern, SelectedBy, SoftwareClient, UnknownSelectedBy, @@ -218,6 +218,10 @@ 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 +508,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..ef3b885f01 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 |_addon| + { + "name" => 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, "a{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 0b67a08c47..469543d290 100644 --- a/service/lib/agama/registration.rb +++ b/service/lib/agama/registration.rb @@ -190,6 +190,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 ||= [] @@ -320,20 +334,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. # From 0f4f266e1bc3927a12e80d187973ab1c157fb256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 4 Apr 2025 11:17:42 +0200 Subject: [PATCH 03/32] Read available addons in frontend --- service/lib/agama/dbus/software/product.rb | 2 +- web/src/api/software.ts | 7 +++++++ .../product/ProductRegistrationPage.tsx | 11 ++++++++--- web/src/queries/software.ts | 19 +++++++++++++++++++ web/src/types/software.ts | 13 +++++++++++++ 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb index ef3b885f01..2be5e3ffde 100644 --- a/service/lib/agama/dbus/software/product.rb +++ b/service/lib/agama/dbus/software/product.rb @@ -157,7 +157,7 @@ def registered_addons def available_addons addons = backend.registration.available_addons || [] - addons.map do |_addon| + addons.map do |a| { "name" => a.identifier, "version" => a.version, diff --git a/web/src/api/software.ts b/web/src/api/software.ts index b190c398d2..7c3252bc63 100644 --- a/web/src/api/software.ts +++ b/web/src/api/software.ts @@ -21,6 +21,7 @@ */ import { + AddonInfo, Pattern, Product, SoftwareConfig, @@ -63,6 +64,11 @@ const fetchLicense = (id: string, lang: string = "en"): Promise */ const fetchRegistration = (): Promise => get("/api/software/registration"); +/** + * Returns an object with the registration info + */ +const fetchAddons = (): Promise => get("/api/software/registration/addons/available"); + /** * Returns the list of patterns for the selected product */ @@ -92,6 +98,7 @@ const register = ({ key, email }: { key: string; email?: string }) => post("/api/software/registration", { key, email }); export { + fetchAddons, fetchConfig, fetchPatterns, fetchProposal, diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index 62b866d579..92dca6168d 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -46,7 +46,7 @@ import { import { Link, Page, PasswordInput } 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"; @@ -129,7 +129,7 @@ const RegistrationFormSection = () => { {error && } - setKey(v)} /> + setKey(v)} size={30} /> @@ -182,6 +182,10 @@ const HostnameAlert = () => { }; const Extensions = () => { + const addons = useAddons(); + + console.log("addons: ", addons); + // just some testing data // TODO: read from backend const extensions = [ @@ -259,6 +263,7 @@ const Extensions = () => { export default function ProductRegistrationPage() { const { selectedProduct: product } = useProduct(); const { key } = useRegistration(); + // FIXME: this needs to be fixed for RMT which allows registering with empty key const isUnregistered = isEmpty(key); // TODO: render something meaningful instead? "Product not registrable"? @@ -274,7 +279,7 @@ export default function ProductRegistrationPage() { {isUnregistered && } {isUnregistered ? : } {/* TODO: display only when registered */} - + {!isUnregistered && } ); diff --git a/web/src/queries/software.ts b/web/src/queries/software.ts index 71d3aa8c66..5298f175e6 100644 --- a/web/src/queries/software.ts +++ b/web/src/queries/software.ts @@ -31,6 +31,7 @@ import { } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; import { + AddonInfo, License, Pattern, PatternsSelection, @@ -42,6 +43,7 @@ import { SoftwareProposal, } from "~/types/software"; import { + fetchAddons, fetchConfig, fetchLicenses, fetchPatterns, @@ -106,6 +108,14 @@ const registrationQuery = () => ({ queryFn: fetchRegistration, }); +/** + * Query to retrieve registration info + */ +const addonsQuery = () => ({ + queryKey: ["software/registration/addons"], + queryFn: fetchAddons, +}); + /** * Query to retrieve available patterns */ @@ -262,6 +272,14 @@ const useRegistration = (): RegistrationInfo => { return registration; }; +/** + * Returns registration info + */ +const useAddons = (): AddonInfo[] => { + const { data: addons } = useSuspenseQuery(addonsQuery()); + return addons; +}; + /** * Returns repository info */ @@ -318,6 +336,7 @@ export { configQuery, productsQuery, selectedProductQuery, + useAddons, useConfigMutation, usePatterns, useProduct, diff --git a/web/src/types/software.ts b/web/src/types/software.ts index e5563c1fd9..3720a68154 100644 --- a/web/src/types/software.ts +++ b/web/src/types/software.ts @@ -111,8 +111,21 @@ type RegistrationInfo = { email?: string; }; +type AddonInfo = { + id: string; + version: string; + label: string; + available: boolean; + free: boolean; + recommended: boolean; + description: string; + type: string; + release: string; +}; + export { SelectedBy }; export type { + AddonInfo, Pattern, PatternsSelection, Product, From 2e211b3622eedbfb20609d439cc866c860a9c88a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 4 Apr 2025 12:10:11 +0200 Subject: [PATCH 04/32] Fixed DBus signature --- service/lib/agama/dbus/software/product.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb index 2be5e3ffde..9c19915e2f 100644 --- a/service/lib/agama/dbus/software/product.rb +++ b/service/lib/agama/dbus/software/product.rb @@ -296,7 +296,7 @@ def deregister dbus_reader(:registered_addons, "a(sss)") - dbus_reader(:available_addons, "a{sv}") + 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"])] From 4567fefe886e8ad3b94888702322664ce6896c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 4 Apr 2025 12:26:42 +0200 Subject: [PATCH 05/32] Display available addons --- .../product/ProductRegistrationPage.tsx | 38 +++---------------- 1 file changed, 6 insertions(+), 32 deletions(-) diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index 92dca6168d..5107c6633b 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -182,35 +182,10 @@ const HostnameAlert = () => { }; const Extensions = () => { - const addons = useAddons(); - - console.log("addons: ", addons); - - // just some testing data - // TODO: read from backend - const extensions = [ - // the current HA extension data - { - identifier: "sle-ha", - version: "16.0", - arch: "x86_64", - isBase: false, - friendlyName: "SUSE Linux Enterprise High Availability Extension 16.0 x86_64 (BETA)", - productLine: "", - available: true, - free: false, - recommended: false, - description: - "SUSE Linux High Availability Extension provides mature, industry-leading open-source high-availability clustering technologies that are easy to set up and use. It can be deployed in physical and/or virtual environments, and can cluster physical servers, virtual servers, or any combination of the two to suit the needs of your business.", - productType: "extension", - shortName: "SLEHA16", - name: "SUSE Linux Enterprise High Availability Extension", - releaseStage: "beta", - }, - ]; + const extensions = useAddons(); const extensionComponents = extensions.map((ext) => ( - + {
{/* remove the "(BETA)" suffix, we display a Beta label instead */} - {ext.friendlyName.replace(/\s*\(beta\)$/i, "")}{" "} - {ext.releaseStage === "beta" && ( + {ext.label.replace(/\s*\(beta\)$/i, "")}{" "} + {ext.release === "beta" && ( @@ -234,7 +209,7 @@ const Extensions = () => {
{!ext.free && ( - + )} @@ -255,7 +230,7 @@ const Extensions = () => { return ( <> {_("Extensions")} - {extensionComponents} + {extensionComponents} ); }; @@ -278,7 +253,6 @@ export default function ProductRegistrationPage() { {isUnregistered && } {isUnregistered ? : } - {/* TODO: display only when registered */} {!isUnregistered && } From 3b1f02dc24207c0eed7edf11b9cd47f8708921dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 4 Apr 2025 15:47:12 +0200 Subject: [PATCH 06/32] UI improvements --- .../product/ProductRegistrationPage.tsx | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index 5107c6633b..81af4afafc 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -56,6 +56,8 @@ const FORM_ID = "productRegistration"; const KEY_LABEL = _("Registration code"); const EMAIL_LABEL = "Email"; +const RegistrationCodeInput = ({ ...props }) => ; + const RegisteredProductSection = () => { const { selectedProduct: product } = useProduct(); const registration = useRegistration(); @@ -129,7 +131,7 @@ const RegistrationFormSection = () => { {error && } - setKey(v)} size={30} /> + setKey(v)} /> @@ -199,26 +201,34 @@ const Extensions = () => { {_("Beta")} )} - {ext.free && ( -
{ext.description}
- - {!ext.free && ( - - - - )} + {ext.available ? ( + + {!ext.free && ( + + + + )} - - - - + + + + + ) : ( + + {_( + "This extension is not available on the server. Please ask the server administrator to mirror the extension.", + )} + + )}
, ]} From 99770377a6e74baabcc080a172c4410effc7f32a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Tue, 8 Apr 2025 13:49:09 +0200 Subject: [PATCH 07/32] Display registered status --- web/src/api/software.ts | 24 ++-- .../product/ProductRegistrationPage.tsx | 130 +++++++++++------- web/src/queries/software.ts | 23 +++- web/src/types/software.ts | 13 +- 4 files changed, 127 insertions(+), 63 deletions(-) diff --git a/web/src/api/software.ts b/web/src/api/software.ts index 7c3252bc63..2bbe7605d2 100644 --- a/web/src/api/software.ts +++ b/web/src/api/software.ts @@ -22,14 +22,15 @@ import { AddonInfo, + License, + LicenseContent, Pattern, Product, - SoftwareConfig, + RegisteredAddonInfo, RegistrationInfo, Repository, + SoftwareConfig, SoftwareProposal, - License, - LicenseContent, } from "~/types/software"; import { get, post, put } from "~/api/http"; @@ -65,10 +66,16 @@ const fetchLicense = (id: string, lang: string = "en"): Promise const fetchRegistration = (): Promise => get("/api/software/registration"); /** - * Returns an object with the registration info + * Returns list of available addons */ const fetchAddons = (): Promise => get("/api/software/registration/addons/available"); +/** + * Returns an object with the registration info + */ +const fetchRegisteredAddons = (): Promise => + get("/api/software/registration/addons/registered"); + /** * Returns the list of patterns for the selected product */ @@ -100,14 +107,15 @@ const register = ({ key, email }: { key: string; email?: string }) => export { fetchAddons, fetchConfig, + fetchLicense, + fetchLicenses, fetchPatterns, - fetchProposal, fetchProducts, - fetchLicenses, - fetchLicense, + fetchProposal, + fetchRegisteredAddons, fetchRegistration, fetchRepositories, - updateConfig, probe, register, + updateConfig, }; diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index 81af4afafc..8e0c47a415 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -44,9 +44,15 @@ import { TextInput, } from "@patternfly/react-core"; import { Link, Page, PasswordInput } from "~/components/core"; -import { RegistrationInfo } from "~/types/software"; +import { RegisteredAddonInfo, RegistrationInfo } from "~/types/software"; import { HOSTNAME } from "~/routes/paths"; -import { useProduct, useRegistration, useRegisterMutation, useAddons } from "~/queries/software"; +import { + useProduct, + useRegistration, + useRegisterMutation, + useAddons, + useRegisteredAddons, +} from "~/queries/software"; import { useHostname } from "~/queries/system"; import { isEmpty, mask } from "~/utils"; import { sprintf } from "sprintf-js"; @@ -56,6 +62,7 @@ const FORM_ID = "productRegistration"; const KEY_LABEL = _("Registration code"); const EMAIL_LABEL = "Email"; +// the registration code might be quite long, make the password field wider const RegistrationCodeInput = ({ ...props }) => ; const RegisteredProductSection = () => { @@ -92,6 +99,20 @@ const RegisteredProductSection = () => { ); }; +const RegisteredExtensionSection = (info: RegisteredAddonInfo) => { + const [showCode, setShowCode] = useState(false); + + return ( + + {_("The extension was registered with key ")} + {showCode ? info.registrationCode : mask(info.registrationCode)}{" "} + + + ); +}; + const RegistrationFormSection = () => { const { mutate: register } = useRegisterMutation(); const [key, setKey] = useState(""); @@ -185,57 +206,66 @@ const HostnameAlert = () => { const Extensions = () => { const extensions = useAddons(); + const registeredExtensions = useRegisteredAddons(); - const extensionComponents = extensions.map((ext) => ( - - - - -
- {/* remove the "(BETA)" suffix, we display a Beta label instead */} - {ext.label.replace(/\s*\(beta\)$/i, "")}{" "} - {ext.release === "beta" && ( - - )} - {ext.recommended && ( - - )} -
-
{ext.description}
- {ext.available ? ( -
- {!ext.free && ( - - - - )} + const extensionComponents = extensions.map((ext) => { + const registeredExtension = registeredExtensions.find( + (e) => e.id === ext.id && (e.version === ext.version || e.version === null), + ); - - - -
- ) : ( - - {_( - "This extension is not available on the server. Please ask the server administrator to mirror the extension.", + return ( + + + + +
+ {/* remove the "(BETA)" suffix, we display a Beta label instead */} + {ext.label.replace(/\s*\(beta\)$/i, "")}{" "} + {ext.release === "beta" && ( + )} - - )} - - , - ]} - /> - - - )); + {ext.recommended && ( + + )} +
+
{ext.description}
+ {registeredExtension ? ( + RegisteredExtensionSection(registeredExtension) + ) : ext.available ? ( +
+ {!ext.free && ( + + + + )} + + + + +
+ ) : ( + + {_( + "This extension is not available on the server. Please ask the server administrator to mirror the extension.", + )} + + )} +
+ , + ]} + /> +
+
+ ); + }); return ( <> diff --git a/web/src/queries/software.ts b/web/src/queries/software.ts index 5298f175e6..672b3f74aa 100644 --- a/web/src/queries/software.ts +++ b/web/src/queries/software.ts @@ -36,6 +36,7 @@ import { Pattern, PatternsSelection, Product, + RegisteredAddonInfo, RegistrationInfo, Repository, SelectedBy, @@ -49,6 +50,7 @@ import { fetchPatterns, fetchProducts, fetchProposal, + fetchRegisteredAddons, fetchRegistration, fetchRepositories, probe, @@ -116,6 +118,14 @@ const addonsQuery = () => ({ queryFn: fetchAddons, }); +/** + * Query to retrieve registration info + */ +const registeredAddonsQuery = () => ({ + queryKey: ["software/registration/addons/registered"], + queryFn: fetchRegisteredAddons, +}); + /** * Query to retrieve available patterns */ @@ -280,6 +290,14 @@ const useAddons = (): AddonInfo[] => { return addons; }; +/** + * Returns registration info + */ +const useRegisteredAddons = (): RegisteredAddonInfo[] => { + const { data: addons } = useSuspenseQuery(registeredAddonsQuery()); + return addons; +}; + /** * Returns repository info */ @@ -338,14 +356,15 @@ export { selectedProductQuery, useAddons, useConfigMutation, + useLicenses, usePatterns, useProduct, - useLicenses, useProductChanges, useProposal, useProposalChanges, - useRegistration, useRegisterMutation, + useRegisteredAddons, + useRegistration, useRepositories, useRepositoryMutation, }; diff --git a/web/src/types/software.ts b/web/src/types/software.ts index 3720a68154..6a1488e3e3 100644 --- a/web/src/types/software.ts +++ b/web/src/types/software.ts @@ -123,16 +123,23 @@ type AddonInfo = { release: string; }; +type RegisteredAddonInfo = { + id: string; + version: string; + registrationCode: string; +}; + export { SelectedBy }; export type { AddonInfo, + License, + LicenseContent, Pattern, PatternsSelection, Product, - License, - LicenseContent, - SoftwareConfig, + RegisteredAddonInfo, RegistrationInfo, Repository, + SoftwareConfig, SoftwareProposal, }; From 05f59e756919dd30d6c7e285a3ea5ec7838fa049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Tue, 8 Apr 2025 22:10:12 +0200 Subject: [PATCH 08/32] Register extension --- web/src/api/software.ts | 7 + .../product/ProductRegistrationPage.tsx | 157 +++++++++++------- web/src/queries/software.ts | 18 ++ web/src/types/software.ts | 2 +- 4 files changed, 122 insertions(+), 62 deletions(-) diff --git a/web/src/api/software.ts b/web/src/api/software.ts index 2bbe7605d2..983c5d01dc 100644 --- a/web/src/api/software.ts +++ b/web/src/api/software.ts @@ -104,6 +104,12 @@ const probe = () => post("/api/software/probe"); const register = ({ key, email }: { key: string; email?: string }) => post("/api/software/registration", { key, email }); +/** + * Request registration of selected product with given key + */ +const registerAddon = (addon: RegisteredAddonInfo) => + post("/api/software/registration/addons/register", addon); + export { fetchAddons, fetchConfig, @@ -117,5 +123,6 @@ export { fetchRepositories, probe, register, + registerAddon, updateConfig, }; diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index 8e0c47a415..a79cdb931d 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -44,7 +44,7 @@ import { TextInput, } from "@patternfly/react-core"; import { Link, Page, PasswordInput } from "~/components/core"; -import { RegisteredAddonInfo, RegistrationInfo } from "~/types/software"; +import { AddonInfo, RegisteredAddonInfo, RegistrationInfo } from "~/types/software"; import { HOSTNAME } from "~/routes/paths"; import { useProduct, @@ -52,6 +52,7 @@ import { useRegisterMutation, useAddons, useRegisteredAddons, + useRegisterAddonMutation, } from "~/queries/software"; import { useHostname } from "~/queries/system"; import { isEmpty, mask } from "~/utils"; @@ -102,10 +103,14 @@ const RegisteredProductSection = () => { const RegisteredExtensionSection = (info: RegisteredAddonInfo) => { 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 ( - {_("The extension was registered with key ")} - {showCode ? info.registrationCode : mask(info.registrationCode)}{" "} + {msg1} + {showCode ? info.registrationCode : mask(info.registrationCode)} + {msg2}{" "} @@ -204,68 +209,98 @@ const HostnameAlert = () => { ); }; -const Extensions = () => { - const extensions = useAddons(); +const Extension = (ext: AddonInfo) => { + const { mutate: registerAddon } = useRegisterAddonMutation(); const registeredExtensions = useRegisteredAddons(); + const [regCode, setRegCode] = useState(""); + const [error, setError] = useState(null); + + const onRegisterError = ({ response }) => { + setError(response.data.message); + }; + + const registered = registeredExtensions.find( + (e) => e.id === ext.id && (e.version === ext.version || e.version === null), + ); + const [loading, setLoading] = useState(false); + + const submit = async (e: React.SyntheticEvent) => { + e.preventDefault(); + setLoading(true); - const extensionComponents = extensions.map((ext) => { - const registeredExtension = registeredExtensions.find( - (e) => e.id === ext.id && (e.version === ext.version || e.version === null), - ); - - return ( - - - - -
- {/* remove the "(BETA)" suffix, we display a Beta label instead */} - {ext.label.replace(/\s*\(beta\)$/i, "")}{" "} - {ext.release === "beta" && ( - + const data: RegisteredAddonInfo = { + id: ext.id, + // TODO: make version optional + version: ext.version, + registrationCode: regCode, + }; + + // @ts-expect-error + registerAddon(data, { onError: onRegisterError, onSettled: () => setLoading(false) }); + }; + + return ( + + + + +
+ {/* remove the "(BETA)" suffix, we display a Beta label instead */} + {ext.label.replace(/\s*\(beta\)$/i, "")}{" "} + {ext.release === "beta" && ( + + )} + {ext.recommended && ( + + )} +
+
{ext.description}
+ {registered ? ( + RegisteredExtensionSection(registered) + ) : ext.available ? ( +
+ {error && } + {!ext.free && ( + + setRegCode(v)} + /> + )} - {ext.recommended && ( - + + + + + + ) : ( + + {_( + "This extension is not available on the server. Please ask the server administrator to mirror the extension.", )} -
-
{ext.description}
- {registeredExtension ? ( - RegisteredExtensionSection(registeredExtension) - ) : ext.available ? ( -
- {!ext.free && ( - - - - )} - - - - -
- ) : ( - - {_( - "This extension is not available on the server. Please ask the server administrator to mirror the extension.", - )} - - )} -
- , - ]} - /> -
-
- ); - }); +
+ )} +
+ , + ]} + /> +
+
+ ); +}; + +const Extensions = () => { + const extensions = useAddons(); + const extensionComponents = extensions.map((ext) => Extension(ext)); return ( <> diff --git a/web/src/queries/software.ts b/web/src/queries/software.ts index 672b3f74aa..d8c40453de 100644 --- a/web/src/queries/software.ts +++ b/web/src/queries/software.ts @@ -55,6 +55,7 @@ import { fetchRepositories, probe, register, + registerAddon, updateConfig, } from "~/api/software"; import { QueryHookOptions } from "~/types/queries"; @@ -186,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: ["software/registration/addons/registered"] }); + }, + }; + return useMutation(query); +}; + /** * Hook that builds a mutation for reloading repositories */ @@ -362,6 +379,7 @@ export { useProductChanges, useProposal, useProposalChanges, + useRegisterAddonMutation, useRegisterMutation, useRegisteredAddons, useRegistration, diff --git a/web/src/types/software.ts b/web/src/types/software.ts index 6a1488e3e3..2cf757fc63 100644 --- a/web/src/types/software.ts +++ b/web/src/types/software.ts @@ -125,7 +125,7 @@ type AddonInfo = { type RegisteredAddonInfo = { id: string; - version: string; + version: string | null; registrationCode: string; }; From 82b241e515cd75b4a7b562f609778259d733eaa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Wed, 9 Apr 2025 10:16:55 +0200 Subject: [PATCH 09/32] Fixed rendering --- .../product/ProductRegistrationPage.tsx | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index a79cdb931d..966ee879fc 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -44,7 +44,7 @@ import { TextInput, } from "@patternfly/react-core"; import { Link, Page, PasswordInput } from "~/components/core"; -import { AddonInfo, RegisteredAddonInfo, RegistrationInfo } from "~/types/software"; +import { RegisteredAddonInfo, RegistrationInfo } from "~/types/software"; import { HOSTNAME } from "~/routes/paths"; import { useProduct, @@ -100,7 +100,7 @@ const RegisteredProductSection = () => { ); }; -const RegisteredExtensionSection = (info: RegisteredAddonInfo) => { +const RegisteredExtensionSection = ({ extension }) => { const [showCode, setShowCode] = useState(false); // TRANSLATORS: %s will be replaced by the registration key. @@ -109,7 +109,7 @@ const RegisteredExtensionSection = (info: RegisteredAddonInfo) => { return ( {msg1} - {showCode ? info.registrationCode : mask(info.registrationCode)} + {showCode ? extension.registrationCode : mask(extension.registrationCode)} {msg2}{" "} - - - ) : ( - - {_( - "This extension is not available on the server. Please ask the server administrator to mirror the extension.", - )} - - )} - - , - ]} - /> -
-
+
+ {/* remove the "(BETA)" suffix, we display a Beta label instead */} + + {extension.label.replace(/\s*\(beta\)$/i, "")}{" "} + {extension.release === "beta" && ( + <Label color="blue" isCompact> + {_("Beta")} + </Label> + )} + {extension.recommended && ( + <Label color="orange" isCompact> + {_("Recommended")} + </Label> + )} + +

 

+

{extension.description}

+

 

+

+ {registered ? ( + + ) : extension.available ? ( +

+ {error && } + {!extension.free && ( + + setRegCode(v)} + /> + + )} + + + + + + ) : ( + + {_( + "This extension is not available on the server. Please ask the server administrator to mirror the extension.", + )} + + )} +

+
); }; const Extensions = () => { const extensions = useAddons(); - const extensionComponents = extensions.map((ext) => ( - + + // count the extension versions + const counts = {}; + extensions.forEach((e) => (counts[e.id] = counts[e.id] ? counts[e.id] + 1 : 1)); + + const extensionComponents = extensions.map((ext, index) => ( + )); return ( <> - {_("Extensions")} - {extensionComponents} + {_("Extensions")} + {extensionComponents} ); }; From e2399a5258fd3a867f6898ea341ddeebb352e00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Wed, 9 Apr 2025 17:55:42 +0200 Subject: [PATCH 11/32] Reformat the Rust code --- rust/agama-lib/src/product/client.rs | 54 +++++++++++++++---- rust/agama-lib/src/product/proxies.rs | 4 +- .../src/software/model/registration.rs | 2 +- rust/agama-server/src/software/web.rs | 7 +-- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index 175f1b8ac2..1b30745bf8 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -153,15 +153,51 @@ impl<'a> ProductClient<'a> { .await? .into_iter() .map(|hash| AddonProperties { - id: hash.get("name").unwrap_or(&zbus::zvariant::Value::new("")).try_into().unwrap_or_default(), - version: hash.get("version").unwrap_or(&zbus::zvariant::Value::new("")).try_into().unwrap_or_default(), - label: hash.get("label").unwrap_or(&zbus::zvariant::Value::new("")).try_into().unwrap_or_default(), - available: hash.get("available").unwrap_or(&zbus::zvariant::Value::new(true)).try_into().unwrap_or(true), - free: hash.get("free").unwrap_or(&zbus::zvariant::Value::new(false)).try_into().unwrap_or(false), - recommended: hash.get("recommended").unwrap_or(&zbus::zvariant::Value::new(false)).try_into().unwrap_or(false), - description: hash.get("description").unwrap_or(&zbus::zvariant::Value::new("")).try_into().unwrap_or_default(), - release: hash.get("release").unwrap_or(&zbus::zvariant::Value::new("")).try_into().unwrap_or_default(), - r#type: hash.get("type").unwrap_or(&zbus::zvariant::Value::new("")).try_into().unwrap_or_default(), + id: hash + .get("name") + .unwrap_or(&zbus::zvariant::Value::new("")) + .try_into() + .unwrap_or_default(), + version: hash + .get("version") + .unwrap_or(&zbus::zvariant::Value::new("")) + .try_into() + .unwrap_or_default(), + label: hash + .get("label") + .unwrap_or(&zbus::zvariant::Value::new("")) + .try_into() + .unwrap_or_default(), + available: hash + .get("available") + .unwrap_or(&zbus::zvariant::Value::new(true)) + .try_into() + .unwrap_or(true), + free: hash + .get("free") + .unwrap_or(&zbus::zvariant::Value::new(false)) + .try_into() + .unwrap_or(false), + recommended: hash + .get("recommended") + .unwrap_or(&zbus::zvariant::Value::new(false)) + .try_into() + .unwrap_or(false), + description: hash + .get("description") + .unwrap_or(&zbus::zvariant::Value::new("")) + .try_into() + .unwrap_or_default(), + release: hash + .get("release") + .unwrap_or(&zbus::zvariant::Value::new("")) + .try_into() + .unwrap_or_default(), + r#type: hash + .get("type") + .unwrap_or(&zbus::zvariant::Value::new("")) + .try_into() + .unwrap_or_default(), }) .collect(); Ok(addons) diff --git a/rust/agama-lib/src/product/proxies.rs b/rust/agama-lib/src/product/proxies.rs index 842780f785..6bc9364584 100644 --- a/rust/agama-lib/src/product/proxies.rs +++ b/rust/agama-lib/src/product/proxies.rs @@ -78,5 +78,7 @@ pub trait Registration { /// available addons property, a hash with string key #[zbus(property)] - fn available_addons(&self) -> zbus::Result>>; + 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 a82ef529e5..8995ac0528 100644 --- a/rust/agama-lib/src/software/model/registration.rs +++ b/rust/agama-lib/src/software/model/registration.rs @@ -55,7 +55,7 @@ pub struct AddonProperties { pub description: String, // "type" is a keyword, use a raw identifier for that pub r#type: String, - pub release: String + pub release: String, } /// Information about registration configuration (product, patterns, etc.). diff --git a/rust/agama-server/src/software/web.rs b/rust/agama-server/src/software/web.rs index ec5fbceee4..5d0e8f37a9 100644 --- a/rust/agama-server/src/software/web.rs +++ b/rust/agama-server/src/software/web.rs @@ -39,7 +39,7 @@ use agama_lib::{ software::{ model::{ AddonParams, AddonProperties, License, LicenseContent, LicensesRepo, RegistrationError, - RegistrationInfo, RegistrationParams, Repository, ResolvableParams, SoftwareConfig + RegistrationInfo, RegistrationParams, Repository, ResolvableParams, SoftwareConfig, }, proxies::{Software1Proxy, SoftwareProductProxy}, Pattern, SelectedBy, SoftwareClient, UnknownSelectedBy, @@ -218,10 +218,7 @@ pub async fn software_service(dbus: zbus::Connection) -> Result Date: Thu, 10 Apr 2025 10:21:03 +0200 Subject: [PATCH 12/32] Update unit tests --- .../product/ProductRegistrationPage.test.tsx | 68 ++++++++++++++++++- .../product/ProductRegistrationPage.tsx | 10 ++- 2 files changed, 74 insertions(+), 4 deletions(-) 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 c114afa3ed..a1b4b9f8a2 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -262,7 +262,7 @@ const Extension = ({ extension, unique }) => { {!extension.free && ( setRegCode(v)} /> @@ -270,7 +270,13 @@ const Extension = ({ extension, unique }) => { )} - From a8d01124502821d21d3880a57449017e4a0859ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 11 Apr 2025 10:10:12 +0200 Subject: [PATCH 13/32] Update web/src/components/product/ProductRegistrationPage.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Better wording Co-authored-by: David Díaz <1691872+dgdavid@users.noreply.github.com> --- web/src/components/product/ProductRegistrationPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index a1b4b9f8a2..abc7bad301 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -284,7 +284,7 @@ const Extension = ({ extension, unique }) => { ) : ( {_( - "This extension is not available on the server. Please ask the server administrator to mirror the extension.", + "This extension is not available on the server. Ask the server administrator to mirror the extension.", )} )} From a4d3cec9d004641bcfbdd35518880a52d0ed5055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 11 Apr 2025 10:26:31 +0200 Subject: [PATCH 14/32] Update web/src/components/product/ProductRegistrationPage.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use `` component. Co-authored-by: David Díaz <1691872+dgdavid@users.noreply.github.com> --- web/src/components/product/ProductRegistrationPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index abc7bad301..7611566bef 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -102,14 +102,14 @@ const RegisteredExtensionSection = ({ extension }) => { const [msg1, msg2] = _("The extension has been registered with key %s.").split("%s"); return ( - + {msg1} {showCode ? extension.registrationCode : mask(extension.registrationCode)} {msg2}{" "} - + ); }; From 84709411a55081d39a11174e42df049479ccd826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 11 Apr 2025 10:34:48 +0200 Subject: [PATCH 15/32] Review fixes --- .../product/ProductRegistrationPage.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index 7611566bef..6f3cf84768 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -37,6 +37,7 @@ import { Label, Title, TextInput, + Stack, } from "@patternfly/react-core"; import { Link, Page, PasswordInput } from "~/components/core"; import { RegisteredAddonInfo, RegistrationInfo } from "~/types/software"; @@ -235,7 +236,7 @@ const Extension = ({ extension, unique }) => { }; return ( -
+ {/* remove the "(BETA)" suffix, we display a Beta label instead */} {extension.label.replace(/\s*\(beta\)$/i, "")}{" "} @@ -250,10 +251,8 @@ const Extension = ({ extension, unique }) => { </Label> )} -

 

-

{extension.description}

-

 

-

+ {extension.description} + {registered ? ( ) : extension.available ? ( @@ -288,8 +287,8 @@ const Extension = ({ extension, unique }) => { )} )} -

-
+ + ); }; @@ -300,8 +299,12 @@ const Extensions = () => { const counts = {}; extensions.forEach((e) => (counts[e.id] = counts[e.id] ? counts[e.id] + 1 : 1)); - const extensionComponents = extensions.map((ext, index) => ( - + const extensionComponents = extensions.map((ext) => ( + )); return ( From be5c0de7690733b59bc7819b558924e5eabc58eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 11 Apr 2025 10:54:01 +0200 Subject: [PATCH 16/32] Update web/src/api/software.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: David Díaz <1691872+dgdavid@users.noreply.github.com> --- web/src/api/software.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/api/software.ts b/web/src/api/software.ts index 983c5d01dc..67ecae56c2 100644 --- a/web/src/api/software.ts +++ b/web/src/api/software.ts @@ -105,7 +105,7 @@ const register = ({ key, email }: { key: string; email?: string }) => post("/api/software/registration", { key, email }); /** - * Request registration of selected product with given key + * Request registration of provided addon */ const registerAddon = (addon: RegisteredAddonInfo) => post("/api/software/registration/addons/register", addon); From 9251f62825486c1b05fd283aacb40afc8bf66ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 11 Apr 2025 16:15:56 +0200 Subject: [PATCH 17/32] Small fixes --- .../product/ProductRegistrationPage.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index 6f3cf84768..cef6d194bb 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -40,7 +40,7 @@ import { Stack, } from "@patternfly/react-core"; import { Link, Page, PasswordInput } from "~/components/core"; -import { RegisteredAddonInfo, RegistrationInfo } from "~/types/software"; +import { AddonInfo, RegisteredAddonInfo, RegistrationInfo } from "~/types/software"; import { HOSTNAME } from "~/routes/paths"; import { useProduct, @@ -205,7 +205,7 @@ const HostnameAlert = () => { ); }; -const Extension = ({ extension, unique }) => { +const Extension = ({ extension, isUnique }: { extension: AddonInfo; isUnique: boolean }) => { const { mutate: registerAddon } = useRegisterAddonMutation(); const registeredExtensions = useRegisteredAddons(); const [regCode, setRegCode] = useState(""); @@ -228,7 +228,7 @@ const Extension = ({ extension, unique }) => { id: extension.id, registrationCode: regCode, // omit the version if only one version of the extension exists - version: unique ? null : extension.version, + version: isUnique ? null : extension.version, }; // @ts-expect-error @@ -252,12 +252,12 @@ const Extension = ({ extension, unique }) => { )} {extension.description} + {error && } {registered ? ( ) : extension.available ? (
- {error && } {!extension.free && ( { const Extensions = () => { const extensions = useAddons(); - - // count the extension versions - const counts = {}; - extensions.forEach((e) => (counts[e.id] = counts[e.id] ? counts[e.id] + 1 : 1)); + if (extensions.length === 0) return null; const extensionComponents = extensions.map((ext) => ( e.id === ext.id).length === 1} /> )); From 7c793f4a1bf378e24fa7e43c0361d159c11f2155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 11 Apr 2025 17:00:18 +0200 Subject: [PATCH 18/32] Small refactoring --- .../product/ProductRegistrationPage.tsx | 128 +------------ .../product/RegistrationComponents.tsx | 39 ++++ .../product/RegistrationExtension.tsx | 173 ++++++++++++++++++ 3 files changed, 218 insertions(+), 122 deletions(-) create mode 100644 web/src/components/product/RegistrationComponents.tsx create mode 100644 web/src/components/product/RegistrationExtension.tsx diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index cef6d194bb..f59c7f4f6e 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -34,34 +34,23 @@ import { Flex, Form, FormGroup, - Label, Title, TextInput, - Stack, } from "@patternfly/react-core"; -import { Link, Page, PasswordInput } from "~/components/core"; -import { AddonInfo, RegisteredAddonInfo, RegistrationInfo } from "~/types/software"; +import { Link, Page } from "~/components/core"; +import { RegistrationInfo } from "~/types/software"; import { HOSTNAME } from "~/routes/paths"; -import { - useProduct, - useRegistration, - useRegisterMutation, - useAddons, - useRegisteredAddons, - useRegisterAddonMutation, -} 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 { KEY_LABEL, RegistrationCodeInput } from "./RegistrationComponents"; const FORM_ID = "productRegistration"; -const KEY_LABEL = _("Registration code"); const EMAIL_LABEL = "Email"; -// the registration code might be quite long, make the password field wider -const RegistrationCodeInput = ({ ...props }) => ; - const RegisteredProductSection = () => { const { selectedProduct: product } = useProduct(); const registration = useRegistration(); @@ -96,24 +85,6 @@ const RegisteredProductSection = () => { ); }; -const RegisteredExtensionSection = ({ extension }) => { - 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 ? extension.registrationCode : mask(extension.registrationCode)} - {msg2}{" "} - - - ); -}; - const RegistrationFormSection = () => { const { mutate: register } = useRegisterMutation(); const [key, setKey] = useState(""); @@ -205,99 +176,12 @@ const HostnameAlert = () => { ); }; -const Extension = ({ 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 registered = registeredExtensions.find( - (e) => e.id === extension.id && (e.version === extension.version || e.version === null), - ); - - const submit = async (e: React.SyntheticEvent) => { - 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, - }; - - // @ts-expect-error - registerAddon(data, { onError: onRegisterError, 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> - {_("Beta")} - </Label> - )} - {extension.recommended && ( - <Label color="orange" isCompact> - {_("Recommended")} - </Label> - )} - - {extension.description} - {error && } - - {registered ? ( - - ) : extension.available ? ( - - {!extension.free && ( - - setRegCode(v)} - /> - - )} - - - - - - ) : ( - - {_( - "This extension is not available on the server. Ask the server administrator to mirror the extension.", - )} - - )} - - - ); -}; - const Extensions = () => { const extensions = useAddons(); if (extensions.length === 0) return null; const extensionComponents = extensions.map((ext) => ( - e.id === ext.id).length === 1} diff --git a/web/src/components/product/RegistrationComponents.tsx b/web/src/components/product/RegistrationComponents.tsx new file mode 100644 index 0000000000..8417569b31 --- /dev/null +++ b/web/src/components/product/RegistrationComponents.tsx @@ -0,0 +1,39 @@ +/* + * 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"; +import { _ } from "~/i18n"; + +// TRANSLATORS: input field label +const KEY_LABEL = _("Registration code"); + +// the registration code might be quite long, make the password field wider +function RegistrationCodeInput({ + ...props +}: TextInputProps & { inputRef?: React.Ref }) { + return ; +} + +export { KEY_LABEL, RegistrationCodeInput }; diff --git a/web/src/components/product/RegistrationExtension.tsx b/web/src/components/product/RegistrationExtension.tsx new file mode 100644 index 0000000000..a1c95ed1a1 --- /dev/null +++ b/web/src/components/product/RegistrationExtension.tsx @@ -0,0 +1,173 @@ +/* + * 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 { KEY_LABEL, RegistrationCodeInput } from "./RegistrationComponents"; + +/** + * 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) => { + 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, + }; + + // @ts-expect-error + registerAddon(data, { onError: onRegisterError, 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 && ( + + setRegCode(v)} + /> + + )} + + + + +
+ )} + {!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.", + )} + + )} +
+
+ ); +} From c6ef50e8bb334c53f9476a8ce54f5bc023d7d5c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Fri, 11 Apr 2025 18:35:42 +0200 Subject: [PATCH 19/32] Cleanup --- live/src/config.sh | 6 ------ web/src/components/product/ProductRegistrationPage.tsx | 8 ++++---- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/live/src/config.sh b/live/src/config.sh index c1a90a9c3c..471d95a09c 100644 --- a/live/src/config.sh +++ b/live/src/config.sh @@ -12,12 +12,6 @@ echo "Configure image: [$kiwi_iname]..." # setup baseproduct link suseSetupProduct -# save the build data -mkdir -p /var/log/build -cat << EOF > /var/log/build/info -Build date: $(LC_ALL=C date -u "+%F %T %Z") -EOF - # enable the corresponding repository DISTRO=$(grep "^NAME" /etc/os-release | cut -f2 -d\= | tr -d '"' | tr " " "_") REPO="/etc/zypp/repos.d/agama-${DISTRO}.repo" diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index f59c7f4f6e..6733ca5c4d 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -200,7 +200,7 @@ export default function ProductRegistrationPage() { const { selectedProduct: product } = useProduct(); const { key } = useRegistration(); // FIXME: this needs to be fixed for RMT which allows registering with empty key - const isUnregistered = isEmpty(key); + const isRegistered = !isEmpty(key); // TODO: render something meaningful instead? "Product not registrable"? if (!product.registration) return; @@ -212,9 +212,9 @@ export default function ProductRegistrationPage() { - {isUnregistered && } - {isUnregistered ? : } - {!isUnregistered && } + {!isRegistered && } + {!isRegistered ? : } + {isRegistered && } ); From 49d124967a773b4d33f6a4da9df43cd0459bd17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Mon, 14 Apr 2025 10:49:02 +0200 Subject: [PATCH 20/32] Review fixes --- web/src/queries/software.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/queries/software.ts b/web/src/queries/software.ts index d8c40453de..7e531044b8 100644 --- a/web/src/queries/software.ts +++ b/web/src/queries/software.ts @@ -115,7 +115,7 @@ const registrationQuery = () => ({ * Query to retrieve registration info */ const addonsQuery = () => ({ - queryKey: ["software/registration/addons"], + queryKey: ["software", "registration", "addons"], queryFn: fetchAddons, }); @@ -123,7 +123,7 @@ const addonsQuery = () => ({ * Query to retrieve registration info */ const registeredAddonsQuery = () => ({ - queryKey: ["software/registration/addons/registered"], + queryKey: ["software", "registration", "addons", "registered"], queryFn: fetchRegisteredAddons, }); @@ -197,7 +197,7 @@ const useRegisterAddonMutation = () => { const query = { mutationFn: registerAddon, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["software/registration/addons/registered"] }); + queryClient.invalidateQueries({ queryKey: registeredAddonsQuery().queryKey }); }, }; return useMutation(query); From 460df0d2511f6015f8224aa98e92ee35fc1404eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Mon, 14 Apr 2025 12:29:33 +0200 Subject: [PATCH 21/32] Refactoring --- web/src/components/product/ProductRegistrationPage.tsx | 5 +++-- ...gistrationComponents.tsx => RegistrationCodeInput.tsx} | 8 +------- web/src/components/product/RegistrationExtension.tsx | 5 +++-- 3 files changed, 7 insertions(+), 11 deletions(-) rename web/src/components/product/{RegistrationComponents.tsx => RegistrationCodeInput.tsx} (86%) diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index 6733ca5c4d..9124c47b83 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -46,7 +46,7 @@ import { isEmpty, mask } from "~/utils"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; import RegistrationExtension from "./RegistrationExtension"; -import { KEY_LABEL, RegistrationCodeInput } from "./RegistrationComponents"; +import RegistrationCodeInput from "./RegistrationCodeInput"; const FORM_ID = "productRegistration"; const EMAIL_LABEL = "Email"; @@ -64,7 +64,8 @@ const RegisteredProductSection = () => {
- {KEY_LABEL} + {/* TRANSLATORS: input field label */} + {_("Registration code")} {showCode ? registration.key : mask(registration.key)} diff --git a/web/src/components/product/RegistrationComponents.tsx b/web/src/components/product/RegistrationCodeInput.tsx similarity index 86% rename from web/src/components/product/RegistrationComponents.tsx rename to web/src/components/product/RegistrationCodeInput.tsx index 8417569b31..f1c5373a5f 100644 --- a/web/src/components/product/RegistrationComponents.tsx +++ b/web/src/components/product/RegistrationCodeInput.tsx @@ -24,16 +24,10 @@ import React from "react"; import { TextInputProps } from "@patternfly/react-core"; import { PasswordInput } from "~/components/core"; -import { _ } from "~/i18n"; - -// TRANSLATORS: input field label -const KEY_LABEL = _("Registration code"); // the registration code might be quite long, make the password field wider -function RegistrationCodeInput({ +export default function RegistrationCodeInput({ ...props }: TextInputProps & { inputRef?: React.Ref }) { return ; } - -export { KEY_LABEL, RegistrationCodeInput }; diff --git a/web/src/components/product/RegistrationExtension.tsx b/web/src/components/product/RegistrationExtension.tsx index a1c95ed1a1..cd78c92473 100644 --- a/web/src/components/product/RegistrationExtension.tsx +++ b/web/src/components/product/RegistrationExtension.tsx @@ -36,7 +36,7 @@ import { AddonInfo, RegisteredAddonInfo } from "~/types/software"; import { useRegisteredAddons, useRegisterAddonMutation } from "~/queries/software"; import { mask } from "~/utils"; import { _ } from "~/i18n"; -import { KEY_LABEL, RegistrationCodeInput } from "./RegistrationComponents"; +import RegistrationCodeInput from "./RegistrationCodeInput"; /** * Display registered status of the extension. @@ -134,7 +134,8 @@ export default function RegistrationExtension({ {!isRegistered && extension.available && (
{!extension.free && ( - + // TRANSLATORS: input field label + Date: Mon, 14 Apr 2025 14:20:25 +0200 Subject: [PATCH 22/32] Omit the form if not needed --- .../product/RegistrationExtension.tsx | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/web/src/components/product/RegistrationExtension.tsx b/web/src/components/product/RegistrationExtension.tsx index cd78c92473..4cbe048ad0 100644 --- a/web/src/components/product/RegistrationExtension.tsx +++ b/web/src/components/product/RegistrationExtension.tsx @@ -92,8 +92,8 @@ export default function RegistrationExtension({ const isRegistered = !!registrationData; - const submit = async (e: React.SyntheticEvent) => { - e.preventDefault(); + const submit = async (e: React.SyntheticEvent | undefined) => { + e?.preventDefault(); setLoading(true); const data: RegisteredAddonInfo = { @@ -131,20 +131,17 @@ export default function RegistrationExtension({ {isRegistered && ( )} - {!isRegistered && extension.available && ( + {!isRegistered && extension.available && !extension.free && ( - {!extension.free && ( - // TRANSLATORS: input field label - - setRegCode(v)} - /> - - )} - + {/* // TRANSLATORS: input field label */} + + setRegCode(v)} + /> + + )} + {!isRegistered && !extension.available && ( // TRANSLATORS: warning title, the extension is not available on the server and cannot be registered From f6d10191dab28e1753027ef5c94c23e8fcc2f068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Mon, 14 Apr 2025 15:32:05 +0200 Subject: [PATCH 23/32] Fixed nesting --- web/src/components/product/RegistrationExtension.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/product/RegistrationExtension.tsx b/web/src/components/product/RegistrationExtension.tsx index 4cbe048ad0..69dbdccf1b 100644 --- a/web/src/components/product/RegistrationExtension.tsx +++ b/web/src/components/product/RegistrationExtension.tsx @@ -127,7 +127,7 @@ export default function RegistrationExtension({ {extension.description} {error && } - + {isRegistered && ( )} From ed554f0a812fbc623ec81cd8afb9500a9d40c678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Mon, 14 Apr 2025 15:37:40 +0200 Subject: [PATCH 24/32] Fix --- web/src/components/product/ProductRegistrationPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index 9124c47b83..ddb4bf44a6 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -123,8 +123,8 @@ const RegistrationFormSection = () => { return (
{error && } - - + {/* // TRANSLATORS: input field label */} + setKey(v)} /> From 2164d989a7fdcb2f587170059f8887df39d6dd91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Mon, 14 Apr 2025 15:52:29 +0200 Subject: [PATCH 25/32] Use "id" everywhere Fixed comments --- rust/agama-lib/src/product/client.rs | 3 ++- service/lib/agama/dbus/software/product.rb | 2 +- web/src/api/software.ts | 4 ++-- web/src/queries/software.ts | 8 ++++---- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index 1b30745bf8..508a8a431b 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -146,6 +146,7 @@ impl<'a> ProductClient<'a> { Ok(addons) } + // details of available addons pub async fn available_addons(&self) -> Result, ServiceError> { let addons: Vec = self .registration_proxy @@ -154,7 +155,7 @@ impl<'a> ProductClient<'a> { .into_iter() .map(|hash| AddonProperties { id: hash - .get("name") + .get("id") .unwrap_or(&zbus::zvariant::Value::new("")) .try_into() .unwrap_or_default(), diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb index 9c19915e2f..b44eb5cb7e 100644 --- a/service/lib/agama/dbus/software/product.rb +++ b/service/lib/agama/dbus/software/product.rb @@ -159,7 +159,7 @@ def available_addons addons.map do |a| { - "name" => a.identifier, + "id" => a.identifier, "version" => a.version, "label" => a.friendly_name, "available" => a.available, # boolean diff --git a/web/src/api/software.ts b/web/src/api/software.ts index 67ecae56c2..e83768d6f6 100644 --- a/web/src/api/software.ts +++ b/web/src/api/software.ts @@ -71,7 +71,7 @@ const fetchRegistration = (): Promise => get("/api/software/re const fetchAddons = (): Promise => get("/api/software/registration/addons/available"); /** - * Returns an object with the registration info + * Returns list of already registered addons */ const fetchRegisteredAddons = (): Promise => get("/api/software/registration/addons/registered"); @@ -105,7 +105,7 @@ const register = ({ key, email }: { key: string; email?: string }) => post("/api/software/registration", { key, email }); /** - * Request registration of provided addon + * Request registration of the selected addon */ const registerAddon = (addon: RegisteredAddonInfo) => post("/api/software/registration/addons/register", addon); diff --git a/web/src/queries/software.ts b/web/src/queries/software.ts index 7e531044b8..d4a5410e64 100644 --- a/web/src/queries/software.ts +++ b/web/src/queries/software.ts @@ -112,7 +112,7 @@ const registrationQuery = () => ({ }); /** - * Query to retrieve registration info + * Query to retrieve available addons info */ const addonsQuery = () => ({ queryKey: ["software", "registration", "addons"], @@ -120,7 +120,7 @@ const addonsQuery = () => ({ }); /** - * Query to retrieve registration info + * Query to retrieve registered addons info */ const registeredAddonsQuery = () => ({ queryKey: ["software", "registration", "addons", "registered"], @@ -300,7 +300,7 @@ const useRegistration = (): RegistrationInfo => { }; /** - * Returns registration info + * Returns details about the available addons */ const useAddons = (): AddonInfo[] => { const { data: addons } = useSuspenseQuery(addonsQuery()); @@ -308,7 +308,7 @@ const useAddons = (): AddonInfo[] => { }; /** - * Returns registration info + * Returns list of registered addons */ const useRegisteredAddons = (): RegisteredAddonInfo[] => { const { data: addons } = useSuspenseQuery(registeredAddonsQuery()); From f9529e0b13c73c6df49b414f4d14d1e400cde1cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Mon, 14 Apr 2025 16:10:30 +0200 Subject: [PATCH 26/32] Hide the previous error after succesful addon registration --- web/src/components/product/RegistrationExtension.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/src/components/product/RegistrationExtension.tsx b/web/src/components/product/RegistrationExtension.tsx index 69dbdccf1b..94b1c409f3 100644 --- a/web/src/components/product/RegistrationExtension.tsx +++ b/web/src/components/product/RegistrationExtension.tsx @@ -103,8 +103,12 @@ export default function RegistrationExtension({ version: isUnique ? null : extension.version, }; - // @ts-expect-error - registerAddon(data, { onError: onRegisterError, onSettled: () => setLoading(false) }); + registerAddon(data, { + // @ts-expect-error + onError: onRegisterError, + onSuccess: () => setError(null), + onSettled: () => setLoading(false), + }); }; return ( From 0e4e24d23b94921d2f633cedc6344d29e7be4bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Tue, 15 Apr 2025 09:18:00 +0200 Subject: [PATCH 27/32] Describe the structure --- rust/agama-lib/src/software/model/registration.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rust/agama-lib/src/software/model/registration.rs b/rust/agama-lib/src/software/model/registration.rs index 8995ac0528..3673cecbb5 100644 --- a/rust/agama-lib/src/software/model/registration.rs +++ b/rust/agama-lib/src/software/model/registration.rs @@ -45,16 +45,23 @@ pub struct AddonParams { #[derive(Clone, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct AddonProperties { - // Addon identifier + /// 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" is a keyword, use a raw identifier for that + /// Type of the addon, like "extension" or "module" pub r#type: String, + /// Release status of the addon, e.g. "beta" pub release: String, } From b40dea6e8e924f0bf80f788d334b0da9f1353468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Tue, 15 Apr 2025 09:44:17 +0200 Subject: [PATCH 28/32] Simplify the conversion from DBus --- rust/agama-lib/src/product/client.rs | 54 +++++---------------------- rust/agama-lib/src/product/proxies.rs | 2 +- 2 files changed, 10 insertions(+), 46 deletions(-) diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index 508a8a431b..ca5b623af6 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -154,51 +154,15 @@ impl<'a> ProductClient<'a> { .await? .into_iter() .map(|hash| AddonProperties { - id: hash - .get("id") - .unwrap_or(&zbus::zvariant::Value::new("")) - .try_into() - .unwrap_or_default(), - version: hash - .get("version") - .unwrap_or(&zbus::zvariant::Value::new("")) - .try_into() - .unwrap_or_default(), - label: hash - .get("label") - .unwrap_or(&zbus::zvariant::Value::new("")) - .try_into() - .unwrap_or_default(), - available: hash - .get("available") - .unwrap_or(&zbus::zvariant::Value::new(true)) - .try_into() - .unwrap_or(true), - free: hash - .get("free") - .unwrap_or(&zbus::zvariant::Value::new(false)) - .try_into() - .unwrap_or(false), - recommended: hash - .get("recommended") - .unwrap_or(&zbus::zvariant::Value::new(false)) - .try_into() - .unwrap_or(false), - description: hash - .get("description") - .unwrap_or(&zbus::zvariant::Value::new("")) - .try_into() - .unwrap_or_default(), - release: hash - .get("release") - .unwrap_or(&zbus::zvariant::Value::new("")) - .try_into() - .unwrap_or_default(), - r#type: hash - .get("type") - .unwrap_or(&zbus::zvariant::Value::new("")) - .try_into() - .unwrap_or_default(), + 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(); Ok(addons) diff --git a/rust/agama-lib/src/product/proxies.rs b/rust/agama-lib/src/product/proxies.rs index 6bc9364584..d351684ecc 100644 --- a/rust/agama-lib/src/product/proxies.rs +++ b/rust/agama-lib/src/product/proxies.rs @@ -80,5 +80,5 @@ pub trait Registration { #[zbus(property)] fn available_addons( &self, - ) -> zbus::Result>>; + ) -> zbus::Result>>; } From 1e7639a89ce9db9101e0de10679662300b6ba0c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Tue, 15 Apr 2025 09:50:48 +0200 Subject: [PATCH 29/32] Fix Rust CI definition --- .github/workflows/ci-rust.yml | 2 -- 1 file changed, 2 deletions(-) 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" From 343776c44852bc7ce865e13e4566039aaef698cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ladislav=20Slez=C3=A1k?= Date: Tue, 15 Apr 2025 10:20:04 +0200 Subject: [PATCH 30/32] Fix compile errors --- rust/agama-lib/src/product/client.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index ca5b623af6..4cf3b80266 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -154,15 +154,15 @@ impl<'a> ProductClient<'a> { .await? .into_iter() .map(|hash| 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")?, + id: get_property(&hash, "id").unwrap_or_default(), + version: get_property(&hash, "version").unwrap_or_default(), + label: get_property(&hash, "label").unwrap_or_default(), + available: get_property(&hash, "available").unwrap_or_default(), + free: get_property(&hash, "free").unwrap_or_default(), + recommended: get_property(&hash, "recommended").unwrap_or(false), + description: get_property(&hash, "description").unwrap_or_default(), + release: get_property(&hash, "release").unwrap_or_default(), + r#type: get_property(&hash, "type").unwrap_or_default(), }) .collect(); Ok(addons) From 37e627d841319b6ca8270ae814666baebd57a695 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 15 Apr 2025 12:28:50 +0200 Subject: [PATCH 31/32] simplify rust --- rust/agama-lib/src/product/client.rs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index 4cf3b80266..d4fb5db940 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -148,24 +148,23 @@ impl<'a> ProductClient<'a> { // details of available addons pub async fn available_addons(&self) -> Result, ServiceError> { - let addons: Vec = self + self .registration_proxy .available_addons() .await? .into_iter() - .map(|hash| AddonProperties { - id: get_property(&hash, "id").unwrap_or_default(), - version: get_property(&hash, "version").unwrap_or_default(), - label: get_property(&hash, "label").unwrap_or_default(), - available: get_property(&hash, "available").unwrap_or_default(), - free: get_property(&hash, "free").unwrap_or_default(), - recommended: get_property(&hash, "recommended").unwrap_or(false), - description: get_property(&hash, "description").unwrap_or_default(), - release: get_property(&hash, "release").unwrap_or_default(), - r#type: get_property(&hash, "type").unwrap_or_default(), - }) - .collect(); - Ok(addons) + .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 From 649bc635c78a0e27174b132bfb93467382edabf4 Mon Sep 17 00:00:00 2001 From: Josef Reidinger Date: Tue, 15 Apr 2025 12:37:29 +0200 Subject: [PATCH 32/32] fix formatting --- rust/agama-lib/src/product/client.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index d4fb5db940..74844f67fb 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -148,22 +148,23 @@ impl<'a> ProductClient<'a> { // details of available addons pub async fn available_addons(&self) -> Result, ServiceError> { - self - .registration_proxy + 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")?, - })) + .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() }