diff --git a/rust/agama-software/examples/write_bench.rs b/rust/agama-software/examples/write_bench.rs index 1f48fc7a78..8a1e6d414e 100644 --- a/rust/agama-software/examples/write_bench.rs +++ b/rust/agama-software/examples/write_bench.rs @@ -21,14 +21,11 @@ use agama_security as security; use agama_software::state::SoftwareStateBuilder; use agama_software::zypp_server::{SoftwareAction, ZyppServer, ZyppServerResult}; +use agama_software::WriteIssues; use agama_utils::api::question::{Answer, AnswerRule, Config}; use agama_utils::api::software::SoftwareConfig; use agama_utils::products::Registry; -use agama_utils::{ - actor, - api::{event::Event, Issue}, - progress, question, -}; +use agama_utils::{actor, api::event::Event, progress, question}; use camino::{Utf8Path, Utf8PathBuf}; use glob::glob; use std::fs; @@ -135,7 +132,7 @@ async fn main() { }) .expect("Failed to send SoftwareAction::Write"); - let result: ZyppServerResult> = + let result: ZyppServerResult = rx.await.expect("Failed to receive response from server"); if let Err(err) = result { panic!("SoftwareAction::Write failed: {:?}", err); @@ -173,7 +170,7 @@ async fn main() { }) .expect("Failed to send SoftwareAction::Write"); - let result: ZyppServerResult> = + let result: ZyppServerResult = rx.await.expect("Failed to receive response from server"); if let Err(err) = result { panic!("SoftwareAction::Write failed: {:?}", err); @@ -197,7 +194,7 @@ async fn main() { }) .expect("Failed to send SoftwareAction::Write"); - let result: ZyppServerResult> = + let result: ZyppServerResult = rx.await.expect("Failed to receive response from server"); if let Err(err) = result { panic!("SoftwareAction::Write failed: {:?}", err); diff --git a/rust/agama-software/src/lib.rs b/rust/agama-software/src/lib.rs index f9e798e00c..066d51d051 100644 --- a/rust/agama-software/src/lib.rs +++ b/rust/agama-software/src/lib.rs @@ -39,7 +39,9 @@ pub mod service; pub use service::Service; mod model; -pub use model::{state, Model, ModelAdapter, Registration, Resolvable, ResolvableType}; +pub use model::{ + state, Model, ModelAdapter, Registration, Resolvable, ResolvableType, WriteIssues, +}; mod callbacks; pub mod message; diff --git a/rust/agama-software/src/model.rs b/rust/agama-software/src/model.rs index 3127fff4f4..77e34e2a6d 100644 --- a/rust/agama-software/src/model.rs +++ b/rust/agama-software/src/model.rs @@ -42,6 +42,19 @@ pub mod state; pub use packages::{Resolvable, ResolvableType}; pub use registration::Registration; +/// Issues found when applying the software configuration. +#[derive(Debug, Default)] +pub struct WriteIssues { + pub product: Vec, + pub software: Vec, +} + +impl WriteIssues { + pub fn is_empty(&self) -> bool { + self.product.is_empty() && self.software.is_empty() + } +} + /// Abstract the software-related configuration from the underlying system. /// /// It offers an API to query and set different software and product elements of a @@ -73,7 +86,7 @@ pub trait ModelAdapter: Send + Sync + 'static { &mut self, software: SoftwareState, progress: Handler, - ) -> Result, service::Error>; + ) -> Result; } /// [ModelAdapter] implementation for libzypp systems. @@ -119,7 +132,7 @@ impl ModelAdapter for Model { &mut self, software: SoftwareState, progress: Handler, - ) -> Result, service::Error> { + ) -> Result { let (tx, rx) = oneshot::channel(); self.zypp_sender.send(SoftwareAction::Write { state: software, diff --git a/rust/agama-software/src/model/packages.rs b/rust/agama-software/src/model/packages.rs index 00dd263abc..060bc4ee09 100644 --- a/rust/agama-software/src/model/packages.rs +++ b/rust/agama-software/src/model/packages.rs @@ -18,7 +18,9 @@ // To contact SUSE LLC about this file by physical or electronic mail, you may // find current contact information at www.suse.com. +use gettextrs::gettext; use serde::{Deserialize, Serialize}; +use std::fmt; /// Represents a software resolvable. #[derive(Clone, Debug, Deserialize, PartialEq, utoipa::ToSchema)] @@ -38,19 +40,7 @@ impl Resolvable { } /// Software resolvable type (package or pattern). -#[derive( - Clone, - Copy, - Debug, - Deserialize, - Serialize, - strum::Display, - utoipa::ToSchema, - PartialEq, - Eq, - Hash, -)] -#[strum(serialize_all = "camelCase")] +#[derive(Clone, Copy, Debug, Deserialize, Serialize, utoipa::ToSchema, PartialEq, Eq, Hash)] #[serde(rename_all = "camelCase")] pub enum ResolvableType { Package = 0, @@ -58,6 +48,17 @@ pub enum ResolvableType { Product = 2, } +impl fmt::Display for ResolvableType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let label = match self { + ResolvableType::Package => gettext("package"), + ResolvableType::Pattern => gettext("pattern"), + ResolvableType::Product => gettext("product"), + }; + write!(f, "{}", label) + } +} + impl From for zypp_agama::ResolvableKind { fn from(value: ResolvableType) -> Self { match value { diff --git a/rust/agama-software/src/service.rs b/rust/agama-software/src/service.rs index f3fa2dee8a..4418e5c22e 100644 --- a/rust/agama-software/src/service.rs +++ b/rust/agama-software/src/service.rs @@ -20,7 +20,9 @@ use crate::{ message, - model::{software_selection::SoftwareSelection, state::SoftwareState, ModelAdapter}, + model::{ + software_selection::SoftwareSelection, state::SoftwareState, ModelAdapter, WriteIssues, + }, zypp_server::{self, SoftwareAction, ZyppServer}, Model, ResolvableType, }; @@ -39,6 +41,7 @@ use agama_utils::{ progress, question, }; use async_trait::async_trait; +use gettextrs::gettext; use std::{path::PathBuf, process::Command, sync::Arc}; use tokio::sync::{broadcast, Mutex, MutexGuard, RwLock}; use url::Url; @@ -274,13 +277,25 @@ impl Service { .await .unwrap_or_else(|e| { let new_issue = Issue::new( - "software.proposal_failed", - "It was not possible to create a software proposal", + "software_proposal_failed", + &gettext("Due to an internal error, it was not possible to create a software proposal."), ) .with_details(&e.to_string()); - vec![new_issue] + WriteIssues { + software: vec![new_issue], + ..Default::default() + } }); - _ = issues.cast(issue::message::Set::new(Scope::Software, found_issues)); + + _ = issues.cast(issue::message::Set::new( + Scope::Software, + found_issues.software, + )); + + _ = issues.cast(issue::message::Set::new( + Scope::Product, + found_issues.product, + )); Self::update_state(state, my_model, events).await; }); diff --git a/rust/agama-software/src/test_utils.rs b/rust/agama-software/src/test_utils.rs index 7ff10930c6..ecb7469498 100644 --- a/rust/agama-software/src/test_utils.rs +++ b/rust/agama-software/src/test_utils.rs @@ -24,7 +24,6 @@ use agama_utils::{ api::{ event, software::{SoftwareProposal, SystemInfo}, - Issue, }, issue, products::ProductSpec, @@ -33,7 +32,7 @@ use agama_utils::{ use async_trait::async_trait; use crate::{ - model::state::SoftwareState, + model::{state::SoftwareState, WriteIssues}, service::{self}, ModelAdapter, Service, }; @@ -78,8 +77,8 @@ impl ModelAdapter for TestModel { &mut self, _software: SoftwareState, _progress: Handler, - ) -> Result, service::Error> { - Ok(vec![]) + ) -> Result { + Ok(Default::default()) } } diff --git a/rust/agama-software/src/zypp_server.rs b/rust/agama-software/src/zypp_server.rs index 6495fac7a9..929c5d81f4 100644 --- a/rust/agama-software/src/zypp_server.rs +++ b/rust/agama-software/src/zypp_server.rs @@ -45,6 +45,7 @@ use crate::{ model::{ registration::RegistrationError, state::{self, SoftwareState}, + WriteIssues, }, state::{Addon, RegistrationState, RepoKey, ResolvableSelection}, Registration, ResolvableType, @@ -121,7 +122,7 @@ pub enum SoftwareAction { progress: Handler, question: Handler, security: Handler, - tx: oneshot::Sender>>, + tx: oneshot::Sender>, }, } @@ -313,10 +314,10 @@ impl ZyppServer { _questions: Handler, security_srv: Handler, security: &mut callbacks::Security, - tx: oneshot::Sender>>, + tx: oneshot::Sender>, zypp: &zypp_agama::Zypp, ) -> Result<(), ZyppDispatchError> { - let mut issues: Vec = vec![]; + let mut issues = WriteIssues::default(); let mut steps = vec![ gettext("Updating the list of repositories"), gettext("Refreshing metadata from the repositories"), @@ -335,6 +336,10 @@ impl ZyppServer { let old_state = self.read(zypp)?; if let Some(registration_config) = &state.registration { self.update_registration(registration_config, zypp, &security_srv, &mut issues); + + if !issues.is_empty() { + return Self::send_issues_and_finish(issues, tx, progress); + } } self.trusted_keys = state.trusted_gpg_keys; @@ -369,11 +374,10 @@ impl ZyppServer { if let Err(error) = result { let message = format!("Could not add the repository {}", repo.alias); - issues.push( + issues.software.push( Issue::new("software.add_repo", &message).with_details(&error.to_string()), ); } - // Add an issue if it was not possible to add the repository. } for repo in &to_remove { @@ -383,8 +387,11 @@ impl ZyppServer { }); if let Err(error) = result { - let message = format!("Could not remove the repository {}", repo.alias); - issues.push( + // TRANSLATORS: %s is the alias of the repository. + let message = gettext("Could not remove the repository %s") + .as_str() + .replace("%s", &repo.alias); + issues.software.push( Issue::new("software.remove_repo", &message).with_details(&error.to_string()), ); } @@ -401,8 +408,8 @@ impl ZyppServer { ); if let Err(error) = result { - let message = "Could not read the repositories".to_string(); - issues.push( + let message = gettext("Could not read the repositories"); + issues.software.push( Issue::new("software.load_source", &message).with_details(&error.to_string()), ); } @@ -418,21 +425,30 @@ impl ZyppServer { zypp_agama::ResolvableSelected::Installation, ); if let Err(error) = result { - let issue = if state.allow_registration { - Issue::new( - "software.missing_registration", - &gettext("The product must be registered"), - ) + tracing::info!( + "Failed to find the product {} in the repositories: {}", + &state.product, + &error + ); + if state.allow_registration && !self.is_registered() { + let message = gettext("Failed to find the product in the repositories. You might need to register the system."); + let issue = + Issue::new("missing_registration", &message).with_details(&error.to_string()); + issues.product.push(issue); } else { - let message = format!("Could not select the product '{}'", &state.product); - Issue::new("software.missing_product", &message).with_details(&error.to_string()) + let message = gettext("Failed to find the product in the repositories."); + let issue = + Issue::new("missing_product", &message).with_details(&error.to_string()); + issues.software.push(issue); }; - issues.push(issue); + + return Self::send_issues_and_finish(issues, tx, progress); } + for (name, r#type, selection) in &state.resolvables.to_vec() { match selection { ResolvableSelection::AutoSelected { skip_if_missing } => { - issues.append(&mut self.select_resolvable( + issues.software.append(&mut self.select_resolvable( zypp, name, *r#type, @@ -441,7 +457,7 @@ impl ZyppServer { )); } ResolvableSelection::Selected => { - issues.append(&mut self.select_resolvable( + issues.software.append(&mut self.select_resolvable( zypp, name, *r#type, @@ -465,6 +481,7 @@ impl ZyppServer { self.only_required = state.options.only_required; tracing::info!("Install only required packages: {}", self.only_required); + // run the solver to select the dependencies, ignore the errors, the solver runs again later // do not save the solver testcase in this intermediate step let _ = zypp.run_solver(self.only_required, false); @@ -476,17 +493,14 @@ impl ZyppServer { }; } - _ = progress.cast(progress::message::Finish::new(Scope::Software)); if let Ok(false) = zypp.run_solver(self.only_required, self.save_solver_testcase) { - let message = gettext("There are software conflict in software selection"); - issues.push(Issue::new("software.conflict", &message)); + let message = gettext("There are conflicts in the software selection"); + issues + .software + .push(Issue::new("software.conflict", &message)); } - if let Err(e) = tx.send(Ok(issues)) { - tracing::error!("failed to send list of issues after write: {:?}", e); - // It is OK to return ok, when tx is closed, we have no other way to indicate issue. - } - Ok(()) + Self::send_issues_and_finish(issues, tx, progress) } fn select_resolvable( @@ -507,7 +521,12 @@ impl ZyppServer { name ); } else { - let message = format!("Could not select '{}'", name); + // TRANSLATORS: the first %s is the kind of resolvable (e.g., "package") + // and the second %s is the name of the resolvable. + let message = gettext("Could not select %s '%s' for installation") + .as_str() + .replacen("%s", &r#type.to_string(), 1) + .replace("%s", name); issues.push( Issue::new("software.select_resolvable", &message) .with_details(&error.to_string()), @@ -628,6 +647,10 @@ impl ZyppServer { Ok(()) } + fn is_registered(&self) -> bool { + matches!(self.registration, RegistrationStatus::Registered(_)) + } + fn modify_zypp_conf(&self) { // write only if different from default if self.only_required { @@ -861,7 +884,7 @@ impl ZyppServer { state: &RegistrationState, zypp: &zypp_agama::Zypp, security_srv: &Handler, - issues: &mut Vec, + issues: &mut WriteIssues, ) { match &self.registration { RegistrationStatus::Failed(_) | RegistrationStatus::NotRegistered => { @@ -880,7 +903,7 @@ impl ZyppServer { state: &RegistrationState, zypp: &zypp_agama::Zypp, security_srv: &Handler, - issues: &mut Vec, + issues: &mut WriteIssues, ) { let mut registration = Registration::builder(self.root_dir.clone(), &state.product, &state.version); @@ -902,10 +925,10 @@ impl ZyppServer { self.registration = RegistrationStatus::Registered(Box::new(registration)); } Err(error) => { - issues.push( + issues.product.push( Issue::new( "system_registration_failed", - "Failed to register the system", + &gettext("Failed to register the system"), ) .with_details(&error.to_string()), ); @@ -918,7 +941,7 @@ impl ZyppServer { &mut self, addons: &Vec, zypp: &zypp_agama::Zypp, - issues: &mut Vec, + issues: &mut WriteIssues, ) { let RegistrationStatus::Registered(registration) = &mut self.registration else { tracing::error!("Could not register addons because the base system is not registered"); @@ -934,8 +957,22 @@ impl ZyppServer { let message = format!("Failed to register the add-on {}", addon.id); let issue_id = format!("addon_registration_failed[{}]", &addon.id); let issue = Issue::new(&issue_id, &message).with_details(&error.to_string()); - issues.push(issue); + issues.product.push(issue); } } } + + /// Ancillary function to send the issues and finish the progress early. + fn send_issues_and_finish( + issues: WriteIssues, + tx: oneshot::Sender>, + progress: Handler, + ) -> Result<(), ZyppDispatchError> { + if let Err(e) = tx.send(Ok(issues)) { + tracing::error!("failed to send list of issues after write: {:?}", e); + // It is OK to return ok, when tx is closed, we have no other way to indicate issue. + } + _ = progress.cast(progress::message::Finish::new(Scope::Software)); + Ok(()) + } } diff --git a/rust/agama-software/tests/zypp_server.rs b/rust/agama-software/tests/zypp_server.rs index 4726032992..09c6463aa0 100644 --- a/rust/agama-software/tests/zypp_server.rs +++ b/rust/agama-software/tests/zypp_server.rs @@ -21,12 +21,12 @@ use agama_security as security; use agama_software::state::{Repository as StateRepository, SoftwareState}; use agama_software::zypp_server::{SoftwareAction, ZyppServer, ZyppServerResult}; +use agama_software::WriteIssues; use agama_utils::{ actor, api::{ event::Event, question::{Answer, AnswerRule, Config}, - Issue, }, progress, question, }; @@ -133,7 +133,7 @@ async fn test_start_zypp_server() { }) .expect("Failed to send SoftwareAction::Write"); - let result: ZyppServerResult> = + let result: ZyppServerResult = rx.await.expect("Failed to receive response from server"); assert!( result.is_ok(), @@ -142,11 +142,11 @@ async fn test_start_zypp_server() { ); let issues = result.unwrap(); assert_eq!( - issues.len(), + issues.software.len(), 1, "There are unexpected issues size {issues:#?}" ); - assert_eq!(issues[0].class, "software.missing_product"); + assert_eq!(issues.software[0].class, "missing_product"); let questions = question_handler .call(question::message::Get) diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 1f474f68da..edf88d50ea 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Thu Mar 5 15:27:13 UTC 2026 - Imobach Gonzalez Sosa + +- Improve registration and software issues reporting (related to bsc#1258034). + ------------------------------------------------------------------- Thu Mar 5 13:44:46 UTC 2026 - Ladislav Slezák diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index b1252cb4b0..853afb56bb 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Thu Mar 5 11:46:24 UTC 2026 - Imobach Gonzalez Sosa + +- Improve registration and software issues reporting (related to bsc#1258034). + ------------------------------------------------------------------- Wed Mar 4 09:12:14 UTC 2026 - David Diaz diff --git a/web/src/components/overview/OverviewPage.tsx b/web/src/components/overview/OverviewPage.tsx index a65063d2af..cde34aabd7 100644 --- a/web/src/components/overview/OverviewPage.tsx +++ b/web/src/components/overview/OverviewPage.tsx @@ -28,6 +28,8 @@ import { Button, Content, Divider, + EmptyState, + EmptyStateBody, Flex, FlexItem, Grid, @@ -97,6 +99,22 @@ const ConfirmationPopup = ({ ); }; +/** + * Renders a PatternFly `EmptyState` block used when no product was found in the + * repositories. + */ +const NoProductFound = () => { + return ( + + + {_( + "The product was not found in the repositories so it is not possible to proceed with the installation.", + )} + + + ); +}; + const OverviewPageContent = ({ product }) => { const issues = useIssues(); const { loading } = useProgressTracking(); @@ -105,6 +123,7 @@ const OverviewPageContent = ({ product }) => { const [showConfirmation, setShowConfirmation] = useState(false); const hasIssues = !isEmpty(issues); const hasDestructiveActions = actions.length > 0; + const missingProduct = issues.find((i) => i.class === "missing_product"); const [buttonLocationStart, buttonLocationLabel, buttonLocationEnd] = _( // TRANSLATORS: This hint helps users locate the install button. Text inside @@ -163,7 +182,7 @@ const OverviewPageContent = ({ product }) => {
- + {missingProduct ? : }
{ )} - {hasIssues && isReady && ( + {hasIssues && !missingProduct && isReady && ( diff --git a/web/src/components/overview/RegistrationSummary.tsx b/web/src/components/overview/RegistrationSummary.tsx index cca3b41e3b..8d602ed45d 100644 --- a/web/src/components/overview/RegistrationSummary.tsx +++ b/web/src/components/overview/RegistrationSummary.tsx @@ -29,6 +29,7 @@ import { REGISTRATION } from "~/routes/paths"; import { _ } from "~/i18n"; import { useSystem } from "~/hooks/model/system/software"; import { useIssues } from "~/hooks/model/issue"; +import { isEmpty } from "radashi"; const RegistrationMessage = ({ code }: { code?: string }) => { if (!code) { @@ -59,8 +60,8 @@ const RegistrationMessage = ({ code }: { code?: string }) => { */ const Content = () => { const { registration } = useSystem(); - const issues = useIssues("software"); - const hasIssues = issues.find((i) => i.class === "software.missing_registration") !== undefined; + const issues = useIssues("product"); + const hasIssues = !isEmpty(issues); return ( { }); }); }); + }); - it("handles and renders errors returned by the registration server", async () => { + describe("when the registration failed", () => { + beforeEach(() => { mockIssues = [ { scope: "software", @@ -316,12 +332,28 @@ describe("ProductRegistrationPage", () => { description: "Unauthorized code", }, ]; + }); + it("renders errors returned by the registration server", async () => { installerRender(, { withL10n: true }); screen.getByText("Warning alert:"); screen.getByText("Unauthorized code"); }); + + it("allows forgetting the registration information", async () => { + const { user } = installerRender(, { withL10n: true }); + + const button = screen.getByRole("button", { name: "Do not register" }); + await user.click(button); + expect(putConfig).toHaveBeenCalledWith({ + ...mockConfig, + product: { + id: "sle", + mode: "standard", + }, + }); + }); }); describe("when selected product is registrable and already registered", () => { @@ -385,7 +417,7 @@ describe("ProductRegistrationPage", () => { }); }); - describe("if not using a resgistration code", () => { + describe("if not using a registration code", () => { beforeEach(() => { mockRegistrationInfo = { code: "", @@ -429,22 +461,7 @@ describe("ProductRegistrationPage", () => { mockRegistrationInfo = { code: "INTERNAL-USE-ONLY-1234-5678", email: "example@company.test", - addons: [ - { - id: "sle-ha", - version: "16.0", - status: "available", - 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...", - release: "beta", - registration: { - status: "notRegistered", - }, - }, - ], + addons: [addon], }; }); @@ -493,6 +510,50 @@ describe("ProductRegistrationPage", () => { screen.getByText("INTERNAL-USE-ONLY-1234-ad42"); }); }); + + describe("and one of them is registered with an error", () => { + beforeEach(() => { + mockConfig = { + product: { + id: "sle", + mode: "standard", + registrationCode: "INTERNAL-USE-ONLY-1234-5678", + addons: [ + { + id: "sle-ha", + }, + ], + }, + }; + + mockIssues = [ + { + scope: "product", + class: "addon_registration_failed[sle-ha]", + description: "Failed to register the add-on sle-ha", + details: "No subscription with registration code 'jkljkljkl' found", + }, + ]; + }); + + it("allows forgetting the registration information", async () => { + const { user } = installerRender(, { withL10n: true }); + + const button = screen.getByRole("button", { name: "Do not register" }); + await user.click(button); + expect(patchConfig).toHaveBeenCalledWith({ + ...mockConfig, + product: { + id: "sle", + mode: "standard", + registrationCode: "INTERNAL-USE-ONLY-1234-5678", + registrationEmail: undefined, + registrationUrl: undefined, + addons: [], + }, + }); + }); + }); }); }); }); diff --git a/web/src/components/product/ProductRegistrationPage.tsx b/web/src/components/product/ProductRegistrationPage.tsx index e8149f9184..7c7c620e6e 100644 --- a/web/src/components/product/ProductRegistrationPage.tsx +++ b/web/src/components/product/ProductRegistrationPage.tsx @@ -43,6 +43,7 @@ import { Title, } from "@patternfly/react-core"; import { + IssuesAlert, Link, NestedContent, Page, @@ -280,6 +281,17 @@ const RegistrationFormSection = () => { const [loading] = useState(false); const config = useConfig(); const product = useProduct(); + const issues = useIssues("product"); + const registrationIssue = issues.find((i) => i.class === "system_registration_failed"); + + const resetForm = useCallback(() => { + setServer("default"); + setUrl(""); + setKey(""); + setEmail(""); + setProvideKey(false); + setProvideEmail(false); + }, [setServer, setUrl, setKey, setEmail, setProvideKey, setProvideEmail]); useEffect(() => { if (product) { @@ -336,6 +348,19 @@ const RegistrationFormSection = () => { }); }; + const submitNoRegister = async (e: React.SyntheticEvent) => { + e.preventDefault(); + setRequestError(null); + resetForm(); + putConfig({ + ...config, + product: { + id: product.id, + mode: product.mode, + }, + }); + }; + // TODO: adjust texts based of registration "type", mandatory or optional return ( @@ -372,9 +397,16 @@ const RegistrationFormSection = () => { /> - + + + {registrationIssue && ( + + )} + ); @@ -410,7 +442,7 @@ const Extensions = () => { const { registration } = useSystem(); const { product } = useConfig(); const extensions = registration?.addons; - const issues = useIssues("software"); + const issues = useIssues("product"); const registrationCallback = useCallback( (addon: Addon) => { @@ -428,7 +460,21 @@ const Extensions = () => { addons: updatedAddons, }; - patchConfig({ product: updatedProduct }); + return patchConfig({ product: updatedProduct }); + }, + [product], + ); + + const noRegistrationCallback = useCallback( + (id: string) => { + const addons = product?.addons || []; + const updatedAddons = addons.filter((a) => a.id !== id); + return patchConfig({ + product: { + ...product, + addons: updatedAddons, + }, + }); }, [product], ); @@ -447,6 +493,7 @@ const Extensions = () => { issue={issue} isUnique={extensions.filter((e) => e.id === ext.id).length === 1} registrationCallback={registrationCallback} + noRegistrationCallback={noRegistrationCallback} /> ); }); @@ -474,14 +521,18 @@ const RegistrationIssueAlert = ({ issue }: { issue: Issue }) => { export default function ProductRegistrationPage() { const { registration } = useSystem(); - const issues = useIssues("software"); + const issues = useIssues("product"); const registrationIssue = issues.find((i) => i.class === "system_registration_failed"); return ( {!registration && } - {registrationIssue && } + {registrationIssue ? ( + + ) : ( + + )} {!registration ? : } {registration && } diff --git a/web/src/components/product/RegistrationExtension.tsx b/web/src/components/product/RegistrationExtension.tsx index 3dba386544..aec76b8e1a 100644 --- a/web/src/components/product/RegistrationExtension.tsx +++ b/web/src/components/product/RegistrationExtension.tsx @@ -26,6 +26,7 @@ import { Alert, Button, Content, + Flex, Form, FormGroup, Label, @@ -80,12 +81,14 @@ export default function RegistrationExtension({ config, isUnique, registrationCallback, + noRegistrationCallback, issue, }: { extension: AddonInfo; config: Addon; isUnique: boolean; - registrationCallback: Function; + registrationCallback: (addon: Addon) => Promise; + noRegistrationCallback: (id: string) => Promise; issue: Issue | undefined; }) { const [regCode, setRegCode] = useState(config?.registrationCode || ""); @@ -101,12 +104,18 @@ export default function RegistrationExtension({ const submit = async (e: React.SyntheticEvent | undefined) => { e?.preventDefault(); - registrationCallback({ + setLoading(true); + await registrationCallback({ id: extension.id, - registrationCode: regCode, + registrationCode: isEmpty(regCode) ? undefined : regCode, version: isUnique ? undefined : extension.version, }); - setLoading(true); + setLoading(false); + }; + + const submitNoRegister = async (e: React.SyntheticEvent | undefined) => { + e?.preventDefault(); + noRegistrationCallback(extension.id); }; return ( @@ -135,38 +144,39 @@ export default function RegistrationExtension({ )} {isRegistered && } - {!isRegistered && extension.available && !extension.free && ( + {!isRegistered && extension.available && (
{/* // TRANSLATORS: input field label */} - - setRegCode(v)} - /> - + {!extension.free && ( + + setRegCode(v)} + /> + + )} - + + + {issue && ( + + )} +
)} - {!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 diff --git a/web/src/model/system/software.ts b/web/src/model/system/software.ts index 66a8c577ae..3890b3c012 100644 --- a/web/src/model/system/software.ts +++ b/web/src/model/system/software.ts @@ -64,7 +64,6 @@ type RegistrationInfo = { type AddonInfo = { id: string; - status: string; version: string; label: string; available: boolean; @@ -84,4 +83,4 @@ type AddonUnregistered = { status: "notRegistered"; }; -export type { System, Pattern, RegistrationInfo, Repository }; +export type { System, Pattern, AddonInfo, RegistrationInfo, Repository };