diff --git a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBody.java b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBody.java index 7e158f74e..0b3cd4992 100644 --- a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBody.java +++ b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/rollout/MgmtRolloutRestRequestBody.java @@ -32,6 +32,8 @@ public class MgmtRolloutRestRequestBody extends AbstractMgmtRolloutConditionsEnt private Long forcetime; + private Long startAt; + private MgmtActionType type; private List groups; @@ -124,4 +126,19 @@ public class MgmtRolloutRestRequestBody extends AbstractMgmtRolloutConditionsEnt public void setGroups(List groups) { this.groups = groups; } + + /** + * @return the start at timestamp in millis or null + */ + public Long getStartAt() { + return startAt; + } + + /** + * @param startAt + * the start at timestamp in millis or null + */ + public void setStartAt(Long startAt) { + this.startAt = startAt; + } } diff --git a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java index 9d97b9bbd..69dabc850 100644 --- a/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java +++ b/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutMapper.java @@ -102,7 +102,7 @@ final class MgmtRolloutMapper { return entityFactory.rollout().create().name(restRequest.getName()).description(restRequest.getDescription()) .set(distributionSet).targetFilterQuery(restRequest.getTargetFilterQuery()) .actionType(MgmtRestModelMapper.convertActionType(restRequest.getType())) - .forcedTime(restRequest.getForcetime()); + .forcedTime(restRequest.getForcetime()).startAt(restRequest.getStartAt()); } static RolloutGroupCreate fromRequest(final EntityFactory entityFactory, final MgmtRolloutGroup restRequest) { diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java index 68108622d..2d8a96d2b 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java @@ -9,6 +9,7 @@ package org.eclipse.hawkbit.repository; import java.util.List; +import java.util.concurrent.Future; import javax.validation.constraints.NotNull; @@ -27,11 +28,13 @@ import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus; import org.eclipse.hawkbit.repository.model.RolloutGroupConditions; +import org.eclipse.hawkbit.repository.model.RolloutGroupsValidation; import org.hibernate.validator.constraints.NotEmpty; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.util.concurrent.ListenableFuture; /** * RolloutManagement to control rollouts e.g. like creating, starting, resuming @@ -42,7 +45,7 @@ public interface RolloutManagement { /** * Checking running rollouts. Rollouts which are checked updating the - * {@link Rollout#setLastCheck(long)} to indicate that the current instance + * lastCheck to indicate that the current instance * is handling the specific rollout. This code should run as system-code. * *
@@ -55,8 +58,8 @@ public interface RolloutManagement {
      *  }
      * 
* - * This method is attend to be called by a scheduler. - * {@link RolloutScheduler}. And must be running in an transaction so it's + * This method is intended to be called by a scheduler. + * And must be running in an transaction so it's * splitted from the scheduler. * * Rollouts which are currently running are investigated, by means the @@ -95,6 +98,17 @@ public interface RolloutManagement { @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE) void checkStartingRollouts(long delayBetweenChecks); + /** + * Checking Rollouts that are currently ready for an auto start. + * + * @param delayBetweenChecks + * the time in milliseconds of the delay between the further and + * this check. This check is only applied if the last check is + * less than (lastcheck-delay). + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE) + void checkReadyRollouts(long delayBetweenChecks); + /** * Counts all {@link Rollout}s in the repository. * @@ -181,6 +195,24 @@ public interface RolloutManagement { Rollout createRollout(@NotNull RolloutCreate rollout, @NotNull List groups, RolloutGroupConditions conditions); + /** + * Calculates how many targets are addressed by each rollout group and + * returns the validation information. + * + * @param groups + * a list of rollout groups + * @param targetFilter + * the rollout + * @param createdAt + * timestamp when the rollout was created + * @return the validation information + * @throws RolloutIllegalStateException + * thrown when no targets are targeted by the rollout + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_READ_AND_TARGET_READ) + ListenableFuture validateTargetsInGroups(List groups, + String targetFilter, Long createdAt); + /** * Can be called on a Rollout in {@link RolloutStatus#CREATING} to * automatically fill it with targets. @@ -365,10 +397,6 @@ public interface RolloutManagement { * * @param update * rollout to be updated - * @param name - * to update or null - * @param description - * to update or null * * @return Rollout updated rollout */ diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutProperties.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutProperties.java index 86ba8d91d..1394cc271 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutProperties.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutProperties.java @@ -25,6 +25,9 @@ public class RolloutProperties { // used by @Scheduled annotation which needs constant public static final String PROP_STARTING_SCHEDULER_DELAY_PLACEHOLDER = "${hawkbit.rollout.startingScheduler.fixedDelay:2000}"; + // used by @Scheduled annotation which needs constant + public static final String PROP_READY_SCHEDULER_DELAY_PLACEHOLDER = "${hawkbit.rollout.readyScheduler.fixedDelay:30000}"; + /** * Rollout scheduler configuration. */ @@ -65,6 +68,8 @@ public class RolloutProperties { private final Scheduler startingScheduler = new Scheduler(2000L); + private final Scheduler readyScheduler = new Scheduler(30000L); + public Scheduler getScheduler() { return scheduler; } @@ -76,4 +81,8 @@ public class RolloutProperties { public Scheduler getStartingScheduler() { return startingScheduler; } + + public Scheduler getReadyScheduler() { + return readyScheduler; + } } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java index 17c92bf03..5ab4266f8 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TargetFilterQueryManagement.java @@ -108,6 +108,18 @@ public interface TargetFilterQueryManagement { @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) Page findTargetFilterQueryByFilter(@NotNull Pageable pageable, @NotNull String rsqlFilter); + /** + * Retrieves all target filter query which have exactly the provided query. + * + * @param pageable + * pagination parameter + * @param query + * the query saved in the target filter query + * @return the page with the found {@link TargetFilterQuery} + */ + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) + Page findTargetFilterQueryByQuery(@NotNull Pageable pageable, String query); + /** * Retrieves all target filter query which {@link TargetFilterQuery}. * diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutCreate.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutCreate.java index 5608c524d..3ddde7ff7 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutCreate.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutCreate.java @@ -76,6 +76,13 @@ public interface RolloutCreate { */ RolloutCreate forcedTime(Long forcedTime); + /** + * @param startAt + * for {@link Rollout#getStartAt()} + * @return updated builder instance + */ + RolloutCreate startAt(Long startAt); + /** * @return peek on current state of {@link Rollout} in the builder */ diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutUpdate.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutUpdate.java index 98dfb514d..1f5d03e5c 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutUpdate.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/builder/RolloutUpdate.java @@ -8,9 +8,14 @@ */ package org.eclipse.hawkbit.repository.builder; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Rollout; import org.hibernate.validator.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Optional; + /** * Builder to update an existing {@link Rollout} entry. Defines all fields that * can be updated. @@ -30,4 +35,33 @@ public interface RolloutUpdate { * @return updated builder instance */ RolloutUpdate description(String description); + + /** + * @param setId + * for {@link Rollout#getDistributionSet()} + * @return updated builder instance + */ + RolloutUpdate set(long setId); + + /** + * @param actionType + * for {@link Rollout#getActionType()} + * @return updated builder instance + */ + RolloutUpdate actionType(@NotNull Action.ActionType actionType); + + /** + * @param forcedTime + * for {@link Rollout#getForcedTime()} + * @return updated builder instance + */ + RolloutUpdate forcedTime(Long forcedTime); + + /** + * @param startAt + * for {@link Rollout#getStartAt()} + * @return updated builder instance + */ + RolloutUpdate startAt(Long startAt); + } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java index 7124d027d..f964d6442 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Rollout.java @@ -59,6 +59,12 @@ public interface Rollout extends NamedEntity { */ long getForcedTime(); + /** + * @return Timestamp when the rollout should be started automatically. Can be null. + */ + Long getStartAt(); + + /** * @return number of {@link Target}s in this rollout. */ diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroupsValidation.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroupsValidation.java new file mode 100644 index 000000000..b937c3ffc --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/RolloutGroupsValidation.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.model; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * Represents information to validate the correct distribution of targets to + * rollout groups. + */ +public class RolloutGroupsValidation { + + /** + * The total amount of targets in a {@link Rollout} + */ + private long totalTargets; + + /** + * A list containing the count of targets for each {@link RolloutGroup} + */ + private List targetsPerGroup; + + /** + * Instantiates a new validation result + * + * @param totalTargets + * The total amount of targets in a {@link Rollout} + * @param targetsPerGroup + * A list containing the count of targets for each + * {@link RolloutGroup} + */ + public RolloutGroupsValidation(final long totalTargets, @NotNull final List targetsPerGroup) { + this.totalTargets = totalTargets; + this.targetsPerGroup = targetsPerGroup; + } + + public long getTotalTargets() { + return totalTargets; + } + + public List getTargetsPerGroup() { + return targetsPerGroup; + } + + /** + * @return the count of targets that are in groups + */ + public long getTargetsInGroups() { + return targetsPerGroup.stream().mapToLong(Long::longValue).sum(); + } + + /** + * @return whether the groups contain all targets + */ + public boolean isValid() { + return totalTargets == getTargetsInGroups(); + } +} diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractRolloutUpdateCreate.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractRolloutUpdateCreate.java index a83744ce7..6c413a929 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractRolloutUpdateCreate.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/builder/AbstractRolloutUpdateCreate.java @@ -10,6 +10,8 @@ package org.eclipse.hawkbit.repository.builder; import org.eclipse.hawkbit.repository.model.Action.ActionType; +import java.util.Optional; + /** * Create and update builder DTO. * @@ -21,6 +23,7 @@ public abstract class AbstractRolloutUpdateCreate extends AbstractNamedEntity protected String targetFilterQuery; protected ActionType actionType; protected Long forcedTime; + protected Long startAt; public T set(final long set) { this.set = set; @@ -42,4 +45,24 @@ public abstract class AbstractRolloutUpdateCreate extends AbstractNamedEntity return (T) this; } + public T startAt(final Long startAt) { + this.startAt = startAt; + return (T) this; + } + + public Optional getSet() { + return Optional.ofNullable(set); + } + + public Optional getActionType() { + return Optional.ofNullable(actionType); + } + + public Optional getForcedTime() { + return Optional.ofNullable(forcedTime); + } + + public Optional getStartAt() { + return Optional.ofNullable(startAt); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java index 17aa916dd..4478de556 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java @@ -8,11 +8,13 @@ */ package org.eclipse.hawkbit.repository.jpa; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Function; import java.util.stream.Collectors; import javax.persistence.EntityNotFoundException; @@ -20,6 +22,7 @@ import javax.validation.ConstraintDeclarationException; import org.apache.commons.lang3.StringUtils; import org.eclipse.hawkbit.repository.DeploymentManagement; +import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.RolloutFields; import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutManagement; @@ -36,7 +39,6 @@ import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecuto import org.eclipse.hawkbit.repository.jpa.model.JpaAction; import org.eclipse.hawkbit.repository.jpa.model.JpaRollout; import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup; -import org.eclipse.hawkbit.repository.jpa.model.JpaRollout_; import org.eclipse.hawkbit.repository.jpa.model.RolloutTargetGroup; import org.eclipse.hawkbit.repository.jpa.rollout.condition.RolloutGroupActionEvaluator; import org.eclipse.hawkbit.repository.jpa.rollout.condition.RolloutGroupConditionEvaluator; @@ -52,6 +54,7 @@ import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupErrorCondit import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupStatus; import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessCondition; import org.eclipse.hawkbit.repository.model.RolloutGroupConditions; +import org.eclipse.hawkbit.repository.model.RolloutGroupsValidation; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TotalTargetCountActionStatus; import org.eclipse.hawkbit.repository.model.TotalTargetCountStatus; @@ -63,12 +66,13 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.Modifying; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; @@ -78,6 +82,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.DefaultTransactionDefinition; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.concurrent.ListenableFuture; import org.springframework.validation.annotation.Validated; /** @@ -89,8 +94,7 @@ public class JpaRolloutManagement implements RolloutManagement { private static final Logger LOGGER = LoggerFactory.getLogger(RolloutManagement.class); /** - * Maximum amount of targets that are assigned to a Rollout Group in one - * transaction. + * Max amount of targets that are handled in one transaction. */ private static final int TRANSACTION_TARGETS = 1000; @@ -112,6 +116,9 @@ public class JpaRolloutManagement implements RolloutManagement { @Autowired private RolloutGroupManagement rolloutGroupManagement; + @Autowired + private DistributionSetManagement distributionSetManagement; + @Autowired private ActionRepository actionRepository; @@ -135,15 +142,7 @@ public class JpaRolloutManagement implements RolloutManagement { @Override public Page findAll(final Pageable pageable) { - return convertPage(rolloutRepository.findAll(pageable), pageable); - } - - private static Page convertPage(final Page findAll, final Pageable pageable) { - return new PageImpl<>(Collections.unmodifiableList(findAll.getContent()), pageable, findAll.getTotalElements()); - } - - private static Slice convertPage(final Slice findAll, final Pageable pageable) { - return new PageImpl<>(Collections.unmodifiableList(findAll.getContent()), pageable, 0); + return RolloutHelper.convertPage(rolloutRepository.findAll(pageable), pageable); } @Override @@ -153,7 +152,7 @@ public class JpaRolloutManagement implements RolloutManagement { virtualPropertyReplacer); final Page findAll = rolloutRepository.findAll(specification, pageable); - return convertPage(findAll, pageable); + return RolloutHelper.convertPage(findAll, pageable); } @Override @@ -246,7 +245,8 @@ public class JpaRolloutManagement implements RolloutManagement { .collect(Collectors.toList()); groups.forEach(RolloutHelper::verifyRolloutGroupHasConditions); - verifyRolloutGroupTargetCounts(groups, savedRollout); + RolloutHelper.verifyRemainingTargets( + calculateRemainingTargets(groups, savedRollout.getTargetFilterQuery(), savedRollout.getCreatedAt())); // Persisting the groups RolloutGroup lastSavedGroup = null; @@ -401,22 +401,51 @@ public class JpaRolloutManagement implements RolloutManagement { targets.forEach(target -> rolloutTargetGroupRepository.save(new RolloutTargetGroup(group, target))); } - private void verifyRolloutGroupTargetCounts(final List groups, final JpaRollout rollout) { - final String baseFilter = RolloutHelper.getTargetFilterQuery(rollout); + private long calculateRemainingTargets(final List groups, final String targetFilter, + final Long createdAt) { + final String baseFilter = RolloutHelper.getTargetFilterQuery(targetFilter, createdAt); final long totalTargets = targetManagement.countTargetByTargetFilterQuery(baseFilter); if (totalTargets == 0) { throw new ConstraintDeclarationException("Rollout target filter does not match any targets"); } - long targetCount = totalTargets; + RolloutGroupsValidation validation = validateTargetsInGroups(groups, baseFilter, totalTargets); + + return totalTargets - validation.getTargetsInGroups(); + } + + @Override + @Async + public ListenableFuture validateTargetsInGroups(final List groups, + final String targetFilter, final Long createdAt) { + + final String baseFilter = RolloutHelper.getTargetFilterQuery(targetFilter, createdAt); + final long totalTargets = targetManagement.countTargetByTargetFilterQuery(baseFilter); + if (totalTargets == 0) { + throw new ConstraintDeclarationException("Rollout target filter does not match any targets"); + } + + return new AsyncResult<>(validateTargetsInGroups( + groups.stream().map(RolloutGroupCreate::build).collect(Collectors.toList()), baseFilter, totalTargets)); + } + + private RolloutGroupsValidation validateTargetsInGroups(final List groups, final String baseFilter, + final long totalTargets) { + final List groupTargetCounts = new ArrayList<>(groups.size()); + final Map targetFilterCounts = groups.stream() + .map(group -> RolloutHelper.getGroupTargetFilter(baseFilter, group)).distinct().collect(Collectors + .toMap(Function.identity(), filter -> targetManagement.countTargetByTargetFilterQuery(filter))); + long unusedTargetsCount = 0; for (int i = 0; i < groups.size(); i++) { final RolloutGroup group = groups.get(i); + String groupTargetFilter = RolloutHelper.getGroupTargetFilter(baseFilter, group); RolloutHelper.verifyRolloutGroupTargetPercentage(group.getTargetPercentage()); - final long targetsInGroupFilter = countTargetsOfGroup(baseFilter, totalTargets, group); - final long overlappingTargets = countOverlappingTargetsWithPreviousGroups(baseFilter, groups, group, i); + final long targetsInGroupFilter = targetFilterCounts.get(groupTargetFilter); + final long overlappingTargets = countOverlappingTargetsWithPreviousGroups(baseFilter, groups, group, i, + targetFilterCounts); final long realTargetsInGroup; // Assume that targets which were not used in the previous groups @@ -430,37 +459,31 @@ public class JpaRolloutManagement implements RolloutManagement { final long reducedTargetsInGroup = Math .round(group.getTargetPercentage() / 100 * (double) realTargetsInGroup); - targetCount -= reducedTargetsInGroup; + groupTargetCounts.add(reducedTargetsInGroup); unusedTargetsCount += realTargetsInGroup - reducedTargetsInGroup; } - RolloutHelper.verifyRemainingTargets(targetCount); - - } - - private long countTargetsOfGroup(final String baseFilter, final long baseFilterCount, final RolloutGroup group) { - if (StringUtils.isEmpty(group.getTargetFilterQuery())) { - return baseFilterCount; - } else { - return targetManagement.countTargetByTargetFilterQuery(baseFilter + ";" + group.getTargetFilterQuery()); - } + return new RolloutGroupsValidation(totalTargets, groupTargetCounts); } private long countOverlappingTargetsWithPreviousGroups(final String baseFilter, final List groups, - final RolloutGroup group, final int groupIndex) { + final RolloutGroup group, final int groupIndex, final Map targetFilterCounts) { // there can't be overlapping targets in the first group if (groupIndex == 0) { return 0; } final List previousGroups = groups.subList(0, groupIndex); - String overlappingTargetsFilter = RolloutHelper.getOverlappingWithGroupsTargetFilter(previousGroups, group); - if (StringUtils.isEmpty(overlappingTargetsFilter)) { - overlappingTargetsFilter = baseFilter; + String overlappingTargetsFilter = RolloutHelper.getOverlappingWithGroupsTargetFilter(baseFilter, previousGroups, + group); + + if (targetFilterCounts.containsKey(overlappingTargetsFilter)) { + return targetFilterCounts.get(overlappingTargetsFilter); } else { - overlappingTargetsFilter = baseFilter + ";" + overlappingTargetsFilter; + final long overlappingTargets = targetManagement.countTargetByTargetFilterQuery(overlappingTargetsFilter); + targetFilterCounts.put(overlappingTargetsFilter, overlappingTargets); + return overlappingTargets; } - return targetManagement.countTargetByTargetFilterQuery(overlappingTargetsFilter); } @Override @@ -469,8 +492,9 @@ public class JpaRolloutManagement implements RolloutManagement { public Rollout startRollout(final Long rolloutId) { final JpaRollout rollout = Optional.ofNullable(rolloutRepository.findOne(rolloutId)) .orElseThrow(() -> new EntityNotFoundException("Rollout with id " + rolloutId + " not found.")); - checkIfRolloutCanStarted(rollout, rollout); + RolloutHelper.checkIfRolloutCanStarted(rollout, rollout); rollout.setStatus(RolloutStatus.STARTING); + rollout.setLastCheck(0); return rolloutRepository.save(rollout); } @@ -649,19 +673,11 @@ public class JpaRolloutManagement implements RolloutManagement { @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED) @Modifying public void checkRunningRollouts(final long delayBetweenChecks) { - final long lastCheck = System.currentTimeMillis(); - final int updated = rolloutRepository.updateLastCheck(lastCheck, delayBetweenChecks, RolloutStatus.RUNNING); - - if (updated == 0) { - // nothing to check, maybe another instance already checked in - // between - LOGGER.debug("No rolloutcheck necessary for current scheduled check {}, next check at {}", lastCheck, - lastCheck + delayBetweenChecks); + final List rolloutsToCheck = getRolloutsToCheckForStatus(delayBetweenChecks, RolloutStatus.RUNNING); + if (rolloutsToCheck.isEmpty()) { return; } - final List rolloutsToCheck = rolloutRepository.findByLastCheckAndStatus(lastCheck, - RolloutStatus.RUNNING); LOGGER.info("Found {} running rollouts to check", rolloutsToCheck.size()); for (final JpaRollout rollout : rolloutsToCheck) { @@ -721,10 +737,9 @@ public class JpaRolloutManagement implements RolloutManagement { final long updatedTargetCount = jpaRollout.getTotalTargets() - (rolloutGroup.getTotalTargets() - countTargetsOfRolloutGroup); jpaRollout.setTotalTargets(updatedTargetCount); - final JpaRolloutGroup jpaRolloutGroup = rolloutGroup; - jpaRolloutGroup.setTotalTargets((int) countTargetsOfRolloutGroup); + rolloutGroup.setTotalTargets((int) countTargetsOfRolloutGroup); rolloutRepository.save(jpaRollout); - rolloutGroupRepository.save(jpaRolloutGroup); + rolloutGroupRepository.save(rolloutGroup); } private long countTargetsFrom(final JpaRolloutGroup rolloutGroup) { @@ -811,18 +826,12 @@ public class JpaRolloutManagement implements RolloutManagement { @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED) @Modifying public void checkCreatingRollouts(final long delayBetweenChecks) { - final long lastCheck = System.currentTimeMillis(); - final int updated = rolloutRepository.updateLastCheck(lastCheck, delayBetweenChecks, RolloutStatus.CREATING); - if (updated == 0) { - // nothing to check, maybe another instance already checked in - // between - LOGGER.debug("No rollouts creating check necessary for current scheduled check {}, next check at {}", - lastCheck, lastCheck + delayBetweenChecks); + final List rolloutsToCheck = getRolloutsToCheckForStatus(delayBetweenChecks, RolloutStatus.CREATING) + .stream().map(Rollout::getId).collect(Collectors.toList()); + if (rolloutsToCheck.isEmpty()) { return; } - final List rolloutsToCheck = rolloutRepository.findByLastCheckAndStatus(lastCheck, RolloutStatus.CREATING) - .stream().map(Rollout::getId).collect(Collectors.toList()); LOGGER.info("Found {} creating rollouts to check", rolloutsToCheck.size()); rolloutsToCheck.forEach(this::fillRolloutGroupsWithTargets); @@ -833,18 +842,12 @@ public class JpaRolloutManagement implements RolloutManagement { @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED) @Modifying public void checkStartingRollouts(final long delayBetweenChecks) { - final long lastCheck = System.currentTimeMillis(); - final int updated = rolloutRepository.updateLastCheck(lastCheck, delayBetweenChecks, RolloutStatus.STARTING); - if (updated == 0) { - // nothing to check, maybe another instance already checked in - // between - LOGGER.debug("No rollouts starting check necessary for current scheduled check {}, next check at {}", - lastCheck, lastCheck + delayBetweenChecks); + final List rolloutsToCheck = getRolloutsToCheckForStatus(delayBetweenChecks, + RolloutStatus.STARTING); + if (rolloutsToCheck.isEmpty()) { return; } - final List rolloutsToCheck = rolloutRepository.findByLastCheckAndStatus(lastCheck, - RolloutStatus.STARTING); LOGGER.info("Found {} starting rollouts to check", rolloutsToCheck.size()); rolloutsToCheck.forEach(rollout -> { @@ -855,6 +858,42 @@ public class JpaRolloutManagement implements RolloutManagement { } + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED) + @Modifying + public void checkReadyRollouts(final long delayBetweenChecks) { + final List rolloutsToCheck = getRolloutsToCheckForStatus(delayBetweenChecks, RolloutStatus.READY); + if (rolloutsToCheck.isEmpty()) { + return; + } + + LOGGER.info("Found {} ready rollouts to check", rolloutsToCheck.size()); + + final long now = System.currentTimeMillis(); + + rolloutsToCheck.forEach(rollout -> { + if (rollout.getStartAt() != null && rollout.getStartAt() <= now) { + startRollout(rollout.getId()); + } + }); + + } + + private List getRolloutsToCheckForStatus(final long delayBetweenChecks, final RolloutStatus status) { + final long lastCheck = System.currentTimeMillis(); + final int updated = rolloutRepository.updateLastCheck(lastCheck, delayBetweenChecks, status); + if (updated == 0) { + // nothing to check, maybe another instance already checked in + // between + LOGGER.debug("No rollouts starting check necessary for current scheduled check {}, next check at {}", + lastCheck, lastCheck + delayBetweenChecks); + return Collections.emptyList(); + } + + return rolloutRepository.findByLastCheckAndStatus(lastCheck, status); + + } + @Override public Long countRolloutsAll() { return rolloutRepository.count(); @@ -862,25 +901,15 @@ public class JpaRolloutManagement implements RolloutManagement { @Override public Long countRolloutsAllByFilters(final String searchText) { - return rolloutRepository.count(likeNameOrDescription(searchText)); - } - - private static Specification likeNameOrDescription(final String searchText) { - return (rolloutRoot, query, criteriaBuilder) -> { - final String searchTextToLower = searchText.toLowerCase(); - return criteriaBuilder.or( - criteriaBuilder.like(criteriaBuilder.lower(rolloutRoot.get(JpaRollout_.name)), searchTextToLower), - criteriaBuilder.like(criteriaBuilder.lower(rolloutRoot.get(JpaRollout_.description)), - searchTextToLower)); - }; + return rolloutRepository.count(RolloutHelper.likeNameOrDescription(searchText)); } @Override public Slice findRolloutWithDetailedStatusByFilters(final Pageable pageable, final String searchText) { - final Specification specs = likeNameOrDescription(searchText); + final Specification specs = RolloutHelper.likeNameOrDescription(searchText); final Slice findAll = criteriaNoCountDao.findAll(specs, pageable, JpaRollout.class); setRolloutStatusDetails(findAll); - return convertPage(findAll, pageable); + return RolloutHelper.convertPage(findAll, pageable); } @Override @@ -898,6 +927,16 @@ public class JpaRolloutManagement implements RolloutManagement { update.getName().ifPresent(rollout::setName); update.getDescription().ifPresent(rollout::setDescription); + update.getActionType().ifPresent(rollout::setActionType); + update.getForcedTime().ifPresent(rollout::setForcedTime); + update.getStartAt().ifPresent(rollout::setStartAt); + update.getSet().ifPresent(setId -> { + final DistributionSet set = distributionSetManagement.findDistributionSetById(setId); + if (set == null) { + throw new EntityNotFoundException("Distribution set cannot be set as it does not exists" + setId); + } + rollout.setDistributionSet(set); + }); return rolloutRepository.save(rollout); } @@ -906,7 +945,7 @@ public class JpaRolloutManagement implements RolloutManagement { public Page findAllRolloutsWithDetailedStatus(final Pageable pageable) { final Page rollouts = rolloutRepository.findAll(pageable); setRolloutStatusDetails(rollouts); - return convertPage(rollouts, pageable); + return RolloutHelper.convertPage(rollouts, pageable); } @@ -939,13 +978,6 @@ public class JpaRolloutManagement implements RolloutManagement { } } - private static void checkIfRolloutCanStarted(final Rollout rollout, final Rollout mergedRollout) { - if (!(RolloutStatus.READY.equals(mergedRollout.getStatus()))) { - throw new RolloutIllegalStateException("Rollout can only be started in state ready but current state is " - + rollout.getStatus().name().toLowerCase()); - } - } - @Override public float getFinishedPercentForRunningGroup(final Long rolloutId, final Long rolloutGroupId) { final RolloutGroup rolloutGroup = Optional.ofNullable(rolloutGroupRepository.findOne(rolloutGroupId)) diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java index 3046fe743..a79685372 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java @@ -123,6 +123,15 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme return convertPage(findTargetFilterQueryByCriteriaAPI(pageable, specList), pageable); } + @Override + public Page findTargetFilterQueryByQuery(final Pageable pageable, final String query) { + List> specList = Collections.emptyList(); + if (!Strings.isNullOrEmpty(query)) { + specList = Collections.singletonList(TargetFilterQuerySpecification.equalsQuery(query)); + } + return convertPage(findTargetFilterQueryByCriteriaAPI(pageable, specList), pageable); + } + @Override public Page findTargetFilterQueryByAutoAssignDS(final Pageable pageable, final Long setId, final String rsqlFilter) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java index e2bbb953f..63f0a7fb1 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java @@ -680,7 +680,10 @@ public class JpaTargetManagement implements TargetManagement { public Long countTargetByTargetFilterQuery(final String targetFilterQuery) { final Specification specs = RSQLUtility.parse(targetFilterQuery, TargetFields.class, virtualPropertyReplacer); - return targetRepository.count(specs); + return targetRepository.count((root, query, cb) -> { + query.distinct(true); + return specs.toPredicate(root, query, cb); + }); } private List getTargetIdNameResultSet(final Pageable pageRequest, final CriteriaBuilder cb, diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutHelper.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutHelper.java index e2e46cfdd..8ed0feb67 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutHelper.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RolloutHelper.java @@ -8,6 +8,8 @@ */ package org.eclipse.hawkbit.repository.jpa; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -16,10 +18,17 @@ import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate; import org.eclipse.hawkbit.repository.exception.ConstraintViolationException; import org.eclipse.hawkbit.repository.exception.RolloutIllegalStateException; import org.eclipse.hawkbit.repository.jpa.builder.JpaRolloutGroupCreate; +import org.eclipse.hawkbit.repository.jpa.model.JpaRollout; import org.eclipse.hawkbit.repository.jpa.model.JpaRolloutGroup; +import org.eclipse.hawkbit.repository.jpa.model.JpaRollout_; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroupConditions; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.domain.Specification; /** * A collection of static helper methods for the {@link JpaRolloutManagement} @@ -69,7 +78,7 @@ final class RolloutHelper { * In case the given group is missing conditions or actions, they will be * set from the supplied default conditions. * - * @param group + * @param create * group to check * @param conditions * default conditions and actions @@ -144,10 +153,22 @@ final class RolloutHelper { * @return resulting target filter query */ static String getTargetFilterQuery(final Rollout rollout) { - if (rollout.getCreatedAt() != null) { - return rollout.getTargetFilterQuery() + ";createdat=le=" + rollout.getCreatedAt().toString(); + return getTargetFilterQuery(rollout.getTargetFilterQuery(), rollout.getCreatedAt()); + } + + /** + * @param targetFilter + * the target filter tp be extended + * @param createdAt + * timestamp + * @return a target filter query that only matches targets that were created + * after the provided timestamp. + */ + static String getTargetFilterQuery(final String targetFilter, final Long createdAt) { + if (createdAt != null) { + return targetFilter + ";createdat=le=" + createdAt.toString(); } - return rollout.getTargetFilterQuery(); + return targetFilter; } /** @@ -215,30 +236,69 @@ final class RolloutHelper { if (groups.stream().anyMatch(group -> StringUtils.isEmpty(group.getTargetFilterQuery()))) { return ""; } - return groups.stream().map(RolloutGroup::getTargetFilterQuery).collect(Collectors.joining(",")); + + return "(" + groups.stream().map(RolloutGroup::getTargetFilterQuery).distinct().sorted() + .collect(Collectors.joining("),(")) + ")"; } /** * Creates an RSQL Filter that matches all targets that are in the provided * group and in the provided groups. * + * @param baseFilter + * the base filter from the rollout * @param groups * the rollout groups * @param group - * the group + * the target group * @return RSQL string without base filter of the Rollout. Can be an empty * string. */ - static String getOverlappingWithGroupsTargetFilter(final List groups, final RolloutGroup group) { + static String getOverlappingWithGroupsTargetFilter(final String baseFilter, final List groups, + final RolloutGroup group) { + final String groupFilter = group.getTargetFilterQuery(); + // when any previous group has the same filter as the target group the + // overlap is 100% + if (isTargetFilterInGroups(groupFilter, groups)) { + return concatAndTargetFilters(baseFilter, groupFilter); + } final String previousGroupFilters = getAllGroupsTargetFilter(groups); - if (StringUtils.isNotEmpty(previousGroupFilters) && StringUtils.isNotEmpty(group.getTargetFilterQuery())) { - return group.getTargetFilterQuery() + ";(" + previousGroupFilters + ")"; - } else if (StringUtils.isNotEmpty(previousGroupFilters)) { - return "(" + previousGroupFilters + ")"; - } else if (StringUtils.isNotEmpty(group.getTargetFilterQuery())) { - return group.getTargetFilterQuery(); + if (StringUtils.isNotEmpty(previousGroupFilters)) { + if (StringUtils.isNotEmpty(groupFilter)) { + return concatAndTargetFilters(baseFilter, groupFilter, previousGroupFilters); + } else { + return concatAndTargetFilters(baseFilter, previousGroupFilters); + } + } + if (StringUtils.isNotEmpty(groupFilter)) { + return concatAndTargetFilters(baseFilter, groupFilter); } else { - return ""; + return baseFilter; + } + } + + private static boolean isTargetFilterInGroups(final String groupFilter, final List groups) { + return StringUtils.isNotEmpty(groupFilter) + && groups.stream().anyMatch(prevGroup -> StringUtils.isNotEmpty(prevGroup.getTargetFilterQuery()) + && prevGroup.getTargetFilterQuery().equals(groupFilter)); + } + + private static String concatAndTargetFilters(String... filters) { + return "(" + Arrays.stream(filters).collect(Collectors.joining(");(")) + ")"; + } + + /** + * @param baseFilter + * the base filter from the rollout + * @param group + * group for which the filter string should be created + * @return the final target filter query for a rollout group + */ + static String getGroupTargetFilter(final String baseFilter, final RolloutGroup group) { + if (StringUtils.isEmpty(group.getTargetFilterQuery())) { + return baseFilter; + } else { + return concatAndTargetFilters(baseFilter, group.getTargetFilterQuery()); } } @@ -258,4 +318,35 @@ final class RolloutHelper { } } + /** + * @param searchText + * search string + * @return criteria specification with a query for name or description of a + * rollout + */ + static Specification likeNameOrDescription(final String searchText) { + return (rolloutRoot, query, criteriaBuilder) -> { + final String searchTextToLower = searchText.toLowerCase(); + return criteriaBuilder.or( + criteriaBuilder.like(criteriaBuilder.lower(rolloutRoot.get(JpaRollout_.name)), searchTextToLower), + criteriaBuilder.like(criteriaBuilder.lower(rolloutRoot.get(JpaRollout_.description)), + searchTextToLower)); + }; + } + + static void checkIfRolloutCanStarted(final Rollout rollout, final Rollout mergedRollout) { + if (!(Rollout.RolloutStatus.READY.equals(mergedRollout.getStatus()))) { + throw new RolloutIllegalStateException("Rollout can only be started in state ready but current state is " + + rollout.getStatus().name().toLowerCase()); + } + } + + static Page convertPage(final Page findAll, final Pageable pageable) { + return new PageImpl<>(Collections.unmodifiableList(findAll.getContent()), pageable, findAll.getTotalElements()); + } + + static Slice convertPage(final Slice findAll, final Pageable pageable) { + return new PageImpl<>(Collections.unmodifiableList(findAll.getContent()), pageable, 0); + } + } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutCreate.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutCreate.java index 20fa4f6bd..9bdf65711 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutCreate.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/builder/JpaRolloutCreate.java @@ -30,6 +30,7 @@ public class JpaRolloutCreate extends AbstractRolloutUpdateCreate rollout.setDescription(description); rollout.setDistributionSet(findDistributionSetAndThrowExceptionIfNotFound(set)); rollout.setTargetFilterQuery(targetFilterQuery); + rollout.setStartAt(startAt); if (actionType != null) { rollout.setActionType(actionType); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRollout.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRollout.java index 3925a82cb..3e52d1b09 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRollout.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaRollout.java @@ -87,6 +87,9 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event @Column(name = "rollout_groups_created") private int rolloutGroupsCreated; + @Column(name = "start_at") + private Long startAt; + @Transient private transient TotalTargetCountStatus totalTargetCountStatus; @@ -134,6 +137,15 @@ public class JpaRollout extends AbstractJpaNamedEntity implements Rollout, Event this.lastCheck = lastCheck; } + @Override + public Long getStartAt() { + return startAt; + } + + public void setStartAt(Long startAt) { + this.startAt = startAt; + } + @Override public ActionType getActionType() { return actionType; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/RolloutScheduler.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/RolloutScheduler.java index 860afe2c2..a4020cc30 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/RolloutScheduler.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rollout/RolloutScheduler.java @@ -25,8 +25,7 @@ import org.springframework.stereotype.Component; /** * Scheduler to schedule the * {@link RolloutManagement#checkRunningRollouts(long)}. The delay between the - * checks be be configured using the property - * {@link #PROP_SCHEDULER_DELAY_PLACEHOLDER}. + * checks be be configured using the properties from {@link RolloutProperties}. */ @Component // don't active the rollout scheduler in test, otherwise it is hard to test @@ -155,4 +154,40 @@ public class RolloutScheduler { return null; }); } + + /** + * Scheduler method called by the spring-async mechanism. Retrieves all + * tenants from the {@link SystemManagement#findTenants()} and runs for each + * tenant the {@link RolloutManagement#checkReadyRollouts(long)} in the + * {@link SystemSecurityContext}. Used to auto start Rollouts as soon as + * their startAt time is reached. + */ + @Scheduled(initialDelayString = RolloutProperties.PROP_READY_SCHEDULER_DELAY_PLACEHOLDER, fixedDelayString = RolloutProperties.PROP_READY_SCHEDULER_DELAY_PLACEHOLDER) + public void readyRolloutScheduler() { + if (!rolloutProperties.getReadyScheduler().isEnabled()) { + return; + } + + LOGGER.debug("rollout ready schedule checker has been triggered."); + // run this code in system code privileged to have the necessary + // permission to query and create entities. + systemSecurityContext.runAsSystem(() -> { + // workaround eclipselink that is currently not possible to + // execute a query without multitenancy if MultiTenant + // annotation is used. + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=355458. So + // iterate through all tenants and execute the rollout check for + // each tenant seperately. + final List tenants = systemManagement.findTenants(); + LOGGER.info("Checking ready rollouts for {} tenants", tenants.size()); + for (final String tenant : tenants) { + tenantAware.runAsTenant(tenant, () -> { + final long fixedDelay = rolloutProperties.getReadyScheduler().getFixedDelay(); + rolloutManagement.checkReadyRollouts(fixedDelay); + return null; + }); + } + return null; + }); + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java index d1100aa05..66412b7fd 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java @@ -13,15 +13,23 @@ import static org.eclipse.hawkbit.repository.FieldNameProvider.SUB_ATTRIBUTE_SEP import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Expression; +import javax.persistence.criteria.From; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.JoinType; import javax.persistence.criteria.MapJoin; import javax.persistence.criteria.Path; +import javax.persistence.criteria.PluralJoin; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; @@ -157,7 +165,6 @@ public final class RSQLUtility { @Override public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder cb) { - final Node rootNode = parseRsql(rsql); final JpqQueryRSQLVisitor jpqQueryRSQLVisitor = new JpqQueryRSQLVisitor<>(root, cb, enumType, @@ -192,6 +199,9 @@ public final class RSQLUtility { private final CriteriaBuilder cb; private final Class enumType; private final VirtualPropertyReplacer virtualPropertyReplacer; + private int level; + private boolean isOrLevel; + private final Map>> joinsInLevel = new HashMap<>(3); private final SimpleTypeConverter simpleTypeConverter; @@ -204,9 +214,41 @@ public final class RSQLUtility { simpleTypeConverter = new SimpleTypeConverter(); } + private void beginLevel(boolean isOr) { + level++; + isOrLevel = isOr; + joinsInLevel.put(level, new HashSet<>(2)); + } + + private void endLevel() { + joinsInLevel.remove(level); + level--; + isOrLevel = false; + } + + private Set> getCurrentJoins() { + if(level > 0) { + return joinsInLevel.get(level); + } + return Collections.emptySet(); + } + + private Optional> findCurrentJoinOfType(final Class type) { + return getCurrentJoins().stream() + .filter(j -> type.equals(j.getJavaType())).findFirst(); + } + + private void addCurrentJoin(Join join) { + if(level > 0) { + getCurrentJoins().add(join); + } + } + @Override public List visit(final AndNode node, final String param) { + beginLevel(false); final List childs = acceptChilds(node); + endLevel(); if (!childs.isEmpty()) { return toSingleList(cb.and(childs.toArray(new Predicate[childs.size()]))); } @@ -215,7 +257,9 @@ public final class RSQLUtility { @Override public List visit(final OrNode node, final String param) { + beginLevel(true); final List childs = acceptChilds(node); + endLevel(); if (!childs.isEmpty()) { return toSingleList(cb.or(childs.toArray(new Predicate[childs.size()]))); } @@ -282,12 +326,28 @@ public final class RSQLUtility { new Exception()); } + /** + * Resolves the Path for a field in the persistence layer and joins the + * required models. This operation is part of a tree traversal through + * an RSQL expression. It creates for every field that is not part of + * the root model a join to the foreign model. This behavior is + * optimized when several joins happen directly under an OR node in the + * traversed tree. The same foreign model is only joined once. + * + * Example: tags.name==M;(tags.name==A,tags.name==B,tags.name==C) This + * example joins the tags model only twice, because for the OR node in + * brackets only one join is used. + * + * @param enumField + * field from a FieldNameProvider to resolve on the + * persistence layer + * @param finalProperty + * dot notated field path + * @return the Path for a field + */ private Path getFieldPath(final A enumField, final String finalProperty) { Path fieldPath = null; final String[] split = finalProperty.split("\\" + SUB_ATTRIBUTE_SEPERATOR); - if (split.length == 0) { - return root.get(split[0]); - } for (int i = 0; i < split.length; i++) { final boolean isMapKeyField = enumField.isMap() && i == (split.length - 1); @@ -297,6 +357,21 @@ public final class RSQLUtility { final String fieldNameSplit = split[i]; fieldPath = (fieldPath != null) ? fieldPath.get(fieldNameSplit) : root.get(fieldNameSplit); + if (fieldPath instanceof PluralJoin) { + final Join join = (Join) fieldPath; + final From joinParent = join.getParent(); + Optional> currentJoinOfType = findCurrentJoinOfType(join.getJavaType()); + if(currentJoinOfType.isPresent() && isOrLevel) { + // remove the additional join and use the existing one + joinParent.getJoins().remove(join); + fieldPath = currentJoinOfType.get(); + } else { + Join newJoin = joinParent.join(fieldNameSplit, JoinType.LEFT); + addCurrentJoin(newJoin); + fieldPath = newJoin; + } + + } } return fieldPath; } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetFilterQuerySpecification.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetFilterQuerySpecification.java index d783738ae..f03ab83fe 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetFilterQuerySpecification.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/TargetFilterQuerySpecification.java @@ -24,6 +24,19 @@ public final class TargetFilterQuerySpecification { // utility class } + /** + * {@link Specification} for retrieving {@link JpaTargetFilterQuery}s based + * on is {@link JpaTargetFilterQuery#getQuery()}. + * + * @param queryValue + * the query of the filter + * @return the {@link JpaTargetFilterQuery} {@link Specification} + */ + public static Specification equalsQuery(final String queryValue) { + return (targetFilterQueryRoot, query, cb) -> cb.equal(targetFilterQueryRoot.get(JpaTargetFilterQuery_.query), + queryValue); + } + /** * {@link Specification} for retrieving {@link JpaTargetFilterQuery}s based * on is {@link JpaTargetFilterQuery#getName()}. diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_10_1__rollout_auto_start__H2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_10_1__rollout_auto_start__H2.sql new file mode 100644 index 000000000..e946026f3 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_10_1__rollout_auto_start__H2.sql @@ -0,0 +1,2 @@ +ALTER TABLE sp_rollout + ADD COLUMN start_at BIGINT; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_10_1__rollout_auto_start__MYSQL.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_10_1__rollout_auto_start__MYSQL.sql new file mode 100644 index 000000000..e946026f3 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_10_1__rollout_auto_start__MYSQL.sql @@ -0,0 +1,2 @@ +ALTER TABLE sp_rollout + ADD COLUMN start_at BIGINT; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java index 31056c50a..86cbc206f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/RolloutManagementTest.java @@ -1096,6 +1096,12 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { "controllerId==" + targetPrefixName + "-*", distributionSet, successCondition, errorCondition); assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.READY); + + rolloutManagement.checkReadyRollouts(0); + + myRollout = rolloutManagement.findRolloutById(myRollout.getId()); + assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.READY); + rolloutManagement.startRollout(myRollout.getId()); // Run here, because scheduler is disabled during tests @@ -1114,6 +1120,57 @@ public class RolloutManagementTest extends AbstractJpaIntegrationTest { validateRolloutActionStatus(myRollout.getId(), expectedTargetCountStatus); } + @Test + @Description("Verify the creation and the automatic start of a rollout.") + public void createAndAutoStartRollout() throws Exception { + + final int amountTargetsForRollout = 500; + final int amountGroups = 5; + final String successCondition = "50"; + final String errorCondition = "80"; + final String rolloutName = "rolloutTest8"; + final String targetPrefixName = rolloutName; + final DistributionSet distributionSet = testdataFactory.createDistributionSet("dsFor" + rolloutName); + testdataFactory.createTargets(amountTargetsForRollout, targetPrefixName + "-", targetPrefixName); + + Rollout myRollout = createRolloutByVariables(rolloutName, "desc", amountGroups, + "controllerId==" + targetPrefixName + "-*", distributionSet, successCondition, errorCondition); + + assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.READY); + + // schedule rollout auto start into the future + rolloutManagement.updateRollout( + entityFactory.rollout().update(myRollout.getId()).startAt(System.currentTimeMillis() + 60000)); + rolloutManagement.checkReadyRollouts(0); + + // rollout should not have been started + myRollout = rolloutManagement.findRolloutById(myRollout.getId()); + assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.READY); + + // schedule to now + rolloutManagement.updateRollout( + entityFactory.rollout().update(myRollout.getId()).startAt(System.currentTimeMillis())); + rolloutManagement.checkReadyRollouts(0); + + myRollout = rolloutManagement.findRolloutById(myRollout.getId()); + assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.STARTING); + + // Run here, because scheduler is disabled during tests + rolloutManagement.checkStartingRollouts(0); + + final SuccessConditionRolloutStatus conditionRolloutTargetCount = new SuccessConditionRolloutStatus( + RolloutStatus.RUNNING); + assertThat(MultipleInvokeHelper.doWithTimeout(new RolloutStatusCallable(myRollout.getId()), + conditionRolloutTargetCount, 15000, 500)).as("Rollout status").isNotNull(); + + myRollout = rolloutManagement.findRolloutById(myRollout.getId()); + assertThat(myRollout.getStatus()).isEqualTo(RolloutStatus.RUNNING); + final Map expectedTargetCountStatus = createInitStatusMap(); + expectedTargetCountStatus.put(TotalTargetCountStatus.Status.RUNNING, 100L); + expectedTargetCountStatus.put(TotalTargetCountStatus.Status.SCHEDULED, 400L); + validateRolloutActionStatus(myRollout.getId(), expectedTargetCountStatus); + } + @Test @Description("Verify the creation of a Rollout with a groups definition.") public void createRolloutWithGroupDefinition() throws Exception { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java index 580ff97b3..3897a7f69 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java @@ -21,7 +21,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import javax.validation.ConstraintViolationException; @@ -49,6 +48,7 @@ import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; import org.eclipse.hawkbit.repository.test.util.WithSpringAuthorityRule; import org.eclipse.hawkbit.repository.test.util.WithUser; import org.junit.Test; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import com.google.common.collect.Iterables; @@ -495,7 +495,7 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { firstList = firstList.stream() .map(t -> targetManagement.updateTarget( entityFactory.target().update(t.getControllerId()).name(t.getName().concat("\tchanged")))) - .collect(Collectors.toList()); + .collect(toList()); // verify that all entries are found _founds: for (final Target foundTarget : allFound) { @@ -706,14 +706,14 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { toggleTagAssignment(targAs, targTagA); assertThat(targetManagement.findTargetsByControllerIDsWithTags( - targAs.stream().map(Target::getControllerId).collect(Collectors.toList()))).as("Target count is wrong") + targAs.stream().map(Target::getControllerId).collect(toList()))).as("Target count is wrong") .hasSize(25); // no lazy loading exception and tag correctly assigned assertThat(targetManagement .findTargetsByControllerIDsWithTags( - targAs.stream().map(Target::getControllerId).collect(Collectors.toList())) - .stream().map(target -> target.getTags().contains(targTagA)).collect(Collectors.toList())) + targAs.stream().map(Target::getControllerId).collect(toList())) + .stream().map(target -> target.getTags().contains(targTagA)).collect(toList())) .as("Tags not correctly assigned").containsOnly(true); } @@ -755,4 +755,23 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { }); } + + @Test + @Description("Test that RSQL filter finds targets with tags or specific ids.") + public void findTargetsWithTagOrId() { + final String rsqlFilter = "tag==Targ-A-Tag,id==target-id-B-00001,id==target-id-B-00008"; + final TargetTag targTagA = tagManagement.createTargetTag(entityFactory.tag().create().name("Targ-A-Tag")); + final List targAs = testdataFactory.createTargets(25, "target-id-A", "first description").stream() + .map(Target::getControllerId).collect(toList()); + targetManagement.toggleTagAssignment(targAs, targTagA.getName()); + + testdataFactory.createTargets(25, "target-id-B", "first description"); + + Page foundTargets = targetManagement.findTargetsAll(rsqlFilter, new PageRequest(0, 100)); + + assertThat(targetManagement.findTargetsAll(new PageRequest(0, 100)).getNumberOfElements()).as("Total targets") + .isEqualTo(50); + assertThat(foundTargets.getTotalElements()).as("Targets in RSQL filter").isEqualTo(27L); + + } } diff --git a/hawkbit-ui/pom.xml b/hawkbit-ui/pom.xml index c9d119346..b0d8d09b6 100644 --- a/hawkbit-ui/pom.xml +++ b/hawkbit-ui/pom.xml @@ -253,6 +253,11 @@ spring-boot-configuration-processor true + + com.github.gwtd3 + gwt-d3-api + + org.eclipse.hawkbit diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/AppWidgetSet.gwt.xml b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/AppWidgetSet.gwt.xml index e4b3aee9c..c722cf56e 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/AppWidgetSet.gwt.xml +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/AppWidgetSet.gwt.xml @@ -33,6 +33,7 @@ + diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/CommonDialogWindow.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/CommonDialogWindow.java index 86841f08e..2adcef834 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/CommonDialogWindow.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/CommonDialogWindow.java @@ -247,6 +247,13 @@ public class CommonDialogWindow extends Window { saveButton.setEnabled(isSaveButtonEnabledAfterValueChange(null, null)); } + /** + * Clears the original values in case no value changed check is wished + */ + public final void clearOriginalValues() { + orginalValues.clear(); + } + protected void addCloseListenerForSaveButton() { saveButton.addClickListener(closeClickListener); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/builder/ComboBoxBuilder.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/builder/ComboBoxBuilder.java new file mode 100644 index 000000000..a96c64c82 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/builder/ComboBoxBuilder.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.common.builder; + +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; + +import com.vaadin.data.Property; +import com.vaadin.ui.ComboBox; +import com.vaadin.ui.themes.ValoTheme; + +/** + * Builds ComboBox Elements with a commonly used properties. + */ +public class ComboBoxBuilder { + + private Property.ValueChangeListener valueChangeListener; + + private String id; + + private String prompt; + + public ComboBoxBuilder setValueChangeListener(final Property.ValueChangeListener valueChangeListener) { + this.valueChangeListener = valueChangeListener; + return this; + } + + public ComboBoxBuilder setId(final String id) { + this.id = id; + return this; + } + + public ComboBoxBuilder setPrompt(String prompt) { + this.prompt = prompt; + return this; + } + + /** + * @return a new ComboBox + */ + public ComboBox buildCombBox() { + final ComboBox targetFilter = SPUIComponentProvider.getComboBox(null, "", null, ValoTheme.COMBOBOX_SMALL, false, + "", prompt); + targetFilter.setImmediate(true); + targetFilter.setPageLength(7); + targetFilter.setItemCaptionPropertyId(SPUILabelDefinitions.VAR_NAME); + targetFilter.setSizeUndefined(); + if (id != null) { + targetFilter.setId(id); + } + if (valueChangeListener != null) { + targetFilter.addValueChangeListener(valueChangeListener); + } + return targetFilter; + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/RolloutView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/RolloutView.java index 9adde32e0..72d251b2b 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/RolloutView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/RolloutView.java @@ -14,6 +14,7 @@ import javax.annotation.PreDestroy; import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutManagement; +import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.ui.HawkbitUI; import org.eclipse.hawkbit.ui.SpPermissionChecker; @@ -65,10 +66,10 @@ public class RolloutView extends VerticalLayout implements View { final UIEventBus eventBus, final RolloutManagement rolloutManagement, final RolloutGroupManagement rolloutGroupManagement, final TargetManagement targetManagement, final UINotification uiNotification, final UiProperties uiProperties, final EntityFactory entityFactory, - final I18N i18n) { + final I18N i18n, final TargetFilterQueryManagement targetFilterQueryManagement) { this.permChecker = permissionChecker; this.rolloutListView = new RolloutListView(permissionChecker, rolloutUIState, eventBus, rolloutManagement, - targetManagement, uiNotification, uiProperties, entityFactory, i18n); + targetManagement, uiNotification, uiProperties, entityFactory, i18n, targetFilterQueryManagement); this.rolloutGroupsListView = new RolloutGroupsListView(i18n, eventBus, rolloutGroupManagement, rolloutUIState, permissionChecker); this.rolloutGroupTargetsListView = new RolloutGroupTargetsListView(eventBus, i18n, rolloutUIState); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/groupschart/GroupsPieChart.gwt.xml b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/groupschart/GroupsPieChart.gwt.xml new file mode 100644 index 000000000..e0b075e41 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/groupschart/GroupsPieChart.gwt.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/groupschart/GroupsPieChart.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/groupschart/GroupsPieChart.java new file mode 100644 index 000000000..9e86a1c49 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/groupschart/GroupsPieChart.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.rollout.groupschart; + +import org.eclipse.hawkbit.ui.rollout.groupschart.client.GroupsPieChartState; +import com.vaadin.ui.AbstractComponent; + +import java.util.List; + +/** + * Draws a pie charts for the provided groups. + */ +public class GroupsPieChart extends AbstractComponent { + + private static final long serialVersionUID = 1311542227339430098L; + + /** + * Updates the state of the chart + * + * @param groupTargetCounts + * list of target counts + * @param totalTargetsCount + * total count of targets that are represented by the pie + */ + public void setChartState(final List groupTargetCounts, final Long totalTargetsCount) { + getState().setGroupTargetCounts(groupTargetCounts); + getState().setTotalTargetCount(totalTargetsCount); + markAsDirty(); + } + + @Override + protected GroupsPieChartState getState() { + return (GroupsPieChartState) super.getState(); + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/groupschart/client/GroupsPieChartConnector.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/groupschart/client/GroupsPieChartConnector.java new file mode 100644 index 000000000..9fd171327 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/groupschart/client/GroupsPieChartConnector.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.rollout.groupschart.client; + +import com.vaadin.client.communication.StateChangeEvent; +import org.eclipse.hawkbit.ui.rollout.groupschart.GroupsPieChart; +import com.google.gwt.core.client.GWT; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.shared.ui.Connect; + +/** + * Connector between client side GroupsPieChartWidget and service side. + */ +@Connect(GroupsPieChart.class) +public class GroupsPieChartConnector extends AbstractComponentConnector { + + private static final long serialVersionUID = -2907528194018611155L; + + @Override + protected Widget createWidget() { + return GWT.create(GroupsPieChartWidget.class); + } + + @Override + public GroupsPieChartWidget getWidget() { + return (GroupsPieChartWidget) super.getWidget(); + } + + @Override + public GroupsPieChartState getState() { + return (GroupsPieChartState) super.getState(); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + getWidget().update(getState().getGroupTargetCounts(), getState().getTotalTargetCount()); + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/groupschart/client/GroupsPieChartState.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/groupschart/client/GroupsPieChartState.java new file mode 100644 index 000000000..be8537fe6 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/groupschart/client/GroupsPieChartState.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.rollout.groupschart.client; + +import com.vaadin.shared.AbstractComponentState; + +import java.util.List; + +/** + * State to transfer for the groups pie chart between server and client. + */ +public class GroupsPieChartState extends AbstractComponentState { + + private static final long serialVersionUID = 7344220498082627571L; + + private transient List groupTargetCounts; + + private Long totalTargetCount; + + public List getGroupTargetCounts() { + return groupTargetCounts; + } + + public void setGroupTargetCounts(List groupTargetCounts) { + this.groupTargetCounts = groupTargetCounts; + } + + public Long getTotalTargetCount() { + return totalTargetCount; + } + + public void setTotalTargetCount(Long totalTargetCount) { + this.totalTargetCount = totalTargetCount; + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/groupschart/client/GroupsPieChartWidget.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/groupschart/client/GroupsPieChartWidget.java new file mode 100644 index 000000000..9b8ac153c --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/groupschart/client/GroupsPieChartWidget.java @@ -0,0 +1,201 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.rollout.groupschart.client; + +import com.github.gwtd3.api.D3; +import com.github.gwtd3.api.arrays.Array; +import com.github.gwtd3.api.core.Selection; +import com.github.gwtd3.api.core.UpdateSelection; +import com.github.gwtd3.api.core.Value; +import com.github.gwtd3.api.functions.DatumFunction; +import com.github.gwtd3.api.svg.Arc; +import com.google.gwt.dom.client.BrowserEvents; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style; +import com.google.gwt.user.client.ui.DockLayoutPanel; + +import java.util.List; + +/** + * Draws a pie chart using D3. The slices are based on the list of Longs and on + * the total target count. The total target count represents 100% of the pie. If + * the sum in the list of Longs is less than the total target count a slice for + * unassigned targets will be displayed. + * + */ +@SuppressWarnings("squid:TrailingCommentCheck") +public class GroupsPieChartWidget extends DockLayoutPanel { + + private static final String ATTR_VISIBILITY = "visibility"; + private static final String ATTR_TRANSFORM = "transform"; + + private List groupTargetCounts; + private Long totalTargetCount; + private long unassignedTargets; + + private Selection svg; + private Selection pieGroup; + private Selection infoText; + + private Arc arc; + + /** + * Initializes the pie chart + */ + public GroupsPieChartWidget() { + super(Style.Unit.PX); + init(); + } + + private void init() { + + initChart(); + + draw(); + + } + + /** + * Updates the pie chart with new data + * + * @param groupTargetCounts + * list of target counts + * @param totalTargetCount + * total count of targets that are represented by the pie + */ + public void update(final List groupTargetCounts, final Long totalTargetCount) { + this.groupTargetCounts = groupTargetCounts; + this.totalTargetCount = totalTargetCount; + + if (groupTargetCounts != null) { + long sum = 0; + for (Long targetCount : groupTargetCounts) { + sum += targetCount; + } + unassignedTargets = totalTargetCount - sum; + } + + draw(); + + } + + private static PieArc getPie(Long count, Long total, double startAngle) { + final Double percentage = count.doubleValue() / total.doubleValue(); + return new PieArc(startAngle, startAngle + percentage * 2 * Math.PI); + } + + private void draw() { + if (svg == null || groupTargetCounts == null || totalTargetCount == null) { + return; + } + + final Array dataArray = Array.create(); + + PieArc pie = getPie(unassignedTargets, totalTargetCount, 0); + dataArray.push(pie.getArc()); + + double lastAngle = pie.getEndAngle(); + for (int i = 0; i < groupTargetCounts.size(); i++) { + final PieArc arcEntry = getPie(groupTargetCounts.get(i), totalTargetCount, lastAngle); + dataArray.push(arcEntry.getArc()); + lastAngle = arcEntry.getEndAngle(); + } + + UpdateSelection pies = pieGroup.selectAll(".pie").data(dataArray); + pies.enter().append("path").classed("pie", true).on(BrowserEvents.MOUSEOVER, new DatumFunction() { + @Override + public Void apply(Element context, Value d, int index) { + Array point = arc.centroid(d.as(Arc.class), index); + double x = point.getNumber(0); + double y = point.getNumber(1); + if (index == 0) { + updateHoverText("Unassigned: " + unassignedTargets, x, y); + } else { + updateHoverText(index + ": " + groupTargetCounts.get(index - 1), x, y); + } + + return null; + } + }).on(BrowserEvents.MOUSEOUT, new DatumFunction() { + @Override + public Void apply(Element context, Value d, int index) { + infoText.attr(ATTR_VISIBILITY, "hidden"); + return null; + } + }); + pies.exit().remove(); + pies.attr("d", arc); + + } + + private void updateHoverText(final String displayText, final double x, final double y) { + final Selection text = infoText.select("text"); + final Selection background = infoText.select("rect"); + + text.html(displayText); + + final double textWidth = getTextWidth(text.node()); + final double textHeight = getTextHeight(text.node()); + + background.attr("width", textWidth * 1.1); + background.attr("height", textHeight); + + moveSelection(background, -textWidth * 1.1 / 2.0, -textHeight*0.8); + moveSelection(infoText, x, y); + infoText.attr(ATTR_VISIBILITY, "visible"); + } + + private static void moveSelection(Selection sel, double x, double y) { + sel.attr(ATTR_TRANSFORM, "translate(" + x + ", " + y + ")"); + } + + + private static final native double getTextWidth(Element e)/*-{ + return e.getBBox().width; + }-*/; + + private static final native double getTextHeight(Element e)/*-{ + return e.getBBox().height; + }-*/; + + private void initChart() { + arc = D3.svg().arc().innerRadius(0).outerRadius(90); + int height = 200; + int width = 260; + + svg = D3.select(this).append("svg").attr("width", width).attr("height", height).append("g"); + moveSelection(svg, (float) width / 2, (float) height / 2); + + pieGroup = svg.append("g"); + + infoText = svg.append("g").attr(ATTR_VISIBILITY, "hidden").classed("pie-info", true); + infoText.append("rect"); + infoText.append("text").attr("text-anchor", "middle"); + + } + + private static class PieArc { + private double startAngle; + + private double endAngle; + + public PieArc(double startAngle, double endAngle) { + this.startAngle = startAngle; + this.endAngle = endAngle; + } + + public double getEndAngle() { + return endAngle; + } + + public Arc getArc() { + return Arc.constantArc().startAngle(startAngle).endAngle(endAngle); + } + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java index be487792b..5a6f398f2 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AddUpdateRolloutWindowLayout.java @@ -9,13 +9,20 @@ package org.eclipse.hawkbit.ui.rollout.rollout; import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.RolloutManagement; +import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetManagement; +import org.eclipse.hawkbit.repository.builder.RolloutCreate; +import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate; +import org.eclipse.hawkbit.repository.builder.RolloutUpdate; import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.RepositoryModelConstants; import org.eclipse.hawkbit.repository.model.Rollout; @@ -26,19 +33,22 @@ import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessActi import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessCondition; import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder; import org.eclipse.hawkbit.repository.model.RolloutGroupConditions; +import org.eclipse.hawkbit.repository.model.RolloutGroupsValidation; +import org.eclipse.hawkbit.repository.model.TargetFilterQuery; import org.eclipse.hawkbit.ui.UiProperties; import org.eclipse.hawkbit.ui.common.CommonDialogWindow; import org.eclipse.hawkbit.ui.common.CommonDialogWindow.SaveDialogCloseListener; import org.eclipse.hawkbit.ui.common.DistributionSetIdName; +import org.eclipse.hawkbit.ui.common.builder.ComboBoxBuilder; import org.eclipse.hawkbit.ui.common.builder.LabelBuilder; import org.eclipse.hawkbit.ui.common.builder.TextAreaBuilder; import org.eclipse.hawkbit.ui.common.builder.TextFieldBuilder; import org.eclipse.hawkbit.ui.common.builder.WindowBuilder; -import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; import org.eclipse.hawkbit.ui.filtermanagement.TargetFilterBeanQuery; import org.eclipse.hawkbit.ui.management.footer.ActionTypeOptionGroupLayout; import org.eclipse.hawkbit.ui.management.footer.ActionTypeOptionGroupLayout.ActionTypeOption; import org.eclipse.hawkbit.ui.rollout.event.RolloutEvent; +import org.eclipse.hawkbit.ui.rollout.groupschart.GroupsPieChart; import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; import org.eclipse.hawkbit.ui.utils.I18N; import org.eclipse.hawkbit.ui.utils.SPDateTimeUtil; @@ -47,6 +57,8 @@ import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; import org.eclipse.hawkbit.ui.utils.UINotification; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.vaadin.addons.lazyquerycontainer.BeanQueryFactory; import org.vaadin.addons.lazyquerycontainer.LazyQueryContainer; import org.vaadin.addons.lazyquerycontainer.LazyQueryDefinition; @@ -66,6 +78,7 @@ import com.vaadin.ui.ComboBox; import com.vaadin.ui.GridLayout; import com.vaadin.ui.Label; import com.vaadin.ui.OptionGroup; +import com.vaadin.ui.TabSheet; import com.vaadin.ui.TextArea; import com.vaadin.ui.TextField; import com.vaadin.ui.themes.ValoTheme; @@ -83,16 +96,22 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { private final ActionTypeOptionGroupLayout actionTypeOptionGroupLayout; + private final AutoStartOptionGroupLayout autoStartOptionGroupLayout; + private final transient RolloutManagement rolloutManagement; private final transient TargetManagement targetManagement; + private final transient TargetFilterQueryManagement targetFilterQueryManagement; + private final UINotification uiNotification; - private final UiProperties uiProperties; + private final transient UiProperties uiProperties; private final transient EntityFactory entityFactory; + private final DefineGroupsLayout defineGroupsLayout; + private final I18N i18n; private final transient EventBus.UIEventBus eventBus; @@ -117,22 +136,29 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { private CommonDialogWindow window; - private Boolean editRolloutEnabled; + private boolean editRolloutEnabled; - private Rollout rolloutForEdit; + private Rollout rollout; private Long totalTargetsCount; - private Label totalTargetsLabel; - private TextArea targetFilterQuery; + private TabSheet groupsDefinitionTabs; + + private GroupsPieChart groupsPieChart; + + private GroupsLegendLayout groupsLegendLayout; + + private final transient RolloutGroupConditions defaultRolloutGroupConditions; + private final NullValidator nullValidator = new NullValidator(null, false); AddUpdateRolloutWindowLayout(final RolloutManagement rolloutManagement, final TargetManagement targetManagement, final UINotification uiNotification, final UiProperties uiProperties, final EntityFactory entityFactory, - final I18N i18n, final UIEventBus eventBus) { + final I18N i18n, final UIEventBus eventBus, final TargetFilterQueryManagement targetFilterQueryManagement) { this.actionTypeOptionGroupLayout = new ActionTypeOptionGroupLayout(i18n); + this.autoStartOptionGroupLayout = new AutoStartOptionGroupLayout(i18n); this.rolloutManagement = rolloutManagement; this.targetManagement = targetManagement; this.uiNotification = uiNotification; @@ -140,10 +166,20 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { this.entityFactory = entityFactory; this.i18n = i18n; this.eventBus = eventBus; + this.targetFilterQueryManagement = targetFilterQueryManagement; + + this.defineGroupsLayout = new DefineGroupsLayout(i18n, entityFactory, rolloutManagement, + targetFilterQueryManagement); + + defaultRolloutGroupConditions = new RolloutGroupConditionBuilder().withDefaults().build(); setSizeUndefined(); createRequiredComponents(); buildLayout(); + + defineGroupsLayout.setValidationListener(this::displayValidationStatus); + defineGroupsLayout.setDefaultErrorThreshold(defaultRolloutGroupConditions.getErrorConditionExp()); + defineGroupsLayout.setDefaultTriggerThreshold(defaultRolloutGroupConditions.getSuccessConditionExp()); } /** @@ -173,27 +209,39 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { * * @param rolloutId * the rollout id + * @param copy + * whether the rollout should be copied * @return the window */ - public CommonDialogWindow getWindow(final Long rolloutId) { - window = getWindow(); - populateData(rolloutId); + public CommonDialogWindow getWindow(final Long rolloutId, final boolean copy) { + resetComponents(); + window = createWindow(); + populateData(rolloutId, copy); return window; } - public CommonDialogWindow getWindow() { - resetComponents(); + private CommonDialogWindow createWindow() { return new WindowBuilder(SPUIDefinitions.CREATE_UPDATE_WINDOW).caption(i18n.get("caption.configure.rollout")) .content(this).layout(this).i18n(i18n) .helpLink(uiProperties.getLinks().getDocumentation().getRolloutView()) .saveDialogCloseListener(new SaveOnDialogCloseListener()).buildCommonDialogWindow(); } + public CommonDialogWindow getWindow() { + resetComponents(); + window = createWindow(); + window.updateAllComponents(noOfGroups); + window.updateAllComponents(triggerThreshold); + window.updateAllComponents(errorThreshold); + return window; + } + /** * Reset the field values. */ public void resetComponents() { - editRolloutEnabled = Boolean.FALSE; + defineGroupsLayout.resetComponents(); + editRolloutEnabled = false; rolloutName.clear(); targetFilterQuery.clear(); resetFields(); @@ -201,22 +249,26 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { populateDistributionSet(); populateTargetFilterQuery(); setDefaultSaveStartGroupOption(); - totalTargetsLabel.setVisible(false); + groupsLegendLayout.reset(); groupSizeLabel.setVisible(false); + noOfGroups.setVisible(true); removeComponent(1, 2); addComponent(targetFilterQueryCombo, 1, 2); actionTypeOptionGroupLayout.selectDefaultOption(); + autoStartOptionGroupLayout.selectDefaultOption(); totalTargetsCount = 0L; - rolloutForEdit = null; + rollout = null; + groupsDefinitionTabs.setVisible(true); + groupsDefinitionTabs.setSelectedTab(0); } private void resetFields() { rolloutName.removeStyleName(SPUIStyleDefinitions.SP_TEXTFIELD_ERROR); noOfGroups.clear(); noOfGroups.removeStyleName(SPUIStyleDefinitions.SP_TEXTFIELD_ERROR); - triggerThreshold.clear(); + triggerThreshold.setValue(defaultRolloutGroupConditions.getSuccessConditionExp()); triggerThreshold.removeStyleName(SPUIStyleDefinitions.SP_TEXTFIELD_ERROR); - errorThreshold.clear(); + errorThreshold.setValue(defaultRolloutGroupConditions.getErrorConditionExp()); errorThreshold.removeStyleName(SPUIStyleDefinitions.SP_TEXTFIELD_ERROR); description.clear(); description.removeStyleName(SPUIStyleDefinitions.SP_TEXTFIELD_ERROR); @@ -224,11 +276,13 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { private void buildLayout() { - setSpacing(Boolean.TRUE); + setSpacing(true); setSizeUndefined(); - setRows(9); - setColumns(3); + setRows(7); + setColumns(4); setStyleName("marginTop"); + setColumnExpandRatio(3, 1); + setWidth(850, Unit.PIXELS); addComponent(getMandatoryLabel("textfield.name"), 0, 0); addComponent(rolloutName, 1, 0); @@ -243,28 +297,19 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { targetFilterQueryCombo.addValidator(nullValidator); targetFilterQuery.removeValidator(nullValidator); - addComponent(totalTargetsLabel, 2, 2); + addComponent(getLabel("textfield.description"), 0, 3); + addComponent(description, 1, 3, 1, 3); - addComponent(getMandatoryLabel("prompt.number.of.groups"), 0, 3); - addComponent(noOfGroups, 1, 3); - noOfGroups.addValidator(nullValidator); + addComponent(groupsLegendLayout, 3, 0, 3, 3); + addComponent(groupsPieChart, 2, 0, 2, 3); - addComponent(groupSizeLabel, 2, 3); + addComponent(getMandatoryLabel("caption.rollout.action.type"), 0, 4); + addComponent(actionTypeOptionGroupLayout, 1, 4, 3, 4); - addComponent(getMandatoryLabel("prompt.tigger.threshold"), 0, 4); - addComponent(triggerThreshold, 1, 4); - triggerThreshold.addValidator(nullValidator); + addComponent(getMandatoryLabel("caption.rollout.start.type"), 0, 5); + addComponent(autoStartOptionGroupLayout, 1, 5, 3, 5); - addComponent(getPercentHintLabel(), 2, 4); - - addComponent(getMandatoryLabel("prompt.error.threshold"), 0, 5); - addComponent(errorThreshold, 1, 5); - errorThreshold.addValidator(nullValidator); - addComponent(errorThresholdOptionGroup, 2, 5); - - addComponent(getLabel("textfield.description"), 0, 6); - addComponent(description, 1, 6, 2, 6); - addComponent(actionTypeOptionGroupLayout, 0, 7, 2, 7); + addComponent(groupsDefinitionTabs, 0, 6, 3, 6); rolloutName.focus(); } @@ -310,15 +355,93 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { noOfGroups = createNoOfGroupsField(); groupSizeLabel = createCountLabel(); + triggerThreshold = createTriggerThreshold(); errorThreshold = createErrorThreshold(); description = createDescription(); errorThresholdOptionGroup = createErrorThresholdOptionGroup(); setDefaultSaveStartGroupOption(); actionTypeOptionGroupLayout.selectDefaultOption(); - totalTargetsLabel = createCountLabel(); + autoStartOptionGroupLayout.selectDefaultOption(); targetFilterQuery = createTargetFilterQuery(); actionTypeOptionGroupLayout.addStyleName(SPUIStyleDefinitions.ROLLOUT_ACTION_TYPE_LAYOUT); + autoStartOptionGroupLayout.addStyleName(SPUIStyleDefinitions.ROLLOUT_ACTION_TYPE_LAYOUT); + + groupsDefinitionTabs = createGroupDefinitionTabs(); + + groupsPieChart = new GroupsPieChart(); + groupsPieChart.setWidth(260, Unit.PIXELS); + groupsPieChart.setHeight(220, Unit.PIXELS); + groupsPieChart.setStyleName(SPUIStyleDefinitions.ROLLOUT_GROUPS_CHART); + + groupsLegendLayout = new GroupsLegendLayout(i18n); + + } + + private void displayValidationStatus(DefineGroupsLayout.ValidationStatus status) { + if(status == DefineGroupsLayout.ValidationStatus.LOADING) { + groupsLegendLayout.displayLoading(); + } else { + validateGroups(); + } + } + + private TabSheet createGroupDefinitionTabs() { + TabSheet tabSheet = new TabSheet(); + tabSheet.setId(UIComponentIdProvider.ROLLOUT_GROUPS); + tabSheet.setWidth(850, Unit.PIXELS); + tabSheet.setHeight(300, Unit.PIXELS); + tabSheet.setStyleName(SPUIStyleDefinitions.ROLLOUT_GROUPS); + + TabSheet.Tab simpleTab = tabSheet.addTab(createSimpleGroupDefinitionTab(), + i18n.get("caption.rollout.tabs.simple")); + simpleTab.setId(UIComponentIdProvider.ROLLOUT_SIMPLE_TAB); + + TabSheet.Tab advancedTab = tabSheet.addTab(defineGroupsLayout, i18n.get("caption.rollout.tabs.advanced")); + advancedTab.setId(UIComponentIdProvider.ROLLOUT_ADVANCED_TAB); + + tabSheet.addSelectedTabChangeListener(event -> validateGroups()); + + return tabSheet; + } + + private static int getPositionOfSelectedTab(final TabSheet tabSheet) { + return tabSheet.getTabPosition(tabSheet.getTab(tabSheet.getSelectedTab())); + } + + private boolean isNumberOfGroups() { + return getPositionOfSelectedTab(groupsDefinitionTabs) == 0; + } + + private boolean isGroupsDefinition() { + return getPositionOfSelectedTab(groupsDefinitionTabs) == 1; + } + + private GridLayout createSimpleGroupDefinitionTab() { + GridLayout layout = new GridLayout(); + layout.setSpacing(true); + layout.setColumns(3); + layout.setRows(4); + layout.setStyleName("marginTop"); + + layout.addComponent(getLabel("caption.rollout.generate.groups"), 0, 0, 2, 0); + + layout.addComponent(getMandatoryLabel("prompt.number.of.groups"), 0, 1); + layout.addComponent(noOfGroups, 1, 1); + noOfGroups.addValidator(nullValidator); + layout.addComponent(groupSizeLabel, 2, 1); + + layout.addComponent(getMandatoryLabel("prompt.tigger.threshold"), 0, 2); + layout.addComponent(triggerThreshold, 1, 2); + triggerThreshold.addValidator(nullValidator); + layout.addComponent(getPercentHintLabel(), 2, 2); + + layout.addComponent(getMandatoryLabel("prompt.error.threshold"), 0, 3); + layout.addComponent(errorThreshold, 1, 3); + errorThreshold.addValidator(nullValidator); + layout.addComponent(errorThresholdOptionGroup, 2, 3); + + return layout; } private static Label createCountLabel() { @@ -365,35 +488,95 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { errorThreshold.getValidators(); } + private void validateGroups() { + if(editRolloutEnabled) { + return; + } + if(isGroupsDefinition()) { + List savedRolloutGroups = defineGroupsLayout.getSavedRolloutGroups(); + if(!defineGroupsLayout.isValid() || savedRolloutGroups == null || savedRolloutGroups.isEmpty()) { + noOfGroups.clear(); + } else { + noOfGroups.setValue(String.valueOf(savedRolloutGroups.size())); + } + updateGroupsChart(defineGroupsLayout.getGroupsValidation()); + } + if(isNumberOfGroups()) { + if(noOfGroups.isValid() && !noOfGroups.getValue().isEmpty()) { + updateGroupsChart(Integer.parseInt(noOfGroups.getValue())); + } else { + updateGroupsChart(0); + } + + } + } + + private void updateGroupsChart(final RolloutGroupsValidation validation) { + if(validation == null) { + groupsPieChart.setChartState(null, null); + return; + } + List targetsPerGroup = validation.getTargetsPerGroup(); + if(validation.getTotalTargets() == 0L || targetsPerGroup.isEmpty()) { + groupsPieChart.setChartState(null, null); + } else { + groupsPieChart.setChartState(targetsPerGroup, validation.getTotalTargets()); + } + + totalTargetsCount = validation.getTotalTargets(); + groupsLegendLayout.populateTotalTargets(validation.getTotalTargets()); + groupsLegendLayout.populateGroupsLegendByValidation(validation, defineGroupsLayout.getSavedRolloutGroups()); + + } + + private void updateGroupsChart(final List savedGroups, long totalTargetsCount) { + List targetsPerGroup = savedGroups.stream().map(group -> (long) group.getTotalTargets()) + .collect(Collectors.toList()); + + groupsPieChart.setChartState(targetsPerGroup, totalTargetsCount); + groupsLegendLayout.populateGroupsLegendByGroups(savedGroups); + } + + private void updateGroupsChart(final int amountOfGroups) { + if (totalTargetsCount == null || totalTargetsCount == 0L || amountOfGroups == 0) { + groupsPieChart.setChartState(null, null); + groupsLegendLayout.populateGroupsLegendByTargetCounts(Collections.emptyList()); + } else { + final List groups = new ArrayList<>(amountOfGroups); + long leftTargets = totalTargetsCount; + for (int i = 0; i < amountOfGroups; i++) { + float percentage = 1.0F / (amountOfGroups - i); + long targetsInGroup = Math.round(percentage * (double) leftTargets); + leftTargets -= targetsInGroup; + groups.add(targetsInGroup); + } + + groupsPieChart.setChartState(groups, totalTargetsCount); + groupsLegendLayout.populateGroupsLegendByTargetCounts(groups); + } + + } + private ComboBox createTargetFilterQueryCombo() { - final ComboBox targetFilter = SPUIComponentProvider.getComboBox(null, "", null, ValoTheme.COMBOBOX_SMALL, false, - "", i18n.get("prompt.target.filter")); - targetFilter.setImmediate(true); - targetFilter.setPageLength(7); - targetFilter.setItemCaptionPropertyId(SPUILabelDefinitions.VAR_NAME); - targetFilter.setId(UIComponentIdProvider.ROLLOUT_TARGET_FILTER_COMBO_ID); - targetFilter.setSizeUndefined(); - targetFilter.addValueChangeListener(this::onTargetFilterChange); - return targetFilter; + return new ComboBoxBuilder().setValueChangeListener(this::onTargetFilterChange) + .setPrompt(i18n.get("prompt.target.filter")).setId(UIComponentIdProvider.ROLLOUT_TARGET_FILTER_COMBO_ID) + .buildCombBox(); } private void onTargetFilterChange(final ValueChangeEvent event) { final String filterQueryString = getTargetFilterQuery(); - if (!Strings.isNullOrEmpty(filterQueryString)) { - totalTargetsCount = targetManagement.countTargetByTargetFilterQuery(filterQueryString); - totalTargetsLabel.setValue(getTotalTargetMessage()); - totalTargetsLabel.setVisible(true); - } else { + if (Strings.isNullOrEmpty(filterQueryString)) { totalTargetsCount = 0L; - totalTargetsLabel.setVisible(false); + groupsLegendLayout.populateTotalTargets(null); + defineGroupsLayout.setTargetFilter(null); + } else { + totalTargetsCount = targetManagement.countTargetByTargetFilterQuery(filterQueryString); + groupsLegendLayout.populateTotalTargets(totalTargetsCount); + defineGroupsLayout.setTargetFilter(filterQueryString); } onGroupNumberChange(event); } - private String getTotalTargetMessage() { - return new StringBuilder(i18n.get("label.target.filter.count")).append(totalTargetsCount).toString(); - } - private String getTargetPerGroupMessage(final String value) { return new StringBuilder(i18n.get("label.target.per.group")).append(value).toString(); } @@ -403,6 +586,16 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { targetFilterQueryCombo.setContainerDataSource(container); } + private void populateTargetFilterQuery(final Rollout rollout) { + final Page filterQueries = targetFilterQueryManagement + .findTargetFilterQueryByQuery(new PageRequest(0, 1), rollout.getTargetFilterQuery()); + if(filterQueries.getTotalElements() > 0) { + final TargetFilterQuery filterQuery = filterQueries.getContent().get(0); + targetFilterQueryCombo.setValue(filterQuery.getName()); + } + + } + private static Container createTargetFilterComboContainer() { final BeanQueryFactory targetFilterQF = new BeanQueryFactory<>( TargetFilterBeanQuery.class); @@ -412,19 +605,32 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { } private void editRollout() { - if (rolloutForEdit == null) { + if (rollout == null) { return; } - final Rollout updatedRollout = rolloutManagement.updateRollout(entityFactory.rollout() - .update(rolloutForEdit.getId()).name(rolloutName.getValue()).description(description.getValue())); - uiNotification.displaySuccess(i18n.get("message.update.success", new Object[] { updatedRollout.getName() })); + final DistributionSetIdName distributionSetIdName = (DistributionSetIdName) distributionSet.getValue(); + + RolloutUpdate rolloutUpdate = entityFactory.rollout().update(rollout.getId()).name(rolloutName.getValue()) + .description(description.getValue()).set(distributionSetIdName.getId()).actionType(getActionType()) + .forcedTime(getForcedTimeStamp()); + + if (AutoStartOptionGroupLayout.AutoStartOption.AUTO_START.equals(getAutoStartOption())) { + rolloutUpdate.startAt(System.currentTimeMillis()); + } + if (AutoStartOptionGroupLayout.AutoStartOption.SCHEDULED.equals(getAutoStartOption())) { + rolloutUpdate.startAt(getScheduledStartTime()); + } + + final Rollout updatedRollout = rolloutManagement.updateRollout(rolloutUpdate); + + uiNotification.displaySuccess(i18n.get("message.update.success", updatedRollout.getName())); eventBus.publish(this, RolloutEvent.UPDATE_ROLLOUT); } private boolean duplicateCheckForEdit() { final String rolloutNameVal = getRolloutName(); - if (!rolloutForEdit.getName().equals(rolloutNameVal) + if (!rollout.getName().equals(rolloutNameVal) && rolloutManagement.findRolloutByName(rolloutNameVal) != null) { uiNotification.displayValidationError(i18n.get("message.rollout.duplicate.check", rolloutNameVal)); return false; @@ -433,10 +639,19 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { } private long getForcedTimeStamp() { - return (((ActionTypeOptionGroupLayout.ActionTypeOption) actionTypeOptionGroupLayout.getActionTypeOptionGroup() - .getValue()) == ActionTypeOption.AUTO_FORCED) - ? actionTypeOptionGroupLayout.getForcedTimeDateField().getValue().getTime() - : RepositoryModelConstants.NO_FORCE_TIME; + return ActionTypeOption.AUTO_FORCED.equals(actionTypeOptionGroupLayout.getActionTypeOptionGroup().getValue()) + ? actionTypeOptionGroupLayout.getForcedTimeDateField().getValue().getTime() + : RepositoryModelConstants.NO_FORCE_TIME; + } + + private Long getScheduledStartTime() { + return AutoStartOptionGroupLayout.AutoStartOption.SCHEDULED.equals(getAutoStartOption()) + ? autoStartOptionGroupLayout.getStartAtDateField().getValue().getTime() : null; + } + + private AutoStartOptionGroupLayout.AutoStartOption getAutoStartOption() { + return (AutoStartOptionGroupLayout.AutoStartOption) autoStartOptionGroupLayout.getAutoStartOptionGroup() + .getValue(); } private ActionType getActionType() { @@ -446,26 +661,40 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { private void createRollout() { final Rollout rolloutToCreate = saveRollout(); - uiNotification.displaySuccess(i18n.get("message.save.success", new Object[] { rolloutToCreate.getName() })); + uiNotification.displaySuccess(i18n.get("message.save.success", rolloutToCreate.getName())); } private Rollout saveRollout() { - final int amountGroup = Integer.parseInt(noOfGroups.getValue()); - final int errorThresoldPercent = getErrorThresoldPercentage(amountGroup); - - final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder().withDefaults() - .successAction(RolloutGroupSuccessAction.NEXTGROUP, null) - .successCondition(RolloutGroupSuccessCondition.THRESHOLD, triggerThreshold.getValue()) - .errorCondition(RolloutGroupErrorCondition.THRESHOLD, String.valueOf(errorThresoldPercent)) - .errorAction(RolloutGroupErrorAction.PAUSE, null).build(); - final DistributionSetIdName distributionSetIdName = (DistributionSetIdName) distributionSet.getValue(); - return rolloutManagement.createRollout(entityFactory.rollout().create().name(rolloutName.getValue()) + final int amountGroup = Integer.parseInt(noOfGroups.getValue()); + final int errorThresholdPercent = getErrorThresholdPercentage(amountGroup); + final RolloutGroupConditions conditions = new RolloutGroupConditionBuilder() + .successAction(RolloutGroupSuccessAction.NEXTGROUP, null) + .successCondition(RolloutGroupSuccessCondition.THRESHOLD, triggerThreshold.getValue()) + .errorCondition(RolloutGroupErrorCondition.THRESHOLD, String.valueOf(errorThresholdPercent)) + .errorAction(RolloutGroupErrorAction.PAUSE, null).build(); + + final RolloutCreate rolloutCreate = entityFactory.rollout().create().name(rolloutName.getValue()) .description(description.getValue()).set(distributionSetIdName.getId()) - .targetFilterQuery(getTargetFilterQuery()).actionType(getActionType()).forcedTime(getForcedTimeStamp()), - amountGroup, conditions); + .targetFilterQuery(getTargetFilterQuery()).actionType(getActionType()).forcedTime(getForcedTimeStamp()); + + if (AutoStartOptionGroupLayout.AutoStartOption.AUTO_START.equals(getAutoStartOption())) { + rolloutCreate.startAt(System.currentTimeMillis()); + } + if (AutoStartOptionGroupLayout.AutoStartOption.SCHEDULED.equals(getAutoStartOption())) { + rolloutCreate.startAt(getScheduledStartTime()); + } + + if (isNumberOfGroups()) { + return rolloutManagement.createRollout(rolloutCreate, amountGroup, conditions); + } else if (isGroupsDefinition()) { + List groups = defineGroupsLayout.getSavedRolloutGroups(); + return rolloutManagement.createRollout(rolloutCreate, groups, conditions); + } + + throw new IllegalStateException("Either of the Tabs must be selected"); } private String getTargetFilterQuery() { @@ -478,7 +707,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { return null; } - private int getErrorThresoldPercentage(final int amountGroup) { + private int getErrorThresholdPercentage(final int amountGroup) { int errorThresoldPercent = Integer.parseInt(errorThreshold.getValue()); if (errorThresholdOptionGroup.getValue().equals(ERRORTHRESOLDOPTIONS.COUNT.getValue())) { final int groupSize = (int) Math.ceil((double) totalTargetsCount / (double) amountGroup); @@ -491,7 +720,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { private boolean duplicateCheck() { if (rolloutManagement.findRolloutByName(getRolloutName()) != null) { uiNotification.displayValidationError( - i18n.get("message.rollout.duplicate.check", new Object[] { getRolloutName() })); + i18n.get("message.rollout.duplicate.check", getRolloutName())); return false; } return true; @@ -515,6 +744,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { UIComponentIdProvider.ROLLOUT_ERROR_THRESOLD_ID); errorField.addValidator(new ThresholdFieldValidator()); errorField.setMaxLength(7); + errorField.setValue(defaultRolloutGroupConditions.getErrorConditionExp()); return errorField; } @@ -522,6 +752,7 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { final TextField thresholdField = createIntegerTextField("prompt.tigger.threshold", UIComponentIdProvider.ROLLOUT_TRIGGER_THRESOLD_ID); thresholdField.addValidator(new ThresholdFieldValidator()); + thresholdField.setValue(defaultRolloutGroupConditions.getSuccessConditionExp()); return thresholdField; } @@ -535,23 +766,25 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { } private void onGroupNumberChange(final ValueChangeEvent event) { - if (event.getProperty().getValue() != null && noOfGroups.isValid()) { + if(editRolloutEnabled) { + return; + } + if (event.getProperty().getValue() != null && noOfGroups.isValid() && totalTargetsCount != null + && isNumberOfGroups()) { groupSizeLabel.setValue(getTargetPerGroupMessage(String.valueOf(getGroupSize()))); groupSizeLabel.setVisible(true); + updateGroupsChart(Integer.parseInt(noOfGroups.getValue())); } else { groupSizeLabel.setVisible(false); + if(isNumberOfGroups()) { + updateGroupsChart(0); + } } } private ComboBox createDistributionSetCombo() { - final ComboBox dsSet = SPUIComponentProvider.getComboBox(null, "", null, ValoTheme.COMBOBOX_SMALL, false, "", - i18n.get("prompt.distribution.set")); - dsSet.setImmediate(true); - dsSet.setPageLength(7); - dsSet.setItemCaptionPropertyId(SPUILabelDefinitions.VAR_NAME); - dsSet.setId(UIComponentIdProvider.ROLLOUT_DS_ID); - dsSet.setSizeUndefined(); - return dsSet; + return new ComboBoxBuilder().setPrompt(i18n.get("prompt.distribution.set")) + .setId(UIComponentIdProvider.ROLLOUT_DS_ID).buildCombBox(); } private void populateDistributionSet() { @@ -618,12 +851,12 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { } class GroupNumberValidator implements Validator { - private static final long serialVersionUID = 9049939751976326550L; + private static final long serialVersionUID = 9043919751971326521L; @Override public void validate(final Object value) { if (value != null) { - new IntegerRangeValidator(i18n.get(MESSAGE_ROLLOUT_FIELD_VALUE_RANGE, 0, 500), 0, 500) + new IntegerRangeValidator(i18n.get(MESSAGE_ROLLOUT_FIELD_VALUE_RANGE, 1, 500), 1, 500) .validate(Integer.valueOf(value.toString())); } } @@ -636,31 +869,47 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { * @param rolloutId * rollout id */ - private void populateData(final Long rolloutId) { + private void populateData(final Long rolloutId, final boolean copy) { if (rolloutId == null) { return; } - editRolloutEnabled = Boolean.TRUE; - rolloutForEdit = rolloutManagement.findRolloutById(rolloutId); - rolloutName.setValue(rolloutForEdit.getName()); - description.setValue(rolloutForEdit.getDescription()); - distributionSet.setValue(DistributionSetIdName.generate(rolloutForEdit.getDistributionSet())); - final List rolloutGroups = rolloutForEdit.getRolloutGroups(); - setThresholdValues(rolloutGroups); - setActionType(rolloutForEdit); - disableRequiredFieldsOnEdit(); - targetFilterQuery.setValue(rolloutForEdit.getTargetFilterQuery()); - removeComponent(1, 2); - targetFilterQueryCombo.removeValidator(nullValidator); - addComponent(targetFilterQuery, 1, 2); - targetFilterQuery.addValidator(nullValidator); + rollout = rolloutManagement.findRolloutById(rolloutId); + description.setValue(rollout.getDescription()); + distributionSet.setValue(DistributionSetIdName.generate(rollout.getDistributionSet())); + setActionType(rollout); + setAutoStartType(rollout); - totalTargetsCount = targetManagement.countTargetByTargetFilterQuery(rolloutForEdit.getTargetFilterQuery()); - totalTargetsLabel.setValue(getTotalTargetMessage()); - totalTargetsLabel.setVisible(true); + if (copy) { + rolloutName.setValue(i18n.get("textfield.rollout.copied.name", rollout.getName())); + populateTargetFilterQuery(rollout); - window.setOrginaleValues(); + defineGroupsLayout.populateByRollout(rollout); + groupsDefinitionTabs.setSelectedTab(1); + + window.clearOriginalValues(); + + } else { + editRolloutEnabled = true; + if (rollout.getStatus() != Rollout.RolloutStatus.READY) { + disableRequiredFieldsOnEdit(); + } + rolloutName.setValue(rollout.getName()); + groupsDefinitionTabs.setVisible(false); + + targetFilterQuery.setValue(rollout.getTargetFilterQuery()); + removeComponent(1, 2); + targetFilterQueryCombo.removeValidator(nullValidator); + addComponent(targetFilterQuery, 1, 2); + targetFilterQuery.addValidator(nullValidator); + + window.setOrginaleValues(); + + updateGroupsChart(rollout.getRolloutGroups(), rollout.getTotalTargets()); + } + + totalTargetsCount = targetManagement.countTargetByTargetFilterQuery(rollout.getTargetFilterQuery()); + groupsLegendLayout.populateTotalTargets(totalTargetsCount); } private void disableRequiredFieldsOnEdit() { @@ -668,9 +917,11 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { distributionSet.setEnabled(false); errorThreshold.setEnabled(false); triggerThreshold.setEnabled(false); - actionTypeOptionGroupLayout.getActionTypeOptionGroup().setEnabled(false); errorThresholdOptionGroup.setEnabled(false); + actionTypeOptionGroupLayout.getActionTypeOptionGroup().setEnabled(false); actionTypeOptionGroupLayout.addStyleName(SPUIStyleDefinitions.DISABLE_ACTION_TYPE_LAYOUT); + autoStartOptionGroupLayout.getAutoStartOptionGroup().setEnabled(false); + autoStartOptionGroupLayout.addStyleName(SPUIStyleDefinitions.DISABLE_ACTION_TYPE_LAYOUT); } private void enableFields() { @@ -679,6 +930,8 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { triggerThreshold.setEnabled(true); actionTypeOptionGroupLayout.getActionTypeOptionGroup().setEnabled(true); actionTypeOptionGroupLayout.removeStyleName(SPUIStyleDefinitions.DISABLE_ACTION_TYPE_LAYOUT); + autoStartOptionGroupLayout.getAutoStartOptionGroup().setEnabled(true); + autoStartOptionGroupLayout.removeStyleName(SPUIStyleDefinitions.DISABLE_ACTION_TYPE_LAYOUT); noOfGroups.setEnabled(true); targetFilterQueryCombo.setEnabled(true); errorThresholdOptionGroup.setEnabled(true); @@ -697,39 +950,22 @@ public class AddUpdateRolloutWindowLayout extends GridLayout { } } - /** - * @param rolloutGroups - */ - private void setThresholdValues(final List rolloutGroups) { - if (rolloutGroups != null && !rolloutGroups.isEmpty()) { - errorThreshold.setValue(rolloutGroups.get(0).getErrorConditionExp()); - triggerThreshold.setValue(rolloutGroups.get(0).getSuccessConditionExp()); - noOfGroups.setValue(String.valueOf(rolloutGroups.size())); + private void setAutoStartType(final Rollout rollout) { + if (rollout.getStartAt() == null) { + autoStartOptionGroupLayout.getAutoStartOptionGroup() + .setValue(AutoStartOptionGroupLayout.AutoStartOption.MANUAL); + } else if (rollout.getStartAt() < System.currentTimeMillis()) { + autoStartOptionGroupLayout.getAutoStartOptionGroup() + .setValue(AutoStartOptionGroupLayout.AutoStartOption.AUTO_START); + autoStartOptionGroupLayout.getStartAtDateField().setValue(new Date(rollout.getStartAt())); } else { - errorThreshold.setValue("0"); - triggerThreshold.setValue("0"); - noOfGroups.setValue("0"); + autoStartOptionGroupLayout.getAutoStartOptionGroup() + .setValue(AutoStartOptionGroupLayout.AutoStartOption.SCHEDULED); + autoStartOptionGroupLayout.getStartAtDateField().setValue(new Date(rollout.getStartAt())); } } - enum SAVESTARTOPTIONS { - SAVE("Save"), START("Start"); - - String value; - - SAVESTARTOPTIONS(final String val) { - this.value = val; - } - - /** - * @return the value - */ - public String getValue() { - return value; - } - } - - enum ERRORTHRESOLDOPTIONS { + private enum ERRORTHRESOLDOPTIONS { PERCENT("%"), COUNT("Count"); String value; diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AutoStartOptionGroupLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AutoStartOptionGroupLayout.java new file mode 100644 index 000000000..17734c321 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/AutoStartOptionGroupLayout.java @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.rollout.rollout; + +import java.time.LocalDateTime; +import java.util.Date; +import java.util.TimeZone; + +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPDateTimeUtil; +import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; +import org.vaadin.hene.flexibleoptiongroup.FlexibleOptionGroup; +import org.vaadin.hene.flexibleoptiongroup.FlexibleOptionGroupItemComponent; + +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.server.FontAwesome; +import com.vaadin.shared.ui.datefield.Resolution; +import com.vaadin.ui.DateField; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.themes.ValoTheme; + +/** + * Rollout start types options layout + */ +public class AutoStartOptionGroupLayout extends HorizontalLayout { + + private static final long serialVersionUID = -8460459258964093525L; + private static final String STYLE_DIST_WINDOW_AUTO_START = "dist-window-actiontype"; + + private final I18N i18n; + + private FlexibleOptionGroup autoStartOptionGroup; + + private DateField startAtDateField; + + /** + * Instantiates the auto start options layout + * + * @param i18n + * the internationalization helper + */ + public AutoStartOptionGroupLayout(final I18N i18n) { + this.i18n = i18n; + + createOptionGroup(); + addValueChangeListener(); + setStyleName("dist-window-actiontype-horz-layout"); + setSizeUndefined(); + } + + private void addValueChangeListener() { + autoStartOptionGroup.addValueChangeListener(new ValueChangeListener() { + private static final long serialVersionUID = 1L; + + @Override + public void valueChange(final ValueChangeEvent event) { + if (event.getProperty().getValue().equals(AutoStartOption.SCHEDULED)) { + startAtDateField.setEnabled(true); + startAtDateField.setRequired(true); + } else { + startAtDateField.setEnabled(false); + startAtDateField.setRequired(false); + } + } + }); + } + + private void createOptionGroup() { + autoStartOptionGroup = new FlexibleOptionGroup(); + autoStartOptionGroup.addItem(AutoStartOption.MANUAL); + autoStartOptionGroup.addItem(AutoStartOption.AUTO_START); + autoStartOptionGroup.addItem(AutoStartOption.SCHEDULED); + selectDefaultOption(); + + final FlexibleOptionGroupItemComponent manualItem = autoStartOptionGroup + .getItemComponent(AutoStartOption.MANUAL); + manualItem.setStyleName(STYLE_DIST_WINDOW_AUTO_START); + // set Id for Forced radio button. + manualItem.setId(UIComponentIdProvider.ROLLOUT_START_MANUAL_ID); + addComponent(manualItem); + final Label manualLabel = new Label(); + manualLabel.setStyleName("statusIconPending"); + manualLabel.setIcon(FontAwesome.HAND_PAPER_O); + manualLabel.setCaption(i18n.get("caption.rollout.start.manual")); + manualLabel.setDescription(i18n.get("caption.rollout.start.manual.desc")); + manualLabel.setStyleName("padding-right-style"); + addComponent(manualLabel); + + final FlexibleOptionGroupItemComponent autoStartItem = autoStartOptionGroup + .getItemComponent(AutoStartOption.AUTO_START); + autoStartItem.setId(UIComponentIdProvider.ROLLOUT_START_AUTO_ID); + autoStartItem.setStyleName(STYLE_DIST_WINDOW_AUTO_START); + addComponent(autoStartItem); + final Label autoStartLabel = new Label(); + autoStartLabel.setSizeFull(); + autoStartLabel.setIcon(FontAwesome.PLAY); + autoStartLabel.setCaption(i18n.get("caption.rollout.start.auto")); + autoStartLabel.setDescription(i18n.get("caption.rollout.start.auto.desc")); + autoStartLabel.setStyleName("padding-right-style"); + addComponent(autoStartLabel); + + final FlexibleOptionGroupItemComponent scheduledItem = autoStartOptionGroup + .getItemComponent(AutoStartOption.SCHEDULED); + scheduledItem.setStyleName(STYLE_DIST_WINDOW_AUTO_START); + // setted Id for Time Forced radio button. + scheduledItem.setId(UIComponentIdProvider.ROLLOUT_START_SCHEDULED_ID); + addComponent(scheduledItem); + final Label scheduledLabel = new Label(); + scheduledLabel.setStyleName("statusIconPending"); + scheduledLabel.setIcon(FontAwesome.CLOCK_O); + scheduledLabel.setCaption(i18n.get("caption.rollout.start.scheduled")); + scheduledLabel.setDescription(i18n.get("caption.rollout.start.scheduled.desc")); + scheduledLabel.setStyleName(STYLE_DIST_WINDOW_AUTO_START); + addComponent(scheduledLabel); + + startAtDateField = new DateField(); + startAtDateField.setInvalidAllowed(false); + startAtDateField.setInvalidCommitted(false); + startAtDateField.setEnabled(false); + startAtDateField.setStyleName("dist-window-forcedtime"); + + final TimeZone tz = SPDateTimeUtil.getBrowserTimeZone(); + startAtDateField.setValue( + Date.from(LocalDateTime.now().plusMinutes(30).atZone(SPDateTimeUtil.getTimeZoneId(tz)).toInstant())); + startAtDateField.setImmediate(true); + startAtDateField.setTimeZone(tz); + startAtDateField.setLocale(i18n.getLocale()); + startAtDateField.setResolution(Resolution.MINUTE); + startAtDateField.addStyleName(ValoTheme.DATEFIELD_SMALL); + addComponent(startAtDateField); + } + + /** + * To Set Default option for save. + */ + + public void selectDefaultOption() { + autoStartOptionGroup.select(AutoStartOption.MANUAL); + } + + /** + * Rollout start options + */ + public enum AutoStartOption { + MANUAL, AUTO_START, SCHEDULED; + + } + + public FlexibleOptionGroup getAutoStartOptionGroup() { + return autoStartOptionGroup; + } + + public DateField getStartAtDateField() { + return startAtDateField; + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/DefineGroupsLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/DefineGroupsLayout.java new file mode 100644 index 000000000..0be334ee7 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/DefineGroupsLayout.java @@ -0,0 +1,617 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.rollout.rollout; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.vaadin.ui.UI; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.hawkbit.repository.EntityFactory; +import org.eclipse.hawkbit.repository.RolloutManagement; +import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; +import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate; +import org.eclipse.hawkbit.repository.model.Rollout; +import org.eclipse.hawkbit.repository.model.RolloutGroup; +import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder; +import org.eclipse.hawkbit.repository.model.RolloutGroupsValidation; +import org.eclipse.hawkbit.repository.model.TargetFilterQuery; +import org.eclipse.hawkbit.ui.common.builder.ComboBoxBuilder; +import org.eclipse.hawkbit.ui.common.builder.LabelBuilder; +import org.eclipse.hawkbit.ui.common.builder.TextAreaBuilder; +import org.eclipse.hawkbit.ui.common.builder.TextFieldBuilder; +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleNoBorderWithIcon; +import org.eclipse.hawkbit.ui.filtermanagement.TargetFilterBeanQuery; +import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; +import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.util.concurrent.ListenableFuture; +import org.vaadin.addons.lazyquerycontainer.BeanQueryFactory; +import org.vaadin.addons.lazyquerycontainer.LazyQueryContainer; +import org.vaadin.addons.lazyquerycontainer.LazyQueryDefinition; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Validator; +import com.vaadin.data.util.converter.StringToFloatConverter; +import com.vaadin.data.util.converter.StringToIntegerConverter; +import com.vaadin.data.validator.FloatRangeValidator; +import com.vaadin.data.validator.IntegerRangeValidator; +import com.vaadin.data.validator.StringLengthValidator; +import com.vaadin.server.FontAwesome; +import com.vaadin.server.UserError; +import com.vaadin.ui.Button; +import com.vaadin.ui.ComboBox; +import com.vaadin.ui.Component; +import com.vaadin.ui.GridLayout; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.TextArea; +import com.vaadin.ui.TextField; + +/** + * Define groups for a Rollout + */ +public class DefineGroupsLayout extends GridLayout { + + private static final long serialVersionUID = 2939193468001472916L; + + private I18N i18n; + + private transient EntityFactory entityFactory; + + private transient RolloutManagement rolloutManagement; + + private transient TargetFilterQueryManagement targetFilterQueryManagement; + + private String defaultTriggerThreshold; + + private String defaultErrorThreshold; + + private String targetFilter; + + private transient List groupRows; + + private int groupsCount; + + private transient List savedRolloutGroups; + + private transient ValidationListener validationListener; + + private ValidationStatus validationStatus = ValidationStatus.VALID; + + private transient RolloutGroupsValidation groupsValidation; + + private transient ListenableFuture runningValidation; + + private boolean validationRequested; + + DefineGroupsLayout(I18N i18n, EntityFactory entityFactory, RolloutManagement rolloutManagement, + TargetFilterQueryManagement targetFilterQueryManagement) { + this.i18n = i18n; + this.entityFactory = entityFactory; + this.rolloutManagement = rolloutManagement; + this.targetFilterQueryManagement = targetFilterQueryManagement; + + groupRows = new ArrayList<>(10); + setSizeUndefined(); + buildLayout(); + + } + + private void buildLayout() { + + setSpacing(Boolean.TRUE); + setSizeUndefined(); + setRows(3); + setColumns(6); + setStyleName("marginTop"); + + addComponent(getLabel("caption.rollout.group.definition.desc"), 0, 0, 5, 0); + + final int headerRow = 1; + addComponent(getLabel("header.name"), 0, headerRow); + addComponent(getLabel("header.target.filter.query"), 1, headerRow); + addComponent(getLabel("header.target.percentage"), 2, headerRow); + addComponent(getLabel("header.rolloutgroup.threshold"), 3, headerRow); + addComponent(getLabel("header.rolloutgroup.threshold.error"), 4, headerRow); + + addComponent(createAddButton(), 0, 2, 5, 2); + + } + + /** + * @param targetFilter + * the target filter which is required for verification + */ + public void setTargetFilter(final String targetFilter) { + this.targetFilter = targetFilter; + updateValidation(); + } + + private int addRow() { + final int insertIndex = getRows() - 1; + insertRow(insertIndex); + return insertIndex; + } + + private Label getLabel(final String key) { + return new LabelBuilder().name(i18n.get(key)).buildLabel(); + } + + private Button createAddButton() { + Button button = SPUIComponentProvider.getButton(UIComponentIdProvider.ROLLOUT_GROUP_ADD_ID, + i18n.get("button.rollout.add.group"), "", "", true, FontAwesome.PLUS, + SPUIButtonStyleNoBorderWithIcon.class); + button.setSizeUndefined(); + button.addStyleName("default-color"); + button.setEnabled(true); + button.setVisible(true); + button.addClickListener(event -> addGroupRow()); + return button; + } + + private GroupRow addGroupRow() { + int rowIndex = addRow(); + GroupRow groupRow = new GroupRow(); + groupRow.addToGridRow(this, rowIndex); + groupRows.add(groupRow); + updateValidation(); + return groupRow; + } + + public List getSavedRolloutGroups() { + return savedRolloutGroups; + } + + /** + * @return the validation instance if was already validated + */ + public RolloutGroupsValidation getGroupsValidation() { + return groupsValidation; + } + + private void removeAllRows() { + for (int i = getRows() - 2; i > 1; i--) { + removeRow(i); + } + + groupRows.clear(); + } + + public void setDefaultTriggerThreshold(final String defaultTriggerThreshold) { + this.defaultTriggerThreshold = defaultTriggerThreshold; + } + + public void setDefaultErrorThreshold(final String defaultErrorThreshold) { + this.defaultErrorThreshold = defaultErrorThreshold; + } + + /** + * Reset the field values. + */ + public void resetComponents() { + + validationStatus = ValidationStatus.VALID; + groupsCount = 0; + + removeAllRows(); + addGroupRow(); + + } + + /** + * Populate groups by rollout + * + * @param rollout + * the rollout + */ + public void populateByRollout(final Rollout rollout) { + if (rollout == null) { + return; + } + + removeAllRows(); + + final List groups = rollout.getRolloutGroups(); + for (final RolloutGroup group : groups) { + final GroupRow groupRow = addGroupRow(); + groupRow.populateByGroup(group); + } + + } + + /** + * @return whether the groups definition form is valid + */ + public boolean isValid() { + if (groupRows.isEmpty() || validationStatus != ValidationStatus.VALID) { + return false; + } + return groupRows.stream().allMatch(GroupRow::isValid); + } + + private void updateValidation() { + validationStatus = ValidationStatus.VALID; + if (isValid()) { + setValidationStatus(ValidationStatus.LOADING); + savedRolloutGroups = getGroupsFromRows(); + validateRemainingTargets(); + + } else { + resetRemainingTargetsError(); + setValidationStatus(ValidationStatus.INVALID); + } + } + + private void setValidationStatus(final ValidationStatus status) { + validationStatus = status; + if (validationListener != null) { + validationListener.validation(status); + } + } + + private void resetRemainingTargetsError() { + groupRows.forEach(GroupRow::hideLastGroupError); + } + + private void validateRemainingTargets() { + resetRemainingTargetsError(); + if (targetFilter == null || (runningValidation != null && !runningValidation.isDone())) { + validationRequested = true; + return; + } + + validationRequested = false; + + final UI ui = UI.getCurrent(); + + runningValidation = rolloutManagement + .validateTargetsInGroups(savedRolloutGroups, targetFilter, System.currentTimeMillis()); + runningValidation.addCallback(validation -> ui.access(() -> this.setGroupsValidation(validation)), + throwable -> ui.access(() -> this.setGroupsValidation(null))); + + } + + private void setGroupsValidation(RolloutGroupsValidation validation) { + groupsValidation = validation; + + if(validationRequested) { + validateRemainingTargets(); + return; + } + + final GroupRow lastRow = groupRows.get(groupRows.size() - 1); + if (groupsValidation != null && groupsValidation.isValid() && validationStatus != ValidationStatus.INVALID) { + lastRow.hideLastGroupError(); + setValidationStatus(ValidationStatus.VALID); + + } else { + lastRow.markWithLastGroupError(); + setValidationStatus(ValidationStatus.INVALID); + } + + } + + private List getGroupsFromRows() { + return groupRows.stream().map(GroupRow::getGroupEntity).collect(Collectors.toList()); + } + + public void setValidationListener(final ValidationListener validationListener) { + this.validationListener = validationListener; + } + + /** + * Status of the groups validation + */ + public enum ValidationStatus { + VALID, + INVALID, + LOADING + } + + /** + * Implement the interface and set the instance with setValidationListener + * to receive updates for any changes within the group rows. + */ + @FunctionalInterface + public interface ValidationListener { + /** + * Is called after user input + * + * @param isValid + * whether the input of the group rows is valid + */ + void validation(ValidationStatus isValid); + } + + + private class GroupRow { + + private TextField groupName; + + private ComboBox targetFilterQueryCombo; + + private TextArea targetFilterQuery; + + private TextField targetPercentage; + + private TextField triggerThreshold; + + private TextField errorThreshold; + + private HorizontalLayout optionsLayout; + + private boolean populated; + + private boolean initialized; + + public GroupRow() { + init(); + } + + private void init() { + groupsCount += 1; + groupName = createTextField("textfield.name", UIComponentIdProvider.ROLLOUT_GROUP_LIST_GRID_ID); + groupName.setValue(i18n.get("textfield.rollout.group.default.name", groupsCount)); + groupName.setStyleName("rollout-group-name"); + + targetFilterQueryCombo = createTargetFilterQueryCombo(); + populateTargetFilterQuery(); + + targetFilterQuery = createTargetFilterQuery(); + + targetPercentage = createPercentageWithDecimalsField("textfield.target.percentage", + UIComponentIdProvider.ROLLOUT_GROUP_TARGET_PERC_ID); + targetPercentage.setValue("100"); + + triggerThreshold = createPercentageField("prompt.tigger.threshold", + UIComponentIdProvider.ROLLOUT_TRIGGER_THRESOLD_ID); + triggerThreshold.setValue(defaultTriggerThreshold); + + errorThreshold = createPercentageField("prompt.error.threshold", + UIComponentIdProvider.ROLLOUT_ERROR_THRESOLD_ID); + errorThreshold.setValue(defaultErrorThreshold); + + optionsLayout = new HorizontalLayout(); + optionsLayout.addComponent(createRemoveButton()); + + initialized = true; + } + + private TextField createTextField(final String in18Key, final String id) { + final TextField textField = new TextFieldBuilder().prompt(i18n.get(in18Key)).immediate(true).id(id) + .buildTextComponent(); + textField.setSizeUndefined(); + textField.addValidator( + new StringLengthValidator(i18n.get("message.rollout.group.name.invalid"), 1, 64, false)); + textField.addValueChangeListener(event -> valueChanged()); + return textField; + } + + private TextField createPercentageField(final String in18Key, final String id) { + final TextField textField = new TextFieldBuilder().prompt(i18n.get(in18Key)).immediate(true).id(id) + .buildTextComponent(); + textField.setWidth(80, Unit.PIXELS); + textField.setNullRepresentation(""); + textField.setConverter(new StringToIntegerConverter()); + textField.addValidator(this::validateMandatoryPercentage); + textField.addValueChangeListener(event -> valueChanged()); + return textField; + } + + private TextField createPercentageWithDecimalsField(final String in18Key, final String id) { + final TextField textField = createPercentageField(in18Key, id); + textField.setConverter(new StringToFloatConverter()); + return textField; + } + + private void removeGroupRow(final GroupRow groupRow) { + groupRows.remove(groupRow); + updateValidation(); + } + + private void validateMandatoryPercentage(final Object value) { + if (value != null) { + final String message = i18n.get("message.rollout.field.value.range", 0, 100); + if (value instanceof Float) { + new FloatRangeValidator(message, 0F, 100F).validate(value); + } + if (value instanceof Integer) { + new IntegerRangeValidator(message, 0, 100).validate(value); + } + } else { + throw new Validator.EmptyValueException(i18n.get("message.enter.number")); + } + } + + private void valueChanged() { + if (initialized) { + updateValidation(); + } + } + + private ComboBox createTargetFilterQueryCombo() { + return new ComboBoxBuilder().setId(UIComponentIdProvider.ROLLOUT_TARGET_FILTER_COMBO_ID) + .setPrompt(i18n.get("prompt.target.filter")).setValueChangeListener(event -> valueChanged()) + .buildCombBox(); + } + + private TextArea createTargetFilterQuery() { + final TextArea filterField = new TextAreaBuilder().style("text-area-style") + .id(UIComponentIdProvider.ROLLOUT_TARGET_FILTER_QUERY_FIELD) + .maxLengthAllowed(SPUILabelDefinitions.TARGET_FILTER_QUERY_TEXT_FIELD_LENGTH).buildTextComponent(); + + filterField.setNullRepresentation(StringUtils.EMPTY); + filterField.setEnabled(false); + filterField.setSizeUndefined(); + return filterField; + } + + private void populateTargetFilterQuery() { + final Container container = createTargetFilterComboContainer(); + targetFilterQueryCombo.setContainerDataSource(container); + } + + private void populateTargetFilterQuery(final RolloutGroup group) { + if (StringUtils.isEmpty(group.getTargetFilterQuery())) { + targetFilterQueryCombo.setValue(null); + } else { + final Page filterQueries = targetFilterQueryManagement + .findTargetFilterQueryByQuery(new PageRequest(0, 1), group.getTargetFilterQuery()); + if (filterQueries.getTotalElements() > 0) { + final TargetFilterQuery filterQuery = filterQueries.getContent().get(0); + targetFilterQueryCombo.setValue(filterQuery.getName()); + } + } + } + + private Container createTargetFilterComboContainer() { + final BeanQueryFactory targetFilterQF = new BeanQueryFactory<>( + TargetFilterBeanQuery.class); + return new LazyQueryContainer( + new LazyQueryDefinition(true, SPUIDefinitions.PAGE_SIZE, SPUILabelDefinitions.VAR_NAME), + targetFilterQF); + } + + private Button createRemoveButton() { + Button button = SPUIComponentProvider.getButton(UIComponentIdProvider.ROLLOUT_GROUP_REMOVE_ID, "", "", "", + true, FontAwesome.MINUS, SPUIButtonStyleNoBorderWithIcon.class); + button.setSizeUndefined(); + button.addStyleName("default-color"); + button.setEnabled(true); + button.setVisible(true); + button.addClickListener(event -> onRemove()); + return button; + } + + private void onRemove() { + int index = findRowIndexFor(groupName, 0); + if (index != -1) { + removeRow(index); + } + removeGroupRow(this); + } + + private int findRowIndexFor(final Component component, final int col) { + final int rows = getRows(); + for (int i = 0; i < rows; i++) { + Component rowComponent = getComponent(col, i); + if (component.equals(rowComponent)) { + return i; + } + } + return -1; + } + + private String getTargetFilterQuery() { + if (null != targetFilterQueryCombo.getValue() + && HawkbitCommonUtil.trimAndNullIfEmpty((String) targetFilterQueryCombo.getValue()) != null) { + final Item filterItem = targetFilterQueryCombo.getContainerDataSource() + .getItem(targetFilterQueryCombo.getValue()); + return (String) filterItem.getItemProperty("query").getValue(); + } + return null; + } + + /** + * Adds this group row to a grid layout + * + * @param layout + * the grid layout + * @param rowIndex + * the row of the grid layout + */ + public void addToGridRow(GridLayout layout, int rowIndex) { + layout.addComponent(groupName, 0, rowIndex); + if (populated) { + layout.addComponent(targetFilterQuery, 1, rowIndex); + } else { + layout.addComponent(targetFilterQueryCombo, 1, rowIndex); + } + layout.addComponent(targetPercentage, 2, rowIndex); + layout.addComponent(triggerThreshold, 3, rowIndex); + layout.addComponent(errorThreshold, 4, rowIndex); + layout.addComponent(optionsLayout, 5, rowIndex); + + } + + /** + * Builds a group definition from this group row + * + * @return the RolloutGroupCreate definition + */ + public RolloutGroupCreate getGroupEntity() { + final RolloutGroupConditionBuilder conditionBuilder = new RolloutGroupConditionBuilder() + .successAction(RolloutGroup.RolloutGroupSuccessAction.NEXTGROUP, null) + .successCondition(RolloutGroup.RolloutGroupSuccessCondition.THRESHOLD, triggerThreshold.getValue()); + if (StringUtils.isNotEmpty(errorThreshold.getValue())) { + conditionBuilder + .errorCondition(RolloutGroup.RolloutGroupErrorCondition.THRESHOLD, errorThreshold.getValue()) + .errorAction(RolloutGroup.RolloutGroupErrorAction.PAUSE, null); + } + String percentageString = targetPercentage.getValue().replace(",", "."); + Float percentage = Float.parseFloat(percentageString); + + return entityFactory.rolloutGroup().create().name(groupName.getValue()).description(groupName.getValue()) + .targetFilterQuery(getTargetFilterQuery()).targetPercentage(percentage) + .conditions(conditionBuilder.build()); + } + + /** + * Populates the row with the data from the provided groups. + * + * @param group + * the data source + */ + public void populateByGroup(final RolloutGroup group) { + + groupName.setValue(group.getName()); + targetFilterQuery.setValue(group.getTargetFilterQuery()); + populateTargetFilterQuery(group); + + targetPercentage.setValue(String.format("%.2f", group.getTargetPercentage())); + triggerThreshold.setValue(group.getSuccessConditionExp()); + errorThreshold.setValue(group.getErrorConditionExp()); + + populated = true; + + } + + /** + * @return whether the data entered in this row is valid + */ + public boolean isValid() { + return StringUtils.isNotEmpty(groupName.getValue()) && targetPercentage.isValid() + && triggerThreshold.isValid() && errorThreshold.isValid(); + } + + /** + * Displays an error for the row + */ + public void markWithLastGroupError() { + targetPercentage.setComponentError(new UserError(i18n.get("message.rollout.remaining.targets.error"))); + } + + /** + * Hides an error of the row + */ + public void hideLastGroupError() { + targetPercentage.setComponentError(null); + } + + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/GroupsLegendLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/GroupsLegendLayout.java new file mode 100644 index 000000000..7bf1a3857 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/GroupsLegendLayout.java @@ -0,0 +1,245 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.rollout.rollout; + +import com.vaadin.ui.Component; +import com.vaadin.ui.Label; +import com.vaadin.ui.VerticalLayout; +import org.eclipse.hawkbit.repository.builder.RolloutGroupCreate; +import org.eclipse.hawkbit.repository.model.RolloutGroup; +import org.eclipse.hawkbit.repository.model.RolloutGroupsValidation; +import org.eclipse.hawkbit.ui.common.builder.LabelBuilder; +import org.eclipse.hawkbit.ui.utils.I18N; + +import java.util.Collections; +import java.util.List; + +/** + * Displays a legend for the Groups of a Rollout with the count of targets in + * each group. On top of the group list, the total targets are displayed. If + * there are unassigned targets, they get display on top of the groups list. + */ +public class GroupsLegendLayout extends VerticalLayout { + + private static final long serialVersionUID = 5483206203739308677L; + + private final I18N i18n; + + private Label totalTargetsLabel; + + private Label loadingLabel; + + private Label unassignedTargetsLabel; + + private VerticalLayout groupsLegend; + + /** + * Initializes a new GroupsLegendLayout + */ + GroupsLegendLayout(final I18N i18n) { + this.i18n = i18n; + + init(); + } + + private void init() { + + totalTargetsLabel = createTotalTargetsLabel(); + unassignedTargetsLabel = createUnassignedTargetsLabel(); + loadingLabel = createLoadingLabel(); + loadingLabel.setVisible(false); + + groupsLegend = new VerticalLayout(); + groupsLegend.setStyleName("groups-legend"); + + addComponent(totalTargetsLabel); + addComponent(loadingLabel); + addComponent(unassignedTargetsLabel); + addComponent(groupsLegend); + for (int i = 0; i < 8; i++) { + groupsLegend.addComponent(createGroupTargetsLabel()); + } + + } + + /** + * Resets the display of the legend and total targets. + */ + public void reset() { + totalTargetsLabel.setVisible(false); + populateGroupsLegendByTargetCounts(Collections.emptyList()); + } + + private static Label createTotalTargetsLabel() { + final Label label = new LabelBuilder().visible(false).name("").buildLabel(); + label.addStyleName("rollout-target-count-title"); + label.setImmediate(true); + label.setSizeUndefined(); + return label; + } + + private Label createLoadingLabel() { + final Label label = new LabelBuilder().visible(false).name("").buildLabel(); + label.addStyleName("rollout-target-count-loading"); + label.setImmediate(true); + label.setSizeUndefined(); + label.setValue(i18n.get("label.rollout.calculating")); + return label; + } + + private static Label createUnassignedTargetsLabel() { + final Label label = new LabelBuilder().visible(false).name("").buildLabel(); + label.addStyleName("rollout-group-unassigned"); + label.setSizeUndefined(); + return label; + } + + private static Label createGroupTargetsLabel() { + final Label label = new LabelBuilder().visible(false).name("").buildLabel(); + label.addStyleName("rollout-group-count"); + label.setSizeUndefined(); + return label; + } + + private String getTotalTargetMessage(final long totalTargetsCount) { + return i18n.get("label.target.filter.count") + totalTargetsCount; + } + + /** + * Display an indication that the legend is being calculated. + * When the loading process is done one of the populate methods should be called. + */ + public void displayLoading() { + populateGroupsLegendByTargetCounts(Collections.emptyList()); + loadingLabel.setVisible(true); + } + + /** + * Displays the total targets or hides the label when null is supplied. + * + * @param totalTargets + * null to hide the label or a count to be displayed as total + * targets message + */ + public void populateTotalTargets(Long totalTargets) { + if (totalTargets == null) { + totalTargetsLabel.setVisible(false); + } else { + totalTargetsLabel.setVisible(true); + totalTargetsLabel.setValue(getTotalTargetMessage(totalTargets)); + } + } + + /** + * Populates the legend based on a list of anonymous groups. They can't have + * unassigned targets. + * + * @param targetsPerGroup + * list of target counts + */ + public void populateGroupsLegendByTargetCounts(final List targetsPerGroup) { + loadingLabel.setVisible(false); + + for (int i = 0; i < groupsLegend.getComponentCount(); i++) { + final Component component = groupsLegend.getComponent(i); + final Label label = (Label) component; + if (targetsPerGroup.size() > i) { + final Long targetCount = targetsPerGroup.get(i); + label.setValue( + getTargetsInGroupMessage(targetCount, i18n.get("textfield.rollout.group.default.name", i + 1))); + label.setVisible(true); + } else { + label.setValue(""); + label.setVisible(false); + } + } + + unassignedTargetsLabel.setValue(""); + unassignedTargetsLabel.setVisible(false); + + } + + /** + * Populates the legend based on a groups validation and a list of groups + * that is used for resolving their names. Positions of the groups in the + * groups list and the validation need to be in correct order. Can have + * unassigned targets that are displayed on top of the groups list which + * results in one group less to be displayed + * + * @param validation + * A rollout validation object that contains a list of target + * counts and the total targets + * @param groups + * List of groups with their name + */ + public void populateGroupsLegendByValidation(final RolloutGroupsValidation validation, final List groups) { + loadingLabel.setVisible(false); + if (validation == null) { + return; + } + List targetsPerGroup = validation.getTargetsPerGroup(); + final long unassigned = validation.getTotalTargets() - validation.getTargetsInGroups(); + final int labelsToUpdate = (unassigned > 0) ? (groupsLegend.getComponentCount() - 1) + : groupsLegend.getComponentCount(); + for (int i = 0; i < groupsLegend.getComponentCount(); i++) { + final Component component = groupsLegend.getComponent(i); + final Label label = (Label) component; + if (targetsPerGroup.size() > i && groups.size() > i && labelsToUpdate > i) { + final Long targetCount = targetsPerGroup.get(i); + final String groupName = groups.get(i).build().getName(); + + label.setValue(getTargetsInGroupMessage(targetCount, groupName)); + label.setVisible(true); + } else { + label.setValue(""); + label.setVisible(false); + } + } + + if (unassigned > 0) { + unassignedTargetsLabel.setValue(getTargetsInGroupMessage(unassigned, "Unassigned")); + unassignedTargetsLabel.setVisible(true); + } else { + unassignedTargetsLabel.setValue(""); + unassignedTargetsLabel.setVisible(false); + } + + } + + /** + * Populates the legend based on a list of groups. + * + * @param groups + * List of groups with their name + */ + public void populateGroupsLegendByGroups(final List groups) { + loadingLabel.setVisible(false); + + for (int i = 0; i < groupsLegend.getComponentCount(); i++) { + final Component component = groupsLegend.getComponent(i); + final Label label = (Label) component; + if (groups.size() > i) { + final int targetCount = groups.get(i).getTotalTargets(); + final String groupName = groups.get(i).getName(); + + label.setValue(getTargetsInGroupMessage((long) targetCount, groupName)); + label.setVisible(true); + } else { + label.setValue(""); + label.setVisible(false); + } + } + + } + + private String getTargetsInGroupMessage(final Long targets, final String groupName) { + return i18n.get("label.rollout.targets.in.group", targets, groupName); + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java index ad47ba5b3..7ca4ab692 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListGrid.java @@ -37,6 +37,7 @@ import java.util.Set; import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.RolloutManagement; +import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus; @@ -87,6 +88,8 @@ public class RolloutListGrid extends AbstractGrid { private static final String UPDATE_OPTION = "Update"; + private static final String COPY_OPTION = "Copy"; + private static final String PAUSE_OPTION = "Pause"; private static final String RUN_OPTION = "Run"; @@ -130,11 +133,12 @@ public class RolloutListGrid extends AbstractGrid { RolloutListGrid(final I18N i18n, final UIEventBus eventBus, final RolloutManagement rolloutManagement, final UINotification uiNotification, final RolloutUIState rolloutUIState, final SpPermissionChecker permissionChecker, final TargetManagement targetManagement, - final EntityFactory entityFactory, final UiProperties uiProperties) { + final EntityFactory entityFactory, final UiProperties uiProperties, + final TargetFilterQueryManagement targetFilterQueryManagement) { super(i18n, eventBus, permissionChecker); this.rolloutManagement = rolloutManagement; this.addUpdateRolloutWindow = new AddUpdateRolloutWindowLayout(rolloutManagement, targetManagement, - uiNotification, uiProperties, entityFactory, i18n, eventBus); + uiNotification, uiProperties, entityFactory, i18n, eventBus, targetFilterQueryManagement); this.uiNotification = uiNotification; this.rolloutUIState = rolloutUIState; } @@ -230,6 +234,10 @@ public class RolloutListGrid extends AbstractGrid { rolloutGridContainer.addContainerProperty(UPDATE_OPTION, String.class, FontAwesome.EDIT.getHtml(), false, false); } + if (permissionChecker.hasRolloutCreatePermission()) { + rolloutGridContainer.addContainerProperty(COPY_OPTION, String.class, FontAwesome.COPY.getHtml(), false, + false); + } } @Override @@ -262,6 +270,10 @@ public class RolloutListGrid extends AbstractGrid { } else { getColumn(PAUSE_OPTION).setMaximumWidth(60); } + if (permissionChecker.hasRolloutCreatePermission()) { + getColumn(COPY_OPTION).setMinimumWidth(25); + getColumn(COPY_OPTION).setMaximumWidth(25); + } getColumn(VAR_TOTAL_TARGETS_COUNT_STATUS).setMinimumWidth(280); } @@ -289,10 +301,13 @@ public class RolloutListGrid extends AbstractGrid { if (permissionChecker.hasRolloutUpdatePermission()) { getColumn(UPDATE_OPTION).setHeaderCaption(i18n.get("header.action.update")); } + if (permissionChecker.hasRolloutCreatePermission()) { + getColumn(COPY_OPTION).setHeaderCaption(i18n.get("header.action.copy")); + } HeaderCell join; if (permissionChecker.hasRolloutUpdatePermission()) { - join = getDefaultHeaderRow().join(RUN_OPTION, PAUSE_OPTION, UPDATE_OPTION); + join = getDefaultHeaderRow().join(RUN_OPTION, PAUSE_OPTION, UPDATE_OPTION, COPY_OPTION); } else { join = getDefaultHeaderRow().join(RUN_OPTION, PAUSE_OPTION); } @@ -323,6 +338,9 @@ public class RolloutListGrid extends AbstractGrid { if (permissionChecker.hasRolloutUpdatePermission()) { columnList.add(UPDATE_OPTION); } + if (permissionChecker.hasRolloutCreatePermission()) { + columnList.add(COPY_OPTION); + } columnList.add(VAR_CREATED_DATE); columnList.add(VAR_CREATED_USER); @@ -377,6 +395,10 @@ public class RolloutListGrid extends AbstractGrid { getColumn(UPDATE_OPTION) .setRenderer(new HtmlButtonRenderer(clickEvent -> updateRollout((Long) clickEvent.getItemId()))); } + if (permissionChecker.hasRolloutCreatePermission()) { + getColumn(COPY_OPTION) + .setRenderer(new HtmlButtonRenderer(clickEvent -> copyRollout((Long) clickEvent.getItemId()))); + } } @@ -430,12 +452,19 @@ public class RolloutListGrid extends AbstractGrid { } private void updateRollout(final Long rolloutId) { - final CommonDialogWindow addTargetWindow = addUpdateRolloutWindow.getWindow(rolloutId); + final CommonDialogWindow addTargetWindow = addUpdateRolloutWindow.getWindow(rolloutId, false); addTargetWindow.setCaption(i18n.get("caption.update.rollout")); UI.getCurrent().addWindow(addTargetWindow); addTargetWindow.setVisible(Boolean.TRUE); } + private void copyRollout(final Long rolloutId) { + final CommonDialogWindow addTargetWindow = addUpdateRolloutWindow.getWindow(rolloutId, true); + addTargetWindow.setCaption(i18n.get("caption.create.rollout")); + UI.getCurrent().addWindow(addTargetWindow); + addTargetWindow.setVisible(Boolean.TRUE); + } + private String getDescription(final CellReference cell) { String description = null; diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListHeader.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListHeader.java index b72dbb8f6..5ba538c60 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListHeader.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListHeader.java @@ -10,6 +10,7 @@ package org.eclipse.hawkbit.ui.rollout.rollout; import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.RolloutManagement; +import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.UiProperties; @@ -43,11 +44,12 @@ public class RolloutListHeader extends AbstractGridHeader { RolloutListHeader(final SpPermissionChecker permissionChecker, final RolloutUIState rolloutUIState, final UIEventBus eventBus, final RolloutManagement rolloutManagement, final TargetManagement targetManagement, final UINotification uiNotification, - final UiProperties uiProperties, final EntityFactory entityFactory, final I18N i18n) { + final UiProperties uiProperties, final EntityFactory entityFactory, final I18N i18n, + final TargetFilterQueryManagement targetFilterQueryManagement) { super(permissionChecker, rolloutUIState, i18n); this.eventBus = eventBus; this.addUpdateRolloutWindow = new AddUpdateRolloutWindowLayout(rolloutManagement, targetManagement, - uiNotification, uiProperties, entityFactory, i18n, eventBus); + uiNotification, uiProperties, entityFactory, i18n, eventBus, targetFilterQueryManagement); } @Override diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListView.java index 767a347aa..9504a8c9c 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/rollout/rollout/RolloutListView.java @@ -10,6 +10,7 @@ package org.eclipse.hawkbit.ui.rollout.rollout; import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.RolloutManagement; +import org.eclipse.hawkbit.repository.TargetFilterQueryManagement; import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.UiProperties; @@ -31,11 +32,12 @@ public class RolloutListView extends AbstractGridLayout { public RolloutListView(final SpPermissionChecker permissionChecker, final RolloutUIState rolloutUIState, final UIEventBus eventBus, final RolloutManagement rolloutManagement, final TargetManagement targetManagement, final UINotification uiNotification, - final UiProperties uiProperties, final EntityFactory entityFactory, final I18N i18n) { + final UiProperties uiProperties, final EntityFactory entityFactory, final I18N i18n, + final TargetFilterQueryManagement targetFilterQueryManagement) { super(new RolloutListHeader(permissionChecker, rolloutUIState, eventBus, rolloutManagement, targetManagement, - uiNotification, uiProperties, entityFactory, i18n), + uiNotification, uiProperties, entityFactory, i18n, targetFilterQueryManagement), new RolloutListGrid(i18n, eventBus, rolloutManagement, uiNotification, rolloutUIState, - permissionChecker, targetManagement, entityFactory, uiProperties)); + permissionChecker, targetManagement, entityFactory, uiProperties, targetFilterQueryManagement)); buildLayout(); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/SPUIStyleDefinitions.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/SPUIStyleDefinitions.java index bdaa3ae6a..057e5b57b 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/SPUIStyleDefinitions.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/SPUIStyleDefinitions.java @@ -212,6 +212,14 @@ public final class SPUIStyleDefinitions { * Query validator icon -error style. */ public static final String ERROR_ICON = "error-icon"; + /** + * Rollout groups + */ + public static final String ROLLOUT_GROUPS = "rollout-groups"; + /** + * Rollout groups pie chart + */ + public static final String ROLLOUT_GROUPS_CHART = "groups-pie-chart"; /** * Rollout action type layout style. */ diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java index 92de97573..ce97eba31 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java @@ -174,6 +174,21 @@ public final class UIComponentIdProvider { */ public static final String ACTION_DETAILS_SOFT_ID = "action.details.soft.group"; + /** + * Start type of rollout manual radio button + */ + public static final String ROLLOUT_START_MANUAL_ID = "rollout.start.manual.radio"; + + /** + * Start type of rollout auto start radio button + */ + public static final String ROLLOUT_START_AUTO_ID = "rollout.start.auto.radio"; + + /** + * Start type of rollout scheduled start radio button + */ + public static final String ROLLOUT_START_SCHEDULED_ID = "rollout.start.scheduled.start.radio"; + /** * ID - Label. */ @@ -723,6 +738,21 @@ public final class UIComponentIdProvider { */ public static final String ROLLOUT_ERROR_THRESOLD_ID = "rollout.error.thresold.id"; + /** + * Rollout group target percentage id. + */ + public static final String ROLLOUT_GROUP_TARGET_PERC_ID = "rollout.group.target.percentage.id"; + + /** + * Rollout group add button id + */ + public static final String ROLLOUT_GROUP_ADD_ID = "rollout.group.add.id"; + + /** + * Rollout group remove button id + */ + public static final String ROLLOUT_GROUP_REMOVE_ID = "rollout.group.remove.id"; + /** * Rollout distribution set combo id. */ @@ -735,7 +765,18 @@ public final class UIComponentIdProvider { * Rollout target filter query combo id. */ public static final String ROLLOUT_TARGET_FILTER_COMBO_ID = "rollout.target.filter.combo.id"; - + /** + * Rollout groups id + */ + public static final String ROLLOUT_GROUPS = "rollout.groups"; + /** + * Rollout simple tab id + */ + public static final String ROLLOUT_SIMPLE_TAB = "rollout.create.tabs.simple"; + /** + * Rollout advanced tab id + */ + public static final String ROLLOUT_ADVANCED_TAB = "rollout.create.tabs.advanced"; /** * Rollout action button id. */ @@ -752,10 +793,15 @@ public final class UIComponentIdProvider { public static final String ROLLOUT_PAUSE_BUTTON_ID = ROLLOUT_ACTION_ID + ".10"; /** - * Rollout resume button id. + * Rollout update button id. */ public static final String ROLLOUT_UPDATE_BUTTON_ID = ROLLOUT_ACTION_ID + ".11"; + /** + * Rollout copy button id. + */ + public static final String ROLLOUT_COPY_BUTTON_ID = ROLLOUT_ACTION_ID + ".12"; + /** * Rollout status label id. */ @@ -771,6 +817,16 @@ public final class UIComponentIdProvider { */ public static final String ROLLOUT_ERROR_THRESOLD_OPTION_ID = "rollout.error.thresold.option.id"; + /** + * Rollout groups options id. + */ + public static final String ROLLOUT_GROUPS_OPTION_ID = "rollout.groups.option.id"; + + /** + * Rollout groups definition button + */ + public static final String ROLLOUT_GROUPS_DEF_BUTTON_ID = "rollout.groups.definition.button.id"; + /** * Rollout target filter query value text area id. */ diff --git a/hawkbit-ui/src/main/resources/VAADIN/themes/hawkbit/customstyles/rollout.scss b/hawkbit-ui/src/main/resources/VAADIN/themes/hawkbit/customstyles/rollout.scss index d55d2eb2c..f5bbb42d9 100644 --- a/hawkbit-ui/src/main/resources/VAADIN/themes/hawkbit/customstyles/rollout.scss +++ b/hawkbit-ui/src/main/resources/VAADIN/themes/hawkbit/customstyles/rollout.scss @@ -7,95 +7,207 @@ * http://www.eclipse.org/legal/epl-v10.html */ @mixin rollout { - .rollout-option-group{ - font-size:12px; - font-weight:400; - margin-left:8px; - } - - .rollout-action-type-layout { - .v-caption-padding-right-style{ - padding-right:0px !important; - } - } - - - .v-context-menu .v-context-menu-item-basic-icon-container{ - height:0px !important; - width:0px !important; - } - - .v-context-menu .v-context-menu-item-basic{ - background-color: #feffff !important; - border-radius: 4px; - font-family : $app-font-family; - font-size : $app-text-font-size; - font-weight : normal; - font-style : normal; - } - - - .v-context-menu{ - background-color: #feffff !important; - border-radius: 4px; - } - - .v-context-menu .v-context-menu-item-basic:focus, .v-context-menu .v-context-menu-item-basic-submenu:focus, .v-context-menu .v-context-menu-item-basic-open { - @include valo-gradient($color: $hawkbit-primary-color); - background-color: $hawkbit-primary-color !important; - color: #e8eef3; - height: 30px; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.05); - } - - .disable-action-type-layout{ - opacity: 0.5; - } - - .action-type-padding{ - padding: 0 0px !important; - } - - .rollout-caption-links{ - font-weight: 400; - height: 25px ; - padding: 0px 4px ; - } - - .rollout-target-count-message{ - color: $info-message-color-grey; - } - - .rollout-table{ - .v-table-cell-wrapper { - cursor: default; + .rollout-option-group { + font-size: 12px; + font-weight: 400; + margin-left: 8px; + } + + .rollout-action-type-layout { + .v-caption-padding-right-style { + padding-right: 0px !important; + } + } + + .v-context-menu .v-context-menu-item-basic-icon-container { + height: 0px !important; + width: 0px !important; + } + + .v-context-menu .v-context-menu-item-basic { + background-color: #feffff !important; + border-radius: 4px; + font-family: $app-font-family; + font-size: $app-text-font-size; + font-weight: normal; + font-style: normal; + } + + .v-context-menu { + background-color: #feffff !important; + border-radius: 4px; + } + + .v-context-menu .v-context-menu-item-basic:focus, .v-context-menu .v-context-menu-item-basic-submenu:focus, .v-context-menu .v-context-menu-item-basic-open { + @include valo-gradient($color: $hawkbit-primary-color); + background-color: $hawkbit-primary-color !important; + color: #e8eef3; + height: 30px; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.05); + } + + .disable-action-type-layout { + opacity: 0.5; + } + + .action-type-padding { + padding: 0 0px !important; + } + + .rollout-caption-links { + font-weight: 400; + height: 25px; + padding: 0px 4px; + } + + .rollout-target-count-message { + color: $info-message-color-grey; + } + + .rollout-table { + .v-table-cell-wrapper { + cursor: default; + } + } + + .v-grid-cell.centeralign { + text-align: center; + } + + .v-grid-cell { + font-size: $v-font-size--small !important; + height: 34px !important; + } + + .v-grid-row { + height: 34px !important; + } + + .v-grid-cell.frozen { + box-shadow: none !important; + } + + .v-grid-cell.frozen + th { + border-left: $v-grid-border-size solid $widget-border-color; + } + + .v-button-boldhide { + text-decoration: none; + } + + + + .groups-pie-chart { + float: right; + + svg { + .pie { + stroke: #ffffff; } - } - - .v-grid-cell.centeralign { - text-align: center; - } - - .v-grid-cell { - font-size: $v-font-size--small !important; - height: 34px !important; - } - - .v-grid-row{ - height: 34px !important; - } - - .v-grid-cell.frozen{ - box-shadow: none !important; - } - - .v-grid-cell.frozen + th{ - border-left: $v-grid-border-size solid $widget-border-color ; - } - - .v-button-boldhide{ - text-decoration:none; - } - + /* + First child is intended for unassigned targets and should not be repeated. + */ + .pie:nth-child(1) { fill: #CBCBCB } + + .pie:nth-child(16n+2) { fill: #E20015 } + .pie:nth-child(16n+3) { fill: #B90276 } + .pie:nth-child(16n+4) { fill: #50237F } + .pie:nth-child(16n+5) { fill: #005691 } + .pie:nth-child(16n+6) { fill: #008ECF } + .pie:nth-child(16n+7) { fill: #00A8B0 } + .pie:nth-child(16n+8) { fill: #78BE20 } + .pie:nth-child(16n+9) { fill: #006249 } + .pie:nth-child(16n+10) { fill: #F07F8A } + .pie:nth-child(16n+11) { fill: #DC80BA } + .pie:nth-child(16n+12) { fill: #A791BF } + .pie:nth-child(16n+13) { fill: #7FAAC8 } + .pie:nth-child(16n+14) { fill: #7FC6E7 } + .pie:nth-child(16n+15) { fill: #7FD3D7 } + .pie:nth-child(16n+16) { fill: #BBDE8F } + .pie:nth-child(16n+17) { fill: #7FB0A4 } + + .pie-info { + pointer-events: none; + + rect { + fill: rgba(255, 255, 255, 0.6); + } + text { + font-size: 12px; + font-weight: bold; + fill: $hawkbit-primary-color; + } + } + + + } + + } + + .rollout-groups { + + .v-button { + margin: 0 3px 3px 3px; + } + + .rollout-group-name { + border-left: 5px solid #000000; + } + .v-gridlayout-slot:nth-child(16n+8) .rollout-group-name { border-left-color: #E20015 } + .v-gridlayout-slot:nth-child(16n+14) .rollout-group-name { border-left-color: #B90276 } + .v-gridlayout-slot:nth-child(16n+20) .rollout-group-name { border-left-color: #50237F } + .v-gridlayout-slot:nth-child(16n+26) .rollout-group-name { border-left-color: #005691 } + .v-gridlayout-slot:nth-child(16n+32) .rollout-group-name { border-left-color: #008ECF } + .v-gridlayout-slot:nth-child(16n+38) .rollout-group-name { border-left-color: #00A8B0 } + .v-gridlayout-slot:nth-child(16n+44) .rollout-group-name { border-left-color: #78BE20 } + .v-gridlayout-slot:nth-child(16n+50) .rollout-group-name { border-left-color: #006249 } + .v-gridlayout-slot:nth-child(16n+56) .rollout-group-name { border-left-color: #F07F8A } + .v-gridlayout-slot:nth-child(16n+62) .rollout-group-name { border-left-color: #DC80BA } + .v-gridlayout-slot:nth-child(16n+68) .rollout-group-name { border-left-color: #A791BF } + .v-gridlayout-slot:nth-child(16n+74) .rollout-group-name { border-left-color: #7FAAC8 } + .v-gridlayout-slot:nth-child(16n+80) .rollout-group-name { border-left-color: #7FC6E7 } + .v-gridlayout-slot:nth-child(16n+86) .rollout-group-name { border-left-color: #7FD3D7 } + .v-gridlayout-slot:nth-child(16n+92) .rollout-group-name { border-left-color: #BBDE8F } + .v-gridlayout-slot:nth-child(16n+98) .rollout-group-name { border-left-color: #7FB0A4 } + } + + .rollout-target-count-title { + } + + .rollout-target-count-loading { + color: #ed473b; + } + + .rollout-group-unassigned { + border-left: 5px solid #CBCBCB; + padding-left: 5px; + margin-bottom: 2px; + font-size: $v-font-size * .7; + } + + .groups-legend { + .v-slot .rollout-group-count { + border-left: 5px solid transparent; + padding-left: 5px; + font-size: $v-font-size * .7; + } + + .v-slot:nth-child(16n+1) .rollout-group-count { border-left-color: #E20015 } + .v-slot:nth-child(16n+2) .rollout-group-count { border-left-color: #B90276 } + .v-slot:nth-child(16n+3) .rollout-group-count { border-left-color: #50237F } + .v-slot:nth-child(16n+4) .rollout-group-count { border-left-color: #005691 } + .v-slot:nth-child(16n+5) .rollout-group-count { border-left-color: #008ECF } + .v-slot:nth-child(16n+6) .rollout-group-count { border-left-color: #00A8B0 } + .v-slot:nth-child(16n+7) .rollout-group-count { border-left-color: #78BE20 } + .v-slot:nth-child(16n+8) .rollout-group-count { border-left-color: #006249 } + .v-slot:nth-child(16n+9) .rollout-group-count { border-left-color: #F07F8A } + .v-slot:nth-child(16n+10) .rollout-group-count { border-left-color: #DC80BA } + .v-slot:nth-child(16n+11) .rollout-group-count { border-left-color: #A791BF } + .v-slot:nth-child(16n+12) .rollout-group-count { border-left-color: #7FAAC8 } + .v-slot:nth-child(16n+13) .rollout-group-count { border-left-color: #7FC6E7 } + .v-slot:nth-child(16n+14) .rollout-group-count { border-left-color: #7FD3D7 } + .v-slot:nth-child(16n+15) .rollout-group-count { border-left-color: #BBDE8F } + .v-slot:nth-child(16n+16) .rollout-group-count { border-left-color: #7FB0A4 } + } + } - \ No newline at end of file diff --git a/hawkbit-ui/src/main/resources/messages.properties b/hawkbit-ui/src/main/resources/messages.properties index 27b128fbd..ba9497616 100644 --- a/hawkbit-ui/src/main/resources/messages.properties +++ b/hawkbit-ui/src/main/resources/messages.properties @@ -60,6 +60,7 @@ header.action=Actions header.action.run=Run header.action.pause=Pause header.action.update=Edit +header.action.copy=Copy header.status=Status # event container @@ -514,12 +515,17 @@ header.rolloutgroup.target.message = Messages rollout.group.label.target.truncated = {0} targets has been truncated in the list due the target size limit of {1} +prompt.groups = Groups prompt.number.of.groups = Number of groups prompt.tigger.threshold = Trigger threshold prompt.error.threshold = Error threshold prompt.distribution.set = Distribution Set +button.rollout.groups.def.button = Define groups +button.rollout.add.group = Add Group caption.configure.rollout = Configure Rollout +caption.configure.rollout.groups = Configure Deployment Groups caption.update.rollout = Update Rollout +caption.create.rollout = Create new Rollout prompt.target.filter = Custom Target Filter message.rollout.nonzero.group.number = Number of groups must be greater than zero message.rollout.max.group.number = Number of groups must not be greater than 500 @@ -536,6 +542,26 @@ label.target.per.group = Targets per group : message.dist.already.assigned = Distribution {0} is already assigned to target message.error.creating.rollout = Server error. Error creating Rollout. Please contact the administrator message.error.starting.rollout = Server error. Error starting Rollout. Please contact the administrator +caption.rollout.group.definition.desc = Define which groups the Rollout should have. +header.target.percentage = Target percentage +textfield.target.percentage = Target percentage +textfield.rollout.group.default.name = Group {0} +message.rollout.group.name.invalid = Enter a group name with a length between 1 and 64 +caption.rollout.tabs.simple = Number of Groups +caption.rollout.tabs.advanced = Advanced Group definition +caption.rollout.generate.groups = Generate the groups automatically with the specified thresholds. +caption.rollout.action.type = Action type +message.rollout.remaining.targets.error = Not all targets are addressed +textfield.rollout.copied.name = Copy of {0} +label.rollout.targets.in.group = {0} in {1} +caption.rollout.start.type = Start type +caption.rollout.start.manual = Manual +caption.rollout.start.manual.desc = The user starts the rollout manually. +caption.rollout.start.auto = Auto +caption.rollout.start.auto.desc = The rollout is started as soon as it is created. +caption.rollout.start.scheduled = Scheduled +caption.rollout.start.scheduled.desc = The rollout starts as soon as it is ready and the set time has passed. +label.rollout.calculating = Calculating groups ... #rollout - end #Menu diff --git a/pom.xml b/pom.xml index 79b1d1c37..5bf1d5303 100644 --- a/pom.xml +++ b/pom.xml @@ -539,6 +539,17 @@ jlorem ${jlorem.version} + + com.github.gwtd3 + gwt-d3-api + 1.2.0 + + + com.google.gwt + gwt-user + + + org.springframework.cloud