From c79a0ea949d9a9ce7f56e7fad24f2549fd5bf00c Mon Sep 17 00:00:00 2001 From: Welder Luz Date: Tue, 11 Jun 2024 09:42:47 -0300 Subject: [PATCH] Configure AppStreams via Activation Keys --- .../common/hibernate/AnnotationRegistry.java | 4 +- .../rhn/common/security/acl/Access.java | 15 ++ .../domain/token/TokenChannelAppStream.java | 114 ++++++++++++++ .../appstreams/SystemAppStreamHandler.java | 2 +- .../manager/token/ActivationKeyManager.java | 123 +++++++++++++++ .../reactor/messaging/RegistrationUtils.java | 35 ++++- .../src/com/suse/manager/webui/Router.java | 4 +- .../ActivationKeysAppStreamsChanges.java | 50 ++++++ .../ActivationKeysController.java | 3 +- .../ActivationKeysViewsController.java | 147 ++++++++++++++++++ .../appstreams/AppStreamsController.java | 2 +- .../response/AppStreamModuleResponse.java | 9 +- .../response/ChannelAppStreamsResponse.java | 13 +- .../templates/activation_keys/appstreams.jade | 31 ++++ .../webapp/WEB-INF/nav/activation_key.xml | 2 + ...a.changes.welder.activationkeys-appstreams | 1 + .../tables/suseRegTokenChannelAppStream.sql | 32 ++++ .../100-suseRegTokenAppStreams-create.sql | 18 +++ .../salt/appstreams/configure.sls | 5 +- .../manager/appstreams/actions-appstreams.tsx | 20 +++ .../src/manager/appstreams/appstreams.tsx | 45 ++---- .../manager/appstreams/channel-appstreams.tsx | 31 ++-- .../manager/appstreams/list-appstreams.tsx | 72 +++------ .../manager/appstreams/panel-appstream.tsx | 41 +++++ web/html/src/manager/appstreams/utils.ts | 46 ++++++ web/html/src/manager/index.ts | 2 + .../activation-keys-appstreams.renderer.tsx | 20 +++ .../activation-keys-appstreams.tsx | 95 +++++++++++ .../manager/systems/activation-key/index.ts | 3 + ...b.changes.welder.activationkeys-appstreams | 1 + 30 files changed, 874 insertions(+), 112 deletions(-) create mode 100644 java/code/src/com/redhat/rhn/domain/token/TokenChannelAppStream.java create mode 100644 java/code/src/com/suse/manager/webui/controllers/activationkeys/ActivationKeysAppStreamsChanges.java rename java/code/src/com/suse/manager/webui/controllers/{ => activationkeys}/ActivationKeysController.java (98%) create mode 100644 java/code/src/com/suse/manager/webui/controllers/activationkeys/ActivationKeysViewsController.java create mode 100644 java/code/src/com/suse/manager/webui/templates/activation_keys/appstreams.jade create mode 100644 java/spacewalk-java.changes.welder.activationkeys-appstreams create mode 100644 schema/spacewalk/common/tables/suseRegTokenChannelAppStream.sql create mode 100644 schema/spacewalk/upgrade/susemanager-schema-5.0.7-to-susemanager-schema-5.0.8/100-suseRegTokenAppStreams-create.sql create mode 100644 web/html/src/manager/appstreams/actions-appstreams.tsx create mode 100644 web/html/src/manager/appstreams/panel-appstream.tsx create mode 100644 web/html/src/manager/appstreams/utils.ts create mode 100644 web/html/src/manager/systems/activation-key/activation-keys-appstreams.renderer.tsx create mode 100644 web/html/src/manager/systems/activation-key/activation-keys-appstreams.tsx create mode 100644 web/html/src/manager/systems/activation-key/index.ts create mode 100644 web/spacewalk-web.changes.welder.activationkeys-appstreams diff --git a/java/code/src/com/redhat/rhn/common/hibernate/AnnotationRegistry.java b/java/code/src/com/redhat/rhn/common/hibernate/AnnotationRegistry.java index 38a204d1b14b..cc52e192612f 100644 --- a/java/code/src/com/redhat/rhn/common/hibernate/AnnotationRegistry.java +++ b/java/code/src/com/redhat/rhn/common/hibernate/AnnotationRegistry.java @@ -91,6 +91,7 @@ import com.redhat.rhn.domain.server.ansible.PlaybookPath; import com.redhat.rhn.domain.server.virtualhostmanager.VirtualHostManagerNodeInfo; import com.redhat.rhn.domain.task.Task; +import com.redhat.rhn.domain.token.TokenChannelAppStream; import com.suse.cloud.domain.PaygDimensionComputation; import com.suse.cloud.domain.PaygDimensionResult; @@ -201,7 +202,8 @@ private AnnotationRegistry() { CoCoResultTypeConverter.class, ServerAppStream.class, AppStream.class, - AppStreamApi.class + AppStreamApi.class, + TokenChannelAppStream.class ); /** diff --git a/java/code/src/com/redhat/rhn/common/security/acl/Access.java b/java/code/src/com/redhat/rhn/common/security/acl/Access.java index ec5f51832613..43a7baa17172 100644 --- a/java/code/src/com/redhat/rhn/common/security/acl/Access.java +++ b/java/code/src/com/redhat/rhn/common/security/acl/Access.java @@ -31,6 +31,8 @@ import com.redhat.rhn.domain.role.RoleFactory; import com.redhat.rhn.domain.server.Server; import com.redhat.rhn.domain.server.ServerFactory; +import com.redhat.rhn.domain.token.ActivationKey; +import com.redhat.rhn.domain.token.ActivationKeyFactory; import com.redhat.rhn.domain.user.User; import com.redhat.rhn.domain.user.UserFactory; import com.redhat.rhn.frontend.dto.ChannelPerms; @@ -465,6 +467,19 @@ public boolean aclIsModularChannel(Map ctx, String[] params) { return chan.isModular(); } + /** + * Checks if a given activation key is linked to any modular channel. + * @param ctx acl context (includes the activation key id tid and the user) + * @param params parameters for acl (ignored) + * @return true if it is an activation key associated with any modular channel. + */ + public boolean aclHasModularChannel(Map ctx, String[] params) { + Long tid = getAsLong(ctx.get("tid")); + User user = (User) ctx.get("user"); + ActivationKey activationKey = ActivationKeyFactory.lookupById(tid, user.getOrg()); + return activationKey.getChannels().stream().anyMatch(Channel::isModular); + } + /** * Returns true if the user is channel admin of the corresponding channel. * If the channel is a vendor channel, the return value is false. diff --git a/java/code/src/com/redhat/rhn/domain/token/TokenChannelAppStream.java b/java/code/src/com/redhat/rhn/domain/token/TokenChannelAppStream.java new file mode 100644 index 000000000000..e6f34fdd790d --- /dev/null +++ b/java/code/src/com/redhat/rhn/domain/token/TokenChannelAppStream.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2024 SUSE LLC + * + * This software is licensed to you under the GNU General Public License, + * version 2 (GPLv2). There is NO WARRANTY for this software, express or + * implied, including the implied warranties of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 + * along with this software; if not, see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * Red Hat trademarks are not licensed under GPLv2. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ +package com.redhat.rhn.domain.token; + +import com.redhat.rhn.domain.channel.Channel; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +@Entity +@Table(name = "suseRegTokenChannelAppStream") +public class TokenChannelAppStream { + + /** + * Constructs a TokenChannelAppStream instance. + */ + public TokenChannelAppStream() { + // Default constructor + } + + /** + * Constructs a TokenChannelAppStream. + * + * @param tokenIn the token + * @param channelIn the channel + * @param nameIn the name of the appStream module + * @param streamIn the stream of the appStream module + */ + public TokenChannelAppStream(Token tokenIn, Channel channelIn, String nameIn, String streamIn) { + token = tokenIn; + channel = channelIn; + name = nameIn; + stream = streamIn; + } + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "suse_reg_tok_ch_as_id_seq") + @SequenceGenerator(name = "suse_reg_tok_ch_as_id_seq", sequenceName = "suse_reg_tok_ch_as_id_seq", + allocationSize = 1) + private Long id; + + @ManyToOne + @JoinColumn(name = "token_id") + private Token token; + + @ManyToOne + @JoinColumn(name = "channel_id") + private Channel channel; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String stream; + + public Long getId() { + return id; + } + + public void setId(Long idIn) { + id = idIn; + } + + public Token getToken() { + return token; + } + + public void setToken(Token tokenIn) { + token = tokenIn; + } + + public String getName() { + return name; + } + + public void setName(String nameIn) { + name = nameIn; + } + + public String getStream() { + return stream; + } + + public void setStream(String streamIn) { + stream = streamIn; + } + + public Channel getChannel() { + return channel; + } + + public void setChannel(Channel channelIn) { + channel = channelIn; + } +} diff --git a/java/code/src/com/redhat/rhn/frontend/xmlrpc/system/appstreams/SystemAppStreamHandler.java b/java/code/src/com/redhat/rhn/frontend/xmlrpc/system/appstreams/SystemAppStreamHandler.java index ee8c24c57b84..2d0adcafd80c 100644 --- a/java/code/src/com/redhat/rhn/frontend/xmlrpc/system/appstreams/SystemAppStreamHandler.java +++ b/java/code/src/com/redhat/rhn/frontend/xmlrpc/system/appstreams/SystemAppStreamHandler.java @@ -187,7 +187,7 @@ public List listModuleStreams(User loggedInUser, Inte .map(channel -> new ChannelAppStreamsResponse( channel, AppStreamsManager.listChannelAppStreams(channel.getId()), - server + server::hasAppStreamModuleEnabled )) .collect(Collectors.toList()); } diff --git a/java/code/src/com/redhat/rhn/manager/token/ActivationKeyManager.java b/java/code/src/com/redhat/rhn/manager/token/ActivationKeyManager.java index 7d4731493146..f5f46735be80 100644 --- a/java/code/src/com/redhat/rhn/manager/token/ActivationKeyManager.java +++ b/java/code/src/com/redhat/rhn/manager/token/ActivationKeyManager.java @@ -37,6 +37,7 @@ import com.redhat.rhn.domain.server.ServerGroupType; import com.redhat.rhn.domain.token.ActivationKey; import com.redhat.rhn.domain.token.ActivationKeyFactory; +import com.redhat.rhn.domain.token.TokenChannelAppStream; import com.redhat.rhn.domain.user.User; import com.redhat.rhn.frontend.struts.Scrubber; import com.redhat.rhn.manager.channel.ChannelManager; @@ -60,6 +61,11 @@ import java.util.Map; import java.util.Set; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + /** * ActivationKeyManager */ @@ -565,4 +571,121 @@ public void setupAutoConfigDeployment(ActivationKey key) { addConfigMgmtPackages(key); } } + + /** + * Checks if a specific app stream module is enabled for the given activation key and channel. + * + * @param activationKey the activation key containing the registration token for matching + * @param channel the channel to check + * @param module the name of the appStream module to check + * @param stream the stream of the appStream module to check + * @return {@code true} if the app stream module is included in the activation key and channel, + * {@code false} otherwise + */ + public boolean hasAppStreamModuleEnabled( + ActivationKey activationKey, + Channel channel, + String module, + String stream) { + return listTokenChannelAppStreams(activationKey, channel) + .stream() + .anyMatch(it -> + it.getChannel().getId().equals(channel.getId()) && + it.getName().equals(module) && + it.getStream().equals(stream) + ); + } + + /** + * Retrieves a list of {@code TokenChannelAppStream} objects that match the given activation key. + * + * @param activationKey the activation key containing the token to match + * @return a list of {@code TokenChannelAppStream} objects related to the activation key. + */ + public List listTokenChannelAppStreams(ActivationKey activationKey) { + return listTokenChannelAppStreams(activationKey, null); + } + + /** + * Retrieves a list of {@code TokenChannelAppStream} objects that match the given activation key and channel. + * If the channel is {@code null}, only the activation key is used as a filter criterion. + * + * @param activationKey the activation key containing the token to match + * @param channel the channel to match; if {@code null}, the channel criterion is ignored + * @return a list of {@code TokenChannelAppStream} objects that match the given criteria + */ + private List listTokenChannelAppStreams(ActivationKey activationKey, Channel channel) { + CriteriaBuilder criteriaBuilder = ActivationKeyFactory.getSession().getCriteriaBuilder(); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(TokenChannelAppStream.class); + Root root = criteriaQuery.from(TokenChannelAppStream.class); + + Predicate tokenPredicate = criteriaBuilder.equal(root.get("token"), activationKey.getToken()); + if (channel != null) { + Predicate channelPredicate = criteriaBuilder.equal(root.get("channel"), channel); + criteriaQuery.where(criteriaBuilder.and(tokenPredicate, channelPredicate)); + } + else { + criteriaQuery.where(tokenPredicate); + } + + return ActivationKeyFactory.getSession().createQuery(criteriaQuery).getResultList(); + } + + /** + * Saves the specified channel app streams by including and removing the given lists of app streams. + * It will first remove the specified app streams from the channel, and then includes the specified + * app streams to the channel. The app streams are specified as a list of strings in the format "name:stream". + * + * @param activationKey the activation key containing the registration token + * @param channel the channel to which the app streams belong + * @param toInclude the list of app streams to include in the activation key + * @param toRemove the list of app streams to remove from the activation key + */ + public void saveChannelAppStreams( + ActivationKey activationKey, + Channel channel, + List toInclude, + List toRemove) { + removeChannelAppStreams(activationKey, channel, toRemove); + includeChannelAppStreams(activationKey, channel, toInclude); + } + + private void removeChannelAppStreams(ActivationKey activationKey, Channel channel, List toRemove) { + var session = ActivationKeyFactory.getSession(); + CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder(); + var criteriaDelete = criteriaBuilder.createCriteriaDelete(TokenChannelAppStream.class); + var root = criteriaDelete.from(TokenChannelAppStream.class); + + Predicate tokenPredicate = criteriaBuilder.equal(root.get("token"), activationKey.getToken()); + Predicate channelPredicate = criteriaBuilder.equal(root.get("channel"), channel); + + criteriaDelete.where( + criteriaBuilder.and( + tokenPredicate, + channelPredicate, + criteriaBuilder.or( + toRemove + .stream() + .map(appStream -> { + String[] parts = appStream.split(":"); + String name = parts[0]; + String stream = parts[1]; + Predicate namePredicate = criteriaBuilder.equal(root.get("name"), name); + Predicate streamPredicate = criteriaBuilder.equal(root.get("stream"), stream); + return criteriaBuilder.and(namePredicate, streamPredicate); + }).toArray(Predicate[]::new))) + ); + session.createQuery(criteriaDelete).executeUpdate(); + } + + private void includeChannelAppStreams(ActivationKey activationKey, Channel channel, List toInclude) { + toInclude.forEach(appStream -> { + String[] parts = appStream.split(":"); + String name = parts[0]; + String stream = parts[1]; + ActivationKeyFactory.getSession().persist( + new TokenChannelAppStream(activationKey.getToken(), channel, name, stream) + ); + }); + } } diff --git a/java/code/src/com/suse/manager/reactor/messaging/RegistrationUtils.java b/java/code/src/com/suse/manager/reactor/messaging/RegistrationUtils.java index 780181ba17e5..0416ed68ae3e 100644 --- a/java/code/src/com/suse/manager/reactor/messaging/RegistrationUtils.java +++ b/java/code/src/com/suse/manager/reactor/messaging/RegistrationUtils.java @@ -19,6 +19,7 @@ import static java.util.Collections.emptySet; import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.partitioningBy; +import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import com.redhat.rhn.GlobalInstanceHolder; @@ -46,12 +47,14 @@ import com.redhat.rhn.manager.action.ActionManager; import com.redhat.rhn.manager.system.SystemManager; import com.redhat.rhn.manager.system.entitling.SystemEntitlementManager; +import com.redhat.rhn.manager.token.ActivationKeyManager; import com.redhat.rhn.taskomatic.TaskomaticApiException; import com.suse.manager.reactor.utils.RhelUtils; import com.suse.manager.reactor.utils.ValueMap; import com.suse.manager.webui.controllers.StatesAPI; import com.suse.manager.webui.controllers.channels.ChannelsUtils; +import com.suse.manager.webui.services.SaltServerActionService; import com.suse.manager.webui.services.iface.RedhatProductInfo; import com.suse.manager.webui.services.iface.SystemQuery; import com.suse.manager.webui.services.pillar.MinionPillarManager; @@ -67,6 +70,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -125,6 +129,7 @@ public static void finishRegistration(MinionServer minion, Optional !e.isBase())); // Apply initial states asynchronously + Map statesToApplyPillar = new HashMap<>(); List statesToApply = new ArrayList<>(); statesToApply.add(ApplyStatesEventMessage.CERTIFICATE); statesToApply.add(ApplyStatesEventMessage.CHANNELS); @@ -136,11 +141,15 @@ public static void finishRegistration(MinionServer minion, Optional activationKey, + List statesToApply, + Map statesToApplyPillar) { + if (activationKey.isPresent() && activationKey.get().getChannels().stream().anyMatch(Channel::isModular)) { + var appStreamsToEnable = ActivationKeyManager.getInstance().listTokenChannelAppStreams(activationKey.get()); + if (!appStreamsToEnable.isEmpty()) { + statesToApply.add(SaltServerActionService.APPSTREAMS_CONFIGURE); + var appStreamsParams = appStreamsToEnable + .stream() + .map(it -> List.of(it.getName(), it.getStream())) + .collect(toList()); + statesToApplyPillar.put(SaltServerActionService.PARAM_APPSTREAMS_ENABLE, appStreamsParams); + } + } + } + private static void triggerHardwareRefresh(MinionServer server) { try { ActionManager.scheduleHardwareRefreshAction(server.getOrg(), server, diff --git a/java/code/src/com/suse/manager/webui/Router.java b/java/code/src/com/suse/manager/webui/Router.java index 31d96c6c6fc3..19adc6f1a3ae 100644 --- a/java/code/src/com/suse/manager/webui/Router.java +++ b/java/code/src/com/suse/manager/webui/Router.java @@ -34,7 +34,6 @@ import com.suse.manager.attestation.AttestationManager; import com.suse.manager.kubernetes.KubernetesManager; import com.suse.manager.utils.SaltKeyUtils; -import com.suse.manager.webui.controllers.ActivationKeysController; import com.suse.manager.webui.controllers.AnsibleController; import com.suse.manager.webui.controllers.CSVDownloadController; import com.suse.manager.webui.controllers.CVEAuditController; @@ -64,6 +63,8 @@ import com.suse.manager.webui.controllers.SystemsController; import com.suse.manager.webui.controllers.TaskoTop; import com.suse.manager.webui.controllers.VirtualHostManagerController; +import com.suse.manager.webui.controllers.activationkeys.ActivationKeysController; +import com.suse.manager.webui.controllers.activationkeys.ActivationKeysViewsController; import com.suse.manager.webui.controllers.admin.AdminApiController; import com.suse.manager.webui.controllers.admin.AdminViewsController; import com.suse.manager.webui.controllers.appstreams.AppStreamsController; @@ -188,6 +189,7 @@ public void init() { // Activation Keys API ActivationKeysController.initRoutes(); + ActivationKeysViewsController.initRoutes(jade); SsmController.initRoutes(); diff --git a/java/code/src/com/suse/manager/webui/controllers/activationkeys/ActivationKeysAppStreamsChanges.java b/java/code/src/com/suse/manager/webui/controllers/activationkeys/ActivationKeysAppStreamsChanges.java new file mode 100644 index 000000000000..91a45bd409de --- /dev/null +++ b/java/code/src/com/suse/manager/webui/controllers/activationkeys/ActivationKeysAppStreamsChanges.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 SUSE LLC + * + * This software is licensed to you under the GNU General Public License, + * version 2 (GPLv2). There is NO WARRANTY for this software, express or + * implied, including the implied warranties of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 + * along with this software; if not, see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * Red Hat trademarks are not licensed under GPLv2. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ +package com.suse.manager.webui.controllers.activationkeys; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class ActivationKeysAppStreamsChanges { + private Map changes; + + /** + * Obtains the list of appStreams to remove + * @param channelId the id of the channel + * @return the list of appStreams to remove + */ + public List getToRemove(Long channelId) { + return changes.get(channelId).toRemove; + } + + /** + * Obtains the list of appStreams to include + * @param channelId the id of the channel + * @return the list of appStreams to include + */ + public List getToInclude(Long channelId) { + return changes.get(channelId).toInclude; + } + + public Set getChannelIds() { + return changes.keySet(); + } + + static class ChannelAppStreamsChanges { + private List toInclude; + private List toRemove; + } +} diff --git a/java/code/src/com/suse/manager/webui/controllers/ActivationKeysController.java b/java/code/src/com/suse/manager/webui/controllers/activationkeys/ActivationKeysController.java similarity index 98% rename from java/code/src/com/suse/manager/webui/controllers/ActivationKeysController.java rename to java/code/src/com/suse/manager/webui/controllers/activationkeys/ActivationKeysController.java index 70b4702599e5..4472a40d51b3 100644 --- a/java/code/src/com/suse/manager/webui/controllers/ActivationKeysController.java +++ b/java/code/src/com/suse/manager/webui/controllers/activationkeys/ActivationKeysController.java @@ -12,7 +12,7 @@ * granted to use or replicate Red Hat trademarks that are incorporated * in this software or its documentation. */ -package com.suse.manager.webui.controllers; +package com.suse.manager.webui.controllers.activationkeys; import static com.suse.manager.webui.controllers.channels.ChannelsUtils.generateChannelJson; import static com.suse.manager.webui.controllers.channels.ChannelsUtils.getPossibleBaseChannels; @@ -74,6 +74,7 @@ private static String withActivationKey(Request request, Response response, return handler.apply(activationKey); } + /** * Get the current channels of an activation key. * diff --git a/java/code/src/com/suse/manager/webui/controllers/activationkeys/ActivationKeysViewsController.java b/java/code/src/com/suse/manager/webui/controllers/activationkeys/ActivationKeysViewsController.java new file mode 100644 index 000000000000..c64c01257937 --- /dev/null +++ b/java/code/src/com/suse/manager/webui/controllers/activationkeys/ActivationKeysViewsController.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2024 SUSE LLC + * + * This software is licensed to you under the GNU General Public License, + * version 2 (GPLv2). There is NO WARRANTY for this software, express or + * implied, including the implied warranties of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 + * along with this software; if not, see + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. + * + * Red Hat trademarks are not licensed under GPLv2. No permission is + * granted to use or replicate Red Hat trademarks that are incorporated + * in this software or its documentation. + */ +package com.suse.manager.webui.controllers.activationkeys; + +import static com.suse.manager.webui.utils.SparkApplicationHelper.result; +import static com.suse.manager.webui.utils.SparkApplicationHelper.withCsrfToken; +import static com.suse.manager.webui.utils.SparkApplicationHelper.withDocsLocale; +import static com.suse.manager.webui.utils.SparkApplicationHelper.withUser; +import static com.suse.manager.webui.utils.gson.ResultJson.success; +import static spark.Spark.get; +import static spark.Spark.post; + +import com.redhat.rhn.domain.channel.Channel; +import com.redhat.rhn.domain.channel.ChannelFactory; +import com.redhat.rhn.domain.role.RoleFactory; +import com.redhat.rhn.domain.token.ActivationKey; +import com.redhat.rhn.domain.token.ActivationKeyFactory; +import com.redhat.rhn.domain.user.User; +import com.redhat.rhn.manager.appstreams.AppStreamsManager; +import com.redhat.rhn.manager.token.ActivationKeyManager; + +import com.suse.manager.webui.controllers.appstreams.response.ChannelAppStreamsResponse; +import com.suse.manager.webui.utils.ViewHelper; +import com.suse.utils.Json; + +import com.google.gson.reflect.TypeToken; + +import org.apache.http.HttpStatus; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import spark.ModelAndView; +import spark.Request; +import spark.Response; +import spark.Spark; +import spark.template.jade.JadeTemplateEngine; + +public class ActivationKeysViewsController { + + private ActivationKeysViewsController() { } + + /** + * Invoked from Router. Init routes for Activation keys views. + * @param jade JadeTemplateEngine + */ + public static void initRoutes(JadeTemplateEngine jade) { + get("/manager/activationkeys/appstreams", + withCsrfToken(withDocsLocale(withUser(ActivationKeysViewsController::appstreams))), jade); + post("/manager/api/activationkeys/appstreams/save", + withUser(ActivationKeysViewsController::saveAppStreamsChanges)); + } + + private static ActivationKey getActivationKey(Request request, User user) { + ActivationKey activationKey; + try { + Long activationKeyId = Long.parseLong(request.raw().getParameter("tid")); + activationKey = ActivationKeyFactory.lookupById(activationKeyId, user.getOrg()); + } + catch (NumberFormatException e) { + throw Spark.halt(HttpStatus.SC_BAD_REQUEST, "Invalid activation key"); + } + if (activationKey == null) { + throw Spark.halt(HttpStatus.SC_NOT_FOUND); + } + return activationKey; + } + + /** + * Handler for the activation keys -> app streams page. + * + * @param request the request object + * @param response the response object + * @param user the current user + * @return the ModelAndView object to render the page + */ + public static ModelAndView appstreams(Request request, Response response, User user) { + Map data = new HashMap<>(); + var activationKey = getActivationKey(request, user); + var channelsAppStreams = getChannelAppStreams(activationKey); + + data.put("channelsAppStreams", Json.GSON.toJson(channelsAppStreams)); + data.put("note", activationKey.getNote()); + data.put("activationKeyId", activationKey.getId()); + data.put("isActivationKeyAdmin", user.hasRole(RoleFactory.ACTIVATION_KEY_ADMIN)); + data.put("tabs", ViewHelper.getInstance().renderNavigationMenu(request, "/WEB-INF/nav/activation_key.xml")); + return new ModelAndView(data, "templates/activation_keys/appstreams.jade"); + } + + private static List getChannelAppStreams(ActivationKey activationKey) { + return activationKey.getChannels() + .stream() + .filter(Channel::isModular) + .map(channel -> new ChannelAppStreamsResponse( + channel, + AppStreamsManager.listChannelAppStreams(channel.getId()), + (module, stream) -> ActivationKeyManager.getInstance().hasAppStreamModuleEnabled( + activationKey, channel, module, stream + ) + )).collect(Collectors.toList()); + } + + /** + * Saves the changes (enable/disable) to AppStreams linked to a given activation key. + * + * @param request the HTTP request + * @param response the HTTP response + * @param user the user performing the action + * @return a JSON response indicating success or failure + */ + public static String saveAppStreamsChanges(Request request, Response response, User user) { + var activationKey = getActivationKey(request, user); + var params = Json.GSON.fromJson(request.body(), ActivationKeysAppStreamsChanges.class); + params + .getChannelIds() + .stream() + .map(channelId -> ChannelFactory.lookupByIdAndUser(channelId, user)) + .filter(Objects::nonNull) + .forEach(channel -> + ActivationKeyManager + .getInstance() + .saveChannelAppStreams( + activationKey, + channel, + params.getToInclude(channel.getId()), + params.getToRemove(channel.getId()) + ) + ); + + return result(response, success(getChannelAppStreams(activationKey)), new TypeToken<>() { }); + } +} diff --git a/java/code/src/com/suse/manager/webui/controllers/appstreams/AppStreamsController.java b/java/code/src/com/suse/manager/webui/controllers/appstreams/AppStreamsController.java index 517a3f171eee..08eedc39d907 100644 --- a/java/code/src/com/suse/manager/webui/controllers/appstreams/AppStreamsController.java +++ b/java/code/src/com/suse/manager/webui/controllers/appstreams/AppStreamsController.java @@ -146,7 +146,7 @@ public static ModelAndView appstreams(Request request, Response response, User u .map(channel -> new ChannelAppStreamsResponse( channel, AppStreamsManager.listChannelAppStreams(channel.getId()), - server + server::hasAppStreamModuleEnabled )) .collect(Collectors.toList()); data.put("channelsAppStreams", GSON.toJson(channelsAppStreams)); diff --git a/java/code/src/com/suse/manager/webui/controllers/appstreams/response/AppStreamModuleResponse.java b/java/code/src/com/suse/manager/webui/controllers/appstreams/response/AppStreamModuleResponse.java index c6898a22f24f..b92a59372cc3 100644 --- a/java/code/src/com/suse/manager/webui/controllers/appstreams/response/AppStreamModuleResponse.java +++ b/java/code/src/com/suse/manager/webui/controllers/appstreams/response/AppStreamModuleResponse.java @@ -15,7 +15,6 @@ package com.suse.manager.webui.controllers.appstreams.response; import com.redhat.rhn.domain.channel.AppStream; -import com.redhat.rhn.domain.server.Server; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; @@ -23,16 +22,16 @@ public class AppStreamModuleResponse { /** - * Constructs an AppStreamModuleResponse object based on the provided AppStream and Server objects. + * Constructs an AppStreamModuleResponse object based on the provided AppStream. * * @param appStreamIn The AppStream object to extract information from. - * @param serverIn The Server object to check for module enablement. + * @param enabledIn Whether the AppStream is enabled. */ - public AppStreamModuleResponse(AppStream appStreamIn, Server serverIn) { + public AppStreamModuleResponse(AppStream appStreamIn, boolean enabledIn) { this.name = appStreamIn.getName(); this.stream = appStreamIn.getStream(); this.arch = appStreamIn.getArch(); - this.enabled = serverIn.hasAppStreamModuleEnabled(this.name, this.stream); + this.enabled = enabledIn; } private final String name; diff --git a/java/code/src/com/suse/manager/webui/controllers/appstreams/response/ChannelAppStreamsResponse.java b/java/code/src/com/suse/manager/webui/controllers/appstreams/response/ChannelAppStreamsResponse.java index f4c2b28392f2..5aed51e675b7 100644 --- a/java/code/src/com/suse/manager/webui/controllers/appstreams/response/ChannelAppStreamsResponse.java +++ b/java/code/src/com/suse/manager/webui/controllers/appstreams/response/ChannelAppStreamsResponse.java @@ -16,32 +16,33 @@ import com.redhat.rhn.domain.channel.AppStream; import com.redhat.rhn.domain.channel.Channel; -import com.redhat.rhn.domain.server.Server; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiPredicate; public class ChannelAppStreamsResponse { /** * Constructs a ChannelAppStreamsResponse object based on the provided parameters. * - * @param channelIn The channel that the AppStreams belong to. - * @param appStreamsIn The set of AppStream objects associated with the channel. - * @param serverIn The Server object used to check module enablement. + * @param channelIn The channel that the AppStreams belong to. + * @param appStreamsIn The set of AppStream objects associated with the channel. + * @param appStreamEnabledChecker The object used to check module enablement. */ public ChannelAppStreamsResponse( Channel channelIn, List appStreamsIn, - Server serverIn + BiPredicate appStreamEnabledChecker ) { channel = new ChannelJson(channelIn); appStreams = new HashMap<>(); appStreamsIn.forEach(it -> { - var module = new AppStreamModuleResponse(it, serverIn); + var enabled = appStreamEnabledChecker.test(it.getName(), it.getStream()); + var module = new AppStreamModuleResponse(it, enabled); if (appStreams.containsKey(it.getName())) { appStreams.get(it.getName()).add(module); } diff --git a/java/code/src/com/suse/manager/webui/templates/activation_keys/appstreams.jade b/java/code/src/com/suse/manager/webui/templates/activation_keys/appstreams.jade new file mode 100644 index 000000000000..8b9904e2ff19 --- /dev/null +++ b/java/code/src/com/suse/manager/webui/templates/activation_keys/appstreams.jade @@ -0,0 +1,31 @@ +.spacewalk-toolbar-h1 + .spacewalk-toolbar + if isActivationKeyAdmin + a(href="/rhn/activationkeys/Clone.do?tid=#{activationKeyId}") + i.fa.fa-files-o(title='Clone Key') + | #{l.t("Clone Key")} + a(href="/rhn/activationkeys/Delete.do?tid=#{activationKeyId}") + i.fa.fa-trash-o(title='Delete Key') + | #{l.t("Delete Key")} + h1 + i.fa.fa-key + | #{' ' + note + ' '} + a(href="/docs/#{docsLocale}/reference/systems/activation-keys.html", target="_blank") + i.fa.fa-question-circle.spacewalk-help-link +!{tabs} +#appstreams + +div#init_channels_appstreams(style="display: none") + | #{channelsAppStreams} + +script(type='text/javascript'). + window.activationKeyId = "#{activationKeyId}"; + window.csrfToken = "#{csrf_token}"; + +script(type='text/javascript'). + spaImportReactPage('activationkeys/appstreams') + .then(function(module) { + module.renderer('appstreams', { + channelsAppStreams: document.getElementById('init_channels_appstreams').textContent, + }) + }); diff --git a/java/code/webapp/WEB-INF/nav/activation_key.xml b/java/code/webapp/WEB-INF/nav/activation_key.xml index 37bdf27fc261..0f2134a5eec3 100644 --- a/java/code/webapp/WEB-INF/nav/activation_key.xml +++ b/java/code/webapp/WEB-INF/nav/activation_key.xml @@ -29,4 +29,6 @@ + diff --git a/java/spacewalk-java.changes.welder.activationkeys-appstreams b/java/spacewalk-java.changes.welder.activationkeys-appstreams new file mode 100644 index 000000000000..57f7a24eb0ba --- /dev/null +++ b/java/spacewalk-java.changes.welder.activationkeys-appstreams @@ -0,0 +1 @@ +- Configure AppStreams via Activation Keys diff --git a/schema/spacewalk/common/tables/suseRegTokenChannelAppStream.sql b/schema/spacewalk/common/tables/suseRegTokenChannelAppStream.sql new file mode 100644 index 000000000000..9722bac6c573 --- /dev/null +++ b/schema/spacewalk/common/tables/suseRegTokenChannelAppStream.sql @@ -0,0 +1,32 @@ +-- +-- Copyright (c) 2024 SUSE LLC +-- +-- This software is licensed to you under the GNU General Public License, +-- version 2 (GPLv2). There is NO WARRANTY for this software, express or +-- implied, including the implied warranties of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 +-- along with this software; if not, see +-- http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. +-- +-- Red Hat trademarks are not licensed under GPLv2. No permission is +-- granted to use or replicate Red Hat trademarks that are incorporated +-- in this software or its documentation. + +CREATE TABLE suseRegTokenChannelAppStream +( + id NUMERIC NOT NULL + CONSTRAINT suse_reg_tok_ch_as_id_pk PRIMARY KEY, + token_id NUMERIC NOT NULL, + channel_id NUMERIC NOT NULL, + name VARCHAR(128) NOT NULL, + stream VARCHAR(128) NOT NULL, + + CONSTRAINT suse_reg_tok_ch_as_id_fk FOREIGN KEY (channel_id, token_id) + REFERENCES rhnRegTokenChannels (channel_id, token_id) + ON DELETE CASCADE +); + +CREATE INDEX suse_reg_tok_ch_as_name_stream_idx + ON suseRegTokenChannelAppStream (name, stream); + +CREATE SEQUENCE suse_reg_tok_ch_as_id_seq; diff --git a/schema/spacewalk/upgrade/susemanager-schema-5.0.7-to-susemanager-schema-5.0.8/100-suseRegTokenAppStreams-create.sql b/schema/spacewalk/upgrade/susemanager-schema-5.0.7-to-susemanager-schema-5.0.8/100-suseRegTokenAppStreams-create.sql new file mode 100644 index 000000000000..5c84e67f04ef --- /dev/null +++ b/schema/spacewalk/upgrade/susemanager-schema-5.0.7-to-susemanager-schema-5.0.8/100-suseRegTokenAppStreams-create.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS suseRegTokenChannelAppStream +( + id NUMERIC NOT NULL + CONSTRAINT suse_reg_tok_ch_as_id_pk PRIMARY KEY, + token_id NUMERIC NOT NULL, + channel_id NUMERIC NOT NULL, + name VARCHAR(128) NOT NULL, + stream VARCHAR(128) NOT NULL, + + CONSTRAINT suse_reg_tok_ch_as_id_fk FOREIGN KEY (channel_id, token_id) + REFERENCES rhnRegTokenChannels (channel_id, token_id) + ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS suse_reg_tok_ch_as_name_stream_idx + ON suseRegTokenChannelAppStream (name, stream); + +CREATE SEQUENCE IF NOT EXISTS suse_reg_tok_ch_as_id_seq; diff --git a/susemanager-utils/susemanager-sls/salt/appstreams/configure.sls b/susemanager-utils/susemanager-sls/salt/appstreams/configure.sls index b5034d7e59f1..5f085bdb66a8 100644 --- a/susemanager-utils/susemanager-sls/salt/appstreams/configure.sls +++ b/susemanager-utils/susemanager-sls/salt/appstreams/configure.sls @@ -1,3 +1,5 @@ +include: +- channels {% if pillar.get('param_appstreams_disable') %} disable_appstreams: appstreams.disabled: @@ -14,8 +16,9 @@ enable_appstreams: {%- for module_name, stream in pillar.get('param_appstreams_enable', []) %} - {{ module_name }}:{{ stream }} {%- endfor %} - {% if pillar.get('param_appstreams_disable') %} - require: + - file: /etc/yum.repos.d/susemanager:channels.repo + {% if pillar.get('param_appstreams_disable') %} - appstreams: disable_appstreams {%- endif %} {%- endif %} diff --git a/web/html/src/manager/appstreams/actions-appstreams.tsx b/web/html/src/manager/appstreams/actions-appstreams.tsx new file mode 100644 index 000000000000..3d8e075b2f1c --- /dev/null +++ b/web/html/src/manager/appstreams/actions-appstreams.tsx @@ -0,0 +1,20 @@ +import { Button } from "components/buttons"; + +export const AppStreamActions = ({ numberOfChanges, onReset, onSubmit }) => { + return ( +
+
+ {numberOfChanges > 0 && ( +
+
+ ); +}; diff --git a/web/html/src/manager/appstreams/appstreams.tsx b/web/html/src/manager/appstreams/appstreams.tsx index 3e395c79df98..8fcdc27908a3 100644 --- a/web/html/src/manager/appstreams/appstreams.tsx +++ b/web/html/src/manager/appstreams/appstreams.tsx @@ -3,45 +3,24 @@ import { useState } from "react"; import { ActionChain } from "components/action-schedule"; import { Messages, MessageType, Utils as MessageUtils } from "components/messages"; -import { AppStreamModule, ChannelAppStream } from "./appstreams.type"; +import { ChannelAppStream } from "./appstreams.type"; import { AppStreamsChangesConfirm } from "./changes-confirm-appstreams"; import { AppStreamsList } from "./list-appstreams"; +import { getStreamName, handleModuleEnableDisable } from "./utils"; type Props = { channelsAppStreams: Array; }; -export const getStreamName = (module: AppStreamModule) => `${module.name}:${module.stream}`; - const AppStreams = ({ channelsAppStreams }: Props) => { const [appStreams, setAppStreams] = useState(channelsAppStreams); - const [toEnable, setToEnable] = useState>([]); - const [toDisable, setToDisable] = useState>([]); + const [toEnable, setToEnable] = useState>>(new Map()); + const [toDisable, setToDisable] = useState>>(new Map()); const [showConfirm, setShowConfirm] = useState(false); const [scheduledMsg, setScheduledMsg] = useState([]); - const isStreamEnabled = (stream: AppStreamModule) => - toEnable.includes(getStreamName(stream)) || (stream.enabled && !toDisable.includes(getStreamName(stream))); - - const handleEnableDisable = (appStream: AppStreamModule) => { - const stream = `${appStream.name}:${appStream.stream}`; - if (appStream.enabled) { - setToDisable((prevState) => - prevState.includes(stream) ? prevState.filter((it) => it !== stream) : prevState.concat(stream) - ); - } else { - setToEnable((prevState) => - prevState.includes(stream) ? prevState.filter((it) => it !== stream) : prevState.concat(stream) - ); - } - - // Disable every other stream of the module - appStreams.forEach((ch) => - ch.appStreams[appStream.name] - ?.filter((as) => getStreamName(as) !== stream && isStreamEnabled(as)) - .forEach((as) => handleEnableDisable(as)) - ); - }; + const handleEnableDisable = (channel, appStream) => + handleModuleEnableDisable(channel, appStream, appStreams, toEnable, toDisable, setToEnable, setToDisable); /** * After scheduling the action, apply the changes optimistically to the currently displayed list @@ -55,7 +34,9 @@ const AppStreams = ({ channelsAppStreams }: Props) => { ...Object.keys(ch.appStreams).reduce((acc, key) => { acc[key] = ch.appStreams[key].map((as) => ({ ...as, - enabled: toEnable.includes(getStreamName(as)) || (!toDisable.includes(getStreamName(as)) && as.enabled), + enabled: + toEnable.get(ch.channel.id)?.includes(getStreamName(as)) || + (!toDisable.get(ch.channel.id)?.includes(getStreamName(as)) && as.enabled), })); return acc; }, {}), @@ -98,16 +79,16 @@ const AppStreams = ({ channelsAppStreams }: Props) => { }; const handleReset = () => { - setToEnable([]); - setToDisable([]); + setToEnable(new Map()); + setToDisable(new Map()); }; const showContent = () => { if (showConfirm) { return ( setShowConfirm(false)} onConfirm={handleConfirmChanges} onError={handleConfirmError} diff --git a/web/html/src/manager/appstreams/channel-appstreams.tsx b/web/html/src/manager/appstreams/channel-appstreams.tsx index 15b306507679..3810b3cffee8 100644 --- a/web/html/src/manager/appstreams/channel-appstreams.tsx +++ b/web/html/src/manager/appstreams/channel-appstreams.tsx @@ -1,16 +1,25 @@ -import { getStreamName } from "./appstreams"; -import { AppStreamModule } from "./appstreams.type"; +import { AppStreamModule, Channel } from "./appstreams.type"; +import { getStreamName } from "./utils"; interface Props { + channel: Channel; streams: AppStreamModule[]; moduleName: string; - toEnable: string[]; - toDisable: string[]; - showPackages: (string) => void; + toEnable: Map; + toDisable: Map; + showPackages?: (string) => void; onToggle: (AppStreamModule) => void; } -export const ChannelAppStreams = ({ streams, moduleName, showPackages, toEnable, toDisable, onToggle }: Props) => { +export const ChannelAppStreams = ({ + channel, + streams, + moduleName, + showPackages, + toEnable, + toDisable, + onToggle, +}: Props) => { // Sort stream names alphanumerically in descending order streams.sort((m1, m2) => getStreamName(m2).localeCompare(getStreamName(m1), "en", { numeric: true })); return ( @@ -19,17 +28,21 @@ export const ChannelAppStreams = ({ streams, moduleName, showPackages, toEnable, const stream = getStreamName(moduleStream); const changedModules = streams.map((s) => { - const isChanged = s.enabled ? toDisable.includes(getStreamName(s)) : toEnable.includes(getStreamName(s)); + const isChanged = s.enabled + ? toDisable.get(channel.id)?.includes(getStreamName(s)) + : toEnable.get(channel.id)?.includes(getStreamName(s)); return isChanged ? s.name : null; }); const changedStatus = changedModules.includes(moduleName); - const enabled = toEnable.includes(stream) || (moduleStream.enabled && !toDisable.includes(stream)); + const enabled = + toEnable.get(channel.id)?.includes(stream) || + (moduleStream.enabled && !toDisable.get(channel.id)?.includes(stream)); return ( {streamIdx === 0 && {moduleName}} - diff --git a/web/html/src/manager/appstreams/list-appstreams.tsx b/web/html/src/manager/appstreams/list-appstreams.tsx index 98fdd301b911..154514fb818e 100644 --- a/web/html/src/manager/appstreams/list-appstreams.tsx +++ b/web/html/src/manager/appstreams/list-appstreams.tsx @@ -1,20 +1,19 @@ import { useState } from "react"; -import { Button } from "components/buttons"; -import { Panel } from "components/panels/Panel"; - import { Dialog } from "../../components/dialog/Dialog"; +import { AppStreamActions } from "./actions-appstreams"; import { AppStreamPackages } from "./appstream-packages"; -import { AppStreamModule, ChannelAppStream } from "./appstreams.type"; -import { ChannelAppStreams } from "./channel-appstreams"; +import { AppStreamModule, Channel, ChannelAppStream } from "./appstreams.type"; +import { AppStreamPanel } from "./panel-appstream"; +import { numberOfChanges } from "./utils"; interface Props { channelsAppStreams: ChannelAppStream[]; - toEnable: string[]; - toDisable: string[]; + toEnable: Map; + toDisable: Map; onReset: () => void; onSubmitChanges: () => void; - onModuleEnableDisable: (module: AppStreamModule) => void; + onModuleEnableDisable: (channel: Channel, module: AppStreamModule) => void; } export const AppStreamsList = ({ @@ -26,8 +25,7 @@ export const AppStreamsList = ({ onModuleEnableDisable, }: Props) => { const [moduleToShowPackages, setModuleToShowPackages] = useState<{ stream: string; channelId: number } | null>(null); - const numberOfChanges = toEnable.length + toDisable.length; - const hasChanges = () => !(toEnable.length === 0 && toDisable.length === 0); + const changes = numberOfChanges(toEnable, toDisable); // Sort channels by label channelsAppStreams.sort((a, b) => a.channel.name.localeCompare(b.channel.name)); @@ -35,52 +33,20 @@ export const AppStreamsList = ({ return ( <>

{t("The following AppStream modules are currently available to the system.")}

-
-
- {hasChanges() && ( -
-
+ {channelsAppStreams.map((channelAppStream) => { const { channel, appStreams } = channelAppStream; + const showPackages = (stream) => setModuleToShowPackages({ stream: stream, channelId: channel.id }); return ( - - - - - - - - - - - {Object.keys(appStreams).map((moduleName) => ( - setModuleToShowPackages({ stream: stream, channelId: channel.id })} - key={moduleName} - streams={appStreams[moduleName]} - moduleName={moduleName} - toEnable={toEnable} - toDisable={toDisable} - onToggle={onModuleEnableDisable} - /> - ))} - -
{t("Modules")}{t("Streams")}{t("Enabled")}
-
+ onModuleEnableDisable(channel, appStream)} + showPackages={showPackages} + /> ); })} ; + toEnable: Map; + toDisable: Map; + onToggle: (arg: AppStreamModule) => void; + showPackages?: (arg: string) => void; +} + +export const AppStreamPanel = ({ channel, appStreams, toEnable, toDisable, onToggle, showPackages }: Props) => ( + + + + + + + + + + + {Object.keys(appStreams).map((moduleName) => ( + + ))} + +
{t("Modules")}{t("Streams")}{t("Enabled")}
+
+); diff --git a/web/html/src/manager/appstreams/utils.ts b/web/html/src/manager/appstreams/utils.ts new file mode 100644 index 000000000000..9341c5a270dd --- /dev/null +++ b/web/html/src/manager/appstreams/utils.ts @@ -0,0 +1,46 @@ +import { AppStreamModule, Channel, ChannelAppStream } from "./appstreams.type"; + +export const getStreamName = (module: AppStreamModule) => `${module.name}:${module.stream}`; + +export const handleModuleEnableDisable = ( + channel: Channel, + appStream: AppStreamModule, + appStreams: ChannelAppStream[], + toEnable: Map, + toDisable: Map, + setToEnable: React.Dispatch>>, + setToDisable: React.Dispatch>> +) => { + const handleReactStateChange = (prevState: Map, stream: string) => { + const updatedMap = new Map(prevState); + let newList = updatedMap.get(channel.id) ?? []; + newList = newList.includes(stream) ? (newList = newList.filter((it) => it !== stream)) : newList.concat(stream); + updatedMap.set(channel.id, newList); + return updatedMap; + }; + + const isStreamEnabled = (stream: AppStreamModule) => + toEnable.get(channel.id)?.includes(getStreamName(stream)) || + (stream.enabled && !toDisable.get(channel.id)?.includes(getStreamName(stream))); + + const stream = getStreamName(appStream); + const setStateFunction = appStream.enabled ? setToDisable : setToEnable; + setStateFunction((prevState) => handleReactStateChange(prevState, stream)); + + // Disable every other stream of the module + appStreams.forEach((ch) => + ch.appStreams[appStream.name] + ?.filter((as) => getStreamName(as) !== stream && isStreamEnabled(as)) + .forEach((as) => + handleModuleEnableDisable(ch.channel, as, appStreams, toEnable, toDisable, setToEnable, setToDisable) + ) + ); +}; + +export const numberOfChanges = (toEnable: Map, toDisable: Map) => { + return reduceMapListLength(toEnable) + reduceMapListLength(toDisable); +}; + +const reduceMapListLength = (mapToReduce: Map) => { + return Array.from(mapToReduce.keys()).reduce((acc, key) => acc + (mapToReduce.get(key)?.length ?? 0), 0); +}; diff --git a/web/html/src/manager/index.ts b/web/html/src/manager/index.ts index 336726312981..ecfbce64573a 100644 --- a/web/html/src/manager/index.ts +++ b/web/html/src/manager/index.ts @@ -33,6 +33,7 @@ import ScheduleOptions from "./schedule-options"; import Shared from "./shared"; import Highstate from "./state"; import Systems from "./systems"; +import ActivationKeys from "./systems/activation-key"; import Virtualization from "./virtualization"; const pages = { @@ -58,6 +59,7 @@ const pages = { ...Systems, ...Virtualization, ...Appstreams, + ...ActivationKeys, }; window.spaImportReactPage = function spaImportReactPage(pageName) { diff --git a/web/html/src/manager/systems/activation-key/activation-keys-appstreams.renderer.tsx b/web/html/src/manager/systems/activation-key/activation-keys-appstreams.renderer.tsx new file mode 100644 index 000000000000..8f5bd83354da --- /dev/null +++ b/web/html/src/manager/systems/activation-key/activation-keys-appstreams.renderer.tsx @@ -0,0 +1,20 @@ +import SpaRenderer from "core/spa/spa-renderer"; + +import AppStreams from "./activation-keys-appstreams"; + +type RendererProps = { + channelsAppStreams?: string; +}; + +export const renderer = (id: string, { channelsAppStreams }: RendererProps) => { + let channelsAppStreamsJson: any[] = []; + try { + channelsAppStreamsJson = JSON.parse(channelsAppStreams || ""); + } catch (error) { + Loggerhead.error(error); + } + SpaRenderer.renderNavigationReact( + , + document.getElementById(id) + ); +}; diff --git a/web/html/src/manager/systems/activation-key/activation-keys-appstreams.tsx b/web/html/src/manager/systems/activation-key/activation-keys-appstreams.tsx new file mode 100644 index 000000000000..bd792dfd9acd --- /dev/null +++ b/web/html/src/manager/systems/activation-key/activation-keys-appstreams.tsx @@ -0,0 +1,95 @@ +import { useState } from "react"; + +import { AppStreamActions } from "manager/appstreams/actions-appstreams"; +import { AppStreamPanel } from "manager/appstreams/panel-appstream"; +import { handleModuleEnableDisable, numberOfChanges } from "manager/appstreams/utils"; + +import { Messages, MessageType, Utils as MessageUtils } from "components/messages"; + +import Network from "utils/network"; + +import { ChannelAppStream } from "../../appstreams/appstreams.type"; + +declare global { + interface Window { + activationKeyId: number; + } +} + +type Props = { + channelsAppStreams: Array; +}; + +const AppStreams = (props: Props) => { + const [channelsAppStreams, setChannelAppStreams] = useState(props.channelsAppStreams); + const [toEnable, setToEnable] = useState>>(new Map()); + const [toDisable, setToDisable] = useState>>(new Map()); + const [statusMessage, setStatusMessage] = useState([]); + const handleEnableDisable = (channel, appStream) => + handleModuleEnableDisable(channel, appStream, channelsAppStreams, toEnable, toDisable, setToEnable, setToDisable); + + const handleReset = () => { + setToEnable(new Map()); + setToDisable(new Map()); + }; + + const handleError = (error) => { + setStatusMessage(MessageUtils.error(t("Error updating activation key."))); + Loggerhead.error(error); + }; + + const handleConfirm = (data) => { + setStatusMessage(MessageUtils.info(t("Activation key has been modified."))); + handleReset(); + setChannelAppStreams(data); + }; + + const handleSubmit = () => { + const channelsKeys = new Set([...toEnable.keys(), ...toDisable.keys()]); + const changes = new Map(); + channelsKeys.forEach((channelKey) => { + changes.set(channelKey, { + toInclude: toEnable.get(channelKey) ?? [], + toRemove: toDisable.get(channelKey) ?? [], + }); + }); + const request = Network.post(`/rhn/manager/api/activationkeys/appstreams/save?tid=${window.activationKeyId}`, { + changes, + }) + .then((data) => { + handleConfirm(data.data); + }) + .catch((jqXHR) => handleError(Network.responseErrorMessage(jqXHR))); + + return request; + }; + + const changes = numberOfChanges(toEnable, toDisable); + + return ( + <> + {statusMessage.length > 0 && } +

+ + {t("AppStreams")} +   +

+ + {channelsAppStreams.map((channelAppStreams) => { + const { channel, appStreams } = channelAppStreams; + return ( + handleEnableDisable(channel, appStream)} + /> + ); + })} + + ); +}; + +export default AppStreams; diff --git a/web/html/src/manager/systems/activation-key/index.ts b/web/html/src/manager/systems/activation-key/index.ts new file mode 100644 index 000000000000..6701e15bf5e0 --- /dev/null +++ b/web/html/src/manager/systems/activation-key/index.ts @@ -0,0 +1,3 @@ +export default { + "activationkeys/appstreams": () => import("./activation-keys-appstreams.renderer"), +}; diff --git a/web/spacewalk-web.changes.welder.activationkeys-appstreams b/web/spacewalk-web.changes.welder.activationkeys-appstreams new file mode 100644 index 000000000000..57f7a24eb0ba --- /dev/null +++ b/web/spacewalk-web.changes.welder.activationkeys-appstreams @@ -0,0 +1 @@ +- Configure AppStreams via Activation Keys