Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
78619b1
add api for solving conflicts
jreidinger May 8, 2025
ae9b22b
remove dropped summary functionality
jreidinger May 8, 2025
b679448
add conflicts to dbus API
jreidinger May 9, 2025
f2de6a0
add doc for dbus methods
jreidinger May 9, 2025
6a8d6d1
implement conflicts clients
jreidinger May 9, 2025
2dcebd9
add software conflict web API
jreidinger May 9, 2025
37619f2
fix formatting
jreidinger May 9, 2025
4d43752
fix setting conflicts attr
jreidinger May 12, 2025
b3c3b65
add initial UI
jreidinger May 14, 2025
5353a43
changes from review
jreidinger May 14, 2025
33912e9
remove last error from solver issues as it is misleading
jreidinger May 14, 2025
74a7383
adapt UI as agreed
jreidinger May 14, 2025
3c107c9
try to provide explanation text
jreidinger May 14, 2025
17a6e57
fix access to solver
jreidinger May 15, 2025
469c37f
ui updates
jreidinger May 15, 2025
306c869
fix solver solutions
jreidinger May 15, 2025
8f9a3f0
move conflict changes to proper place
jreidinger May 15, 2025
c8a1650
change visual appearannce of solve conflict link
jreidinger May 15, 2025
c795e3d
use different icon
jreidinger May 15, 2025
1100722
fix prettier
jreidinger May 15, 2025
acf8bb6
fix ruby documentation
jreidinger May 16, 2025
4eb41df
Adapt tests
jreidinger May 16, 2025
e8a4f66
Merge remote-tracking branch 'origin/master' into conflicts
jreidinger May 16, 2025
7c43d95
fix crash when solving the last conflict
jreidinger May 19, 2025
098373a
fix(web): improve text and styling of conflict resolution link
dgdavid May 20, 2025
429d150
refactor(web): move conflict navigation to the top
dgdavid May 20, 2025
e27382b
fix(web): extract conflict-related components and improve markup sema…
dgdavid May 21, 2025
931b752
refactor(web): add tests and keep restructuring internals
dgdavid May 21, 2025
d538188
feat(web): validate solution selection on submit
dgdavid May 21, 2025
baa62e8
fix(web): show multi-conflict info only when needed
dgdavid May 21, 2025
6884dd2
fix(web): improve no conflicts found content
dgdavid May 21, 2025
17fb614
doc(web): add misisng documentation
dgdavid May 21, 2025
dfa0628
fix(web): improve queryKey for software conflicts
dgdavid May 21, 2025
19f2175
fix(web): rename conflict types
dgdavid May 21, 2025
f22ae09
Merge branch 'master' into conflicts
dgdavid May 21, 2025
f3046f2
fix(web): small texts adjustments
dgdavid May 21, 2025
3d039d9
fix(rust): avoid error introduced at merge commit
dgdavid May 21, 2025
8dbf6ee
fix(web): drop not needed probing
dgdavid May 21, 2025
09b5749
fix(web): update reference to conflicts query
dgdavid May 21, 2025
5503415
fix(web): add context for translators
dgdavid May 22, 2025
f8c5b62
fix(web): adjust casing for skip actions
dgdavid May 22, 2025
e8d9f85
fix(web): avoid crashing because missplaced commnet
dgdavid May 22, 2025
7c76232
Update doc/dbus/org.opensuse.Agama.Software1.doc.xml
jreidinger May 22, 2025
30812b0
changes from review
jreidinger May 22, 2025
544314e
fix api documentation
jreidinger May 22, 2025
ddf7970
Merge remote-tracking branch 'origin/master' into conflicts
jreidinger May 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion doc/dbus/bus/org.opensuse.Agama.Software1.bus.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,13 @@
<arg name="result" direction="out" type="b"/>
</method>
<method name="SetUserPatterns">
<arg name="ids" direction="in" type="as"/>
<arg name="add" direction="in" type="as"/>
<arg name="remove" direction="in" type="as"/>
<arg name="wrong" direction="out" type="as"/>
</method>
<method name="SolveConflicts">
<arg name="solutions" direction="in" type="a(uu)"/>
</method>
<method name="ProvisionsSelected">
<arg name="Provisions" direction="in" type="as"/>
<arg name="Result" direction="out" type="ab"/>
Expand All @@ -69,6 +73,7 @@
<method name="Finish">
</method>
<property type="a{sy}" name="SelectedPatterns" access="read"/>
<property type="a(ussa(uss))" name="Conflicts" access="read"/>
</interface>
<interface name="org.opensuse.Agama1.Issues">
<property type="a(ssuu)" name="All" access="read"/>
Expand Down
15 changes: 13 additions & 2 deletions doc/dbus/org.opensuse.Agama.Software1.doc.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,25 @@
-->
<method name="SetUserPatterns">
<!--
List of pattern ids.
List of pattern ids to add.
-->
<arg name="ids" direction="in" type="as"/>
<arg name="add" direction="in" type="as"/>
<!--
List of pattern ids to remove.
-->
<arg name="remove" direction="in" type="as"/>
<!--
Return list of non existing patterns.
Operation was basically cancelled if result is not empty.
-->
<arg name="wrong" direction="out" type="as"/>
</method>
<method name="SolveConflicts">
<!--
List of solutions in format of id of conflict and id of solution.
-->
<arg name="solutions" direction="in" type="a(uu)"/>
</method>
<method name="ProvisionsSelected">
<arg name="Provisions" direction="in" type="as"/>
<arg name="Result" direction="out" type="ab"/>
Expand Down Expand Up @@ -97,5 +107,6 @@
<method name="Finish">
</method>
<property type="a{sy}" name="SelectedPatterns" access="read"/>
<property type="a(ussa(uss))" name="Conflicts" access="read"/>
</interface>
</node>
5 changes: 4 additions & 1 deletion rust/agama-lib/src/http/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::{
manager::InstallationPhase,
network::model::NetworkChange,
progress::Progress,
software::SelectedBy,
software::{model::Conflict, SelectedBy},
storage::{
model::{
dasd::{DASDDevice, DASDFormatSummary},
Expand Down Expand Up @@ -69,6 +69,9 @@ pub enum Event {
SoftwareProposalChanged {
patterns: HashMap<String, SelectedBy>,
},
ConflictsChanged {
conflicts: Vec<Conflict>,
},
QuestionsChanged,
InstallationPhaseChanged {
phase: InstallationPhase,
Expand Down
20 changes: 19 additions & 1 deletion rust/agama-lib/src/software/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
// find current contact information at www.suse.com.

use super::{
model::{Repository, ResolvableType},
model::{Conflict, ConflictSolve, Repository, ResolvableType},
proxies::{ProposalProxy, Software1Proxy},
};
use crate::error::ServiceError;
Expand Down Expand Up @@ -169,6 +169,24 @@ impl<'a> SoftwareClient<'a> {
Ok(patterns)
}

/// returns current list of conflicts
pub async fn get_conflicts(&self) -> Result<Vec<Conflict>, ServiceError> {
let conflicts = self.software_proxy.conflicts().await?;
let conflicts = conflicts
.into_iter()
.map(|c| Conflict::from_dbus(c))
.collect();

Ok(conflicts)
}

/// Sets solutions ( not necessary for all conflicts ) and recompute conflicts
pub async fn solve_conflicts(&self, solutions: Vec<ConflictSolve>) -> Result<(), ServiceError> {
let solutions: Vec<(u32, u32)> = solutions.into_iter().map(|s| s.into()).collect();

Ok(self.software_proxy.solve_conflicts(&solutions).await?)
}

/// Selects patterns by user
pub async fn select_patterns(
&self,
Expand Down
2 changes: 2 additions & 0 deletions rust/agama-lib/src/software/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
// To contact SUSE LLC about this file by physical or electronic mail, you may
// find current contact information at www.suse.com.

mod conflict;
mod license;
mod packages;
mod registration;

pub use conflict::*;
pub use license::*;
pub use packages::*;
pub use registration::*;
104 changes: 104 additions & 0 deletions rust/agama-lib/src/software/model/conflict.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// 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.

use serde::{Deserialize, Serialize};

/// Information about conflict when resolving software
#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ConflictSolve {
/// conflict id
pub conflict_id: u32,
/// selected solution id
pub solution_id: u32,
}

impl From<ConflictSolve> for (u32, u32) {
fn from(solve: ConflictSolve) -> Self {
(solve.conflict_id, solve.solution_id)
}
}

/// Information about possible solution for conflict
#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct Solution {
/// conflict id
pub id: u32,
/// localized description of solution
pub description: String,
/// localized details about solution. Can be missing
pub details: Option<String>,
}

/// Information about conflict when resolving software
#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct Conflict {
/// conflict id
pub id: u32,
/// localized description of conflict
pub description: String,
/// localized details about conflict. Can be missing
pub details: Option<String>,
/// list of possible solutions
pub solutions: Vec<Solution>,
}

impl Solution {
pub fn from_dbus(dbus_solution: (u32, String, String)) -> Self {
let details = dbus_solution.2;
let details = if details.is_empty() {
None
} else {
Some(details)
};

Self {
id: dbus_solution.0,
description: dbus_solution.1,
details,
}
}
}

impl Conflict {
pub fn from_dbus(dbus_conflict: (u32, String, String, Vec<(u32, String, String)>)) -> Self {
let details = dbus_conflict.2;
let details = if details.is_empty() {
None
} else {
Some(details)
};

let solutions = dbus_conflict.3;
let solutions = solutions
.into_iter()
.map(|s| Solution::from_dbus(s))
.collect();

Self {
id: dbus_conflict.0,
description: dbus_conflict.1,
details,
solutions,
}
}
}
8 changes: 8 additions & 0 deletions rust/agama-lib/src/software/proxies/software.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,21 @@ pub trait Software1 {
/// SetUserPatterns method
fn set_user_patterns(&self, add: &[&str], remove: &[&str]) -> zbus::Result<Vec<String>>;

/// SolveConflicts method
fn solve_conflicts(&self, solutions: &[(u32, u32)]) -> zbus::Result<()>;

/// UsedDiskSpace method
fn used_disk_space(&self) -> zbus::Result<String>;

/// ProbeFinished signal
#[zbus(signal)]
fn probe_finished(&self) -> zbus::Result<()>;

/// Conflicts property
#[zbus(property)]
#[allow(clippy::type_complexity)]
fn conflicts(&self) -> zbus::Result<Vec<(u32, String, String, Vec<(u32, String, String)>)>>;

/// SelectedPatterns property
#[zbus(property)]
fn selected_patterns(&self) -> zbus::Result<std::collections::HashMap<String, u8>>;
Expand Down
71 changes: 69 additions & 2 deletions rust/agama-server/src/software/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ use agama_lib::{
product::{proxies::RegistrationProxy, Product, ProductClient},
software::{
model::{
AddonParams, AddonProperties, License, LicenseContent, LicensesRepo, RegistrationError,
RegistrationInfo, RegistrationParams, Repository, ResolvableParams, SoftwareConfig,
AddonParams, AddonProperties, Conflict, ConflictSolve, License, LicenseContent,
LicensesRepo, RegistrationError, RegistrationInfo, RegistrationParams, Repository,
ResolvableParams, SoftwareConfig,
},
proxies::{Software1Proxy, SoftwareProductProxy},
Pattern, SelectedBy, SoftwareClient, UnknownSelectedBy,
Expand Down Expand Up @@ -81,6 +82,10 @@ pub async fn software_streams(dbus: zbus::Connection) -> Result<EventStreams, Er
"patterns_changed",
Box::pin(patterns_changed_stream(dbus.clone()).await?),
),
(
"conflicts_changed",
Box::pin(conflicts_changed_stream(dbus.clone()).await?),
),
(
"product_changed",
Box::pin(product_changed_stream(dbus.clone()).await?),
Expand Down Expand Up @@ -138,6 +143,28 @@ async fn patterns_changed_stream(
Ok(stream)
}

async fn conflicts_changed_stream(
dbus: zbus::Connection,
) -> Result<impl Stream<Item = Event>, Error> {
let proxy = Software1Proxy::new(&dbus).await?;
let stream = proxy
.receive_conflicts_changed()
.await
.then(|change| async move {
if let Ok(conflicts) = change.get().await {
return Some(
conflicts
.into_iter()
.map(|c| Conflict::from_dbus(c))
.collect(),
);
}
None
})
.filter_map(|e| e.map(|conflicts| Event::ConflictsChanged { conflicts }));
Ok(stream)
}

async fn registration_email_changed_stream(
dbus: zbus::Connection,
) -> Result<impl Stream<Item = Event>, Error> {
Expand Down Expand Up @@ -237,6 +264,7 @@ pub async fn software_service(

let router = Router::new()
.route("/patterns", get(patterns))
.route("/conflicts", get(get_conflicts).patch(solve_conflicts))
.route("/repositories", get(repositories))
.route("/products", get(products))
.route("/licenses", get(licenses))
Expand Down Expand Up @@ -311,6 +339,45 @@ async fn repositories(
Ok(Json(repositories))
}

/// Returns the list of conflicts that proposal found.
///
/// * `state`: service state.
#[utoipa::path(
get,
path = "/conflicts",
context_path = "/api/software",
responses(
(status = 200, description = "List of software conflicts", body = Vec<Conflict>),
(status = 400, description = "The D-Bus service could not perform the action")
)
)]
async fn get_conflicts(
State(state): State<SoftwareState<'_>>,
) -> Result<Json<Vec<Conflict>>, Error> {
let conflicts = state.software.get_conflicts().await?;
Ok(Json(conflicts))
}

/// Solve conflicts. Not all conflicts needs to be solved at once.
///
/// * `state`: service state.
#[utoipa::path(
patch,
path = "/conflicts",
context_path = "/api/software",
request_body = Vec<ConflictSolve>,
responses(
(status = 200, description = "Operation success"),
(status = 400, description = "The D-Bus service could not perform the action")
)
)]
async fn solve_conflicts(
State(state): State<SoftwareState<'_>>,
Json(solutions): Json<Vec<ConflictSolve>>,
) -> Result<(), Error> {
Ok(state.software.solve_conflicts(solutions).await?)
}

/// returns registration info
///
/// * `state`: service state.
Expand Down
18 changes: 18 additions & 0 deletions service/lib/agama/dbus/software/manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def initialize(backend, logger)
register_progress_callbacks
register_service_status_callbacks
@selected_patterns = {}
@conflicts = []
end

# List of software related issues, see {DBus::Interfaces::Issues}
Expand Down Expand Up @@ -115,6 +116,12 @@ def issues
[backend.assign_patterns(add, remove)]
end

dbus_reader_attr_accessor :conflicts, "a(ussa(uss))"

dbus_method :SolveConflicts, "in solutions:a(uu)" do |solutions|
backend.proposal.solve_conflicts(solutions)
end

dbus_method :ProvisionsSelected, "in Provisions:as, out Result:ab" do |provisions|
[provisions.map { |p| backend.provision_selected?(p) }]
end
Expand Down Expand Up @@ -203,6 +210,17 @@ def register_callbacks
self.selected_patterns = compute_patterns
end

backend.proposal.on_conflicts_change do |conflicts|
self.conflicts = conflicts.map do |conflict|
[
conflict["id"], conflict["description"], conflict["details"] || "",
conflict["solutions"].map do |solution|
[solution["id"], solution["description"], solution["details"] || ""]
end
]
end
end

backend.on_issues_change { issues_properties_changed }
end

Expand Down
Loading