Create Rollout UI with Groups definition (#369)

* Rollout creation UI with groups definition

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>

* Improved code quality

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>

* Added a legend to the Rollouts Creation UI's PieChart

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>

* Updated color palette for pie chart. Using RolloutGroupCreate for validation. Displaying pie chart for group editing.

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>

* Optimized ComboBoxBuilder

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>

* Removed not necessary RPC interfaces.

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>

* Fix Rollout verification count query. Improved UI style. Added dependency management for d3.

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>

* Auto start and scheduled start of Rollouts

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>

* Optimized RSQL Utility to avoid multiple joins on the same table.

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>

* Fixed RSQL Utility to avoid multiple joins on the same table to only work with OR nodes. Optimized validation queries for rollout group creation.

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>

* Asynchronous handling of groups validation in the create rollouts UI.

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>

* Added loading indicator to create rollout UI.

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>

* Avoid parallel validation for a user session.

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>

* Fix issues after merge of master into branch.

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>

* Improve code quality

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>

* Refreshing the total target count after validation on the rollouts creation UI.

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>

* Remove unused RolloutUpdate method. Optimization of the RolloutHelper.
Comments for RolloutGroupsValidation and RSQLUtility with explanation.

Signed-off-by: Dominik Herbst <dominik.herbst@bosch-si.com>
This commit is contained in:
Dominik Herbst
2017-01-26 09:37:20 +01:00
committed by Kai Zimmermann
parent 430bf632cf
commit 833b0795e3
45 changed files with 2864 additions and 369 deletions

View File

@@ -32,6 +32,8 @@ public class MgmtRolloutRestRequestBody extends AbstractMgmtRolloutConditionsEnt
private Long forcetime;
private Long startAt;
private MgmtActionType type;
private List<MgmtRolloutGroup> groups;
@@ -124,4 +126,19 @@ public class MgmtRolloutRestRequestBody extends AbstractMgmtRolloutConditionsEnt
public void setGroups(List<MgmtRolloutGroup> 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;
}
}

View File

@@ -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) {

View File

@@ -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.
*
* <pre>
@@ -55,8 +58,8 @@ public interface RolloutManagement {
* }
* </pre>
*
* 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<RolloutGroupCreate> 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<RolloutGroupsValidation> validateTargetsInGroups(List<RolloutGroupCreate> 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 <code>null</code>
* @param description
* to update or <code>null</code>
*
* @return Rollout updated rollout
*/

View File

@@ -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;
}
}

View File

@@ -108,6 +108,18 @@ public interface TargetFilterQueryManagement {
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET)
Page<TargetFilterQuery> 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<TargetFilterQuery> findTargetFilterQueryByQuery(@NotNull Pageable pageable, String query);
/**
* Retrieves all target filter query which {@link TargetFilterQuery}.
*

View File

@@ -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
*/

View File

@@ -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);
}

View File

@@ -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.
*/

View File

@@ -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<Long> 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<Long> targetsPerGroup) {
this.totalTargets = totalTargets;
this.targetsPerGroup = targetsPerGroup;
}
public long getTotalTargets() {
return totalTargets;
}
public List<Long> 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();
}
}

View File

@@ -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<T> 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<T> extends AbstractNamedEntity
return (T) this;
}
public T startAt(final Long startAt) {
this.startAt = startAt;
return (T) this;
}
public Optional<Long> getSet() {
return Optional.ofNullable(set);
}
public Optional<ActionType> getActionType() {
return Optional.ofNullable(actionType);
}
public Optional<Long> getForcedTime() {
return Optional.ofNullable(forcedTime);
}
public Optional<Long> getStartAt() {
return Optional.ofNullable(startAt);
}
}

View File

@@ -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<Rollout> findAll(final Pageable pageable) {
return convertPage(rolloutRepository.findAll(pageable), pageable);
}
private static Page<Rollout> convertPage(final Page<JpaRollout> findAll, final Pageable pageable) {
return new PageImpl<>(Collections.unmodifiableList(findAll.getContent()), pageable, findAll.getTotalElements());
}
private static Slice<Rollout> convertPage(final Slice<JpaRollout> 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<JpaRollout> 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<RolloutGroup> groups, final JpaRollout rollout) {
final String baseFilter = RolloutHelper.getTargetFilterQuery(rollout);
private long calculateRemainingTargets(final List<RolloutGroup> 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<RolloutGroupsValidation> validateTargetsInGroups(final List<RolloutGroupCreate> 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<RolloutGroup> groups, final String baseFilter,
final long totalTargets) {
final List<Long> groupTargetCounts = new ArrayList<>(groups.size());
final Map<String, Long> 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<RolloutGroup> groups,
final RolloutGroup group, final int groupIndex) {
final RolloutGroup group, final int groupIndex, final Map<String, Long> targetFilterCounts) {
// there can't be overlapping targets in the first group
if (groupIndex == 0) {
return 0;
}
final List<RolloutGroup> 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<JpaRollout> rolloutsToCheck = getRolloutsToCheckForStatus(delayBetweenChecks, RolloutStatus.RUNNING);
if (rolloutsToCheck.isEmpty()) {
return;
}
final List<JpaRollout> 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<Long> rolloutsToCheck = getRolloutsToCheckForStatus(delayBetweenChecks, RolloutStatus.CREATING)
.stream().map(Rollout::getId).collect(Collectors.toList());
if (rolloutsToCheck.isEmpty()) {
return;
}
final List<Long> 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<JpaRollout> rolloutsToCheck = getRolloutsToCheckForStatus(delayBetweenChecks,
RolloutStatus.STARTING);
if (rolloutsToCheck.isEmpty()) {
return;
}
final List<JpaRollout> 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<JpaRollout> 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<JpaRollout> 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<JpaRollout> 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<Rollout> findRolloutWithDetailedStatusByFilters(final Pageable pageable, final String searchText) {
final Specification<JpaRollout> specs = likeNameOrDescription(searchText);
final Specification<JpaRollout> specs = RolloutHelper.likeNameOrDescription(searchText);
final Slice<JpaRollout> 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<Rollout> findAllRolloutsWithDetailedStatus(final Pageable pageable) {
final Page<JpaRollout> 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))

View File

@@ -123,6 +123,15 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
return convertPage(findTargetFilterQueryByCriteriaAPI(pageable, specList), pageable);
}
@Override
public Page<TargetFilterQuery> findTargetFilterQueryByQuery(final Pageable pageable, final String query) {
List<Specification<JpaTargetFilterQuery>> specList = Collections.emptyList();
if (!Strings.isNullOrEmpty(query)) {
specList = Collections.singletonList(TargetFilterQuerySpecification.equalsQuery(query));
}
return convertPage(findTargetFilterQueryByCriteriaAPI(pageable, specList), pageable);
}
@Override
public Page<TargetFilterQuery> findTargetFilterQueryByAutoAssignDS(final Pageable pageable, final Long setId,
final String rsqlFilter) {

View File

@@ -680,7 +680,10 @@ public class JpaTargetManagement implements TargetManagement {
public Long countTargetByTargetFilterQuery(final String targetFilterQuery) {
final Specification<JpaTarget> 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<Object[]> getTargetIdNameResultSet(final Pageable pageRequest, final CriteriaBuilder cb,

View File

@@ -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<RolloutGroup> groups, final RolloutGroup group) {
static String getOverlappingWithGroupsTargetFilter(final String baseFilter, final List<RolloutGroup> 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<RolloutGroup> 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<JpaRollout> 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<Rollout> convertPage(final Page<JpaRollout> findAll, final Pageable pageable) {
return new PageImpl<>(Collections.unmodifiableList(findAll.getContent()), pageable, findAll.getTotalElements());
}
static Slice<Rollout> convertPage(final Slice<JpaRollout> findAll, final Pageable pageable) {
return new PageImpl<>(Collections.unmodifiableList(findAll.getContent()), pageable, 0);
}
}

View File

@@ -30,6 +30,7 @@ public class JpaRolloutCreate extends AbstractRolloutUpdateCreate<RolloutCreate>
rollout.setDescription(description);
rollout.setDistributionSet(findDistributionSetAndThrowExceptionIfNotFound(set));
rollout.setTargetFilterQuery(targetFilterQuery);
rollout.setStartAt(startAt);
if (actionType != null) {
rollout.setActionType(actionType);

View File

@@ -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;

View File

@@ -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<String> 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;
});
}
}

View File

@@ -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<T> root, final CriteriaQuery<?> query, final CriteriaBuilder cb) {
final Node rootNode = parseRsql(rsql);
final JpqQueryRSQLVisitor<A, T> jpqQueryRSQLVisitor = new JpqQueryRSQLVisitor<>(root, cb, enumType,
@@ -192,6 +199,9 @@ public final class RSQLUtility {
private final CriteriaBuilder cb;
private final Class<A> enumType;
private final VirtualPropertyReplacer virtualPropertyReplacer;
private int level;
private boolean isOrLevel;
private final Map<Integer, Set<Join<Object, Object>>> 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<Join<Object, Object>> getCurrentJoins() {
if(level > 0) {
return joinsInLevel.get(level);
}
return Collections.emptySet();
}
private Optional<Join<Object, Object>> findCurrentJoinOfType(final Class<?> type) {
return getCurrentJoins().stream()
.filter(j -> type.equals(j.getJavaType())).findFirst();
}
private void addCurrentJoin(Join<Object, Object> join) {
if(level > 0) {
getCurrentJoins().add(join);
}
}
@Override
public List<Predicate> visit(final AndNode node, final String param) {
beginLevel(false);
final List<Predicate> 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<Predicate> visit(final OrNode node, final String param) {
beginLevel(true);
final List<Predicate> 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<Object> getFieldPath(final A enumField, final String finalProperty) {
Path<Object> 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<Object, ?> join = (Join<Object, ?>) fieldPath;
final From<?, Object> joinParent = join.getParent();
Optional<Join<Object, Object>> 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<Object, Object> newJoin = joinParent.join(fieldNameSplit, JoinType.LEFT);
addCurrentJoin(newJoin);
fieldPath = newJoin;
}
}
}
return fieldPath;
}

View File

@@ -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<JpaTargetFilterQuery> 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()}.

View File

@@ -0,0 +1,2 @@
ALTER TABLE sp_rollout
ADD COLUMN start_at BIGINT;

View File

@@ -0,0 +1,2 @@
ALTER TABLE sp_rollout
ADD COLUMN start_at BIGINT;

View File

@@ -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<TotalTargetCountStatus.Status, Long> 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 {

View File

@@ -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<String> 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<Target> 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);
}
}

View File

@@ -253,6 +253,11 @@
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.gwtd3</groupId>
<artifactId>gwt-d3-api</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.eclipse.hawkbit</groupId>

View File

@@ -33,6 +33,7 @@
<inherits name="org.eclipse.hawkbit.ui.customrenderers.CustomRendererWidgetSet" />
<inherits name="org.eclipse.hawkbit.ui.filtermanagement.TextFieldSuggestionBox" />
<inherits name="org.eclipse.hawkbit.ui.rollout.groupschart.GroupsPieChart" />
<inherits name="org.eclipse.hawkbit.ui.dd.CustomAcceptCriteria" />

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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
-->
<module>
<inherits name="com.vaadin.DefaultWidgetSet"/>
<inherits name="org.eclipse.hawkbit.ui.AppWidgetSet"/>
<inherits name="org.vaadin.peter.contextmenu.ContextmenuWidgetset"/>
<inherits name="org.vaadin.tokenfield.TokenfieldWidgetset"/>
<inherits name="org.eclipse.hawkbit.ui.customrenderers.CustomRendererWidgetSet"/>
<inherits name="org.vaadin.hene.flexibleoptiongroup.widgetset.FlexibleOptionGroupWidgetset"/>
<inherits name="org.vaadin.alump.distributionbar.gwt.DistributionBarWidgetset"/>
<inherits name="com.github.gwtd3.D3"/>
</module>

View File

@@ -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<Long> groupTargetCounts, final Long totalTargetsCount) {
getState().setGroupTargetCounts(groupTargetCounts);
getState().setTotalTargetCount(totalTargetsCount);
markAsDirty();
}
@Override
protected GroupsPieChartState getState() {
return (GroupsPieChartState) super.getState();
}
}

View File

@@ -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());
}
}

View File

@@ -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<Long> groupTargetCounts;
private Long totalTargetCount;
public List<Long> getGroupTargetCounts() {
return groupTargetCounts;
}
public void setGroupTargetCounts(List<Long> groupTargetCounts) {
this.groupTargetCounts = groupTargetCounts;
}
public Long getTotalTargetCount() {
return totalTargetCount;
}
public void setTotalTargetCount(Long totalTargetCount) {
this.totalTargetCount = totalTargetCount;
}
}

View File

@@ -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<Long> 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<Long> 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<Arc> 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<Void>() {
@Override
public Void apply(Element context, Value d, int index) {
Array<Double> 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<Void>() {
@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);
}
}
}

View File

@@ -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<RolloutGroupCreate> 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<Long> 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<RolloutGroup> savedGroups, long totalTargetsCount) {
List<Long> 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<Long> 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<TargetFilterQuery> 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<TargetFilterBeanQuery> 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<RolloutGroupCreate> 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<RolloutGroup> 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<RolloutGroup> 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;

View File

@@ -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;
}
}

View File

@@ -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<GroupRow> groupRows;
private int groupsCount;
private transient List<RolloutGroupCreate> savedRolloutGroups;
private transient ValidationListener validationListener;
private ValidationStatus validationStatus = ValidationStatus.VALID;
private transient RolloutGroupsValidation groupsValidation;
private transient ListenableFuture<RolloutGroupsValidation> 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<RolloutGroupCreate> 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<RolloutGroup> 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<RolloutGroupCreate> 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<TargetFilterQuery> 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<TargetFilterBeanQuery> 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);
}
}
}

View File

@@ -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<Long> 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<RolloutGroupCreate> groups) {
loadingLabel.setVisible(false);
if (validation == null) {
return;
}
List<Long> 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<RolloutGroup> 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);
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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.
*/

View File

@@ -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.
*/

View File

@@ -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 }
}
}

View File

@@ -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

11
pom.xml
View File

@@ -539,6 +539,17 @@
<artifactId>jlorem</artifactId>
<version>${jlorem.version}</version>
</dependency>
<dependency>
<groupId>com.github.gwtd3</groupId>
<artifactId>gwt-d3-api</artifactId>
<version>1.2.0</version>
<exclusions>
<exclusion>
<groupId>com.google.gwt</groupId>
<artifactId>gwt-user</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework.cloud</groupId>