Fix optimize ui maintenance window (#668)

* Optimize maintenance window UI

Signed-off-by: Melanie Retter <melanie.retter@bosch-si.com>

* Refactor

Signed-off-by: Melanie Retter <melanie.retter@bosch-si.com>

* Add new downloaded status to UI.

Signed-off-by: kaizimmerm <kai.zimmermann@bosch-si.com>

* Changed Accordion to Tabsheet for better visualization of action types
and maintanance window.

Signed-off-by: Markus Block <markus.block@bosch-si.com>

* Refined UI for maintenance window, refactoring

Added ENTER shortcut for save button in dialog windows

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Extended validation for maintenance window, refactored the maintenance window helper class

Added text change listeners for the schedule and duration text fields in order to activate "save all" button
Added client Locale identification for cron expression translation
Moved maintenance window validation from TargetWithActionType constructor to saveAll method of assignment tab

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Added SupressWarnings annotation for exception handling cases

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Fixed Sonar issue: added private constructor to Maintenance schedule helper class

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Sonar Issue: make utility class Maintenance Schedule final

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Added Maintenance Window validation to Distribution Set and Target Management API

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Added unit tests for MaintenanceScheduleHelper class

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Added the license header to MaintenanceScheduleHelperTest class

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Small changes after PR review

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>

* Added Id for Maintenance Window layout for UI Tests

Signed-off-by: Bogdan Bondar <Bogdan.Bondar@bosch-si.com>
This commit is contained in:
Bondar Bogdan
2018-04-04 10:01:55 +02:00
committed by Dominic Schabel
parent 8fd601f8b9
commit 4c28c4d905
21 changed files with 632 additions and 293 deletions

View File

@@ -29,6 +29,7 @@ import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.EntityFactory;
import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper;
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest; import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
import org.eclipse.hawkbit.repository.SoftwareModuleManagement; import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.SystemManagement;
@@ -256,10 +257,14 @@ public class MgmtDistributionSetResource implements MgmtDistributionSetRestApi {
t.getForcetime()); t.getForcetime());
} }
final String cronSchedule = t.getMaintenanceWindow().getMaintenanceSchedule();
final String duration = t.getMaintenanceWindow().getMaintenanceWindowDuration();
final String timezone = t.getMaintenanceWindow().getMaintenanceWindowTimeZone();
MaintenanceScheduleHelper.validateMaintenanceSchedule(cronSchedule, duration, timezone);
return new TargetWithActionType(t.getId(), MgmtRestModelMapper.convertActionType(t.getType()), return new TargetWithActionType(t.getId(), MgmtRestModelMapper.convertActionType(t.getType()),
t.getForcetime(), t.getMaintenanceWindow().getMaintenanceSchedule(), t.getForcetime(), cronSchedule, duration, timezone);
t.getMaintenanceWindow().getMaintenanceWindowDuration(),
t.getMaintenanceWindow().getMaintenanceWindowTimeZone());
}).collect(Collectors.toList())); }).collect(Collectors.toList()));
return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(assignDistributionSet)); return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(assignDistributionSet));

View File

@@ -31,6 +31,7 @@ import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetRestApi; import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetRestApi;
import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.EntityFactory;
import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper;
import org.eclipse.hawkbit.repository.OffsetBasedPageRequest; import org.eclipse.hawkbit.repository.OffsetBasedPageRequest;
import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
@@ -283,12 +284,17 @@ public class MgmtTargetResource implements MgmtTargetRestApi {
MgmtRestModelMapper.convertActionType(dsId.getType()), dsId.getForcetime()))))); MgmtRestModelMapper.convertActionType(dsId.getType()), dsId.getForcetime())))));
} }
return ResponseEntity.ok(MgmtDistributionSetMapper.toResponse(this.deploymentManagement.assignDistributionSet( final String cronSchedule = maintenanceWindow.getMaintenanceSchedule();
dsId.getId(), final String duration = maintenanceWindow.getMaintenanceWindowDuration();
Arrays.asList(new TargetWithActionType(controllerId, final String timezone = maintenanceWindow.getMaintenanceWindowTimeZone();
MgmtRestModelMapper.convertActionType(dsId.getType()), dsId.getForcetime(),
maintenanceWindow.getMaintenanceSchedule(), maintenanceWindow.getMaintenanceWindowDuration(), MaintenanceScheduleHelper.validateMaintenanceSchedule(cronSchedule, duration, timezone);
maintenanceWindow.getMaintenanceWindowTimeZone())))));
return ResponseEntity
.ok(MgmtDistributionSetMapper.toResponse(this.deploymentManagement.assignDistributionSet(dsId.getId(),
Arrays.asList(new TargetWithActionType(controllerId,
MgmtRestModelMapper.convertActionType(dsId.getType()), dsId.getForcetime(),
cronSchedule, duration, timezone)))));
} }

View File

@@ -8,8 +8,6 @@
*/ */
package org.eclipse.hawkbit.repository; package org.eclipse.hawkbit.repository;
import static com.cronutils.model.CronType.QUARTZ;
import java.time.Duration; import java.time.Duration;
import java.time.LocalTime; import java.time.LocalTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;
@@ -21,7 +19,7 @@ import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleExcept
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import com.cronutils.model.Cron; import com.cronutils.model.Cron;
import com.cronutils.model.definition.CronDefinition; import com.cronutils.model.CronType;
import com.cronutils.model.definition.CronDefinitionBuilder; import com.cronutils.model.definition.CronDefinitionBuilder;
import com.cronutils.model.time.ExecutionTime; import com.cronutils.model.time.ExecutionTime;
import com.cronutils.parser.CronParser; import com.cronutils.parser.CronParser;
@@ -32,62 +30,67 @@ import com.cronutils.parser.CronParser;
* also provides a helper method for conversion of duration specified in * also provides a helper method for conversion of duration specified in
* HH:mm:ss format to ISO format. * HH:mm:ss format to ISO format.
*/ */
public class MaintenanceScheduleHelper { public final class MaintenanceScheduleHelper {
private final ExecutionTime scheduleExecutor; private static final CronParser cronParser = new CronParser(
CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ));
/** private MaintenanceScheduleHelper() {
* Constructor that accepts a cron expression, duration and time zone and throw new IllegalStateException("Utility class");
* instantiates the cron parser and scheduler executor.
*
* @param cronSchedule
* is the cron expression to be used for scheduling the
* maintenance window. Expression has 6 mandatory fields and 1
* last optional field: "second minute hour dayofmonth month
* weekday year"
*/
public MaintenanceScheduleHelper(final String cronSchedule) {
final CronDefinition cronDefinition = CronDefinitionBuilder.instanceDefinitionFor(QUARTZ);
final CronParser parser = new CronParser(cronDefinition);
final Cron quartzCron = parser.parse(cronSchedule);
this.scheduleExecutor = ExecutionTime.forCron(quartzCron);
} }
/** /**
* Method calculates the next available maintenance window within the * Calculate the next available maintenance window.
* schedule but after a given time.
* *
* @param after * @param cronSchedule
* is the {@link ZonedDateTime} after which the window is * is a cron expression with 6 mandatory fields and 1 last
* required * optional field: "second minute hour dayofmonth month weekday
* year".
* @param duration
* in HH:mm:ss format specifying the duration of a maintenance
* window, for example 00:30:00 for 30 minutes.
* @param timezone
* is the time zone specified as +/-hh:mm offset from UTC. For
* example +02:00 for CET summer time and +00:00 for UTC. The
* start time of a maintenance window calculated based on the
* cron expression is relative to this time zone.
* *
* @return {@link Optional<ZonedDateTime>} of the next available window. In * @return {@link Optional<ZonedDateTime>} of the next available window. In
* case there is none, returns empty value. * case there is none, or there are maintenance window validation
* errors, returns empty value.
*
*/ */
// Exception squid:S1166 - lib throws exception as well if no value found // Exception squid:S1166 - if there are validation error(format of cron
// expression or duration is wrong), we simply return empty value
@SuppressWarnings("squid:S1166") @SuppressWarnings("squid:S1166")
public Optional<ZonedDateTime> nextExecution(final ZonedDateTime after) { public static Optional<ZonedDateTime> getNextMaintenanceWindow(final String cronSchedule, final String duration,
final String timezone) {
try { try {
final ZonedDateTime next = this.scheduleExecutor.nextExecution(after); final ExecutionTime scheduleExecutor = ExecutionTime.forCron(getCronFromExpression(cronSchedule));
return Optional.ofNullable(next); final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.of(timezone));
} catch (final IllegalArgumentException ignored) { final ZonedDateTime after = now.minus(convertToISODuration(duration));
final ZonedDateTime next = scheduleExecutor.nextExecution(after);
return Optional.of(next);
} catch (final RuntimeException ignored) {
return Optional.empty(); return Optional.empty();
} }
} }
/** /**
* Method checks if there are any more valid maintenance windows after a * Parse the given cron expression with quartz parser.
* given time.
* *
* @param after * @param cronSchedule
* is the {@link ZonedDateTime} after which the windows are * is a cron expression with 6 mandatory fields and 1 last
* checked * optional field: "second minute hour dayofmonth month weekday
* year".
* *
* @return true if there is at least one valid schedule remaining, else * @return {@link Cron} object, that corresponds to the expression.
* false. *
* @throws IllegalArgumentException
* if the cron expression doesn't have a valid format.
*/ */
private boolean hasValidScheduleAfter(final ZonedDateTime after) { public static Cron getCronFromExpression(final String cronSchedule) {
return nextExecution(after).isPresent(); return cronParser.parse(cronSchedule);
} }
/** /**
@@ -100,39 +103,30 @@ public class MaintenanceScheduleHelper {
* @param cronSchedule * @param cronSchedule
* is a cron expression with 6 mandatory fields and 1 last * is a cron expression with 6 mandatory fields and 1 last
* optional field: "second minute hour dayofmonth month weekday * optional field: "second minute hour dayofmonth month weekday
* year" * year".
* @param duration * @param duration
* in HH:mm:ss format specifying the duration of a maintenance * in HH:mm:ss format specifying the duration of a maintenance
* window, for example 00:30:00 for 30 minutes * window, for example 00:30:00 for 30 minutes.
* @param timezone * @param timezone
* is the time zone specified as +/-hh:mm offset from UTC. For * is the time zone specified as +/-hh:mm offset from UTC. For
* example +02:00 for CET summer time and +00:00 for UTC. The * example +02:00 for CET summer time and +00:00 for UTC. The
* start time of a maintenance window calculated based on the * start time of a maintenance window calculated based on the
* cron expression is relative to this time zone * cron expression is relative to this time zone.
* *
* @throws InvalidMaintenanceScheduleException * @throws InvalidMaintenanceScheduleException
* if the defined schedule fails the validity criteria. * if the defined schedule fails the validity criteria.
*/ */
public static void validateMaintenanceSchedule(final String cronSchedule, final String duration, public static void validateMaintenanceSchedule(final String cronSchedule, final String duration,
final String timezone) { final String timezone) {
// check if schedule, duration and timezone are all not empty.
if (allNotEmpty(cronSchedule, duration, timezone)) { if (allNotEmpty(cronSchedule, duration, timezone)) {
final ZonedDateTime now; validateCronSchedule(cronSchedule);
try { validateDuration(duration);
now = ZonedDateTime.now(ZoneOffset.of(timezone));
Duration.parse(convertToISODuration(duration));
} catch (final RuntimeException validationFailed) {
throw new InvalidMaintenanceScheduleException("No valid maintenance window provided", validationFailed);
}
final MaintenanceScheduleHelper scheduleHelper = new MaintenanceScheduleHelper(cronSchedule);
// check if there is a window currently active or available in // check if there is a window currently active or available in
// future. // future.
if (!scheduleHelper.hasValidScheduleAfter(now.minus(Duration.parse(convertToISODuration(duration))))) { if (!getNextMaintenanceWindow(cronSchedule, duration, timezone).isPresent()) {
throw new InvalidMaintenanceScheduleException( throw new InvalidMaintenanceScheduleException(
"No valid maintenance window available after current time"); "No valid maintenance window available after current time");
} }
} else if (atLeastOneNotEmpty(cronSchedule, duration, timezone)) { } else if (atLeastOneNotEmpty(cronSchedule, duration, timezone)) {
throw new InvalidMaintenanceScheduleException( throw new InvalidMaintenanceScheduleException(
"All of schedule, duration and timezone should either be null or non empty."); "All of schedule, duration and timezone should either be null or non empty.");
@@ -154,14 +148,61 @@ public class MaintenanceScheduleHelper {
* @param timeInterval * @param timeInterval
* in "HH:mm:ss" string format. This format is popularly used but * in "HH:mm:ss" string format. This format is popularly used but
* can be confused with time of the day, hence conversion to ISO * can be confused with time of the day, hence conversion to ISO
* specified format for time duration is required * specified format for time duration is required.
* *
* @return the time interval or duration in ISO format * @return {@link Duration} in ISO format.
* *
* @throws DateTimeParseException * @throws DateTimeParseException
* if the text cannot be converted to ISO format. * if the text cannot be converted to ISO format.
*/ */
public static String convertToISODuration(final String timeInterval) { public static Duration convertToISODuration(final String timeInterval) {
return Duration.between(LocalTime.MIN, LocalTime.parse(timeInterval)).toString(); return Duration.between(LocalTime.MIN, convertDurationToLocalTime(timeInterval));
}
private static LocalTime convertDurationToLocalTime(final String timeInterval) {
return LocalTime.parse(StringUtils.trimWhitespace(timeInterval));
}
/**
* Validates the format of the maintenance window duration
*
* @param duration
* in "HH:mm:ss" string format. This format is popularly used but
* can be confused with time of the day, hence conversion to ISO
* specified format for time duration is required.
*
* @throws InvalidMaintenanceScheduleException
* if the duration doesn't have a valid format to be converted
* to ISO.
*/
public static void validateDuration(final String duration) {
try {
if (StringUtils.hasText(duration)) {
convertDurationToLocalTime(duration);
}
} catch (final DateTimeParseException e) {
throw new InvalidMaintenanceScheduleException("Provided duration is not valid", e, e.getErrorIndex());
}
}
/**
* Validates the format of the maintenance window cron expression
*
* @param cronSchedule
* is a cron expression with 6 mandatory fields and 1 last
* optional field: "second minute hour dayofmonth month weekday
* year".
*
* @throws InvalidMaintenanceScheduleException
* if the cron expression doesn't have a valid quartz format.
*/
public static void validateCronSchedule(final String cronSchedule) {
try {
if (StringUtils.hasText(cronSchedule)) {
getCronFromExpression(cronSchedule);
}
} catch (final IllegalArgumentException e) {
throw new InvalidMaintenanceScheduleException(e.getMessage(), e);
}
} }
} }

View File

@@ -20,6 +20,8 @@ import org.eclipse.hawkbit.exception.SpServerError;
public class InvalidMaintenanceScheduleException extends AbstractServerRtException { public class InvalidMaintenanceScheduleException extends AbstractServerRtException {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private final int durationErrorIndex;
/** /**
* Constructor for {@link InvalidMaintenanceScheduleException}. * Constructor for {@link InvalidMaintenanceScheduleException}.
* *
@@ -27,7 +29,20 @@ public class InvalidMaintenanceScheduleException extends AbstractServerRtExcepti
* the message for this exception. * the message for this exception.
*/ */
public InvalidMaintenanceScheduleException(final String message) { public InvalidMaintenanceScheduleException(final String message) {
this(message, -1);
}
/**
* Constructor for {@link InvalidMaintenanceScheduleException}.
*
* @param message
* the message for this exception.
* @param errorIndex
* the error index of maintenance duration.
*/
public InvalidMaintenanceScheduleException(final String message, final int errorIndex) {
super(message, SpServerError.SP_MAINTENANCE_SCHEDULE_INVALID); super(message, SpServerError.SP_MAINTENANCE_SCHEDULE_INVALID);
this.durationErrorIndex = errorIndex;
} }
/** /**
@@ -39,6 +54,30 @@ public class InvalidMaintenanceScheduleException extends AbstractServerRtExcepti
* the cause for this exception. * the cause for this exception.
*/ */
public InvalidMaintenanceScheduleException(final String message, final Throwable cause) { public InvalidMaintenanceScheduleException(final String message, final Throwable cause) {
this(message, cause, -1);
}
/**
* Constructor for {@link InvalidMaintenanceScheduleException}.
*
* @param message
* the message for this exception
* @param cause
* the cause for this exception.
* @param errorIndex
* the error index of maintenance duration.
*/
public InvalidMaintenanceScheduleException(final String message, final Throwable cause, final int errorIndex) {
super(message, SpServerError.SP_MAINTENANCE_SCHEDULE_INVALID, cause); super(message, SpServerError.SP_MAINTENANCE_SCHEDULE_INVALID, cause);
this.durationErrorIndex = errorIndex;
}
/**
* Get the error index position for maintenance window duration.
*
* @return error index.
*/
public int getDurationErrorIndex() {
return durationErrorIndex;
} }
} }

View File

@@ -8,7 +8,6 @@
*/ */
package org.eclipse.hawkbit.repository.model; package org.eclipse.hawkbit.repository.model;
import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper;
import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException; import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException;
import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.ActionType;
@@ -69,9 +68,6 @@ public class TargetWithActionType {
this.maintenanceSchedule = maintenanceSchedule; this.maintenanceSchedule = maintenanceSchedule;
this.maintenanceWindowDuration = maintenanceWindowDuration; this.maintenanceWindowDuration = maintenanceWindowDuration;
this.maintenanceWindowTimeZone = maintenanceWindowTimeZone; this.maintenanceWindowTimeZone = maintenanceWindowTimeZone;
MaintenanceScheduleHelper.validateMaintenanceSchedule(maintenanceSchedule, maintenanceWindowDuration,
maintenanceWindowTimeZone);
} }
public ActionType getActionType() { public ActionType getActionType() {

View File

@@ -0,0 +1,96 @@
/**
* 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;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.time.Duration;
import java.time.ZonedDateTime;
import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException;
import org.junit.Test;
import com.cronutils.model.Cron;
import ru.yandex.qatools.allure.annotations.Description;
import ru.yandex.qatools.allure.annotations.Features;
import ru.yandex.qatools.allure.annotations.Stories;
@Features("Unit Tests - Repository")
@Stories("Maintenance Schedule Utility")
public class MaintenanceScheduleHelperTest {
@Test
@Description("Verifies that the Cron object is returned for valid cron expression")
public void getCronFromExpressionValid() {
final String validCron = "0 0 0 ? * 6"; // at 00:00 every Saturday
assertThat(MaintenanceScheduleHelper.getCronFromExpression(validCron)).isNotNull().isInstanceOf(Cron.class);
}
@Test
@Description("Verifies that the Duration object is returned for valid duration format (hh:mm or hh:mm:ss)")
public void convertToISODurationValid() {
final String duration = "00:10";
assertThat(MaintenanceScheduleHelper.convertToISODuration(duration)).isNotNull().isInstanceOf(Duration.class);
}
@Test
@Description("Verifies that the InvalidMaintenanceScheduleException is thrown for invalid duration format")
public void validateDurationInvalid() {
final String duration = "10";
assertThatThrownBy(() -> MaintenanceScheduleHelper.validateDuration(duration))
.isInstanceOf(InvalidMaintenanceScheduleException.class).hasMessage("Provided duration is not valid")
.extracting("durationErrorIndex").containsExactly(2);
}
@Test
@Description("Verifies that the InvalidMaintenanceScheduleException is thrown for invalid cron expression")
public void validateCronScheduleInvalid() {
final String invalidCron = "0 0 0 * * 6";
assertThatThrownBy(() -> MaintenanceScheduleHelper.validateCronSchedule(invalidCron))
.isInstanceOf(InvalidMaintenanceScheduleException.class)
.hasMessageContaining("Both, a day-of-week AND a day-of-month parameter, are not supported");
}
@Test
@Description("Verifies that there is a maintenance window available for correct schedule, duration and timezone")
public void getNextMaintenanceWindowValid() {
final ZonedDateTime currentTime = ZonedDateTime.now();
final String cronSchedule = String.format("0 %d %d %d %d ? %d", currentTime.getMinute(), currentTime.getHour(),
currentTime.getDayOfMonth(), currentTime.getMonthValue(), currentTime.getYear());
final String duration = "00:10";
final String timezone = ZonedDateTime.now().getOffset().getId().replace("Z", "+00:00");
assertThat(MaintenanceScheduleHelper.getNextMaintenanceWindow(cronSchedule, duration, timezone)).isPresent();
}
@Test
@Description("Verifies the maintenance schedule when only one required field is present")
public void validateMaintenanceScheduleAtLeastOneNotEmpty() {
final String duration = "00:10";
assertThatThrownBy(() -> MaintenanceScheduleHelper.validateMaintenanceSchedule(null, duration, null))
.isInstanceOf(InvalidMaintenanceScheduleException.class)
.hasMessage("All of schedule, duration and timezone should either be null or non empty.");
}
@Test
@Description("Verifies that there is no valid maintenance window available, scheduled before current time")
public void validateMaintenanceScheduleBeforeCurrentTime() {
ZonedDateTime currentTime = ZonedDateTime.now();
currentTime = currentTime.plusMinutes(-30);
final String cronSchedule = String.format("0 %d %d %d %d ? %d", currentTime.getMinute(), currentTime.getHour(),
currentTime.getDayOfMonth(), currentTime.getMonthValue(), currentTime.getYear());
final String duration = "00:10";
final String timezone = ZonedDateTime.now().getOffset().getId().replace("Z", "+00:00");
assertThatThrownBy(
() -> MaintenanceScheduleHelper.validateMaintenanceSchedule(cronSchedule, duration, timezone))
.isInstanceOf(InvalidMaintenanceScheduleException.class)
.hasMessage("No valid maintenance window available after current time");
}
}

View File

@@ -252,12 +252,10 @@ public class JpaControllerManagement implements ControllerManagement {
*/ */
EventTimer(final String defaultEventInterval, final String minimumEventInterval, final TemporalUnit timeUnit) { EventTimer(final String defaultEventInterval, final String minimumEventInterval, final TemporalUnit timeUnit) {
this.defaultEventInterval = defaultEventInterval; this.defaultEventInterval = defaultEventInterval;
this.defaultEventIntervalDuration = Duration this.defaultEventIntervalDuration = MaintenanceScheduleHelper.convertToISODuration(defaultEventInterval);
.parse(MaintenanceScheduleHelper.convertToISODuration(defaultEventInterval));
this.minimumEventInterval = minimumEventInterval; this.minimumEventInterval = minimumEventInterval;
this.minimumEventIntervalDuration = Duration this.minimumEventIntervalDuration = MaintenanceScheduleHelper.convertToISODuration(minimumEventInterval);
.parse(MaintenanceScheduleHelper.convertToISODuration(minimumEventInterval));
this.timeUnit = timeUnit; this.timeUnit = timeUnit;
} }

View File

@@ -8,7 +8,6 @@
*/ */
package org.eclipse.hawkbit.repository.jpa.model; package org.eclipse.hawkbit.repository.jpa.model;
import java.time.Duration;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Collections; import java.util.Collections;
@@ -127,11 +126,6 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio
@Column(name = "maintenance_time_zone", updatable = false, length = Action.MAINTENANCE_WINDOW_TIMEZONE_LENGTH) @Column(name = "maintenance_time_zone", updatable = false, length = Action.MAINTENANCE_WINDOW_TIMEZONE_LENGTH)
private String maintenanceWindowTimeZone; private String maintenanceWindowTimeZone;
/**
* A transient (non serialized) maintenance schedule helper.
*/
private transient MaintenanceScheduleHelper scheduleHelper = null;
@Override @Override
public DistributionSet getDistributionSet() { public DistributionSet getDistributionSet() {
return distributionSet; return distributionSet;
@@ -271,28 +265,6 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio
this.maintenanceWindowTimeZone = maintenanceWindowTimeZone; this.maintenanceWindowTimeZone = maintenanceWindowTimeZone;
} }
/**
* Get the transient schedule helper. Instantiate one if not already done
* after deserialization.
*
* @return the {@link MaintenanceScheduleHelper} object.
*/
private MaintenanceScheduleHelper getScheduler() {
if (this.scheduleHelper == null) {
this.scheduleHelper = new MaintenanceScheduleHelper(maintenanceSchedule);
}
return this.scheduleHelper;
}
/**
* Returns the duration of each maintenance window in ISO 8601 format.
*
* @return the {@link Duration} of each maintenance window.
*/
private Duration getMaintenanceWindowDuration() {
return Duration.parse(MaintenanceScheduleHelper.convertToISODuration(this.maintenanceWindowDuration));
}
/** /**
* Returns the start time of next available maintenance window for the * Returns the start time of next available maintenance window for the
* {@link Action} as {@link ZonedDateTime}. If a maintenance window is * {@link Action} as {@link ZonedDateTime}. If a maintenance window is
@@ -301,8 +273,8 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio
* @return the start time as {@link Optional<ZonedDateTime>}. * @return the start time as {@link Optional<ZonedDateTime>}.
*/ */
public Optional<ZonedDateTime> getMaintenanceWindowStartTime() { public Optional<ZonedDateTime> getMaintenanceWindowStartTime() {
final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.of(maintenanceWindowTimeZone)); return MaintenanceScheduleHelper.getNextMaintenanceWindow(maintenanceSchedule, maintenanceWindowDuration,
return getScheduler().nextExecution(now.minus(getMaintenanceWindowDuration())); maintenanceWindowTimeZone);
} }
/** /**
@@ -313,7 +285,8 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio
* @return the end time of window as {@link Optional<ZonedDateTime>}. * @return the end time of window as {@link Optional<ZonedDateTime>}.
*/ */
private Optional<ZonedDateTime> getMaintenanceWindowEndTime() { private Optional<ZonedDateTime> getMaintenanceWindowEndTime() {
return getMaintenanceWindowStartTime().map(start -> start.plus(getMaintenanceWindowDuration())); return getMaintenanceWindowStartTime()
.map(start -> start.plus(MaintenanceScheduleHelper.convertToISODuration(maintenanceWindowDuration)));
} }
@Override @Override
@@ -323,8 +296,7 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio
@Override @Override
public boolean isMaintenanceScheduleLapsed() { public boolean isMaintenanceScheduleLapsed() {
final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.of(maintenanceWindowTimeZone)); return !getMaintenanceWindowStartTime().isPresent();
return !getScheduler().nextExecution(now.minus(getMaintenanceWindowDuration())).isPresent();
} }
@Override @Override

View File

@@ -49,3 +49,4 @@ hawkbit.server.ui.links.documentation.rollout-view=https://www.eclipse.org/hawkb
hawkbit.server.ui.links.documentation.system-configuration-view=https://www.eclipse.org/hawkbit/documentation/interfaces/management-ui.html hawkbit.server.ui.links.documentation.system-configuration-view=https://www.eclipse.org/hawkbit/documentation/interfaces/management-ui.html
hawkbit.server.ui.links.documentation.targetfilter-view=https://www.eclipse.org/hawkbit/documentation/interfaces/management-ui.html hawkbit.server.ui.links.documentation.targetfilter-view=https://www.eclipse.org/hawkbit/documentation/interfaces/management-ui.html
hawkbit.server.ui.links.documentation.upload-view=https://www.eclipse.org/hawkbit/documentation/interfaces/management-ui.html hawkbit.server.ui.links.documentation.upload-view=https://www.eclipse.org/hawkbit/documentation/interfaces/management-ui.html
hawkbit.server.ui.links.documentation.maintenance-window-view=http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger.html

View File

@@ -234,6 +234,10 @@
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.cronutils</groupId>
<artifactId>cron-utils</artifactId>
</dependency>
<!-- Test --> <!-- Test -->
<dependency> <dependency>

View File

@@ -122,6 +122,11 @@ public class UiProperties implements Serializable {
*/ */
private String uploadView = ""; private String uploadView = "";
/**
* Link to documentation of maintenance window view.
*/
private String maintenanceWindowView = "";
/** /**
* Link to documentation of system configuration view. * Link to documentation of system configuration view.
*/ */
@@ -174,6 +179,10 @@ public class UiProperties implements Serializable {
return uploadView; return uploadView;
} }
public String getMaintenanceWindowView() {
return maintenanceWindowView;
}
public void setDeploymentView(final String deploymentView) { public void setDeploymentView(final String deploymentView) {
this.deploymentView = deploymentView; this.deploymentView = deploymentView;
} }
@@ -206,6 +215,10 @@ public class UiProperties implements Serializable {
this.uploadView = uploadView; this.uploadView = uploadView;
} }
public void setMaintenanceWindowView(final String maintenanceWindowView) {
this.maintenanceWindowView = maintenanceWindowView;
}
} }
private final Documentation documentation = new Documentation(); private final Documentation documentation = new Documentation();

View File

@@ -41,6 +41,7 @@ import com.vaadin.data.validator.NullValidator;
import com.vaadin.event.FieldEvents.TextChangeEvent; import com.vaadin.event.FieldEvents.TextChangeEvent;
import com.vaadin.event.FieldEvents.TextChangeListener; import com.vaadin.event.FieldEvents.TextChangeListener;
import com.vaadin.event.FieldEvents.TextChangeNotifier; import com.vaadin.event.FieldEvents.TextChangeNotifier;
import com.vaadin.event.ShortcutAction.KeyCode;
import com.vaadin.server.FontAwesome; import com.vaadin.server.FontAwesome;
import com.vaadin.ui.AbstractComponent; import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.AbstractField; import com.vaadin.ui.AbstractField;
@@ -466,6 +467,7 @@ public class CommonDialogWindow extends Window {
saveButton.addStyleName("default-color"); saveButton.addStyleName("default-color");
addCloseListenerForSaveButton(); addCloseListenerForSaveButton();
saveButton.setEnabled(false); saveButton.setEnabled(false);
saveButton.setClickShortcut(KeyCode.ENTER);
buttonsLayout.addComponent(saveButton); buttonsLayout.addComponent(saveButton);
buttonsLayout.setComponentAlignment(saveButton, Alignment.MIDDLE_RIGHT); buttonsLayout.setComponentAlignment(saveButton, Alignment.MIDDLE_RIGHT);
buttonsLayout.setExpandRatio(saveButton, 1.0F); buttonsLayout.setExpandRatio(saveButton, 1.0F);

View File

@@ -26,6 +26,8 @@ public class ComboBoxBuilder {
private String prompt; private String prompt;
private String caption;
public ComboBoxBuilder setValueChangeListener(final Property.ValueChangeListener valueChangeListener) { public ComboBoxBuilder setValueChangeListener(final Property.ValueChangeListener valueChangeListener) {
this.valueChangeListener = valueChangeListener; this.valueChangeListener = valueChangeListener;
return this; return this;
@@ -36,27 +38,35 @@ public class ComboBoxBuilder {
return this; return this;
} }
public ComboBoxBuilder setPrompt(String prompt) { public ComboBoxBuilder setPrompt(final String prompt) {
this.prompt = prompt; this.prompt = prompt;
return this; return this;
} }
public ComboBoxBuilder setCaption(final String caption) {
this.caption = caption;
return this;
}
/** /**
* @return a new ComboBox * @return a new ComboBox
*/ */
public ComboBox buildCombBox() { public ComboBox buildCombBox() {
final ComboBox targetFilter = SPUIComponentProvider.getComboBox(null, "", null, ValoTheme.COMBOBOX_SMALL, false, final ComboBox comboBox = SPUIComponentProvider.getComboBox(null, "", null, ValoTheme.COMBOBOX_SMALL, false, "",
"", prompt); prompt);
targetFilter.setImmediate(true); comboBox.setImmediate(true);
targetFilter.setPageLength(7); comboBox.setPageLength(7);
targetFilter.setItemCaptionPropertyId(SPUILabelDefinitions.VAR_NAME); comboBox.setItemCaptionPropertyId(SPUILabelDefinitions.VAR_NAME);
targetFilter.setSizeUndefined(); comboBox.setSizeUndefined();
if (caption != null) {
comboBox.setCaption(caption);
}
if (id != null) { if (id != null) {
targetFilter.setId(id); comboBox.setId(id);
} }
if (valueChangeListener != null) { if (valueChangeListener != null) {
targetFilter.addValueChangeListener(valueChangeListener); comboBox.addValueChangeListener(valueChangeListener);
} }
return targetFilter; return comboBox;
} }
} }

View File

@@ -24,9 +24,6 @@ import com.vaadin.ui.themes.ValoTheme;
/** /**
* Confirmation tab of confirmation window. * Confirmation tab of confirmation window.
*
*
*
*/ */
public class ConfirmationTab extends VerticalLayout { public class ConfirmationTab extends VerticalLayout {

View File

@@ -159,7 +159,7 @@ public class DeploymentView extends AbstractNotificationView implements BrowserW
uiExecutor); uiExecutor);
this.deleteAndActionsLayout = new DeleteActionsLayout(i18n, permChecker, eventBus, uiNotification, this.deleteAndActionsLayout = new DeleteActionsLayout(i18n, permChecker, eventBus, uiNotification,
targetTagManagement, distributionSetTagManagement, managementViewClientCriterion, managementUIState, targetTagManagement, distributionSetTagManagement, managementViewClientCriterion, managementUIState,
targetManagement, targetTable, deploymentManagement, distributionSetManagement); targetManagement, targetTable, deploymentManagement, distributionSetManagement, uiproperties);
actionHistoryLayout.registerDetails(((ActionStatusGrid) actionStatusLayout.getGrid()).getDetailsSupport()); actionHistoryLayout.registerDetails(((ActionStatusGrid) actionStatusLayout.getGrid()).getDetailsSupport());
actionStatusLayout actionStatusLayout

View File

@@ -24,6 +24,7 @@ import org.eclipse.hawkbit.repository.TargetTagManagement;
import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.ui.SpPermissionChecker; import org.eclipse.hawkbit.ui.SpPermissionChecker;
import org.eclipse.hawkbit.ui.UiProperties;
import org.eclipse.hawkbit.ui.common.entity.DistributionSetIdName; import org.eclipse.hawkbit.ui.common.entity.DistributionSetIdName;
import org.eclipse.hawkbit.ui.common.entity.TargetIdName; import org.eclipse.hawkbit.ui.common.entity.TargetIdName;
import org.eclipse.hawkbit.ui.common.footer.AbstractDeleteActionsLayout; import org.eclipse.hawkbit.ui.common.footer.AbstractDeleteActionsLayout;
@@ -72,7 +73,7 @@ public class DeleteActionsLayout extends AbstractDeleteActionsLayout {
private final ManagementUIState managementUIState; private final ManagementUIState managementUIState;
private final ManangementConfirmationWindowLayout manangementConfirmationWindowLayout; private final ManagementConfirmationWindowLayout managementConfirmationWindowLayout;
private final CountMessageLabel countMessageLabel; private final CountMessageLabel countMessageLabel;
@@ -86,14 +87,15 @@ public class DeleteActionsLayout extends AbstractDeleteActionsLayout {
final ManagementViewClientCriterion managementViewClientCriterion, final ManagementViewClientCriterion managementViewClientCriterion,
final ManagementUIState managementUIState, final TargetManagement targetManagement, final ManagementUIState managementUIState, final TargetManagement targetManagement,
final TargetTable targetTable, final DeploymentManagement deploymentManagement, final TargetTable targetTable, final DeploymentManagement deploymentManagement,
final DistributionSetManagement distributionSetManagement) { final DistributionSetManagement distributionSetManagement, final UiProperties uiProperties) {
super(i18n, permChecker, eventBus, notification); super(i18n, permChecker, eventBus, notification);
this.distributionSetTagManagement = distributionSetTagManagement; this.distributionSetTagManagement = distributionSetTagManagement;
this.targetTagManagement = targetTagManagement; this.targetTagManagement = targetTagManagement;
this.managementViewClientCriterion = managementViewClientCriterion; this.managementViewClientCriterion = managementViewClientCriterion;
this.managementUIState = managementUIState; this.managementUIState = managementUIState;
this.manangementConfirmationWindowLayout = new ManangementConfirmationWindowLayout(i18n, eventBus, this.managementConfirmationWindowLayout = new ManagementConfirmationWindowLayout(i18n, eventBus,
managementUIState, targetManagement, deploymentManagement, distributionSetManagement); managementUIState, targetManagement, deploymentManagement, distributionSetManagement, uiProperties,
notification);
this.countMessageLabel = new CountMessageLabel(eventBus, targetManagement, i18n, managementUIState, this.countMessageLabel = new CountMessageLabel(eventBus, targetManagement, i18n, managementUIState,
targetTable); targetTable);
this.targetManagement = targetManagement; this.targetManagement = targetManagement;
@@ -114,7 +116,7 @@ public class DeleteActionsLayout extends AbstractDeleteActionsLayout {
UI.getCurrent().access(() -> { UI.getCurrent().access(() -> {
if (!hasUnsavedActions()) { if (!hasUnsavedActions()) {
closeUnsavedActionsWindow(); closeUnsavedActionsWindow();
final String message = manangementConfirmationWindowLayout.getConsolidatedMessage(); final String message = managementConfirmationWindowLayout.getConsolidatedMessage();
if (message != null && message.length() > 0) { if (message != null && message.length() > 0) {
notification.displaySuccess(message); notification.displaySuccess(message);
} }
@@ -215,7 +217,7 @@ public class DeleteActionsLayout extends AbstractDeleteActionsLayout {
@Override @Override
protected void unsavedActionsWindowClosed() { protected void unsavedActionsWindowClosed() {
final String message = manangementConfirmationWindowLayout.getConsolidatedMessage(); final String message = managementConfirmationWindowLayout.getConsolidatedMessage();
if (message != null && message.length() > 0) { if (message != null && message.length() > 0) {
notification.displaySuccess(message); notification.displaySuccess(message);
} }
@@ -223,8 +225,8 @@ public class DeleteActionsLayout extends AbstractDeleteActionsLayout {
@Override @Override
protected Component getUnsavedActionsWindowContent() { protected Component getUnsavedActionsWindowContent() {
manangementConfirmationWindowLayout.initialize(); managementConfirmationWindowLayout.initialize();
return manangementConfirmationWindowLayout; return managementConfirmationWindowLayout;
} }
@Override @Override
@@ -248,7 +250,7 @@ public class DeleteActionsLayout extends AbstractDeleteActionsLayout {
final String tagName = HawkbitCommonUtil.removePrefix(source.getId(), final String tagName = HawkbitCommonUtil.removePrefix(source.getId(),
SPUIDefinitions.DISTRIBUTION_TAG_ID_PREFIXS); SPUIDefinitions.DISTRIBUTION_TAG_ID_PREFIXS);
if (managementUIState.getDistributionTableFilters().getDistSetTags().contains(tagName)) { if (managementUIState.getDistributionTableFilters().getDistSetTags().contains(tagName)) {
notification.displayValidationError(i18n.getMessage("message.tag.delete", new Object[] { tagName })); notification.displayValidationError(i18n.getMessage("message.tag.delete", tagName));
} else { } else {
distributionSetTagManagement.delete(tagName); distributionSetTagManagement.delete(tagName);
@@ -258,14 +260,14 @@ public class DeleteActionsLayout extends AbstractDeleteActionsLayout {
new DistributionSetTagTableEvent(BaseEntityEventType.REMOVE_ENTITY, Arrays.asList(id))); new DistributionSetTagTableEvent(BaseEntityEventType.REMOVE_ENTITY, Arrays.asList(id)));
} }
notification.displaySuccess(i18n.getMessage("message.delete.success", new Object[] { tagName })); notification.displaySuccess(i18n.getMessage("message.delete.success", tagName));
} }
} }
private void deleteTargetTag(final Component source) { private void deleteTargetTag(final Component source) {
final String tagName = HawkbitCommonUtil.removePrefix(source.getId(), SPUIDefinitions.TARGET_TAG_ID_PREFIXS); final String tagName = HawkbitCommonUtil.removePrefix(source.getId(), SPUIDefinitions.TARGET_TAG_ID_PREFIXS);
if (managementUIState.getTargetTableFilters().getClickedTargetTags().contains(tagName)) { if (managementUIState.getTargetTableFilters().getClickedTargetTags().contains(tagName)) {
notification.displayValidationError(i18n.getMessage("message.tag.delete", new Object[] { tagName })); notification.displayValidationError(i18n.getMessage("message.tag.delete", tagName));
} else { } else {
targetTagManagement.delete(tagName); targetTagManagement.delete(tagName);
@@ -274,7 +276,7 @@ public class DeleteActionsLayout extends AbstractDeleteActionsLayout {
eventBus.publish(this, new TargetTagTableEvent(BaseEntityEventType.REMOVE_ENTITY, Arrays.asList(id))); eventBus.publish(this, new TargetTagTableEvent(BaseEntityEventType.REMOVE_ENTITY, Arrays.asList(id)));
} }
notification.displaySuccess(i18n.getMessage("message.delete.success", new Object[] { tagName })); notification.displaySuccess(i18n.getMessage("message.delete.success", tagName));
} }
} }

View File

@@ -11,22 +11,26 @@ package org.eclipse.hawkbit.ui.management.footer;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeParseException;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper; import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper;
import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException;
import org.eclipse.hawkbit.ui.common.builder.LabelBuilder;
import org.eclipse.hawkbit.ui.common.builder.TextFieldBuilder;
import org.eclipse.hawkbit.ui.utils.SPDateTimeUtil; import org.eclipse.hawkbit.ui.utils.SPDateTimeUtil;
import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider;
import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; import org.eclipse.hawkbit.ui.utils.VaadinMessageSource;
import org.springframework.util.StringUtils;
import com.vaadin.data.Property.ValueChangeEvent; import com.cronutils.descriptor.CronDescriptor;
import com.vaadin.data.Property.ValueChangeListener;
import com.vaadin.data.Validator; import com.vaadin.data.Validator;
import com.vaadin.ui.CheckBox; import com.vaadin.event.FieldEvents.TextChangeEvent;
import com.vaadin.event.FieldEvents.TextChangeListener;
import com.vaadin.server.Page;
import com.vaadin.ui.ComboBox; import com.vaadin.ui.ComboBox;
import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Notification; import com.vaadin.ui.Label;
import com.vaadin.ui.TextField; import com.vaadin.ui.TextField;
import com.vaadin.ui.VerticalLayout; import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.themes.ValoTheme; import com.vaadin.ui.themes.ValoTheme;
@@ -36,15 +40,16 @@ import com.vaadin.ui.themes.ValoTheme;
* maintenance schedule while assigning distribution set(s) to the target(s). * maintenance schedule while assigning distribution set(s) to the target(s).
*/ */
public class MaintenanceWindowLayout extends VerticalLayout { public class MaintenanceWindowLayout extends VerticalLayout {
private static final long serialVersionUID = 1L;
private static final long serialVersionUID = 722511089585562455L;
private final VaadinMessageSource i18n; private final VaadinMessageSource i18n;
private CheckBox maintenanceWindowSelection; private static final String CRON_VALIDATION_ERROR = "message.maintenancewindow.schedule.validation.error";
private TextField schedule; private TextField schedule;
private TextField duration; private TextField duration;
private ComboBox timeZone; private ComboBox timeZone;
private Label scheduleTranslator;
/** /**
* Constructor for the control to specify the maintenance schedule. * Constructor for the control to specify the maintenance schedule.
@@ -55,123 +60,137 @@ public class MaintenanceWindowLayout extends VerticalLayout {
*/ */
public MaintenanceWindowLayout(final VaadinMessageSource i18n) { public MaintenanceWindowLayout(final VaadinMessageSource i18n) {
HorizontalLayout optionContainer;
HorizontalLayout controlContainer;
this.i18n = i18n; this.i18n = i18n;
optionContainer = new HorizontalLayout();
controlContainer = new HorizontalLayout();
addComponent(optionContainer);
addComponent(controlContainer);
createMaintenanceWindowOption();
createMaintenanceScheduleControl(); createMaintenanceScheduleControl();
createMaintenanceDurationControl(); createMaintenanceDurationControl();
createMaintenanceTimeZoneControl(); createMaintenanceTimeZoneControl();
createMaintenanceScheduleTranslatorControl();
optionContainer.addComponent(maintenanceWindowSelection); final HorizontalLayout controlContainer = new HorizontalLayout();
controlContainer.addComponent(schedule); controlContainer.addComponent(schedule);
controlContainer.addComponent(duration); controlContainer.addComponent(duration);
controlContainer.addComponent(timeZone); controlContainer.addComponent(timeZone);
addComponent(controlContainer);
addComponent(scheduleTranslator);
addValueChangeListener();
maintenanceWindowSelection.setValue(false);
setStyleName("dist-window-maintenance-window-layout"); setStyleName("dist-window-maintenance-window-layout");
} setId(UIComponentIdProvider.MAINTENANCE_WINDOW_LAYOUT_ID);
/**
* Validates if the maintenance schedule is a valid cron expression.
*/
private static class CronValidation implements Validator {
private static final long serialVersionUID = 1L;
@Override
public void validate(final Object value) throws InvalidValueException {
try {
final String expr = (String) value;
if (!expr.isEmpty()) {
MaintenanceScheduleHelper.validateMaintenanceSchedule((String) value, "00:00:00",
getClientTimeZone());
}
} catch (final IllegalArgumentException e) {
Notification.show(e.getMessage());
throw e;
}
}
}
/**
* Validates if the duration is specified in expected format.
*/
private static class DurationValidator implements Validator {
private static final long serialVersionUID = 1L;
@Override
public void validate(final Object value) {
try {
final String expr = (String) value;
if (!StringUtils.isEmpty(expr)) {
MaintenanceScheduleHelper.convertToISODuration((String) value);
}
} catch (final DateTimeParseException e) {
Notification.show(e.getMessage());
throw e;
}
}
}
/**
* Create check box to enable or disable maintenance window.
*/
private void createMaintenanceWindowOption() {
maintenanceWindowSelection = new CheckBox(i18n.getMessage("caption.maintenancewindow.enable"));
maintenanceWindowSelection.addStyleName(ValoTheme.CHECKBOX_SMALL);
} }
/** /**
* Text field to specify the schedule. * Text field to specify the schedule.
*/ */
private void createMaintenanceScheduleControl() { private void createMaintenanceScheduleControl() {
schedule = new TextField(); schedule = new TextFieldBuilder().id(UIComponentIdProvider.MAINTENANCE_WINDOW_SCHEDULE_ID)
schedule.setCaption(i18n.getMessage("caption.maintenancewindow.schedule")); .caption(i18n.getMessage("caption.maintenancewindow.schedule")).immediate(true)
schedule.addValidator(new CronValidation()); .validator(new CronValidator()).prompt("0 0 3 ? * 6").required(true).buildTextComponent();
schedule.setEnabled(false); schedule.addTextChangeListener(new CronTranslationListener());
schedule.addStyleName(ValoTheme.TEXTFIELD_SMALL); }
/**
* Validates if the maintenance schedule is a valid cron expression.
*/
private class CronValidator implements Validator {
private static final long serialVersionUID = 1L;
// Exception squid:S1166 - Vaadin validation class,
// InvalidValueException,
// doesn't have the constructor to pass throwable, but shows the
// validation
// errors to the user
@SuppressWarnings("squid:S1166")
@Override
public void validate(final Object value) {
try {
MaintenanceScheduleHelper.validateCronSchedule((String) value);
} catch (final InvalidMaintenanceScheduleException e) {
throw new InvalidValueException(i18n.getMessage(CRON_VALIDATION_ERROR) + ": " + e.getMessage());
}
}
}
/**
* Used for cron expression translation.
*/
private class CronTranslationListener implements TextChangeListener {
private static final long serialVersionUID = 1L;
private final transient CronDescriptor cronDescriptor;
public CronTranslationListener() {
cronDescriptor = CronDescriptor.instance(getClientsLocale());
}
@Override
public void textChange(final TextChangeEvent event) {
scheduleTranslator.setValue(translateCron(event.getText()));
}
// Exception squid:S1166 - when the format of the cron expression is not
// valid, the hint is shown to provide the valid one
@SuppressWarnings("squid:S1166")
private String translateCron(final String cronExpression) {
try {
return cronDescriptor.describe(MaintenanceScheduleHelper.getCronFromExpression(cronExpression));
} catch (final IllegalArgumentException ex) {
return i18n.getMessage(CRON_VALIDATION_ERROR);
}
}
private Locale getClientsLocale() {
return Page.getCurrent().getWebBrowser().getLocale();
}
} }
/** /**
* Text field to specify the duration. * Text field to specify the duration.
*/ */
private void createMaintenanceDurationControl() { private void createMaintenanceDurationControl() {
duration = new TextField(); duration = new TextFieldBuilder().id(UIComponentIdProvider.MAINTENANCE_WINDOW_DURATION_ID)
duration.setCaption(i18n.getMessage("caption.maintenancewindow.duration")); .caption(i18n.getMessage("caption.maintenancewindow.duration")).immediate(true)
duration.addValidator(new DurationValidator()); .validator(new DurationValidator()).prompt("hh:mm:ss").required(true).buildTextComponent();
duration.setEnabled(false); }
schedule.addStyleName(ValoTheme.TEXTFIELD_SMALL);
/**
* Validates if the duration is specified in expected format.
*/
private class DurationValidator implements Validator {
private static final long serialVersionUID = 1L;
// Exception squid:S1166 - Vaadin validation class,
// InvalidValueException,
// doesn't have the constructor to pass throwable, but shows the
// validation
// errors to the user
@SuppressWarnings("squid:S1166")
@Override
public void validate(final Object value) {
try {
MaintenanceScheduleHelper.validateDuration((String) value);
} catch (final InvalidMaintenanceScheduleException e) {
throw new InvalidValueException(i18n.getMessage("message.maintenancewindow.duration.validation.error",
e.getDurationErrorIndex()));
}
}
} }
/** /**
* Combo box to pick the time zone offset. * Combo box to pick the time zone offset.
*/ */
private void createMaintenanceTimeZoneControl() { private void createMaintenanceTimeZoneControl() {
// ComboBoxBuilder cannot be used here, because Builder do
// 'comboBox.setItemCaptionPropertyId(SPUILabelDefinitions.VAR_NAME);'
// which interferes our code: 'timeZone.addItems(getAllTimeZones());'
timeZone = new ComboBox(); timeZone = new ComboBox();
timeZone.setId(UIComponentIdProvider.MAINTENANCE_WINDOW_TIME_ZONE_ID);
timeZone.setCaption(i18n.getMessage("caption.maintenancewindow.timezone")); timeZone.setCaption(i18n.getMessage("caption.maintenancewindow.timezone"));
timeZone.addItems(getAllTimeZones()); timeZone.addItems(getAllTimeZones());
timeZone.setTextInputAllowed(false);
timeZone.setValue(getClientTimeZone()); timeZone.setValue(getClientTimeZone());
timeZone.setEnabled(false);
timeZone.addStyleName(ValoTheme.COMBOBOX_SMALL); timeZone.addStyleName(ValoTheme.COMBOBOX_SMALL);
} timeZone.setTextInputAllowed(false);
timeZone.setNullSelectionAllowed(false);
/**
* Get time zone of the browser client to be used as default.
*/
private static String getClientTimeZone() {
return ZonedDateTime.now(ZoneId.of(SPDateTimeUtil.getBrowserTimeZone().getID())).getOffset().getId()
.replaceAll("Z", "+00:00");
} }
/** /**
@@ -186,36 +205,20 @@ public class MaintenanceWindowLayout extends VerticalLayout {
} }
/** /**
* Create a listener to enable and disable maintenance schedule controls. * Get time zone of the browser client to be used as default.
*/ */
private void addValueChangeListener() { private static String getClientTimeZone() {
maintenanceWindowSelection.addValueChangeListener(new ValueChangeListener() { return ZonedDateTime.now(ZoneId.of(SPDateTimeUtil.getBrowserTimeZone().getID())).getOffset().getId()
private static final long serialVersionUID = 1L; .replaceAll("Z", "+00:00");
@Override
public void valueChange(final ValueChangeEvent event) {
schedule.setEnabled(maintenanceWindowSelection.getValue());
schedule.setRequired(maintenanceWindowSelection.getValue());
schedule.setValue("");
duration.setEnabled(maintenanceWindowSelection.getValue());
duration.setRequired(maintenanceWindowSelection.getValue());
duration.setValue("");
timeZone.setEnabled(maintenanceWindowSelection.getValue());
timeZone.setRequired(maintenanceWindowSelection.getValue());
timeZone.setValue(getClientTimeZone());
}
});
} }
/** /**
* Get whether the maintenance schedule option is enabled or not. * Label to translate the cron schedule to human readable format.
*
* @return boolean.
*/ */
public boolean isMaintenanceWindowEnabled() { private void createMaintenanceScheduleTranslatorControl() {
return maintenanceWindowSelection.getValue(); scheduleTranslator = new LabelBuilder().id(UIComponentIdProvider.MAINTENANCE_WINDOW_SCHEDULE_TRANSLATOR_ID)
.name(i18n.getMessage(CRON_VALIDATION_ERROR)).buildLabel();
scheduleTranslator.addStyleName(ValoTheme.LABEL_TINY);
} }
/** /**
@@ -237,7 +240,7 @@ public class MaintenanceWindowLayout extends VerticalLayout {
} }
/** /**
* Get the cron expression for maintenance window timezone. * Get the timezone for maintenance window.
* *
* @return {@link String}. * @return {@link String}.
*/ */
@@ -245,4 +248,55 @@ public class MaintenanceWindowLayout extends VerticalLayout {
public String getMaintenanceTimeZone() { public String getMaintenanceTimeZone() {
return timeZone.getValue().toString(); return timeZone.getValue().toString();
} }
/**
* Set all the controls to their default values.
*/
public void clearAllControls() {
schedule.setValue("");
duration.setValue("");
timeZone.setValue(getClientTimeZone());
}
/**
* Method, used for validity check, when schedule text is changed.
*
* @param event
* (@link TextChangeEvent} the event object after schedule text
* change.
* @return validity of maintenance window controls.
*/
public boolean onScheduleChange(final TextChangeEvent event) {
schedule.setValue(event.getText());
return isScheduleAndDurationValid();
}
/**
* Method, used for validity check, when duration text is changed.
*
* @param event
* (@link TextChangeEvent} the event object after duration text
* change.
* @return validity of maintenance window controls.
*/
public boolean onDurationChange(final TextChangeEvent event) {
duration.setValue(event.getText());
return isScheduleAndDurationValid();
}
private boolean isScheduleAndDurationValid() {
if (schedule.isEmpty() || duration.isEmpty()) {
return false;
}
return schedule.isValid() && duration.isValid();
}
public TextField getScheduleControl() {
return schedule;
}
public TextField getDurationControl() {
return duration;
}
} }

View File

@@ -21,16 +21,20 @@ import java.util.stream.Collectors;
import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.DistributionSetManagement; import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.MaintenanceScheduleHelper;
import org.eclipse.hawkbit.repository.TargetManagement; import org.eclipse.hawkbit.repository.TargetManagement;
import org.eclipse.hawkbit.repository.model.TargetWithActionType; import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException;
import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult;
import org.eclipse.hawkbit.repository.model.RepositoryModelConstants; import org.eclipse.hawkbit.repository.model.RepositoryModelConstants;
import org.eclipse.hawkbit.repository.model.TargetWithActionType;
import org.eclipse.hawkbit.ui.UiProperties;
import org.eclipse.hawkbit.ui.common.confirmwindow.layout.AbstractConfirmationWindowLayout; import org.eclipse.hawkbit.ui.common.confirmwindow.layout.AbstractConfirmationWindowLayout;
import org.eclipse.hawkbit.ui.common.confirmwindow.layout.ConfirmationTab; import org.eclipse.hawkbit.ui.common.confirmwindow.layout.ConfirmationTab;
import org.eclipse.hawkbit.ui.common.entity.DistributionSetIdName; import org.eclipse.hawkbit.ui.common.entity.DistributionSetIdName;
import org.eclipse.hawkbit.ui.common.entity.TargetIdName; import org.eclipse.hawkbit.ui.common.entity.TargetIdName;
import org.eclipse.hawkbit.ui.common.table.BaseEntityEventType; import org.eclipse.hawkbit.ui.common.table.BaseEntityEventType;
import org.eclipse.hawkbit.ui.components.SPUIComponentProvider;
import org.eclipse.hawkbit.ui.management.event.DistributionTableEvent; import org.eclipse.hawkbit.ui.management.event.DistributionTableEvent;
import org.eclipse.hawkbit.ui.management.event.PinUnpinEvent; import org.eclipse.hawkbit.ui.management.event.PinUnpinEvent;
import org.eclipse.hawkbit.ui.management.event.SaveActionWindowEvent; import org.eclipse.hawkbit.ui.management.event.SaveActionWindowEvent;
@@ -41,6 +45,7 @@ import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil;
import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; import org.eclipse.hawkbit.ui.utils.SPUIDefinitions;
import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions;
import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider;
import org.eclipse.hawkbit.ui.utils.UINotification;
import org.eclipse.hawkbit.ui.utils.VaadinMessageSource; import org.eclipse.hawkbit.ui.utils.VaadinMessageSource;
import org.vaadin.spring.events.EventBus.UIEventBus; import org.vaadin.spring.events.EventBus.UIEventBus;
@@ -49,13 +54,17 @@ import com.vaadin.data.Item;
import com.vaadin.data.util.IndexedContainer; import com.vaadin.data.util.IndexedContainer;
import com.vaadin.server.FontAwesome; import com.vaadin.server.FontAwesome;
import com.vaadin.ui.Button.ClickListener; import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.CheckBox;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Link;
import com.vaadin.ui.Table.Align; import com.vaadin.ui.Table.Align;
import com.vaadin.ui.themes.ValoTheme;
/** /**
* Confirmation window for target/distributionSet delete and assignment * Confirmation window for target/distributionSet delete and assignment
* operations on the Deployment View. * operations on the Deployment View.
*/ */
public class ManangementConfirmationWindowLayout extends AbstractConfirmationWindowLayout { public class ManagementConfirmationWindowLayout extends AbstractConfirmationWindowLayout {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -69,6 +78,10 @@ public class ManangementConfirmationWindowLayout extends AbstractConfirmationWin
private static final String TARGET_ID = "TargetId"; private static final String TARGET_ID = "TargetId";
private final UiProperties uiProperties;
private final UINotification uiNotification;
private final ManagementUIState managementUIState; private final ManagementUIState managementUIState;
private final transient TargetManagement targetManagement; private final transient TargetManagement targetManagement;
@@ -83,11 +96,13 @@ public class ManangementConfirmationWindowLayout extends AbstractConfirmationWin
private ConfirmationTab assignmentTab; private ConfirmationTab assignmentTab;
public ManangementConfirmationWindowLayout(final VaadinMessageSource i18n, final UIEventBus eventBus, public ManagementConfirmationWindowLayout(final VaadinMessageSource i18n, final UIEventBus eventBus,
final ManagementUIState managementUIState, final TargetManagement targetManagement, final ManagementUIState managementUIState, final TargetManagement targetManagement,
final DeploymentManagement deploymentManagement, final DeploymentManagement deploymentManagement, final DistributionSetManagement distributionSetManagement,
final DistributionSetManagement distributionSetManagement) { final UiProperties uiProperties, final UINotification uiNotification) {
super(i18n, eventBus); super(i18n, eventBus);
this.uiProperties = uiProperties;
this.uiNotification = uiNotification;
this.managementUIState = managementUIState; this.managementUIState = managementUIState;
this.targetManagement = targetManagement; this.targetManagement = targetManagement;
this.deploymentManagement = deploymentManagement; this.deploymentManagement = deploymentManagement;
@@ -142,12 +157,61 @@ public class ManangementConfirmationWindowLayout extends AbstractConfirmationWin
actionTypeOptionGroupLayout.selectDefaultOption(); actionTypeOptionGroupLayout.selectDefaultOption();
assignmentTab.addComponent(actionTypeOptionGroupLayout, 1); assignmentTab.addComponent(actionTypeOptionGroupLayout, 1);
assignmentTab.addComponent(maintenanceWindowLayout, 1); assignmentTab.addComponent(enableMaintenanceWindowLayout(), 2);
initMaintenanceWindow();
assignmentTab.addComponent(maintenanceWindowLayout, 3);
return assignmentTab; return assignmentTab;
} }
private HorizontalLayout enableMaintenanceWindowLayout() {
final HorizontalLayout layout = new HorizontalLayout();
layout.addComponent(enableMaintenanceWindowControl());
layout.addComponent(maintenanceWindowHelpLinkControl());
return layout;
}
private CheckBox enableMaintenanceWindowControl() {
final CheckBox enableMaintenanceWindow = new CheckBox(i18n.getMessage("caption.maintenancewindow.enabled"));
enableMaintenanceWindow.setId(UIComponentIdProvider.MAINTENANCE_WINDOW_ENABLED_ID);
enableMaintenanceWindow.addStyleName(ValoTheme.CHECKBOX_SMALL);
enableMaintenanceWindow.addStyleName("dist-window-maintenance-window-enable");
enableMaintenanceWindow.addValueChangeListener(event -> {
final Boolean isMaintenanceWindowEnabled = enableMaintenanceWindow.getValue();
maintenanceWindowLayout.setVisible(isMaintenanceWindowEnabled);
maintenanceWindowLayout.setEnabled(isMaintenanceWindowEnabled);
enableSaveButton(!isMaintenanceWindowEnabled);
maintenanceWindowLayout.clearAllControls();
});
return enableMaintenanceWindow;
}
private void enableSaveButton(final boolean enabled) {
assignmentTab.getConfirmAll().setEnabled(enabled);
}
private Link maintenanceWindowHelpLinkControl() {
final String maintenanceWindowHelpUrl = uiProperties.getLinks().getDocumentation().getMaintenanceWindowView();
return SPUIComponentProvider.getHelpLink(maintenanceWindowHelpUrl);
}
private void initMaintenanceWindow() {
maintenanceWindowLayout.setVisible(false);
maintenanceWindowLayout.setEnabled(false);
maintenanceWindowLayout.getScheduleControl()
.addTextChangeListener(event -> enableSaveButton(maintenanceWindowLayout.onScheduleChange(event)));
maintenanceWindowLayout.getDurationControl()
.addTextChangeListener(event -> enableSaveButton(maintenanceWindowLayout.onDurationChange(event)));
}
private void saveAllAssignments(final ConfirmationTab tab) { private void saveAllAssignments(final ConfirmationTab tab) {
if (!isMaintenanceWindowValid()) {
return;
}
final Set<TargetIdName> itemIds = managementUIState.getAssignedList().keySet(); final Set<TargetIdName> itemIds = managementUIState.getAssignedList().keySet();
Long distId; Long distId;
List<TargetIdName> targetIdSetList; List<TargetIdName> targetIdSetList;
@@ -159,15 +223,6 @@ public class ManangementConfirmationWindowLayout extends AbstractConfirmationWin
? actionTypeOptionGroupLayout.getForcedTimeDateField().getValue().getTime() ? actionTypeOptionGroupLayout.getForcedTimeDateField().getValue().getTime()
: RepositoryModelConstants.NO_FORCE_TIME; : RepositoryModelConstants.NO_FORCE_TIME;
final String maintenanceSchedule = maintenanceWindowLayout.isMaintenanceWindowEnabled()
? maintenanceWindowLayout.getMaintenanceSchedule() : null;
final String maintenanceDuration = maintenanceWindowLayout.isMaintenanceWindowEnabled()
? maintenanceWindowLayout.getMaintenanceDuration() : null;
final String maintenanceTimeZone = maintenanceWindowLayout.isMaintenanceWindowEnabled()
? maintenanceWindowLayout.getMaintenanceTimeZone() : null;
final Map<Long, List<TargetIdName>> saveAssignedList = Maps.newHashMapWithExpectedSize(itemIds.size()); final Map<Long, List<TargetIdName>> saveAssignedList = Maps.newHashMapWithExpectedSize(itemIds.size());
int successAssignmentCount = 0; int successAssignmentCount = 0;
@@ -185,13 +240,18 @@ public class ManangementConfirmationWindowLayout extends AbstractConfirmationWin
saveAssignedList.put(distId, targetIdSetList); saveAssignedList.put(distId, targetIdSetList);
} }
final String maintenanceSchedule = maintenanceWindowLayout.getMaintenanceSchedule();
final String maintenanceDuration = maintenanceWindowLayout.getMaintenanceDuration();
final String maintenanceTimeZone = maintenanceWindowLayout.getMaintenanceTimeZone();
for (final Map.Entry<Long, List<TargetIdName>> mapEntry : saveAssignedList.entrySet()) { for (final Map.Entry<Long, List<TargetIdName>> mapEntry : saveAssignedList.entrySet()) {
tempIdList = saveAssignedList.get(mapEntry.getKey()); tempIdList = saveAssignedList.get(mapEntry.getKey());
final DistributionSetAssignmentResult distributionSetAssignmentResult = deploymentManagement final DistributionSetAssignmentResult distributionSetAssignmentResult = deploymentManagement
.assignDistributionSet(mapEntry.getKey(), .assignDistributionSet(mapEntry.getKey(),
tempIdList.stream() tempIdList.stream().map(t -> maintenanceWindowLayout.isEnabled()
.map(t -> new TargetWithActionType(t.getControllerId(), actionType, forcedTimeStamp, ? new TargetWithActionType(t.getControllerId(), actionType, forcedTimeStamp,
maintenanceSchedule, maintenanceDuration, maintenanceTimeZone)) maintenanceSchedule, maintenanceDuration, maintenanceTimeZone)
: new TargetWithActionType(t.getControllerId(), actionType, forcedTimeStamp))
.collect(Collectors.toList())); .collect(Collectors.toList()));
if (distributionSetAssignmentResult.getAssigned() > 0) { if (distributionSetAssignmentResult.getAssigned() > 0) {
@@ -210,6 +270,24 @@ public class ManangementConfirmationWindowLayout extends AbstractConfirmationWin
eventBus.publish(this, SaveActionWindowEvent.SAVED_ASSIGNMENTS); eventBus.publish(this, SaveActionWindowEvent.SAVED_ASSIGNMENTS);
} }
// Exception squid:S1166 - the user is notified when the maintenance
// schedule validation fails
@SuppressWarnings("squid:S1166")
private boolean isMaintenanceWindowValid() {
if (maintenanceWindowLayout.isEnabled()) {
try {
MaintenanceScheduleHelper.validateMaintenanceSchedule(maintenanceWindowLayout.getMaintenanceSchedule(),
maintenanceWindowLayout.getMaintenanceDuration(),
maintenanceWindowLayout.getMaintenanceTimeZone());
} catch (final InvalidMaintenanceScheduleException e) {
uiNotification.displayValidationError(e.getMessage());
return false;
}
}
return true;
}
private void addMessage(final int successAssignmentCount, final int duplicateAssignmentCount) { private void addMessage(final int successAssignmentCount, final int duplicateAssignmentCount) {
if (successAssignmentCount > 0) { if (successAssignmentCount > 0) {
addToConsolitatedMsg(getAssigmentSuccessMessage(successAssignmentCount)); addToConsolitatedMsg(getAssigmentSuccessMessage(successAssignmentCount));
@@ -239,12 +317,12 @@ public class ManangementConfirmationWindowLayout extends AbstractConfirmationWin
private String getAssigmentSuccessMessage(final int assignedCount) { private String getAssigmentSuccessMessage(final int assignedCount) {
return FontAwesome.TASKS.getHtml() + SPUILabelDefinitions.HTML_SPACE return FontAwesome.TASKS.getHtml() + SPUILabelDefinitions.HTML_SPACE
+ i18n.getMessage("message.target.assignment", new Object[] { assignedCount }); + i18n.getMessage("message.target.assignment", assignedCount);
} }
private String getDuplicateAssignmentMessage(final int alreadyAssignedCount) { private String getDuplicateAssignmentMessage(final int alreadyAssignedCount) {
return FontAwesome.TASKS.getHtml() + SPUILabelDefinitions.HTML_SPACE return FontAwesome.TASKS.getHtml() + SPUILabelDefinitions.HTML_SPACE
+ i18n.getMessage("message.target.alreadyAssigned", new Object[] { alreadyAssignedCount }); + i18n.getMessage("message.target.alreadyAssigned", alreadyAssignedCount);
} }
private void discardAllAssignments(final ConfirmationTab tab) { private void discardAllAssignments(final ConfirmationTab tab) {

View File

@@ -294,6 +294,36 @@ public final class UIComponentIdProvider {
*/ */
public static final String ACTION_BUTTON_LAYOUT = "ActionButtonLayout"; public static final String ACTION_BUTTON_LAYOUT = "ActionButtonLayout";
/**
* Id for maintenance window layout
*/
public static final String MAINTENANCE_WINDOW_LAYOUT_ID = "maintenance.window.layout";
/**
* Id for maintenance window - enabled checkbox
*/
public static final String MAINTENANCE_WINDOW_ENABLED_ID = "maintenance.window.enabled";
/**
* Id for maintenance window - field schedule
*/
public static final String MAINTENANCE_WINDOW_SCHEDULE_ID = "maintenance.window.schedule";
/**
* Id for maintenance window - field duration
*/
public static final String MAINTENANCE_WINDOW_DURATION_ID = "maintenance.window.duration";
/**
* Id for maintenance window - field time zone
*/
public static final String MAINTENANCE_WINDOW_TIME_ZONE_ID = "maintenance.window.time.zone";
/**
* Id for maintenance window - label schedule translator
*/
public static final String MAINTENANCE_WINDOW_SCHEDULE_TRANSLATOR_ID = "maintenance.window.schedule.translator";
/** /**
* Delete button wrapper id. * Delete button wrapper id.
*/ */

View File

@@ -83,39 +83,36 @@
@include sp-button-icon-only-href; @include sp-button-icon-only-href;
} }
.v-slot-dist-window-maintenance-window-enable {
padding-left: 9px;
padding-top: 5px;
padding-right: 5px;
font-weight: 400;
}
.dist-window-maintenance-window-layout { .dist-window-maintenance-window-layout {
vertical-align: middle; vertical-align: middle;
horizontal-align: middle; horizontal-align: middle;
padding-left: 14px;
.v-slot { .v-slot {
vertical-align: middle; vertical-align: middle;
padding-left: 8px; padding-right: 20px;
padding-right: 5px;
} }
.v-caption { .v-caption {
vertical-align: middle; vertical-align: middle;
margin-top: 0; margin-top: 0;
font-size: 12px;
}
.v-checkbox {
font-size: 12px;
font-weight: 400;
} }
.v-filterselect { .v-filterselect {
font-size: 12px;
font-weight: 400; font-weight: 400;
width: 160px; width: 160px;
height: 24px;
} }
.v-textfield { .v-textfield {
font-size: 12px;
font-weight: 400; font-weight: 400;
width: 160px; width: 160px;
height: 24px;
} }
} }
@@ -142,10 +139,6 @@
.v-caption { .v-caption {
vertical-align: middle; vertical-align: middle;
margin-top: 0;
font-size: 12px;
padding-right: 0;
padding-left: 0;
} }
.v-radiobutton { .v-radiobutton {

View File

@@ -139,10 +139,10 @@ caption.metadata.delete.action.confirmbox = Confirm Metadata Delete Action
caption.confirm.assign.consequences = Auto assign consequences caption.confirm.assign.consequences = Auto assign consequences
caption.auto.assignment.ds = Auto assignment caption.auto.assignment.ds = Auto assignment
caption.maintenancewindow.enable = Maintenance Schedule caption.maintenancewindow.enabled = Use maintenance window
caption.maintenancewindow.schedule = Schedule (Cron Expression) caption.maintenancewindow.schedule = Schedule
caption.maintenancewindow.duration = Duration (hh:mm:ss) caption.maintenancewindow.duration = Duration
caption.maintenancewindow.timezone = Time Zone caption.maintenancewindow.timezone = Time Zone (from GMT)
# Labels prefix with - label # Labels prefix with - label
label.dist.details.type = Type : label.dist.details.type = Type :
@@ -367,6 +367,8 @@ message.bulk.upload.tag.assignment.failed = Tag {0} assignment failed as tag no
message.bulk.upload.tag.assignments.failed= Few tag assignments failed as tags no longer exists message.bulk.upload.tag.assignments.failed= Few tag assignments failed as tags no longer exists
message.confirm.assign.consequences.none = This auto assignment will not have any effect on the currently available targets. In future added targets might match the filter and will receive the selected distribution set automatically. message.confirm.assign.consequences.none = This auto assignment will not have any effect on the currently available targets. In future added targets might match the filter and will receive the selected distribution set automatically.
message.confirm.assign.consequences.text = When you confirm this auto assignment, {0} targets which match the filter will immediately get assigned with the selected distribution set. message.confirm.assign.consequences.text = When you confirm this auto assignment, {0} targets which match the filter will immediately get assigned with the selected distribution set.
message.maintenancewindow.schedule.validation.error = Please enter a valid Cron expression
message.maintenancewindow.duration.validation.error = Please enter the duration in the format hh:mm:ss, error is at {0} position
# action info # action info
action.target.table.selectall = Select all (Ctrl+A) action.target.table.selectall = Select all (Ctrl+A)