diff --git a/products.d/leap_160.yaml b/products.d/leap_160.yaml index 0a8e1cdddc..9d431580fa 100644 --- a/products.d/leap_160.yaml +++ b/products.d/leap_160.yaml @@ -8,6 +8,7 @@ name: Leap 16.0 Alpha description: 'Leap 16.0 is the latest version of a community distribution based on the latest SUSE Linux Enterprise Server.' # Do not manually change any translations! See README.md for more details. +icon: Leap16.svg translations: description: ca: El Leap 16.0 és la darrera versió d'una distribució comunitària basada en diff --git a/products.d/microos.yaml b/products.d/microos.yaml index f3a74a14c5..4e847af284 100644 --- a/products.d/microos.yaml +++ b/products.d/microos.yaml @@ -9,6 +9,7 @@ description: 'A quick, small distribution designed to host container workloads with automated administration & patching. openSUSE MicroOS provides transactional (atomic) updates upon a read-only btrfs root file system. As rolling release distribution the software is always up-to-date.' +icon: MicroOS.svg # Do not manually change any translations! See README.md for more details. translations: description: diff --git a/products.d/sles_160.yaml b/products.d/sles_160.yaml index 439505d0fb..0594745a3e 100644 --- a/products.d/sles_160.yaml +++ b/products.d/sles_160.yaml @@ -10,6 +10,7 @@ description: "SUSE Linux Enterprise Server is the open, reliable, compliant, and continuity. It is the secure and adaptable OS for long-term supported, innovation-ready infrastructure running business-critical workloads on-premises, in the cloud, and at the edge." +icon: SUSE.svg # Do not manually change any translations! See README.md for more details. translations: description: diff --git a/products.d/tumbleweed.yaml b/products.d/tumbleweed.yaml index 9b921f661f..839bcf0db9 100644 --- a/products.d/tumbleweed.yaml +++ b/products.d/tumbleweed.yaml @@ -9,6 +9,7 @@ description: 'The Tumbleweed distribution is a pure rolling release version of openSUSE containing the latest "stable" versions of all software instead of relying on rigid periodic release cycles. The project does this for users that want the newest stable software.' +icon: Tumbleweed.svg # Do not manually change any translations! See README.md for more details. translations: description: diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 11381e2da1..29890553ae 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -30,6 +30,7 @@ "id": { "title": "Product identifier", "description": "The id field from a products.d/foo.yaml file", + "icon": "Product Icon path specified in products.d/foo.yaml file", "type": "string" }, "registrationCode": { diff --git a/rust/agama-lib/src/product/client.rs b/rust/agama-lib/src/product/client.rs index a75824e8f1..3283678694 100644 --- a/rust/agama-lib/src/product/client.rs +++ b/rust/agama-lib/src/product/client.rs @@ -16,6 +16,8 @@ pub struct Product { pub name: String, /// Product description pub description: String, + /// Product icon (e.g., "default.svg") + pub icon: String, } #[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema)] @@ -74,10 +76,15 @@ impl<'a> ProductClient<'a> { Some(value) => value.try_into().unwrap(), None => "", }; + let icon = match data.get("icon") { + Some(value) => value.try_into().unwrap(), + None => "default.svg", + }; Product { id, name, description: description.to_string(), + icon: icon.to_string(), } }) .collect(); diff --git a/service/lib/agama/dbus/software/product.rb b/service/lib/agama/dbus/software/product.rb index 1c85087b8c..84fd69a353 100644 --- a/service/lib/agama/dbus/software/product.rb +++ b/service/lib/agama/dbus/software/product.rb @@ -54,7 +54,7 @@ def issues def available_products backend.products.map do |product| - [product.id, product.display_name, { "description" => product.localized_description }] + [product.id, product.display_name, { "description" => product.localized_description, "icon" => product.icon }] end end diff --git a/service/lib/agama/software/product.rb b/service/lib/agama/software/product.rb index 5ea4e1fd69..6089403389 100644 --- a/service/lib/agama/software/product.rb +++ b/service/lib/agama/software/product.rb @@ -48,6 +48,14 @@ class Product # @return [String, nil] E.g., "1.0". attr_accessor :version + + # Product icon. Please use specify filename with svg suffix and ensure referenced + # file exists inside agama/web/src/assests/product. + # default.svg unless be used nless specified otherwise. + # + # @return [String, "default.svg"] E.g., "1.0". + attr_accessor :icon + # List of repositories. # # @return [Array] Empty if the product requires registration. @@ -99,6 +107,7 @@ class Product # @param id [string] Product id. def initialize(id) @id = id + @icon = "default.svg" @repositories = [] @labels = [] @mandatory_packages = [] diff --git a/service/lib/agama/software/product_builder.rb b/service/lib/agama/software/product_builder.rb index d679f39dba..ba3e26f5de 100644 --- a/service/lib/agama/software/product_builder.rb +++ b/service/lib/agama/software/product_builder.rb @@ -64,6 +64,8 @@ def initialize_product(id, data, attrs) product.description = attrs["description"] product.name = data[:name] product.version = data[:version] + product.icon = attrs["icon"] if attrs.key?("icon") && !attrs["icon"].nil? + @logger.info(product.name + " uses icon " + product.icon) end end @@ -98,6 +100,7 @@ def product_data_from_config(id) { name: config.products.dig(id, "software", "base_product"), version: config.products.dig(id, "software", "version"), + icon: config.products.dig(id, "software", "icon"), labels: config.arch_elements_from( id, "software", "installation_labels", property: :label ), diff --git a/web/src/assets/products/Leap16.svg b/web/src/assets/products/Leap16.svg new file mode 100644 index 0000000000..2ee4a146bd --- /dev/null +++ b/web/src/assets/products/Leap16.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + Alpha + diff --git a/web/src/assets/products/MicroOS.svg b/web/src/assets/products/MicroOS.svg new file mode 100644 index 0000000000..a177ef72f8 --- /dev/null +++ b/web/src/assets/products/MicroOS.svg @@ -0,0 +1,66 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/web/src/assets/products/SUSE.svg b/web/src/assets/products/SUSE.svg new file mode 100644 index 0000000000..3cb5d5c64c --- /dev/null +++ b/web/src/assets/products/SUSE.svg @@ -0,0 +1,86 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/web/src/assets/products/Tumbleweed.svg b/web/src/assets/products/Tumbleweed.svg new file mode 100644 index 0000000000..27ac560b1d --- /dev/null +++ b/web/src/assets/products/Tumbleweed.svg @@ -0,0 +1,66 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/web/src/assets/products/default.svg b/web/src/assets/products/default.svg new file mode 100644 index 0000000000..26c6d811e8 --- /dev/null +++ b/web/src/assets/products/default.svg @@ -0,0 +1,76 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/web/src/components/product/ProductSelectionPage.jsx b/web/src/components/product/ProductSelectionPage.jsx index a875e4b663..7e483ad0e5 100644 --- a/web/src/components/product/ProductSelectionPage.jsx +++ b/web/src/components/product/ProductSelectionPage.jsx @@ -1,3 +1,4 @@ +/* eslint @typescript-eslint/no-var-requires: "off" */ /* * Copyright (c) [2022-2024] SUSE LLC * @@ -20,7 +21,7 @@ */ import React, { useState } from "react"; -import { Card, CardBody, Flex, Form, Grid, GridItem, Radio } from "@patternfly/react-core"; +import { Card, CardBody, Flex, Form, Grid, GridItem } from "@patternfly/react-core"; import { Page } from "~/components/core"; import { Center } from "~/components/layout"; import { useConfigMutation, useProduct } from "~/queries/software"; @@ -46,16 +47,32 @@ function ProductSelectionPage() { } }; - const Item = ({ children }) => { + const Item = ({ children }) => ( + + {children} + + ); + + const ProductIcon = ({ src, alt }) => { + // Ensure that we display something even if icon path is incorrect + const productIcon = require(`../../assets/products/${src}`); + return ( - - {children} - + {alt} ); }; const isSelectionDisabled = !nextProduct || nextProduct === selectedProduct; + const handleCardClick = (product) => { + setNextProduct(product); + }; + return (
@@ -63,17 +80,24 @@ function ProductSelectionPage() { {products.map((product, index) => ( - + handleCardClick(product)} + style={{ + cursor: 'pointer', // Change the cursor to indicate clickable + border: nextProduct === product ? '2px solid #0066cc' : 'none', // Optional: highlight selected card + }} + > - {product.name}} - body={product.description} - isChecked={nextProduct === product} - onChange={() => setNextProduct(product)} + +
+ +

{product.description}

+
@@ -98,4 +122,4 @@ function ProductSelectionPage() { ); } -export default ProductSelectionPage; +export default ProductSelectionPage; \ No newline at end of file diff --git a/web/src/types/software.ts b/web/src/types/software.ts index 42a9d8edc9..355e1f6ac3 100644 --- a/web/src/types/software.ts +++ b/web/src/types/software.ts @@ -38,6 +38,8 @@ type Product = { name: string; /** Product description */ description: string; + /** Product icon (e.g., "default.svg") */ + icon: string; }; type PatternsSelection = { [key: string]: SelectedBy };