diff --git a/rust/agama-lib/share/package.json b/rust/agama-lib/share/package.json index 7d5210a4f5..90f7f687f5 100644 --- a/rust/agama-lib/share/package.json +++ b/rust/agama-lib/share/package.json @@ -1,6 +1,6 @@ { "scripts": { - "validate": "ajv compile --spec=draft2019 --verbose --all-errors -r storage.schema.json -r iscsi.schema.json -s profile.schema.json && ajv compile --spec=draft2019 --verbose --all-errors -s storage.model.schema.json" + "validate": "ajv compile --spec=draft2019 --verbose --all-errors -r storage.schema.json -r iscsi.schema.json -r software.schema.json -s profile.schema.json && ajv compile --spec=draft2019 --verbose --all-errors -s storage.model.schema.json" }, "dependencies": { "ajv-cli": "^5.0.0" diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 8ddb120f88..d7f10b68cf 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -200,74 +200,7 @@ } }, "software": { - "title": "Software settings", - "type": "object", - "properties": { - "patterns": { - "anyOf": [ - { "$ref": "#/$defs/patternsArray" }, - { "$ref": "#/$defs/patternsObject" } - ] - }, - "packages": { - "title": "List of packages to install", - "type": "array", - "items": { - "type": "string", - "examples": ["vim"] - } - }, - "onlyRequired": { - "title": "Flag if only minimal hard dependencies should be used in solver", - "type": "boolean" - }, - "extraRepositories": { - "title": "List of user specified repositories that will be used on top of default ones", - "type": "array", - "items": { - "type": "object", - "required": ["alias", "url"], - "properties": { - "alias": { - "title": "alias used for repository. Acting as identifier", - "type": "string" - }, - "url": { - "title": "URL pointing to repository", - "type": "string" - }, - "priority": { - "title": "Repository priority", - "type": "integer" - }, - "name": { - "title": "User visible name. Defaults to alias", - "type": "string" - }, - "productDir": { - "title": "product directory on multi repo DVD. Usually not needed", - "type": "string" - }, - "enabled": { - "title": "If repository should be enabled. Defaults to true. Useful when adding additional repo that should not be immediately use.", - "type": "boolean" - }, - "allowUnsigned": { - "title": "If unsigned repositories are allowed. Mainly useful for repositories that is hand crafted without GPG signature.", - "type": "boolean" - }, - "gpgFingerprints": { - "title": "List of GPG fingerprints that is accepted for this repository. Useful for own repositories with proper GPG signature.", - "type": "array", - "items": { - "type": "string", - "pattern": "^[0-9a-fA-F ]+" - } - } - } - } - } - } + "$ref": "software.schema.json" }, "questions": { "title": "How to handle Agama questions", @@ -1081,31 +1014,6 @@ "type": "string" } } - }, - "patternsArray": { - "title": "List of user-selected patterns to install", - "type": "array", - "items": { - "type": "string", - "examples": ["minimal_base"] - } - }, - "patternsObject": { - "title": "Modifications for the list of user-selected patterns to install", - "type": "object", - "additionalProperties": false, - "properties": { - "add": { - "title": "List of user-selected patterns to add to the list", - "type": "array", - "items": { "type": "string" } - }, - "remove": { - "title": "List of user-selected patterns to remove from the list", - "type": "array", - "items": { "type": "string" } - } - } } } } diff --git a/rust/agama-lib/share/software.schema.json b/rust/agama-lib/share/software.schema.json new file mode 100644 index 0000000000..d615ee2eac --- /dev/null +++ b/rust/agama-lib/share/software.schema.json @@ -0,0 +1,117 @@ +{ + "$comment": "Software configuration", + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://github.com/agama-project/agama/blob/master/rust/agama-lib/share/software.schema.json", + "title": "Config", + "description": "Software configuration.", + "type": "object", + "properties": { + "patterns": { + "anyOf": [ + { + "$ref": "#/$defs/patternsArray" + }, + { + "$ref": "#/$defs/patternsObject" + } + ] + }, + "packages": { + "description": "List of packages to install", + "type": "array", + "items": { + "type": "string", + "examples": [ + "vim" + ] + } + }, + "onlyRequired": { + "description": "Flag if only minimal hard dependencies should be used in solver", + "type": "boolean" + }, + "extraRepositories": { + "description": "List of user specified repositories that will be used on top of default ones", + "type": "array", + "items": { + "$ref": "#/$defs/repository" + } + } + }, + "$defs": { + "patternsArray": { + "description": "List of user-selected patterns to install", + "type": "array", + "items": { + "type": "string", + "examples": [ + "minimal_base" + ] + } + }, + "patternsObject": { + "description": "Modifications for the list of user-selected patterns to install", + "type": "object", + "additionalProperties": false, + "properties": { + "add": { + "description": "List of user-selected patterns to add to the list", + "type": "array", + "items": { + "type": "string" + } + }, + "remove": { + "description": "List of user-selected patterns to remove from the list", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "repository": { + "description": "Packages repository", + "type": "object", + "additionalProperties": false, + "properties": { + "alias": { + "description": "alias used for repository. Acting as identifier", + "type": "string" + }, + "url": { + "description": "URL pointing to repository", + "type": "string" + }, + "priority": { + "description": "Repository priority", + "type": "integer" + }, + "name": { + "description": "User visible name. Defaults to alias", + "type": "string" + }, + "productDir": { + "description": "product directory on multi repo DVD. Usually not needed", + "type": "string" + }, + "enabled": { + "description": "If repository should be enabled. Defaults to true. Useful when adding additional repo that should not be immediately use.", + "type": "boolean" + }, + "allowUnsigned": { + "description": "If unsigned repositories are allowed. Mainly useful for repositories that is hand crafted without GPG signature.", + "type": "boolean" + }, + "gpgFingerprints": { + "description": "List of GPG fingerprints that is accepted for this repository. Useful for own repositories with proper GPG signature.", + "type": "array", + "items": { + "type": "string", + "pattern": "^[0-9a-fA-F ]+" + } + } + } + } + } +} diff --git a/rust/agama-software/src/model/packages.rs b/rust/agama-software/src/model/packages.rs index d04ff4c640..00dd263abc 100644 --- a/rust/agama-software/src/model/packages.rs +++ b/rust/agama-software/src/model/packages.rs @@ -39,7 +39,16 @@ impl Resolvable { /// Software resolvable type (package or pattern). #[derive( - Clone, Copy, Debug, Deserialize, Serialize, strum::Display, utoipa::ToSchema, PartialEq, + Clone, + Copy, + Debug, + Deserialize, + Serialize, + strum::Display, + utoipa::ToSchema, + PartialEq, + Eq, + Hash, )] #[strum(serialize_all = "camelCase")] #[serde(rename_all = "camelCase")] diff --git a/rust/agama-software/src/model/state.rs b/rust/agama-software/src/model/state.rs index 151b87a227..e0b1672053 100644 --- a/rust/agama-software/src/model/state.rs +++ b/rust/agama-software/src/model/state.rs @@ -22,6 +22,8 @@ //! configuration and a mechanism to build it starting from the product //! definition, the user configuration, etc. +use std::collections::HashMap; + use agama_utils::{ api::software::{Config, PatternsConfig, RepositoryConfig, SystemInfo}, products::{ProductSpec, UserPattern}, @@ -40,10 +42,22 @@ use crate::{model::software_selection::SoftwareSelection, Resolvable, Resolvable pub struct SoftwareState { pub product: String, pub repositories: Vec, - pub resolvables: Vec, + pub resolvables: ResolvablesState, pub options: SoftwareOptions, } +impl SoftwareState { + /// Builds an empty software state for the given product. + pub fn new(product: &str) -> Self { + SoftwareState { + product: product.to_string(), + repositories: Default::default(), + resolvables: Default::default(), + options: Default::default(), + } + } +} + /// Builder to create a [SoftwareState] struct from different sources. /// /// At this point it uses the following sources: @@ -144,30 +158,37 @@ impl<'a> SoftwareStateBuilder<'a> { if let Some(patterns) = &software.patterns { match patterns { PatternsConfig::PatternsList(list) => { - // Replaces the list, keeping only the non-optional elements. - state.resolvables.retain(|p| !p.reason.is_optional()); - state.resolvables.extend(list.iter().map(|n| { - SelectedResolvable::new(n, ResolvableType::Pattern, SelectedReason::User) - })); + state.resolvables.reset(); + for name in list.iter() { + state.resolvables.add_or_replace( + name, + ResolvableType::Pattern, + ResolvableSelection::Selected, + ) + } } PatternsConfig::PatternsMap(map) => { - // Adds or removes elements to the list + let mut list: Vec<(&str, ResolvableSelection)> = vec![]; + if let Some(add) = &map.add { - state.resolvables.extend(add.iter().map(|n| { - SelectedResolvable::new( - n, - ResolvableType::Pattern, - SelectedReason::User, - ) - })); + list.extend( + add.iter() + .map(|n| (n.as_str(), ResolvableSelection::Selected)), + ); } if let Some(remove) = &map.remove { - // NOTE: should we notify when a user wants to remove a - // pattern which is not optional? - state.resolvables.retain(|p| { - !(p.reason.is_optional() && remove.contains(&p.resolvable.name)) - }); + list.extend( + remove + .iter() + .map(|n| (n.as_str(), ResolvableSelection::Removed)), + ); + } + + for (name, selection) in list.into_iter() { + state + .resolvables + .add_or_replace(name, ResolvableType::Pattern, selection); } } } @@ -180,13 +201,12 @@ impl<'a> SoftwareStateBuilder<'a> { /// It adds the software selection from Agama modules. fn add_selection(&self, state: &mut SoftwareState, selection: &SoftwareSelection) { - let resolvables = selection.resolvables().map(|r| { - SelectedResolvable::new_with_resolvable( - &r, - SelectedReason::Installer { optional: false }, - ) - }); - state.resolvables.extend(resolvables) + for resolvable in selection.resolvables() { + state.resolvables.add_or_replace_resolvable( + &resolvable, + ResolvableSelection::AutoSelected { optional: false }, + ); + } } fn from_product_spec(&self) -> SoftwareState { @@ -206,40 +226,34 @@ impl<'a> SoftwareStateBuilder<'a> { }) .collect(); - let mut resolvables: Vec = software - .mandatory_patterns - .iter() - .map(|p| { - SelectedResolvable::new( - p, - ResolvableType::Pattern, - SelectedReason::Installer { optional: false }, - ) - }) - .collect(); + let mut resolvables = ResolvablesState::default(); + for pattern in &software.mandatory_patterns { + resolvables.add_or_replace( + pattern, + ResolvableType::Pattern, + ResolvableSelection::AutoSelected { optional: false }, + ); + } - resolvables.extend(software.optional_patterns.iter().map(|p| { - SelectedResolvable::new( - p, + for pattern in &software.optional_patterns { + resolvables.add_or_replace( + pattern, ResolvableType::Pattern, - SelectedReason::Installer { optional: true }, - ) - })); - - resolvables.extend(software.user_patterns.iter().filter_map(|p| match p { - UserPattern::Plain(_) => None, - UserPattern::Preselected(pattern) => { - if pattern.selected { - Some(SelectedResolvable::new( - &pattern.name, + ResolvableSelection::AutoSelected { optional: true }, + ); + } + + for pattern in &software.user_patterns { + if let UserPattern::Preselected(user_pattern) = pattern { + if user_pattern.selected { + resolvables.add_or_replace( + &user_pattern.name, ResolvableType::Pattern, - SelectedReason::Installer { optional: true }, - )) - } else { - None + ResolvableSelection::Selected, + ) } } - })); + } SoftwareState { product: software.base_product.clone(), @@ -296,40 +310,90 @@ impl From<&agama_utils::api::software::Repository> for Repository { } } -/// Defines a resolvable to be selected. -#[derive(Debug, PartialEq)] -pub struct SelectedResolvable { - /// Resolvable name. - pub resolvable: Resolvable, - /// The reason to select the resolvable. - pub reason: SelectedReason, -} +/// Holds states for resolvables. +/// +/// Check the [ResolvableSelection] enum for possible states. +#[derive(Debug, Default)] +pub struct ResolvablesState(HashMap<(String, ResolvableType), ResolvableSelection>); + +impl ResolvablesState { + /// Add or replace the state for the resolvable with the given name and type. + /// + /// If the resolvable is auto selected and mandatory, it does not update the + /// state. + /// + /// * `name`: resolvable name. + /// * `r#type`: resolvable type. + /// * `selection`: selection state. + pub fn add_or_replace( + &mut self, + name: &str, + r#type: ResolvableType, + selection: ResolvableSelection, + ) { + if let Some(entry) = self.0.get(&(name.to_string(), r#type)) { + if let ResolvableSelection::AutoSelected { optional: _ } = entry { + tracing::debug!("Could not modify the {name} state because it is mandatory."); + return; + } + } + self.0.insert((name.to_string(), r#type), selection); + } -impl SelectedResolvable { - pub fn new(name: &str, r#type: ResolvableType, reason: SelectedReason) -> Self { - Self::new_with_resolvable(&Resolvable::new(name, r#type), reason) + /// Add or replace the state for the given resolvable. + /// + /// If the resolvable is auto selected and mandatory, it does not update the + /// state. + /// + /// * `resolvable`: resolvable. + /// * `selection`: selection state. + pub fn add_or_replace_resolvable( + &mut self, + resolvable: &Resolvable, + selection: ResolvableSelection, + ) { + self.add_or_replace(&resolvable.name, resolvable.r#type, selection); } - pub fn new_with_resolvable(resolvable: &Resolvable, reason: SelectedReason) -> Self { - Self { - resolvable: resolvable.clone(), - reason, - } + /// Reset the list of resolvables. + /// + /// The mandatory resolvables are preserved. + pub fn reset(&mut self) { + self.0.retain(|_, selection| match selection { + ResolvableSelection::AutoSelected { optional: false } => true, + _ => false, + }); + } + + /// Turns the list of resolvables into a vector. + /// + /// FIXME: return an interator instead. + pub fn to_vec(&self) -> Vec<(String, ResolvableType, ResolvableSelection)> { + let mut vector: Vec<_> = self + .0 + .iter() + .map(|(key, selection)| (key.0.to_string(), key.1, *selection)) + .collect(); + vector.sort_by(|a, b| a.0.cmp(&b.0)); + vector } } -/// Defines the reason to select a resolvable. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum SelectedReason { +/// Define the wanted resolvable selection state. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ResolvableSelection { /// Selected by the user. - User, + Selected, /// Selected by the installer itself and whether it is optional or not. - Installer { optional: bool }, + AutoSelected { optional: bool }, + /// Removed by the user. It allows to remove resolvables that might be auto-selected + /// by the solver (e.g., recommended patterns). + Removed, } -impl SelectedReason { +impl ResolvableSelection { pub fn is_optional(&self) -> bool { - if let SelectedReason::Installer { optional } = self { + if let ResolvableSelection::AutoSelected { optional } = self { return *optional; } @@ -337,13 +401,14 @@ impl SelectedReason { } } -impl From for zypp_agama::ResolvableSelected { - fn from(value: SelectedReason) -> Self { +impl From for zypp_agama::ResolvableSelected { + fn from(value: ResolvableSelection) -> Self { match value { - SelectedReason::User => zypp_agama::ResolvableSelected::User, - SelectedReason::Installer { optional: _ } => { + ResolvableSelection::Selected => zypp_agama::ResolvableSelected::User, + ResolvableSelection::AutoSelected { optional: _ } => { zypp_agama::ResolvableSelected::Installation } + ResolvableSelection::Removed => zypp_agama::ResolvableSelected::Not, } } } @@ -369,7 +434,7 @@ mod tests { use crate::model::{ packages::ResolvableType, - state::{SelectedReason, SelectedResolvable, SoftwareStateBuilder}, + state::{ResolvableSelection, SoftwareStateBuilder}, }; fn build_user_config(patterns: Option) -> Config { @@ -423,17 +488,17 @@ mod tests { assert_eq!(state.product, "openSUSE".to_string()); assert_eq!( - state.resolvables, + state.resolvables.to_vec(), vec![ - SelectedResolvable::new( - "enhanced_base", + ( + "enhanced_base".to_string(), ResolvableType::Pattern, - SelectedReason::Installer { optional: false } + ResolvableSelection::AutoSelected { optional: false } ), - SelectedResolvable::new( - "selinux", + ( + "selinux".to_string(), ResolvableType::Pattern, - SelectedReason::Installer { optional: true } + ResolvableSelection::Selected, ), ] ); @@ -471,19 +536,23 @@ mod tests { .with_config(&config) .build(); assert_eq!( - state.resolvables, + state.resolvables.to_vec(), vec![ - SelectedResolvable::new( - "enhanced_base", + ( + "enhanced_base".to_string(), + ResolvableType::Pattern, + ResolvableSelection::AutoSelected { optional: false } + ), + ( + "gnome".to_string(), ResolvableType::Pattern, - SelectedReason::Installer { optional: false } + ResolvableSelection::Selected ), - SelectedResolvable::new( - "selinux", + ( + "selinux".to_string(), ResolvableType::Pattern, - SelectedReason::Installer { optional: true } + ResolvableSelection::Selected ), - SelectedResolvable::new("gnome", ResolvableType::Pattern, SelectedReason::User) ] ); } @@ -501,12 +570,19 @@ mod tests { .with_config(&config) .build(); assert_eq!( - state.resolvables, - vec![SelectedResolvable::new( - "enhanced_base", - ResolvableType::Pattern, - SelectedReason::Installer { optional: false } - ),] + state.resolvables.to_vec(), + vec![ + ( + "enhanced_base".to_string(), + ResolvableType::Pattern, + ResolvableSelection::AutoSelected { optional: false } + ), + ( + "selinux".to_string(), + ResolvableType::Pattern, + ResolvableSelection::Removed + ) + ] ); } @@ -523,17 +599,17 @@ mod tests { .with_config(&config) .build(); assert_eq!( - state.resolvables, + state.resolvables.to_vec(), vec![ - SelectedResolvable::new( - "enhanced_base", + ( + "enhanced_base".to_string(), ResolvableType::Pattern, - SelectedReason::Installer { optional: false } + ResolvableSelection::AutoSelected { optional: false } ), - SelectedResolvable::new( - "selinux", + ( + "selinux".to_string(), ResolvableType::Pattern, - SelectedReason::Installer { optional: true } + ResolvableSelection::Selected ) ] ); @@ -549,14 +625,18 @@ mod tests { .with_config(&config) .build(); assert_eq!( - state.resolvables, + state.resolvables.to_vec(), vec![ - SelectedResolvable::new( - "enhanced_base", + ( + "enhanced_base".to_string(), ResolvableType::Pattern, - SelectedReason::Installer { optional: false } + ResolvableSelection::AutoSelected { optional: false } ), - SelectedResolvable::new("gnome", ResolvableType::Pattern, SelectedReason::User,) + ( + "gnome".to_string(), + ResolvableType::Pattern, + ResolvableSelection::Selected, + ) ] ); } diff --git a/rust/agama-software/src/zypp_server.rs b/rust/agama-software/src/zypp_server.rs index dfea008d2f..aab214c658 100644 --- a/rust/agama-software/src/zypp_server.rs +++ b/rust/agama-software/src/zypp_server.rs @@ -40,6 +40,8 @@ use zypp_agama::{errors::ZyppResult, ZyppError}; use crate::{ callbacks, model::state::{self, SoftwareState}, + state::ResolvableSelection, + Resolvable, ResolvableType, }; const GPG_KEYS: &str = "/usr/lib/rpm/gnupg/keys/gpg-*"; @@ -227,14 +229,10 @@ impl ZyppServer { }) .collect(); - let state = SoftwareState { - // FIXME: read the real product. It is not a problem because it is replaced - // later. - product: "SLES".to_string(), - repositories, - resolvables: vec![], - options: Default::default(), - }; + // FIXME: read the real product. It is not a problem because it is replaced + // later. + let mut state = SoftwareState::new("SLES"); + state.repositories = repositories; Ok(state) } @@ -338,28 +336,28 @@ impl ZyppServer { Issue::new("software.select_product", &message).with_details(&error.to_string()), ); } - for resolvable_state in &state.resolvables { - let resolvable = &resolvable_state.resolvable; - let result = zypp.select_resolvable( - &resolvable.name, - resolvable.r#type.into(), - resolvable_state.reason.into(), - ); - - if let Err(error) = result { - if resolvable_state.reason.is_optional() { - tracing::info!( - "Could not select '{}' but it is optional.", - &resolvable.name - ); - } else { - let message = format!("Could not select '{}'", &resolvable.name); - issues.push( - Issue::new("software.select_resolvable", &message) - .with_details(&error.to_string()), - ); + for (name, r#type, selection) in &state.resolvables.to_vec() { + match selection { + ResolvableSelection::AutoSelected { optional } => { + issues.append(&mut self.select_resolvable( + &zypp, + name, + *r#type, + zypp_agama::ResolvableSelected::Installation, + *optional, + )); } - } + ResolvableSelection::Selected => { + issues.append(&mut self.select_resolvable( + &zypp, + name, + *r#type, + zypp_agama::ResolvableSelected::User, + false, + )); + } + ResolvableSelection::Removed => self.unselect_resolvable(&zypp, name, *r#type), + }; } _ = progress.cast(progress::message::Finish::new(Scope::Software)); @@ -375,6 +373,39 @@ impl ZyppServer { Ok(()) } + fn select_resolvable( + &self, + zypp: &zypp_agama::Zypp, + name: &str, + r#type: ResolvableType, + reason: zypp_agama::ResolvableSelected, + optional: bool, + ) -> Vec { + let mut issues = vec![]; + let result = zypp.select_resolvable(name, r#type.into(), reason); + + if let Err(error) = result { + if optional { + tracing::info!("Could not select '{}' but it is optional.", name); + } else { + let message = format!("Could not select '{}'", name); + issues.push( + Issue::new("software.select_resolvable", &message) + .with_details(&error.to_string()), + ); + } + } + issues + } + + fn unselect_resolvable(&self, zypp: &zypp_agama::Zypp, name: &str, r#type: ResolvableType) { + if let Err(error) = + zypp.unselect_resolvable(name, r#type.into(), zypp_agama::ResolvableSelected::User) + { + tracing::info!("Could not unselect '{name}': {error}"); + } + } + fn finish( &mut self, zypp: &zypp_agama::Zypp, @@ -471,17 +502,29 @@ impl ZyppServer { .map(|p| p.name()) .collect(); + let preselected_patterns: Vec<_> = product + .software + .user_patterns + .iter() + .filter(|p| p.preselected()) + .map(|p| p.name()) + .collect(); + let patterns = zypp.patterns_info(pattern_names)?; let patterns = patterns .into_iter() - .map(|p| Pattern { - name: p.name, - category: p.category, - description: p.description, - icon: p.icon, - summary: p.summary, - order: p.order, + .map(|p| { + let preselected = preselected_patterns.contains(&p.name.as_str()); + Pattern { + name: p.name, + category: p.category, + description: p.description, + icon: p.icon, + summary: p.summary, + order: p.order, + preselected, + } }) .collect(); Ok(patterns) diff --git a/rust/agama-software/tests/zypp_server.rs b/rust/agama-software/tests/zypp_server.rs index 18cef66800..a863ed4432 100644 --- a/rust/agama-software/tests/zypp_server.rs +++ b/rust/agama-software/tests/zypp_server.rs @@ -113,12 +113,8 @@ async fn test_start_zypp_server() { enabled: true, }; - let software_state = SoftwareState { - product: "test_product".to_string(), - repositories: vec![repo_s], - resolvables: vec![], - options: Default::default(), - }; + let mut software_state = SoftwareState::new("test_product"); + software_state.repositories = vec![repo_s]; client .send(SoftwareAction::Write { diff --git a/rust/agama-utils/src/api/software/system_info.rs b/rust/agama-utils/src/api/software/system_info.rs index 27abf8f7dc..a2e7398274 100644 --- a/rust/agama-utils/src/api/software/system_info.rs +++ b/rust/agama-utils/src/api/software/system_info.rs @@ -62,6 +62,8 @@ pub struct Pattern { pub summary: String, /// Pattern order pub order: String, + /// Whether the pattern is selected by default + pub preselected: bool, } /// Addon registration diff --git a/rust/agama-utils/src/products.rs b/rust/agama-utils/src/products.rs index 53d8608646..80fc25eb0f 100644 --- a/rust/agama-utils/src/products.rs +++ b/rust/agama-utils/src/products.rs @@ -193,12 +193,21 @@ pub enum UserPattern { } impl UserPattern { + /// Pattern name. pub fn name(&self) -> &str { match self { UserPattern::Plain(name) => name, UserPattern::Preselected(pattern) => &pattern.name, } } + + /// Whether the pattern is preselected. + pub fn preselected(&self) -> bool { + match self { + UserPattern::Plain(_) => false, + UserPattern::Preselected(pattern) => pattern.selected, + } + } } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] diff --git a/rust/package/agama.spec b/rust/package/agama.spec index 94a19ae49b..49d2ffaa73 100644 --- a/rust/package/agama.spec +++ b/rust/package/agama.spec @@ -245,6 +245,7 @@ echo $PATH %dir %{_datadir}/agama/schema %{_datadir}/agama/schema/iscsi.schema.json %{_datadir}/agama/schema/profile.schema.json +%{_datadir}/agama/schema/software.schema.json %{_datadir}/agama/schema/storage.schema.json %{_datadir}/agama/schema/storage.model.schema.json diff --git a/web/src/App.tsx b/web/src/App.tsx index d41a455684..04807a3eab 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -27,10 +27,9 @@ import { useSystemChanges } from "~/hooks/model/system"; import { useProposalChanges } from "~/hooks/model/proposal"; import { useIssuesChanges } from "~/hooks/model/issue"; import { useProduct } from "~/hooks/model/config"; -import { ROOT, PRODUCT } from "~/routes/paths"; +import { ROOT } from "~/routes/paths"; import { useQueryClient } from "@tanstack/react-query"; import AlertOutOfSync from "~/components/core/AlertOutOfSync"; -import { isEmpty } from "radashi"; /** * Content guard and flow control component. @@ -42,7 +41,6 @@ const Content = () => { const location = useLocation(); const product = useProduct(); const { progresses, stage } = useStatus(); - const isBusy = !isEmpty(progresses); console.log("App Content component", { progresses, @@ -61,11 +59,6 @@ const Content = () => { return ; } - if (product?.id === undefined && !isBusy && location.pathname !== PRODUCT.root) { - console.log("Navigating to the product selection page"); - return ; - } - return ( <> {/* So far, only the storage backend is able to detect external changes.*/} diff --git a/web/src/components/layout/Sidebar.tsx b/web/src/components/layout/Sidebar.tsx index 310cb158ed..85a8bc0ebe 100644 --- a/web/src/components/layout/Sidebar.tsx +++ b/web/src/components/layout/Sidebar.tsx @@ -39,6 +39,10 @@ const MainNavigation = (): React.ReactNode => { const product = useProduct(); const location = useLocation(); + if (!product) { + return null; + } + const links = rootRoutes().map((route) => { const { path, handle: data } = route; if (!data) return null; diff --git a/web/src/components/overview/OverviewPage.tsx b/web/src/components/overview/OverviewPage.tsx index 0852b55ac6..3ebae8a41f 100644 --- a/web/src/components/overview/OverviewPage.tsx +++ b/web/src/components/overview/OverviewPage.tsx @@ -27,8 +27,17 @@ import L10nSection from "./L10nSection"; import StorageSection from "./StorageSection"; import SoftwareSection from "./SoftwareSection"; import { _ } from "~/i18n"; +import { PRODUCT } from "~/routes/paths"; +import { useProduct } from "~/hooks/model/config"; +import { Navigate } from "react-router"; export default function OverviewPage() { + const product = useProduct(); + + if (!product) { + return ; + } + return ( diff --git a/web/src/components/overview/SoftwareSection.tsx b/web/src/components/overview/SoftwareSection.tsx index dee9e96906..e4950398b5 100644 --- a/web/src/components/overview/SoftwareSection.tsx +++ b/web/src/components/overview/SoftwareSection.tsx @@ -31,6 +31,11 @@ import xbytes from "xbytes"; export default function SoftwareSection(): React.ReactNode { const system = useSystem(); const proposal = useProposal(); + + if (!proposal) { + return null; + } + const usedSpace = xbytes(proposal.usedSpace * 1024); if (isEmpty(proposal.patterns)) return; diff --git a/web/src/components/product/ProductSelectionPage.tsx b/web/src/components/product/ProductSelectionPage.tsx index acdeefe56e..bca2e6fe33 100644 --- a/web/src/components/product/ProductSelectionPage.tsx +++ b/web/src/components/product/ProductSelectionPage.tsx @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Bullseye, Button, @@ -41,8 +41,6 @@ import { useNavigate } from "react-router"; import { Page } from "~/components/core"; import pfTextStyles from "@patternfly/react-styles/css/utilities/Text/text"; import pfRadioStyles from "@patternfly/react-styles/css/components/Radio/radio"; -// import { PATHS } from "~/router"; -import { Product } from "~/types/software"; import { isEmpty } from "radashi"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; @@ -51,6 +49,8 @@ import LicenseDialog from "./LicenseDialog"; import { useProduct } from "~/hooks/model/config"; import { useSystem } from "~/hooks/model/system"; import { patchConfig } from "~/api"; +import { ROOT } from "~/routes/paths"; +import { Product } from "~/model/system"; const ResponsiveGridItem = ({ children }) => ( @@ -110,7 +110,7 @@ const BackLink = () => { }; function ProductSelectionPage() { - // const registration = useRegistration(); + const navigate = useNavigate(); const { products } = useSystem(); const selectedProduct = useProduct(); const [nextProduct, setNextProduct] = useState(selectedProduct); @@ -118,16 +118,24 @@ function ProductSelectionPage() { // because it's a singleProduct iso. const [licenseAccepted, setLicenseAccepted] = useState(!!selectedProduct); const [showLicense, setShowLicense] = useState(false); - const [isLoading, setIsLoading] = useState(false); + const [isWaiting, setIsWaiting] = useState(false); // if (registration?.registered && selectedProduct) return ; + useEffect(() => { + if (!isWaiting) return; + + if (selectedProduct?.id === nextProduct?.id) { + navigate(ROOT.root); + } + }, [isWaiting, navigate, nextProduct, selectedProduct]); + const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (nextProduct) { patchConfig({ product: { id: nextProduct.id } }); - setIsLoading(true); + setIsWaiting(true); } }; @@ -204,14 +212,10 @@ function ProductSelectionPage() { - + {_("Select")} - {selectedProduct && !isLoading && } + {selectedProduct && } diff --git a/web/src/components/software/SoftwarePage.test.tsx b/web/src/components/software/SoftwarePage.test.tsx index 47a15c9cfe..4018b9a602 100644 --- a/web/src/components/software/SoftwarePage.test.tsx +++ b/web/src/components/software/SoftwarePage.test.tsx @@ -32,16 +32,16 @@ jest.mock("~/components/product/ProductRegistrationAlert", () => () => (
ProductRegistrationAlert Mock
)); -jest.mock("~/queries/issues", () => ({ +jest.mock("~/hooks/model/issue", () => ({ useIssues: () => [], })); -jest.mock("~/queries/software", () => ({ - usePatterns: () => testingPatterns, - useSoftwareProposal: () => testingProposal, - useSoftwareProposalChanges: jest.fn(), - useRepositories: () => [], - useRepositoryMutation: () => ({ mutate: jest.fn() }), +jest.mock("~/hooks/model/proposal/software", () => ({ + useProposal: () => testingProposal, +})); + +jest.mock("~/hooks/model/system/software", () => ({ + useSystem: () => ({ patterns: testingPatterns }), })); describe("SoftwarePage", () => { @@ -52,14 +52,14 @@ describe("SoftwarePage", () => { screen.getByText("YaST Desktop Utilities"); screen.getByText("Multimedia"); screen.getAllByText(/Office software/); - expect(screen.queryByText("KDE")).toBeNull(); - expect(screen.queryByText("XFCE")).toBeNull(); + expect(screen.queryByText(/KDE/)).toBeNull(); + expect(screen.queryByText(/XFCE/)).toBeNull(); expect(screen.queryByText("YaST Server Utilities")).toBeNull(); }); it("renders amount of size selected product and patterns will need", () => { installerRender(); - screen.getByText("Installation will take 4.6 GiB."); + screen.getByText("Installation will take 4.60 GiB."); }); it("renders a button for navigating to patterns selection", () => { diff --git a/web/src/components/software/SoftwarePage.tsx b/web/src/components/software/SoftwarePage.tsx index 06181d8655..db2ddea048 100644 --- a/web/src/components/software/SoftwarePage.tsx +++ b/web/src/components/software/SoftwarePage.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023-2024] SUSE LLC + * Copyright (c) [2023-2025] SUSE LLC * * All Rights Reserved. * @@ -37,21 +37,26 @@ import { import { Link, Page, IssuesAlert } from "~/components/core"; import UsedSize from "./UsedSize"; import { useIssues } from "~/hooks/model/issue"; -import { - usePatterns, - useSoftwareProposal, - useSoftwareProposalChanges, - useRepositoryMutation, -} from "~/queries/software"; -import { Pattern, SelectedBy } from "~/types/software"; +import { useProposal } from "~/hooks/model/proposal/software"; +import { useSystem } from "~/hooks/model/system/software"; import { _ } from "~/i18n"; import { SOFTWARE as PATHS } from "~/routes/paths"; +import xbytes from "xbytes"; +import { PatternsSelection, SelectedBy } from "~/model/proposal/software"; +import { Pattern } from "~/model/system/software"; +import { isEmpty } from "radashi"; /** * List of selected patterns. */ -const SelectedPatternsList = ({ patterns }: { patterns: Pattern[] }): React.ReactNode => { - const selected = patterns.filter((p) => p.selectedBy !== SelectedBy.NONE); +const SelectedPatternsList = ({ + patterns, + selection, +}: { + patterns: Pattern[]; + selection: PatternsSelection; +}): React.ReactNode => { + const selected = patterns.filter((p) => selection[p.name] !== SelectedBy.NONE); if (selected.length === 0) { return <>{_("No additional software was selected.")}; @@ -72,7 +77,7 @@ const SelectedPatternsList = ({ patterns }: { patterns: Pattern[] }): React.Reac ); }; -const SelectedPatterns = ({ patterns }): React.ReactNode => ( +const SelectedPatterns = ({ patterns, selection }): React.ReactNode => ( ( } > - + ); @@ -132,24 +137,28 @@ const ReloadSection = ({ * Software page component */ function SoftwarePage(): React.ReactNode { + const { patterns } = useSystem(); + const proposal = useProposal(); const issues = useIssues("software"); - const proposal = useSoftwareProposal(); - const patterns = usePatterns(); - // FIXME: temporarily disabled, the API end point is not implemented yet - const repos = []; // useRepositories(); - const [loading, setLoading] = useState(false); - const { mutate: probe } = useRepositoryMutation(() => setLoading(false)); - useSoftwareProposalChanges(); + if (!proposal) { + return null; + } + + // FIXME: temporarily disabled, the API end point is not implemented yet + const repos = []; // useRepositories(); + const usedSpace = proposal.usedSpace + ? xbytes(proposal.usedSpace * 1024, { iec: true }) + : undefined; // Selected patterns section should fill the full width in big screen too when // there is no information for rendering the Proposal Size section. - const selectedPatternsXlSize = proposal.size ? 6 : 12; + const selectedPatternsXlSize = usedSpace ? 6 : 12; const startProbing = () => { setLoading(true); - probe(); + // TODO: probe(); }; const showReposAlert = repos.some((r) => !r.loaded); @@ -169,12 +178,16 @@ function SoftwarePage(): React.ReactNode {
)} - {patterns.length === 0 ? : } + {isEmpty(proposal.patterns) ? ( + + ) : ( + + )} - {proposal.size && ( + {usedSpace && ( - + )} diff --git a/web/src/components/software/SoftwarePatternsSelection.test.tsx b/web/src/components/software/SoftwarePatternsSelection.test.tsx index 0d89580cbf..6f3ad48f3e 100644 --- a/web/src/components/software/SoftwarePatternsSelection.test.tsx +++ b/web/src/components/software/SoftwarePatternsSelection.test.tsx @@ -24,16 +24,29 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import testingPatterns from "./patterns.test.json"; +import testingProposal from "./proposal.test.json"; import SoftwarePatternsSelection from "./SoftwarePatternsSelection"; +import { patchConfig } from "~/api"; const onConfigMutationMock = { mutate: jest.fn() }; +jest.mock("~/hooks/model/system/software", () => ({ + useSystem: () => ({ patterns: testingPatterns }), +})); + +jest.mock("~/hooks/model/proposal/software", () => ({ + useProposal: () => ({ patterns: testingProposal.patterns }), +})); + +jest.mock("~/api", () => ({ + patchConfig: jest.fn(), +})); + jest.mock("~/components/product/ProductRegistrationAlert", () => () => (
ProductRegistrationAlert Mock
)); jest.mock("~/queries/software", () => ({ - usePatterns: () => testingPatterns, useConfigMutation: () => onConfigMutationMock, })); @@ -43,6 +56,7 @@ describe("SoftwarePatternsSelection", () => { const headings = screen.getAllByRole("heading", { level: 3 }); const headingsText = headings.map((node) => node.textContent); expect(headingsText).toEqual([ + "Patterns", "Graphical Environments", "Base Technologies", "Desktop Functions", @@ -66,7 +80,7 @@ describe("SoftwarePatternsSelection", () => { const headings = screen.getAllByRole("heading", { level: 3 }); const headingsText = headings.map((node) => node.textContent); - expect(headingsText).toEqual(["Desktop Functions"]); + expect(headingsText).toEqual(["Patterns", "Desktop Functions"]); const desktopGroup = screen.getByRole("list", { name: "Desktop Functions" }); expect(within(desktopGroup).queryByText(/Multimedia$/)).toBeInTheDocument(); @@ -100,8 +114,6 @@ describe("SoftwarePatternsSelection", () => { expect(basisCheckbox).toBeChecked(); await user.click(basisCheckbox); - expect(onConfigMutationMock.mutate).toHaveBeenCalledWith({ - patterns: expect.objectContaining({ yast2_basis: false }), - }); + expect(patchConfig).toHaveBeenCalled(); }); }); diff --git a/web/src/components/software/SoftwarePatternsSelection.tsx b/web/src/components/software/SoftwarePatternsSelection.tsx index 93c7732f29..cbcd56c75f 100644 --- a/web/src/components/software/SoftwarePatternsSelection.tsx +++ b/web/src/components/software/SoftwarePatternsSelection.tsx @@ -34,10 +34,13 @@ import { Content, } from "@patternfly/react-core"; import { Page } from "~/components/core"; -import { useConfigMutation, usePatterns } from "~/queries/software"; -import { Pattern, SelectedBy } from "~/types/software"; import { _ } from "~/i18n"; import a11yStyles from "@patternfly/react-styles/css/utilities/Accessibility/accessibility"; +import { useSystem } from "~/hooks/model/system/software"; +import { Pattern } from "~/model/system/software"; +import { SelectedBy } from "~/model/proposal/software"; +import { useProposal } from "~/hooks/model/proposal/software"; +import { patchConfig } from "~/api"; /** * PatternGroups mapping "group name" => list of patterns @@ -102,21 +105,25 @@ const NoMatches = (): React.ReactNode => {_("None of the patterns match the f * Pattern selector component */ function SoftwarePatternsSelection(): React.ReactNode { - const patterns = usePatterns(); - const config = useConfigMutation(); + const { patterns } = useSystem(); + const { patterns: selection } = useProposal(); const [searchValue, setSearchValue] = useState(""); - const onToggle = (name: string) => { - const selected = patterns - .filter((p) => p.selectedBy === SelectedBy.USER) - .reduce((all, p) => { - all[p.name] = true; - return all; - }, {}); - const pattern = patterns.find((p) => p.name === name); - selected[name] = pattern.selectedBy === SelectedBy.NONE; - - config.mutate({ patterns: selected }); + const onToggle = (name: string, selected: boolean) => { + const add = patterns + .filter((p) => selection[p.name] === SelectedBy.USER && p.name !== name) + .map((p) => p.name); + const remove = patterns + .filter((p) => selection[p.name] === SelectedBy.NONE && p.name !== name) + .map((p) => p.name); + + if (selected) { + add.push(name); + } else { + remove.push(name); + } + + patchConfig({ software: { patterns: { add, remove } } }); }; // FIXME: use loading indicator when busy, we cannot know if it will be @@ -133,8 +140,9 @@ function SoftwarePatternsSelection(): React.ReactNode { // TODO: extract to a DataListSelector component or so. const selector = sortGroups(groups).map((groupName) => { const selectedIds = groups[groupName] - .filter((p) => p.selectedBy !== SelectedBy.NONE) + .filter((p) => selection[p.name] !== SelectedBy.NONE) .map((p) => p.name); + return (
{groupName} @@ -149,7 +157,7 @@ function SoftwarePatternsSelection(): React.ReactNode { onToggle(option.name)} + onChange={(_, value) => onToggle(option.name, value)} aria-labelledby={[nextActionId, titleId].join(" ")} isChecked={selected} /> @@ -159,7 +167,7 @@ function SoftwarePatternsSelection(): React.ReactNode {
{option.summary}{" "} - {option.selectedBy === SelectedBy.AUTO && ( + {selection[option.name] === SelectedBy.AUTO && ( @@ -198,7 +206,7 @@ function SoftwarePatternsSelection(): React.ReactNode { - + {selector.length > 0 ? {selector} : } diff --git a/web/src/components/software/patterns.test.json b/web/src/components/software/patterns.test.json index b5c10a3c50..6bf73553ec 100644 --- a/web/src/components/software/patterns.test.json +++ b/web/src/components/software/patterns.test.json @@ -5,8 +5,7 @@ "icon": "./pattern-gnome-wayland", "description": "The GNOME desktop environment is an intuitive and attractive desktop for users.\nThis pattern installs components for GNOME to run with Wayland and X11 technologies.", "summary": "GNOME Desktop Environment (Wayland)", - "order": "1010", - "selectedBy": 0 + "order": "1010" }, { "name": "kde", @@ -14,8 +13,7 @@ "icon": "./pattern-kde", "description": "Packages providing the Plasma desktop environment and applications from KDE.", "summary": "KDE Applications and Plasma 5 Desktop", - "order": "1110", - "selectedBy": 2 + "order": "1110" }, { "name": "yast2_basis", @@ -23,8 +21,7 @@ "icon": "./yast", "description": "YaST tools for basic system administration.", "summary": "YaST Base Utilities", - "order": "1220", - "selectedBy": 1 + "order": "1220" }, { "name": "yast2_desktop", @@ -32,8 +29,7 @@ "icon": "./yast", "description": "YaST tools for desktop system administration.", "summary": "YaST Desktop Utilities", - "order": "1222", - "selectedBy": 1 + "order": "1222" }, { "name": "yast2_server", @@ -41,8 +37,7 @@ "icon": "./yast", "description": "YaST tools for server system administration.", "summary": "YaST Server Utilities", - "order": "1224", - "selectedBy": 2 + "order": "1224" }, { "name": "xfce", @@ -50,8 +45,7 @@ "icon": "./pattern-xfce", "description": "Xfce is a lightweight desktop environment for various *NIX systems.", "summary": "XFCE Desktop Environment", - "order": "1310", - "selectedBy": 2 + "order": "1310" }, { "name": "multimedia", @@ -59,8 +53,7 @@ "icon": "./pattern-multimedia", "description": "Multimedia players, sound editing tools, video and image manipulation applications.", "summary": "Multimedia", - "order": "1580", - "selectedBy": 1 + "order": "1580" }, { "name": "office", @@ -68,8 +61,7 @@ "icon": "./pattern-office", "description": "Office software for your desktop environment including LibreOffice.", "summary": "Office Software", - "order": "1640", - "selectedBy": 1 + "order": "1640" }, { "name": "basic_desktop", @@ -77,7 +69,6 @@ "icon": "./pattern-x11", "description": "This pattern installs a rather basic desktop (icewm)", "summary": "A very basic desktop (previously part of x11 pattern)", - "order": "1802", - "selectedBy": 2 + "order": "1802" } ] diff --git a/web/src/components/software/proposal.test.json b/web/src/components/software/proposal.test.json index e2166a658d..af2c927c44 100644 --- a/web/src/components/software/proposal.test.json +++ b/web/src/components/software/proposal.test.json @@ -1,35 +1,38 @@ { - "size": "4.6 GiB", + "usedSpace": 4823450, "patterns": { - "fonts": 1, - "gnome_basis_opt": 1, - "minimal_base": 1, - "gnome_imaging": 1, - "fonts_opt": 1, - "x86_64_v3": 1, - "gnome_office": 1, - "x11_yast": 1, - "gnome": 0, - "base": 1, - "sw_management": 1, - "x11": 1, - "gnome_utilities": 1, - "enhanced_base": 1, - "sw_management_gnome": 1, - "gnome_basic": 1, - "x11_enhanced": 1, - "gnome_x11": 1, - "office": 1, - "yast2_desktop": 1, - "gnome_basis": 1, - "basesystem": 1, - "multimedia": 1, - "apparmor": 1, - "yast2_basis": 1, - "gnome_games": 1, - "imaging": 1, - "gnome_multimedia": 1, - "gnome_yast": 1, - "gnome_internet": 1 + "fonts": "auto", + "gnome_basis_opt": "auto", + "minimal_base": "auto", + "gnome_imaging": "auto", + "fonts_opt": "auto", + "x86_64_v3": "auto", + "gnome_office": "auto", + "x11_yast": "auto", + "gnome": "user", + "base": "auto", + "sw_management": "auto", + "x11": "auto", + "gnome_utilities": "auto", + "enhanced_base": "auto", + "sw_management_gnome": "auto", + "gnome_basic": "auto", + "x11_enhanced": "auto", + "gnome_x11": "auto", + "office": "auto", + "yast2_desktop": "auto", + "gnome_basis": "auto", + "basesystem": "auto", + "multimedia": "auto", + "apparmor": "auto", + "yast2_basis": "auto", + "gnome_games": "auto", + "imaging": "auto", + "gnome_multimedia": "auto", + "gnome_yast": "auto", + "gnome_internet": "auto", + "kde": "none", + "xfce": "none", + "yast2_server": "none" } } diff --git a/web/src/model/config.ts b/web/src/model/config.ts index 144ec652e9..1951665b0d 100644 --- a/web/src/model/config.ts +++ b/web/src/model/config.ts @@ -22,6 +22,7 @@ import type * as L10n from "~/model/config/l10n"; import type * as Network from "~/model/config/network"; +import type * as Software from "~/model/config/software"; import type * as Storage from "~/model/config/storage"; type Config = { @@ -29,6 +30,7 @@ type Config = { network?: Network.Config; product?: Product; storage?: Storage.Config; + software?: Software.Config }; type Product = { diff --git a/web/src/model/config/software.ts b/web/src/model/config/software.ts new file mode 100644 index 0000000000..62d68ae4aa --- /dev/null +++ b/web/src/model/config/software.ts @@ -0,0 +1,81 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * List of user-selected patterns to install + */ +export type PatternsArray = string[]; + +/** + * Software configuration. + */ +export interface Config { + patterns?: PatternsArray | PatternsObject; + /** + * List of packages to install + */ + packages?: string[]; + /** + * Flag if only minimal hard dependencies should be used in solver + */ + onlyRequired?: boolean; + /** + * List of user specified repositories that will be used on top of default ones + */ + extraRepositories?: Repository[]; + [k: string]: unknown; +} +/** + * Modifications for the list of user-selected patterns to install + */ +export interface PatternsObject { + /** + * List of user-selected patterns to add to the list + */ + add?: string[]; + /** + * List of user-selected patterns to remove from the list + */ + remove?: string[]; +} +/** + * Packages repository + */ +export interface Repository { + /** + * alias used for repository. Acting as identifier + */ + alias?: string; + /** + * URL pointing to repository + */ + url?: string; + /** + * Repository priority + */ + priority?: number; + /** + * User visible name. Defaults to alias + */ + name?: string; + /** + * product directory on multi repo DVD. Usually not needed + */ + productDir?: string; + /** + * If repository should be enabled. Defaults to true. Useful when adding additional repo that should not be immediately use. + */ + enabled?: boolean; + /** + * If unsigned repositories are allowed. Mainly useful for repositories that is hand crafted without GPG signature. + */ + allowUnsigned?: boolean; + /** + * List of GPG fingerprints that is accepted for this repository. Useful for own repositories with proper GPG signature. + */ + gpgFingerprints?: string[]; +} diff --git a/web/src/model/proposal/software.ts b/web/src/model/proposal/software.ts index 2050f4f9c6..a0f18533ea 100644 --- a/web/src/model/proposal/software.ts +++ b/web/src/model/proposal/software.ts @@ -31,11 +31,13 @@ type PatternsSelection = { [key: string]: SelectedBy }; enum SelectedBy { /** Selected by the user */ - USER = 0, + USER = "user", /** Automatically selected as a dependency of another package */ - AUTO = 1, + AUTO = "auto", /** No selected */ - NONE = 2, + NONE = "none", } -export type { Proposal, PatternsSelection, SelectedBy }; +export type { Proposal, PatternsSelection }; + +export { SelectedBy }; diff --git a/web/src/model/system/software.ts b/web/src/model/system/software.ts index b4e8b0bf01..5ef330a509 100644 --- a/web/src/model/system/software.ts +++ b/web/src/model/system/software.ts @@ -39,8 +39,8 @@ type Pattern = { order: number; /** Icon name (not path or file name!) */ icon: string; - /** Whether the pattern if selected and by whom */ - selectedBy?: SelectedBy; + /** Whether the pattern is selected by default */ + preselected: boolean }; type Repository = { @@ -65,31 +65,4 @@ type AddonInfo = { release: string; }; -/** - * Enum for the reasons to select a pattern - */ -enum SelectedBy { - /** Selected by the user */ - USER = 0, - /** Automatically selected as a dependency of another package */ - AUTO = 1, - /** No selected */ - NONE = 2, -} - -type Product = { - /** Product ID (e.g., "Leap") */ - id: string; - /** Product name (e.g., "openSUSE Leap 15.4") */ - name: string; - /** Product description */ - description?: string; - /** Product icon (e.g., "default.svg") */ - icon?: string; - /** If product is registrable or not */ - registration: boolean; - /** The product license id, if any */ - license?: string; -}; - -export type { System, Pattern, Repository, SelectedBy, Product }; +export type { System, Pattern, Repository };