Improve Simple UI (#2554)

* feat[Simple-UI]: add action status list

* fix[Simple-UI]: various ui issues

* chore: add devtool

* feat[Simple-UI: add DS metadata

* feat[Simple-UI]: add sort in DS view

* feat[Simple-UI]: add created at to DS view

* style[Simple-UI]: remove id from DS view

* feat[Simple-UI]: add rsql filter & url filter

* feat[Simple-UI]: if one ds in result show details

* feat[Simple-UI]: add filter from url to targetview

* feat[Simple-UI]: add link from target details view to DS

* feat[Simple-UI]: add sort & version on target view

* refacto[Simple-UI]: linkted text area

* feat[Simple-UI]: dynamic homepage depending on permissions

* feat[Simple-UI]: sort by newest version

* feat[Simple-UI]: add target address

* feat[Simple-UI]: sort by last modified target

* fix[Simple-UI]: securityToken null if no permission

* fix[Simple-UI]: green circle on bad update

* feat[Simple-UI]: use local date format

* docs: update user config

* fix: tag filter

* feat[Simple-UI]: search on multiple attributes

* refacto: rename TargetActions -> TargetActionsHistory

* refacto: move TargetActionsHistory to a new file
This commit is contained in:
Florian BEZANNIER
2025-07-28 15:07:25 +02:00
committed by GitHub
parent 2b66449ff1
commit d2b8e74056
24 changed files with 1143 additions and 538 deletions

View File

@@ -9,7 +9,7 @@
# SPDX-License-Identifier: EPL-2.0 # SPDX-License-Identifier: EPL-2.0
# #
#set -xe set -xe
# Usage: builds all docker images. Use: # Usage: builds all docker images. Use:
# -v <version> to pass version # -v <version> to pass version
@@ -20,6 +20,8 @@
VERSION=0-SNAPSHOT VERSION=0-SNAPSHOT
MVN_REPO=~/.m2/repository MVN_REPO=~/.m2/repository
TAG=latest TAG=latest
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
cd "${SCRIPT_DIR}"
while getopts v:r:t: option while getopts v:r:t: option
do do

View File

@@ -497,7 +497,7 @@
<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/> <setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries" value="true"/> <setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/> <setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="false"/> <setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="@formatter:on"/> <setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="@formatter:on"/>
<setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="@formatter:off"/> <setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="@formatter:off"/>
</profile> </profile>

View File

@@ -20,6 +20,7 @@ import lombok.Getter;
@Getter @Getter
public enum DistributionSetFields implements RsqlQueryField { public enum DistributionSetFields implements RsqlQueryField {
// @formatter:off
ID("id"), ID("id"),
TYPE("type", "key"), TYPE("type", "key"),
NAME("name"), NAME("name"),
@@ -30,8 +31,10 @@ public enum DistributionSetFields implements RsqlQueryField {
COMPLETE("complete"), COMPLETE("complete"),
MODULE("modules", SoftwareModuleFields.ID.getJpaEntityFieldName(), SoftwareModuleFields.NAME.getJpaEntityFieldName()), MODULE("modules", SoftwareModuleFields.ID.getJpaEntityFieldName(), SoftwareModuleFields.NAME.getJpaEntityFieldName()),
TAG("tags", "name"), TAG("tags", "name"),
TYPENAME("typeName"),
METADATA("metadata"), METADATA("metadata"),
VALID("valid"); VALID("valid");
// @formatter:on
private final String jpaEntityFieldName; private final String jpaEntityFieldName;
private final List<String> subEntityAttributes; private final List<String> subEntityAttributes;

View File

@@ -11,9 +11,11 @@ package org.eclipse.hawkbit.mgmt.json.model.action;
import java.util.List; import java.util.List;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonValue;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
@@ -31,7 +33,7 @@ public class MgmtActionStatus {
private Long id; private Long id;
@Schema(example = "running") @Schema(example = "running")
private String type; private StatusType type;
private List<String> messages; private List<String> messages;
@@ -43,4 +45,79 @@ public class MgmtActionStatus {
@Schema(example = "200") @Schema(example = "200")
private Integer code; private Integer code;
public enum StatusType {
/**
* Action is finished successfully for this target.
*/
FINISHED,
/**
* Action has failed for this target.
*/
ERROR,
/**
* Action is still running but with warnings.
*/
WARNING,
/**
* Action is still running for this target.
*/
RUNNING,
/**
* Action has been canceled for this target.
*/
CANCELED,
/**
* Action is in canceling state and waiting for controller confirmation.
*/
CANCELING,
/**
* Action has been send to the target.
*/
RETRIEVED,
/**
* Action requests download by this target which has now started.
*/
DOWNLOAD,
/**
* Action is in waiting state, e.g. the action is scheduled in a rollout
* but not yet activated.
*/
SCHEDULED,
/**
* Cancellation has been rejected by the controller.
*/
CANCEL_REJECTED,
/**
* Action has been downloaded by the target and waiting for update to
* start.
*/
DOWNLOADED,
/**
* Action is waiting to be confirmed by the user
*/
WAIT_FOR_CONFIRMATION;
@JsonValue
public String getName() {
return this.name().toLowerCase();
}
@JsonCreator
public static StatusType forValue(String s) {
return StatusType.valueOf(s.toUpperCase());
}
}
} }

View File

@@ -207,7 +207,7 @@ public final class MgmtTargetMapper {
return actionStatus.stream() return actionStatus.stream()
.map(status -> toResponse(status, .map(status -> toResponse(status,
deploymentManagement.findMessagesByActionStatusId( deploymentManagement.findMessagesByActionStatusId(
status.getId(), PageRequest.of(0, MgmtRestConstants.REQUEST_PARAMETER_PAGING_MAX_LIMIT)) status.getId(), PageRequest.of(0, MgmtRestConstants.REQUEST_PARAMETER_PAGING_MAX_LIMIT))
.getContent())) .getContent()))
.toList(); .toList();
} }
@@ -346,7 +346,7 @@ public final class MgmtTargetMapper {
result.setReportedAt(actionStatus.getCreatedAt()); result.setReportedAt(actionStatus.getCreatedAt());
result.setTimestamp(actionStatus.getOccurredAt()); result.setTimestamp(actionStatus.getOccurredAt());
result.setId(actionStatus.getId()); result.setId(actionStatus.getId());
result.setType(actionStatus.getStatus().name().toLowerCase()); result.setType(MgmtActionStatus.StatusType.forValue(actionStatus.getStatus().name()));
actionStatus.getCode().ifPresent(result::setCode); actionStatus.getCode().ifPresent(result::setCode);
return result; return result;

View File

@@ -65,12 +65,6 @@
<dependency> <dependency>
<groupId>com.vaadin</groupId> <groupId>com.vaadin</groupId>
<artifactId>vaadin-core</artifactId> <artifactId>vaadin-core</artifactId>
<exclusions>
<exclusion>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-dev</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.vaadin</groupId> <groupId>com.vaadin</groupId>
@@ -85,6 +79,11 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId> <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies> </dependencies>
<build> <build>
@@ -93,6 +92,11 @@
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
<finalName>Simple-UI</finalName>
<mainClass>org.eclipse.hawkbit.ui.simple.SimpleUIApp</mainClass>
</configuration>
</plugin> </plugin>
<plugin> <plugin>

View File

@@ -1 +1,7 @@
/* Import your application global css files here or add the styles directly to this file */ a.nocolor:link {
color: inherit;
}
a.nocolor:visited {
color: inherit;
}

View File

@@ -9,8 +9,11 @@
*/ */
package org.eclipse.hawkbit.ui.simple; package org.eclipse.hawkbit.ui.simple;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.Unit; import com.vaadin.flow.component.Unit;
import com.vaadin.flow.component.applayout.AppLayout; import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.applayout.DrawerToggle; import com.vaadin.flow.component.applayout.DrawerToggle;
@@ -47,9 +50,12 @@ import org.eclipse.hawkbit.ui.simple.view.TargetView;
*/ */
public class MainLayout extends AppLayout { public class MainLayout extends AppLayout {
static final List<Class<? extends Component>> DEFAULT_VIEW_PRIORITY = List.of(TargetView.class, DistributionSetView.class,
SoftwareModuleView.class, RolloutView.class);
private final transient AuthenticatedUser authenticatedUser; private final transient AuthenticatedUser authenticatedUser;
private final AccessAnnotationChecker accessChecker; private final AccessAnnotationChecker accessChecker;
private H2 viewTitle; private H2 viewTitle;
private transient Optional<Class<? extends Component>> defaultView;
public MainLayout(final AuthenticatedUser authenticatedUser, final AccessAnnotationChecker accessChecker) { public MainLayout(final AuthenticatedUser authenticatedUser, final AccessAnnotationChecker accessChecker) {
this.authenticatedUser = authenticatedUser; this.authenticatedUser = authenticatedUser;
@@ -68,6 +74,9 @@ public class MainLayout extends AppLayout {
Optional.ofNullable(getContent().getClass().getAnnotation(PageTitle.class)) Optional.ofNullable(getContent().getClass().getAnnotation(PageTitle.class))
.map(PageTitle::value) .map(PageTitle::value)
.orElse("")); .orElse(""));
if (UI.getCurrent().getActiveViewLocation().getPath().isEmpty()) {
defaultView.ifPresent(c -> UI.getCurrent().navigate(c));
}
} }
private void addHeaderContent() { private void addHeaderContent() {
@@ -81,7 +90,7 @@ public class MainLayout extends AppLayout {
} }
private void addDrawerContent() { private void addDrawerContent() {
final H1 appName = new H1("hawkBit UI (Experimental!)"); final H1 appName = new H1("hawkBit UI");
final HorizontalLayout layout = new HorizontalLayout(); final HorizontalLayout layout = new HorizontalLayout();
layout.setPadding(true); layout.setPadding(true);
layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
@@ -117,6 +126,7 @@ public class MainLayout extends AppLayout {
if (accessChecker.hasAccess(AboutView.class)) { if (accessChecker.hasAccess(AboutView.class)) {
nav.addItem(new SideNavItem("About", AboutView.class, VaadinIcon.INFO_CIRCLE.create())); nav.addItem(new SideNavItem("About", AboutView.class, VaadinIcon.INFO_CIRCLE.create()));
} }
defaultView = DEFAULT_VIEW_PRIORITY.stream().filter(accessChecker::hasAccess).findFirst();
return nav; return nav;
} }

View File

@@ -0,0 +1,24 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple;
import java.util.Arrays;
import java.util.Locale;
import com.vaadin.flow.i18n.DefaultI18NProvider;
import org.springframework.stereotype.Component;
@Component
public class SimpleI18NProvider extends DefaultI18NProvider {
SimpleI18NProvider() {
super(Arrays.stream(Locale.getAvailableLocales()).toList());
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple;
import com.vaadin.flow.server.ServiceInitEvent;
import com.vaadin.flow.server.VaadinServiceInitListener;
import com.vaadin.flow.spring.annotation.SpringComponent;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@SpringComponent
public class VaadinServiceInit implements VaadinServiceInitListener {
@Override
public void serviceInit(ServiceInitEvent event) {
// cache zoneId of client as soon as possible
event.getSource().addUIInitListener(uiEvent -> {
uiEvent.getUI().getPage().retrieveExtendedClientDetails(details -> {});
});
}
}

View File

@@ -0,0 +1,250 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple.component;
import static org.eclipse.hawkbit.ui.simple.view.Constants.STATUS;
import java.util.List;
import java.util.Optional;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Unit;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.confirmdialog.ConfirmDialog;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.theme.lumo.LumoUtility;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction;
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
import org.eclipse.hawkbit.ui.simple.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.simple.view.TargetView;
import org.eclipse.hawkbit.ui.simple.view.util.Utils;
@Slf4j
public class TargetActionsHistory extends Grid<TargetActionsHistory.ActionStatusEntry> {
private final transient HawkbitMgmtClient hawkbitClient;
private transient MgmtTarget target;
private final TargetView.TargetActionsHistoryLayout.ActionStepsGrid actionStepsGrid;
public TargetActionsHistory(final HawkbitMgmtClient hawkbitClient, TargetView.TargetActionsHistoryLayout.ActionStepsGrid actionStepsGrid) {
this.hawkbitClient = hawkbitClient;
setWidthFull();
addColumn(new ComponentRenderer<>(ActionStatusEntry::getStatusIcon)).setHeader(STATUS).setAutoWidth(true).setFlexGrow(0);
addColumn(ActionStatusEntry::getDistributionSetName).setHeader("Distribution Set").setAutoWidth(true);
addColumn(Utils.localDateTimeRenderer(ActionStatusEntry::getLastModifiedAt))
.setHeader("Last Modified")
.setAutoWidth(true)
.setFlexGrow(0)
.setComparator(ActionStatusEntry::getLastModifiedAt);
addColumn(new ComponentRenderer<>(ActionStatusEntry::getForceTypeIcon)).setHeader("Type").setAutoWidth(true).setFlexGrow(0);
addColumn(new ComponentRenderer<>(ActionStatusEntry::getActionsLayout)).setHeader("Actions").setAutoWidth(true).setFlexGrow(0);
addColumn(new ComponentRenderer<>(ActionStatusEntry::getForceQuitLayout)).setHeader("Force Quit").setAutoWidth(true)
.setFlexGrow(0);
addItemClickListener(e -> actionStepsGrid.setActionId(e.getItem().action.getId()));
this.actionStepsGrid = actionStepsGrid;
}
public void setItem(final MgmtTarget target) {
this.target = target;
this.actionStepsGrid.setTarget(target);
}
private List<ActionStatusEntry> fetchActions() {
return hawkbitClient.getTargetRestApi().getActionHistory(target.getControllerId(), null, 0, 30, null)
.getBody()
.getContent()
.stream()
.map(action -> new ActionStatusEntry(action, () -> setItems(fetchActions())))
.filter(value -> value.action != null)
.toList();
}
@Override
protected void onAttach(AttachEvent attachEvent) {
List<ActionStatusEntry> actionStatusEntries = fetchActions();
setItems(actionStatusEntries);
actionStatusEntries.stream().findFirst().ifPresentOrElse(e -> {
// select first action in the list by default
asSingleSelect().setValue(e);
actionStepsGrid.setActionId(e.action.getId());
}, () -> actionStepsGrid.setActionId(null));
}
protected class ActionStatusEntry {
final MgmtAction action;
final Runnable onUpdate;
MgmtDistributionSet distributionSet;
public ActionStatusEntry(final MgmtAction mgmtAction, final Runnable onUpdate) {
this.action = hawkbitClient.getActionRestApi().getAction(mgmtAction.getId()).getBody();
this.onUpdate = onUpdate;
if (action == null) {
log.error("Unable to fetch the action with id : {}", mgmtAction.getId());
return;
}
this.action.getLink("distributionset").ifPresent(link -> {
try {
Long dsId = Long.parseLong(link.getHref().substring(link.getHref().lastIndexOf("/") + 1));
this.distributionSet = hawkbitClient.getDistributionSetRestApi().getDistributionSet(dsId).getBody();
} catch (NumberFormatException e) {
log.error("Error parsing distribution set ID", e);
}
});
}
private boolean isActive() {
return action.getStatus().equals(MgmtAction.ACTION_PENDING);
}
private boolean isCancelingOrCanceled() {
return action.getType().equals(MgmtAction.ACTION_CANCEL);
}
public Component getStatusIcon() {
final HorizontalLayout layout = new HorizontalLayout();
final Icon icon;
if (isActive()) {
if (isCancelingOrCanceled()) {
icon = Utils.tooltip(VaadinIcon.ADJUST.create(), "Pending Cancellation");
icon.setColor("red");
} else {
icon = Utils.tooltip(VaadinIcon.ADJUST.create(), "Pending Update");
icon.setColor("orange");
}// todo getDetailStatus should return an enum from src/main/java/org/eclipse/hawkbit/repository/model/Action.java
} else if (action.getType().equals(MgmtAction.ACTION_UPDATE) && action.getDetailStatus().equals("finished")) {
icon = Utils.tooltip(VaadinIcon.CHECK_CIRCLE.create(), "Updated");
icon.setColor("green");
} else {
icon = Utils.tooltip(VaadinIcon.CLOSE_CIRCLE.create(), "Canceled");
icon.setColor("red");
}
icon.addClassNames(LumoUtility.IconSize.SMALL);
layout.add(icon);
layout.setWidth(50, Unit.PIXELS);
layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
return layout;
}
public String getDistributionSetName() {
return Optional.ofNullable(distributionSet).map(d -> d.getName() + ":" + d.getVersion()).orElse(
"Distribution Set not found");
}
public Long getLastModifiedAt() {
return action.getLastModifiedAt();
}
public Icon getForceTypeIcon() {
Icon icon = switch (action.getForceType()) {
case FORCED -> VaadinIcon.BOLT.create();
case TIMEFORCED -> VaadinIcon.USER_CLOCK.create();
case SOFT -> VaadinIcon.USER_CHECK.create();
case DOWNLOAD_ONLY -> VaadinIcon.DOWNLOAD.create();
};
return Utils.tooltip(icon, action.getForceType().getName());
}
public HorizontalLayout getActionsLayout() {
final HorizontalLayout actionsLayout = new HorizontalLayout();
actionsLayout.setSpacing(true);
final Button cancelButton = Utils.tooltip(new Button(VaadinIcon.CLOSE.create()), "Cancel Action");
if (isActive() && !isCancelingOrCanceled()) {
cancelButton.addClickListener(e -> {
String message = "Are you sure you want to cancel the action ?";
promptForConfirmAction(
message, onUpdate,
() -> hawkbitClient.getTargetRestApi().cancelAction(target.getControllerId(), action.getId(), false))
.open();
});
} else {
cancelButton.setEnabled(false);
}
final Button forceButton = Utils.tooltip(new Button(VaadinIcon.BOLT.create()), "Force Action");
if (isActive() && !isCancelingOrCanceled() && action.getForceType() != MgmtActionType.FORCED) {
forceButton.addClickListener(e -> {
String message = "Are you sure you want to force the action ?";
promptForConfirmAction(
message, onUpdate, () -> {
MgmtActionRequestBodyPut setForced = new MgmtActionRequestBodyPut();
setForced.setForceType(MgmtActionType.FORCED);
hawkbitClient.getTargetRestApi()
.updateAction(target.getControllerId(), action.getId(), setForced);
}
).open();
});
} else {
forceButton.setEnabled(false);
}
actionsLayout.add(cancelButton, forceButton);
return actionsLayout;
}
public HorizontalLayout getForceQuitLayout() {
final HorizontalLayout forceQuitLayout = new HorizontalLayout();
forceQuitLayout.setSpacing(true);
forceQuitLayout.setPadding(true);
forceQuitLayout.setWidthFull();
forceQuitLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
final Button forceQuitButton = Utils.tooltip(new Button(VaadinIcon.CLOSE.create()), "Force Cancel");
forceQuitButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY_INLINE);
if (isActive() && isCancelingOrCanceled()) {
forceQuitButton.addClickListener(e -> {
String message = "Are you sure you want to force cancel the action ?";
promptForConfirmAction(
message, onUpdate,
() -> hawkbitClient.getTargetRestApi().cancelAction(target.getControllerId(), action.getId(), true)).open();
});
} else {
forceQuitButton.setEnabled(false);
}
forceQuitLayout.add(forceQuitButton);
return forceQuitLayout;
}
private static ConfirmDialog promptForConfirmAction(String message, Runnable refreshActions, Runnable actionConsumer) {
final ConfirmDialog dialog = new ConfirmDialog();
dialog.setHeader("Confirm Action");
dialog.setText(message);
dialog.setCancelable(true);
dialog.addCancelListener(event -> dialog.close());
dialog.setConfirmButtonTheme(ButtonVariant.LUMO_ERROR.getVariantName());
dialog.setConfirmText("Confirm");
dialog.addConfirmListener(event -> {
actionConsumer.run();
refreshActions.run();
dialog.close();
});
return dialog;
}
}
}

View File

@@ -10,12 +10,13 @@
package org.eclipse.hawkbit.ui.simple.view; package org.eclipse.hawkbit.ui.simple.view;
// java:S1214 - implementations of Constants interface extends other classes, so if make this class we shall go for static imports // java:S1214 - implementations of Constants interface extends other classes, so if make this class we shall go for static imports
// which is not not better // which is not not better
@SuppressWarnings("java:S1214") @SuppressWarnings("java:S1214")
public interface Constants { public interface Constants {
// properties // properties
String ID = "Id"; String ID = "Id";
String ADDRESS = "Address";
String NAME = "Name"; String NAME = "Name";
String DESCRIPTION = "Description"; String DESCRIPTION = "Description";
String VERSION = "Version"; String VERSION = "Version";
@@ -54,7 +55,10 @@ public interface Constants {
String CANCEL = "Cancel"; String CANCEL = "Cancel";
String CANCEL_ESC = "Cancel (Esc)"; String CANCEL_ESC = "Cancel (Esc)";
String CREATED_AT_DESC = "createdAt:desc";
String NAME_ASC = "name:asc"; String NAME_ASC = "name:asc";
String NAME_DESC = "name:desc";
String NOT_AVAILABLE_NULL = "n/a (null)"; String NOT_AVAILABLE_NULL = "n/a (null)";
} }

View File

@@ -11,15 +11,17 @@ package org.eclipse.hawkbit.ui.simple.view;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import com.vaadin.flow.component.grid.GridSortOrder;
import com.vaadin.flow.data.provider.SortDirection;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
import com.vaadin.flow.component.Component; import com.vaadin.flow.component.Component;
@@ -64,25 +66,32 @@ public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
public DistributionSetView(final HawkbitMgmtClient hawkbitClient) { public DistributionSetView(final HawkbitMgmtClient hawkbitClient) {
super( super(
new DistributionSetFilter(hawkbitClient), new DistributionSetFilter(hawkbitClient),
new DistributionSetRawFilter(),
new SelectionGrid.EntityRepresentation<>(MgmtDistributionSet.class, MgmtDistributionSet::getId) { new SelectionGrid.EntityRepresentation<>(MgmtDistributionSet.class, MgmtDistributionSet::getId) {
private final DistributionSetDetails details = new DistributionSetDetails(hawkbitClient); private final DistributionSetDetails details = new DistributionSetDetails(hawkbitClient);
@Override @Override
protected void addColumns(Grid<MgmtDistributionSet> grid) { protected void addColumns(Grid<MgmtDistributionSet> grid) {
grid.addColumn(MgmtDistributionSet::getId).setHeader(Constants.ID).setAutoWidth(true); var createdAtCol = grid.addColumn(Utils.localDateTimeRenderer(MgmtDistributionSet::getCreatedAt)).setHeader(
grid.addColumn(MgmtDistributionSet::getName).setHeader(Constants.NAME).setAutoWidth(true); Constants.CREATED_AT).setAutoWidth(true).setKey("createdAt").setSortable(true);
grid.addColumn(MgmtDistributionSet::getVersion).setHeader(Constants.VERSION).setAutoWidth(true); grid.addColumn(MgmtDistributionSet::getName).setHeader(Constants.NAME).setAutoWidth(true).setKey("name").setSortable(
grid.addColumn(MgmtDistributionSet::getTypeName).setHeader(Constants.TYPE).setAutoWidth(true); true);
grid.addColumn(MgmtDistributionSet::getVersion).setHeader(Constants.VERSION).setAutoWidth(true).setKey("version")
.setSortable(true);
grid.addColumn(MgmtDistributionSet::getTypeName).setHeader(Constants.TYPE).setAutoWidth(true).setKey("typename")
.setSortable(true);
grid.sort(List.of(new GridSortOrder<>(createdAtCol, SortDirection.DESCENDING)));
grid.setItemDetailsRenderer(new ComponentRenderer<>( grid.setItemDetailsRenderer(new ComponentRenderer<>(
() -> details, DistributionSetDetails::setItem)); () -> details, DistributionSetDetails::setItem));
} }
}, },
(query, rsqlFilter) -> Optional.ofNullable( (query, rsqlFilter) -> Optional.ofNullable(
hawkbitClient.getDistributionSetRestApi() hawkbitClient.getDistributionSetRestApi()
.getDistributionSets(rsqlFilter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC) .getDistributionSets(rsqlFilter, query.getOffset(), query.getPageSize(), Utils.getSortParam(query
.getBody()) .getSortOrders()))
.getBody())
.stream().flatMap(body -> body.getContent().stream()), .stream().flatMap(body -> body.getContent().stream()),
e -> new CreateDialog(hawkbitClient).result(), e -> new CreateDialog(hawkbitClient).result(),
selectionGrid -> { selectionGrid -> {
@@ -90,7 +99,7 @@ public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
distributionSet -> hawkbitClient.getDistributionSetRestApi() distributionSet -> hawkbitClient.getDistributionSetRestApi()
.deleteDistributionSet(distributionSet.getId())); .deleteDistributionSet(distributionSet.getId()));
return CompletableFuture.completedFuture(null); return CompletableFuture.completedFuture(null);
}); }, null);
} }
private static SelectionGrid<MgmtSoftwareModule, Long> selectSoftwareModuleGrid() { private static SelectionGrid<MgmtSoftwareModule, Long> selectSoftwareModuleGrid() {
@@ -109,42 +118,66 @@ public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
}); });
} }
private static class DistributionSetFilter implements Filter.Rsql { private static class DistributionSetRawFilter implements Filter.Rsql, Filter.RsqlRw {
private final TextField name = Utils.textField("Name"); private final TextField name = Utils.textField("Name");
private DistributionSetRawFilter() {
name.setPlaceholder("<rsql filter>");
}
@Override
public List<Component> components() {
return List.of(name);
}
@Override
public String filter() {
return name.getOptionalValue().orElse(null);
}
@Override
public void setFilter(String filter) {
name.setValue(filter);
}
}
private static class DistributionSetFilter implements Filter.Rsql {
private final TextField textFilter = Utils.textField("Filter");
private final CheckboxGroup<MgmtDistributionSetType> type = new CheckboxGroup<>("Type"); private final CheckboxGroup<MgmtDistributionSetType> type = new CheckboxGroup<>("Type");
private final CheckboxGroup<MgmtTag> tag = new CheckboxGroup<>("Tag"); private final CheckboxGroup<MgmtTag> tag = new CheckboxGroup<>("Tag");
private DistributionSetFilter(final HawkbitMgmtClient hawkbitClient) { private DistributionSetFilter(final HawkbitMgmtClient hawkbitClient) {
name.setPlaceholder("<name filter>"); textFilter.setPlaceholder("<name/version filter>");
type.setItemLabelGenerator(MgmtDistributionSetType::getName); type.setItemLabelGenerator(MgmtDistributionSetType::getName);
type.setItems(Optional.ofNullable( type.setItems(Optional.ofNullable(
hawkbitClient.getDistributionSetTypeRestApi() hawkbitClient.getDistributionSetTypeRestApi()
.getDistributionSetTypes(null, 0, 20, Constants.NAME_ASC) .getDistributionSetTypes(null, 0, 20, Constants.NAME_ASC)
.getBody()) .getBody())
.map(PagedList::getContent) .map(PagedList::getContent)
.orElseGet(Collections::emptyList)); .orElseGet(Collections::emptyList));
tag.setItemLabelGenerator(MgmtTag::getName); tag.setItemLabelGenerator(MgmtTag::getName);
tag.setItems(Optional.ofNullable( tag.setItems(Optional.ofNullable(
hawkbitClient.getDistributionSetTagRestApi() hawkbitClient.getDistributionSetTagRestApi()
.getDistributionSetTags(null, 0, 20, Constants.NAME_ASC) .getDistributionSetTags(null, 0, 20, Constants.NAME_ASC)
.getBody()) .getBody())
.map(PagedList::getContent) .map(PagedList::getContent)
.orElseGet(Collections::emptyList)); .orElseGet(Collections::emptyList));
} }
@Override @Override
public List<Component> components() { public List<Component> components() {
return List.of(name, type); return List.of(textFilter, type);
} }
@Override @Override
public String filter() { public String filter() {
return Filter.filter( return Filter.filter(
Map.of( Map.of(
"name", name.getOptionalValue(), List.of("version", "name"), textFilter.getOptionalValue().map(s -> "*" + s + "*"),
"type", type.getSelectedItems().stream().map(MgmtDistributionSetType::getKey).toList(), "type", type.getSelectedItems().stream().map(MgmtDistributionSetType::getKey).toList(),
"tag", tag.getSelectedItems())); "tag", tag.getSelectedItems().stream().map(MgmtTag::getName).toList()));
} }
} }
@@ -157,6 +190,7 @@ public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
private final TextField createdAt = Utils.textField("Created at"); private final TextField createdAt = Utils.textField("Created at");
private final TextField lastModifiedBy = Utils.textField("Last modified by"); private final TextField lastModifiedBy = Utils.textField("Last modified by");
private final TextField lastModifiedAt = Utils.textField("Last modified at"); private final TextField lastModifiedAt = Utils.textField("Last modified at");
private final TextArea metadata = new TextArea("Metadata");
private final SelectionGrid<MgmtSoftwareModule, Long> softwareModulesGrid = selectSoftwareModuleGrid(); private final SelectionGrid<MgmtSoftwareModule, Long> softwareModulesGrid = selectSoftwareModuleGrid();
private DistributionSetDetails(final HawkbitMgmtClient hawkbitClient) { private DistributionSetDetails(final HawkbitMgmtClient hawkbitClient) {
@@ -164,9 +198,9 @@ public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
description.setMinLength(2); description.setMinLength(2);
Stream.of( Stream.of(
description, description,
createdBy, createdAt, createdBy, createdAt,
lastModifiedBy, lastModifiedAt) lastModifiedBy, lastModifiedAt, metadata)
.forEach(field -> { .forEach(field -> {
field.setReadOnly(true); field.setReadOnly(true);
add(field); add(field);
@@ -181,9 +215,14 @@ public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
private void setItem(final MgmtDistributionSet distributionSet) { private void setItem(final MgmtDistributionSet distributionSet) {
description.setValue(distributionSet.getDescription()); description.setValue(distributionSet.getDescription());
createdBy.setValue(distributionSet.getCreatedBy()); createdBy.setValue(distributionSet.getCreatedBy());
createdAt.setValue(new Date(distributionSet.getCreatedAt()).toString()); createdAt.setValue(Utils.localDateTimeFromTs(distributionSet.getCreatedAt()));
lastModifiedBy.setValue(distributionSet.getLastModifiedBy()); lastModifiedBy.setValue(distributionSet.getLastModifiedBy());
lastModifiedAt.setValue(new Date(distributionSet.getLastModifiedAt()).toString()); lastModifiedAt.setValue(Utils.localDateTimeFromTs(distributionSet.getLastModifiedAt()));
metadata.setValue(Optional.ofNullable(
hawkbitClient.getDistributionSetRestApi().getMetadata(distributionSet.getId()).getBody())
.map(body -> body.getContent().stream()
.map(b -> String.format("%s: %s\n", b.getKey(), b.getValue())).collect(
Collectors.joining())).orElse(""));
softwareModulesGrid.setItems(query -> Optional.ofNullable( softwareModulesGrid.setItems(query -> Optional.ofNullable(
hawkbitClient.getDistributionSetRestApi() hawkbitClient.getDistributionSetRestApi()
@@ -214,9 +253,9 @@ public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
"Type", "Type",
this::readyToCreate, this::readyToCreate,
Optional.ofNullable( Optional.ofNullable(
hawkbitClient.getDistributionSetTypeRestApi() hawkbitClient.getDistributionSetTypeRestApi()
.getDistributionSetTypes(null, 0, 30, Constants.NAME_ASC) .getDistributionSetTypes(null, 0, 30, Constants.CREATED_AT_DESC)
.getBody()) .getBody())
.map(body -> body.getContent().toArray(new MgmtDistributionSetType[0])) .map(body -> body.getContent().toArray(new MgmtDistributionSetType[0]))
.orElseGet(() -> new MgmtDistributionSetType[0])); .orElseGet(() -> new MgmtDistributionSetType[0]));
type.focus(); type.focus();
@@ -262,15 +301,15 @@ public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
create.addClickListener(e -> { create.addClickListener(e -> {
close(); close();
final long distributionSetId = Optional.ofNullable( final long distributionSetId = Optional.ofNullable(
hawkbitClient.getDistributionSetRestApi() hawkbitClient.getDistributionSetRestApi()
.createDistributionSets( .createDistributionSets(
List.of((MgmtDistributionSetRequestBodyPost) new MgmtDistributionSetRequestBodyPost() List.of((MgmtDistributionSetRequestBodyPost) new MgmtDistributionSetRequestBodyPost()
.setType(type.getValue().getKey()) .setType(type.getValue().getKey())
.setName(name.getValue()) .setName(name.getValue())
.setVersion(version.getValue()) .setVersion(version.getValue())
.setDescription(description.getValue()) .setDescription(description.getValue())
.setRequiredMigrationStep(requiredMigrationStep.getValue()))) .setRequiredMigrationStep(requiredMigrationStep.getValue())))
.getBody()) .getBody())
.stream() .stream()
.flatMap(Collection::stream) .flatMap(Collection::stream)
.findFirst() .findFirst()
@@ -281,7 +320,7 @@ public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
} }
} }
@SuppressWarnings({"java:S1171", "java:S3599"}) @SuppressWarnings({ "java:S1171", "java:S3599" })
private static class AddSoftwareModulesDialog extends Utils.BaseDialog<Void> { private static class AddSoftwareModulesDialog extends Utils.BaseDialog<Void> {
private final transient Set<MgmtSoftwareModule> softwareModules = Collections.synchronizedSet(new HashSet<>()); private final transient Set<MgmtSoftwareModule> softwareModules = Collections.synchronizedSet(new HashSet<>());
@@ -296,19 +335,22 @@ public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
}); });
final Component addRemoveControls = Utils.addRemoveControls( final Component addRemoveControls = Utils.addRemoveControls(
v -> new Utils.BaseDialog<Void>("Add Software Modules") {{ v -> new Utils.BaseDialog<Void>("Add Software Modules") {
final SoftwareModuleView softwareModulesView = new SoftwareModuleView(false, hawkbitClient);
add(softwareModulesView); {
final Button addBtn = new Button("Add"); final SoftwareModuleView softwareModulesView = new SoftwareModuleView(false, hawkbitClient);
addBtn.addClickListener(e -> { add(softwareModulesView);
softwareModules.addAll(softwareModulesView.getSelection()); final Button addBtn = new Button("Add");
softwareModulesGrid.refreshGrid(false); addBtn.addClickListener(e -> {
close(); softwareModules.addAll(softwareModulesView.getSelection());
}); softwareModulesGrid.refreshGrid(false);
addBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); close();
getFooter().add(addBtn); });
open(); addBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
}}.result(), getFooter().add(addBtn);
open();
}
}.result(),
v -> { v -> {
Utils.remove(softwareModulesGrid.getSelectedItems(), softwareModules, MgmtSoftwareModule::getId); Utils.remove(softwareModulesGrid.getSelectedItems(), softwareModules, MgmtSoftwareModule::getId);
softwareModulesGrid.refreshGrid(false); softwareModulesGrid.refreshGrid(false);

View File

@@ -10,7 +10,6 @@
package org.eclipse.hawkbit.ui.simple.view; package org.eclipse.hawkbit.ui.simple.view;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@@ -61,7 +60,7 @@ import org.springframework.util.ObjectUtils;
@Route(value = "rollouts", layout = MainLayout.class) @Route(value = "rollouts", layout = MainLayout.class)
@RolesAllowed({ "ROLLOUT_READ" }) @RolesAllowed({ "ROLLOUT_READ" })
@Uses(Icon.class) @Uses(Icon.class)
@SuppressWarnings({"java:S1171", "java:S3599"}) @SuppressWarnings({ "java:S1171", "java:S3599" })
public class RolloutView extends TableView<MgmtRolloutResponseBody, Long> { public class RolloutView extends TableView<MgmtRolloutResponseBody, Long> {
public RolloutView(final HawkbitMgmtClient hawkbitClient) { public RolloutView(final HawkbitMgmtClient hawkbitClient) {
@@ -118,33 +117,45 @@ public class RolloutView extends TableView<MgmtRolloutResponseBody, Long> {
private void init(final MgmtRolloutResponseBody rollout) { private void init(final MgmtRolloutResponseBody rollout) {
if ("READY".equalsIgnoreCase(rollout.getStatus())) { if ("READY".equalsIgnoreCase(rollout.getStatus())) {
add(Utils.tooltip(new Button(VaadinIcon.START_COG.create()) {{ add(Utils.tooltip(new Button(VaadinIcon.START_COG.create()) {
addClickListener(v -> {
hawkbitClient.getRolloutRestApi().start(rollout.getId()); {
refresh(); addClickListener(v -> {
}); hawkbitClient.getRolloutRestApi().start(rollout.getId());
}}, "Start")); refresh();
});
}
}, "Start"));
} else if ("RUNNING".equalsIgnoreCase(rollout.getStatus())) { } else if ("RUNNING".equalsIgnoreCase(rollout.getStatus())) {
add(Utils.tooltip(new Button(VaadinIcon.PAUSE.create()) {{ add(Utils.tooltip(new Button(VaadinIcon.PAUSE.create()) {
addClickListener(v -> {
hawkbitClient.getRolloutRestApi().pause(rollout.getId()); {
refresh(); addClickListener(v -> {
}); hawkbitClient.getRolloutRestApi().pause(rollout.getId());
}}, "Pause")); refresh();
});
}
}, "Pause"));
} else if ("PAUSED".equalsIgnoreCase(rollout.getStatus())) { } else if ("PAUSED".equalsIgnoreCase(rollout.getStatus())) {
add(Utils.tooltip(new Button(VaadinIcon.START_COG.create()) {{ add(Utils.tooltip(new Button(VaadinIcon.START_COG.create()) {
addClickListener(v -> {
hawkbitClient.getRolloutRestApi().resume(rollout.getId()); {
refresh(); addClickListener(v -> {
}); hawkbitClient.getRolloutRestApi().resume(rollout.getId());
}}, "Resume")); refresh();
});
}
}, "Resume"));
} }
add(Utils.tooltip(new Button(VaadinIcon.TRASH.create()) {{ add(Utils.tooltip(new Button(VaadinIcon.TRASH.create()) {
addClickListener(v -> {
hawkbitClient.getRolloutRestApi().delete(rollout.getId()); {
grid.getDataProvider().refreshAll(); addClickListener(v -> {
}); hawkbitClient.getRolloutRestApi().delete(rollout.getId());
}}, "Cancel and Remove")); grid.getDataProvider().refreshAll();
});
}
}, "Cancel and Remove"));
} }
private void refresh() { private void refresh() {
@@ -197,11 +208,11 @@ public class RolloutView extends TableView<MgmtRolloutResponseBody, Long> {
description.setMinLength(2); description.setMinLength(2);
groupGrid = createGroupGrid(); groupGrid = createGroupGrid();
Stream.of( Stream.of(
description, description,
createdBy, createdAt, createdBy, createdAt,
lastModifiedBy, lastModifiedAt, lastModifiedBy, lastModifiedAt,
targetFilter, distributionSet, targetFilter, distributionSet,
actonType, startAt) actonType, startAt)
.forEach(field -> { .forEach(field -> {
field.setReadOnly(true); field.setReadOnly(true);
add(field); add(field);
@@ -219,9 +230,9 @@ public class RolloutView extends TableView<MgmtRolloutResponseBody, Long> {
private void setItem(final MgmtRolloutResponseBody rollout) { private void setItem(final MgmtRolloutResponseBody rollout) {
description.setValue(rollout.getDescription()); description.setValue(rollout.getDescription());
createdBy.setValue(rollout.getCreatedBy()); createdBy.setValue(rollout.getCreatedBy());
createdAt.setValue(new Date(rollout.getCreatedAt()).toString()); createdAt.setValue(Utils.localDateTimeFromTs(rollout.getCreatedAt()));
lastModifiedBy.setValue(rollout.getLastModifiedBy()); lastModifiedBy.setValue(rollout.getLastModifiedBy());
lastModifiedAt.setValue(new Date(rollout.getLastModifiedAt()).toString()); lastModifiedAt.setValue(Utils.localDateTimeFromTs(rollout.getLastModifiedAt()));
targetFilter.setValue(rollout.getTargetFilterQuery()); targetFilter.setValue(rollout.getTargetFilterQuery());
final MgmtDistributionSet distributionSetMgmt = hawkbitClient.getDistributionSetRestApi() final MgmtDistributionSet distributionSetMgmt = hawkbitClient.getDistributionSetRestApi()
.getDistributionSet(rollout.getDistributionSetId()).getBody(); .getDistributionSet(rollout.getDistributionSetId()).getBody();
@@ -232,18 +243,18 @@ public class RolloutView extends TableView<MgmtRolloutResponseBody, Long> {
case SOFT -> Constants.SOFT; case SOFT -> Constants.SOFT;
case FORCED -> Constants.FORCED; case FORCED -> Constants.FORCED;
case DOWNLOAD_ONLY -> Constants.DOWNLOAD_ONLY; case DOWNLOAD_ONLY -> Constants.DOWNLOAD_ONLY;
case TIMEFORCED -> "Scheduled at " + new Date(rollout.getForcetime()); case TIMEFORCED -> "Scheduled at " + Utils.localDateTimeFromTs(rollout.getForcetime());
}); });
startAt.setValue(ObjectUtils.isEmpty(rollout.getStartAt()) ? "" : new Date(rollout.getStartAt()).toString()); startAt.setValue(ObjectUtils.isEmpty(rollout.getStartAt()) ? "" : Utils.localDateTimeFromTs(rollout.getStartAt()));
dynamic.setValue(rollout.isDynamic()); dynamic.setValue(rollout.isDynamic());
groupGrid.setItems(query -> Optional.ofNullable( groupGrid.setItems(query -> Optional.ofNullable(
hawkbitClient.getRolloutRestApi() hawkbitClient.getRolloutRestApi()
.getRolloutGroups( .getRolloutGroups(
rollout.getId(), rollout.getId(),
null, query.getOffset(), query.getPageSize(), null, query.getOffset(), query.getPageSize(),
null, "full") null, "full")
.getBody()) .getBody())
.stream().flatMap(body -> body.getContent().stream()) .stream().flatMap(body -> body.getContent().stream())
.skip(query.getOffset()) .skip(query.getOffset())
.limit(query.getPageSize())); .limit(query.getPageSize()));
@@ -259,7 +270,8 @@ public class RolloutView extends TableView<MgmtRolloutResponseBody, Long> {
grid.addColumn(MgmtRolloutGroupResponseBody::getId).setHeader(Constants.ID).setAutoWidth(true); grid.addColumn(MgmtRolloutGroupResponseBody::getId).setHeader(Constants.ID).setAutoWidth(true);
grid.addColumn(MgmtRolloutGroupResponseBody::getName).setHeader(Constants.NAME).setAutoWidth(true); grid.addColumn(MgmtRolloutGroupResponseBody::getName).setHeader(Constants.NAME).setAutoWidth(true);
grid.addColumn(MgmtRolloutGroupResponseBody::getTotalTargets).setHeader(Constants.TARGET_COUNT).setAutoWidth(true); grid.addColumn(MgmtRolloutGroupResponseBody::getTotalTargets).setHeader(Constants.TARGET_COUNT).setAutoWidth(true);
grid.addColumn(MgmtRolloutGroupResponseBody::getTotalTargetsPerStatus).setHeader(Constants.STATS).setAutoWidth(true); grid.addColumn(MgmtRolloutGroupResponseBody::getTotalTargetsPerStatus).setHeader(Constants.STATS).setAutoWidth(
true);
grid.addColumn(MgmtRolloutGroupResponseBody::getStatus).setHeader(Constants.STATUS).setAutoWidth(true); grid.addColumn(MgmtRolloutGroupResponseBody::getStatus).setHeader(Constants.STATUS).setAutoWidth(true);
} }
}); });
@@ -291,22 +303,21 @@ public class RolloutView extends TableView<MgmtRolloutResponseBody, Long> {
"Distribution Set", "Distribution Set",
this::readyToCreate, this::readyToCreate,
Optional.ofNullable( Optional.ofNullable(
hawkbitClient.getDistributionSetRestApi() hawkbitClient.getDistributionSetRestApi()
.getDistributionSets(null, 0, 30, Constants.NAME_ASC) .getDistributionSets(null, 0, 30, Constants.CREATED_AT_DESC)
.getBody()) .getBody())
.map(body -> body.getContent().toArray(new MgmtDistributionSet[0])) .map(body -> body.getContent().toArray(new MgmtDistributionSet[0]))
.orElseGet(() -> new MgmtDistributionSet[0])); .orElseGet(() -> new MgmtDistributionSet[0]));
distributionSet.setRequiredIndicatorVisible(true); distributionSet.setRequiredIndicatorVisible(true);
distributionSet.setItemLabelGenerator(distributionSetO -> distributionSet.setItemLabelGenerator(distributionSetO -> distributionSetO.getName() + ":" + distributionSetO.getVersion());
distributionSetO.getName() + ":" + distributionSetO.getVersion());
distributionSet.setWidthFull(); distributionSet.setWidthFull();
targetFilter = new Select<>( targetFilter = new Select<>(
"Target Filter", "Target Filter",
this::readyToCreate, this::readyToCreate,
Optional.ofNullable( Optional.ofNullable(
hawkbitClient.getTargetFilterQueryRestApi() hawkbitClient.getTargetFilterQueryRestApi()
.getFilters(null, 0, 30, Constants.NAME_ASC, null) .getFilters(null, 0, 30, Constants.NAME_ASC, null)
.getBody()) .getBody())
.map(body -> body.getContent().toArray(new MgmtTargetFilterQuery[0])) .map(body -> body.getContent().toArray(new MgmtTargetFilterQuery[0]))
.orElseGet(() -> new MgmtTargetFilterQuery[0])); .orElseGet(() -> new MgmtTargetFilterQuery[0]));
targetFilter.setRequiredIndicatorVisible(true); targetFilter.setRequiredIndicatorVisible(true);
@@ -323,20 +334,18 @@ public class RolloutView extends TableView<MgmtRolloutResponseBody, Long> {
startType.setLabel(Constants.START_TYPE); startType.setLabel(Constants.START_TYPE);
startType.setItems(StartType.values()); startType.setItems(StartType.values());
startType.setValue(StartType.MANUAL); startType.setValue(StartType.MANUAL);
final ComponentRenderer<Component, StartType> startTypeRenderer = new ComponentRenderer<>(startTypeO -> final ComponentRenderer<Component, StartType> startTypeRenderer = new ComponentRenderer<>(startTypeO -> switch (startTypeO) {
switch (startTypeO) { case MANUAL -> new Text(Constants.MANUAL);
case MANUAL -> new Text(Constants.MANUAL); case AUTO -> new Text(Constants.AUTO);
case AUTO -> new Text(Constants.AUTO); case SCHEDULED -> startAt;
case SCHEDULED -> startAt; });
});
startType.setRenderer(startTypeRenderer); startType.setRenderer(startTypeRenderer);
startType.addValueChangeListener(e -> startType.setRenderer(startTypeRenderer)); startType.addValueChangeListener(e -> startType.setRenderer(startTypeRenderer));
startType.setItemLabelGenerator(startTypeO -> startType.setItemLabelGenerator(startTypeO -> switch (startTypeO) {
switch (startTypeO) { case MANUAL -> Constants.MANUAL;
case MANUAL -> Constants.MANUAL; case AUTO -> Constants.AUTO;
case AUTO -> Constants.AUTO; case SCHEDULED -> "Scheduled" + (startAt.isEmpty() ? "" : " at " + startAt.getValue());
case SCHEDULED -> "Scheduled" + (startAt.isEmpty() ? "" : " at " + startAt.getValue()); });
});
startType.setWidthFull(); startType.setWidthFull();
final Div percentSuffix = new Div(); final Div percentSuffix = new Div();
@@ -377,12 +386,8 @@ public class RolloutView extends TableView<MgmtRolloutResponseBody, Long> {
} }
private void readyToCreate(final Object v) { private void readyToCreate(final Object v) {
final boolean createEnabled = !name.isEmpty() && final boolean createEnabled = !name.isEmpty() && !distributionSet.isEmpty() && !targetFilter.isEmpty() && !groupNumber
!distributionSet.isEmpty() && .isEmpty() && !triggerThreshold.isEmpty() && !errorThreshold.isEmpty();
!targetFilter.isEmpty() &&
!groupNumber.isEmpty() &&
!triggerThreshold.isEmpty() &&
!errorThreshold.isEmpty();
if (create.isEnabled() != createEnabled) { if (create.isEnabled() != createEnabled) {
create.setEnabled(createEnabled); create.setEnabled(createEnabled);
} }
@@ -400,16 +405,12 @@ public class RolloutView extends TableView<MgmtRolloutResponseBody, Long> {
request.setType(actionType.getValue()); request.setType(actionType.getValue());
if (actionType.getValue() == MgmtActionType.TIMEFORCED) { if (actionType.getValue() == MgmtActionType.TIMEFORCED) {
request.setForcetime( request.setForcetime(
forceTime.isEmpty() ? forceTime.isEmpty() ? System.currentTimeMillis() : forceTime.getValue().toEpochSecond(ZoneOffset.UTC) * 1000);
System.currentTimeMillis() :
forceTime.getValue().toEpochSecond(ZoneOffset.UTC) * 1000);
} }
switch (startType.getValue()) { switch (startType.getValue()) {
case AUTO -> request.setStartAt(System.currentTimeMillis()); case AUTO -> request.setStartAt(System.currentTimeMillis());
case SCHEDULED -> request.setStartAt( case SCHEDULED -> request.setStartAt(
startAt.isEmpty() ? startAt.isEmpty() ? System.currentTimeMillis() : startAt.getValue().toEpochSecond(ZoneOffset.UTC) * 1000);
System.currentTimeMillis() :
startAt.getValue().toEpochSecond(ZoneOffset.UTC) * 1000);
case MANUAL -> { case MANUAL -> {
// do nothing, will be started manually // do nothing, will be started manually
} }

View File

@@ -14,7 +14,6 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -93,9 +92,9 @@ public class SoftwareModuleView extends TableView<MgmtSoftwareModule, Long> {
} }
}, },
(query, rsqlFilter) -> Optional.ofNullable( (query, rsqlFilter) -> Optional.ofNullable(
hawkbitClient.getSoftwareModuleRestApi() hawkbitClient.getSoftwareModuleRestApi()
.getSoftwareModules(rsqlFilter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC) .getSoftwareModules(rsqlFilter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC)
.getBody()) .getBody())
.stream().map(PagedList::getContent).flatMap(List::stream), .stream().map(PagedList::getContent).flatMap(List::stream),
isParent ? v -> new CreateDialog(hawkbitClient).result() : null, isParent ? v -> new CreateDialog(hawkbitClient).result() : null,
isParent ? selectionGrid -> { isParent ? selectionGrid -> {
@@ -132,9 +131,9 @@ public class SoftwareModuleView extends TableView<MgmtSoftwareModule, Long> {
name.setPlaceholder("<name filter>"); name.setPlaceholder("<name filter>");
type.setItemLabelGenerator(MgmtSoftwareModuleType::getName); type.setItemLabelGenerator(MgmtSoftwareModuleType::getName);
type.setItems(Optional.ofNullable( type.setItems(Optional.ofNullable(
hawkbitClient.getSoftwareModuleTypeRestApi() hawkbitClient.getSoftwareModuleTypeRestApi()
.getTypes(null, 0, 20, Constants.NAME_ASC) .getTypes(null, 0, 20, Constants.NAME_ASC)
.getBody()) .getBody())
.map(PagedList::getContent) .map(PagedList::getContent)
.orElseGet(Collections::emptyList)); .orElseGet(Collections::emptyList));
} }
@@ -184,14 +183,14 @@ public class SoftwareModuleView extends TableView<MgmtSoftwareModule, Long> {
private void setItem(final MgmtSoftwareModule softwareModule) { private void setItem(final MgmtSoftwareModule softwareModule) {
description.setValue(softwareModule.getDescription()); description.setValue(softwareModule.getDescription());
createdBy.setValue(softwareModule.getCreatedBy()); createdBy.setValue(softwareModule.getCreatedBy());
createdAt.setValue(new Date(softwareModule.getCreatedAt()).toString()); createdAt.setValue(Utils.localDateTimeFromTs(softwareModule.getCreatedAt()));
lastModifiedBy.setValue(softwareModule.getLastModifiedBy()); lastModifiedBy.setValue(softwareModule.getLastModifiedBy());
lastModifiedAt.setValue(new Date(softwareModule.getLastModifiedAt()).toString()); lastModifiedAt.setValue(Utils.localDateTimeFromTs(softwareModule.getLastModifiedAt()));
artifactGrid.setItems(query -> Optional.ofNullable( artifactGrid.setItems(query -> Optional.ofNullable(
hawkbitClient.getSoftwareModuleRestApi() hawkbitClient.getSoftwareModuleRestApi()
.getArtifacts(softwareModule.getId(), null, null) .getArtifacts(softwareModule.getId(), null, null)
.getBody()) .getBody())
.stream() .stream()
.flatMap(Collection::stream) .flatMap(Collection::stream)
.skip(query.getOffset()) .skip(query.getOffset())
@@ -220,9 +219,9 @@ public class SoftwareModuleView extends TableView<MgmtSoftwareModule, Long> {
Constants.TYPE, Constants.TYPE,
this::readyToCreate, this::readyToCreate,
Optional.ofNullable( Optional.ofNullable(
hawkbitClient.getSoftwareModuleTypeRestApi() hawkbitClient.getSoftwareModuleTypeRestApi()
.getTypes(null, 0, 30, Constants.NAME_ASC) .getTypes(null, 0, 30, Constants.NAME_ASC)
.getBody()) .getBody())
.map(body -> body.getContent().toArray(new MgmtSoftwareModuleType[0])) .map(body -> body.getContent().toArray(new MgmtSoftwareModuleType[0]))
.orElseGet(() -> new MgmtSoftwareModuleType[0])); .orElseGet(() -> new MgmtSoftwareModuleType[0]));
type.setWidthFull(); type.setWidthFull();
@@ -253,9 +252,9 @@ public class SoftwareModuleView extends TableView<MgmtSoftwareModule, Long> {
if (Boolean.TRUE.equals(createDistributionSet.getValue()) && distType.isEmpty()) { if (Boolean.TRUE.equals(createDistributionSet.getValue()) && distType.isEmpty()) {
distType.setItems( distType.setItems(
Optional.ofNullable( Optional.ofNullable(
hawkbitClient.getDistributionSetTypeRestApi() hawkbitClient.getDistributionSetTypeRestApi()
.getDistributionSetTypes(null, 0, 30, Constants.NAME_ASC) .getDistributionSetTypes(null, 0, 30, Constants.NAME_ASC)
.getBody()) .getBody())
.map(body -> body.getContent().toArray(new MgmtDistributionSetType[0])) .map(body -> body.getContent().toArray(new MgmtDistributionSetType[0]))
.orElseGet(() -> new MgmtDistributionSetType[0])); .orElseGet(() -> new MgmtDistributionSetType[0]));
} }
@@ -286,8 +285,8 @@ public class SoftwareModuleView extends TableView<MgmtSoftwareModule, Long> {
} }
private void readyToCreate(final Object v) { private void readyToCreate(final Object v) {
final boolean createEnabled = !type.isEmpty() && !name.isEmpty() && !version.isEmpty() && final boolean createEnabled = !type.isEmpty() && !name.isEmpty() && !version.isEmpty() && (!createDistributionSet
(!createDistributionSet.getValue() || !distType.isEmpty()); .getValue() || !distType.isEmpty());
if (create.isEnabled() != createEnabled) { if (create.isEnabled() != createEnabled) {
create.setEnabled(createEnabled); create.setEnabled(createEnabled);
} }
@@ -297,30 +296,30 @@ public class SoftwareModuleView extends TableView<MgmtSoftwareModule, Long> {
create.addClickListener(e -> { create.addClickListener(e -> {
close(); close();
final long softwareModuleId = Optional.ofNullable( final long softwareModuleId = Optional.ofNullable(
hawkbitClient.getSoftwareModuleRestApi().createSoftwareModules( hawkbitClient.getSoftwareModuleRestApi().createSoftwareModules(
List.of(new MgmtSoftwareModuleRequestBodyPost() List.of(new MgmtSoftwareModuleRequestBodyPost()
.setType(type.getValue().getKey()) .setType(type.getValue().getKey())
.setName(name.getValue()) .setName(name.getValue())
.setVersion(version.getValue()) .setVersion(version.getValue())
.setVendor(vendor.getValue()) .setVendor(vendor.getValue())
.setDescription(description.getValue()) .setDescription(description.getValue())
.setEncrypted(enableArtifactEncryption.getValue()))) .setEncrypted(enableArtifactEncryption.getValue())))
.getBody()) .getBody())
.stream().flatMap(Collection::stream) .stream().flatMap(Collection::stream)
.findFirst() .findFirst()
.orElseThrow() .orElseThrow()
.getId(); .getId();
if (Boolean.TRUE.equals(createDistributionSet.getValue())) { if (Boolean.TRUE.equals(createDistributionSet.getValue())) {
final long distributionSetId = Optional.ofNullable( final long distributionSetId = Optional.ofNullable(
hawkbitClient.getDistributionSetRestApi() hawkbitClient.getDistributionSetRestApi()
.createDistributionSets( .createDistributionSets(
List.of((MgmtDistributionSetRequestBodyPost) new MgmtDistributionSetRequestBodyPost() List.of((MgmtDistributionSetRequestBodyPost) new MgmtDistributionSetRequestBodyPost()
.setType(distType.getValue().getKey()) .setType(distType.getValue().getKey())
.setName(name.getValue()) .setName(name.getValue())
.setVersion(version.getValue()) .setVersion(version.getValue())
.setDescription(description.getValue()) .setDescription(description.getValue())
.setRequiredMigrationStep(distRequiredMigrationStep.getValue()))) .setRequiredMigrationStep(distRequiredMigrationStep.getValue())))
.getBody()) .getBody())
.stream() .stream()
.flatMap(Collection::stream) .flatMap(Collection::stream)
.findFirst() .findFirst()

View File

@@ -9,11 +9,9 @@
*/ */
package org.eclipse.hawkbit.ui.simple.view; package org.eclipse.hawkbit.ui.simple.view;
import java.time.Instant;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -27,6 +25,8 @@ import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vaadin.flow.data.provider.ListDataProvider;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
import com.vaadin.flow.component.AttachEvent; import com.vaadin.flow.component.AttachEvent;
@@ -39,7 +39,6 @@ import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.checkbox.CheckboxGroup; import com.vaadin.flow.component.checkbox.CheckboxGroup;
import com.vaadin.flow.component.combobox.ComboBox; import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.confirmdialog.ConfirmDialog;
import com.vaadin.flow.component.datetimepicker.DateTimePicker; import com.vaadin.flow.component.datetimepicker.DateTimePicker;
import com.vaadin.flow.component.dependency.Uses; import com.vaadin.flow.component.dependency.Uses;
import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.formlayout.FormLayout;
@@ -60,11 +59,10 @@ import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import com.vaadin.flow.shared.Registration; import com.vaadin.flow.shared.Registration;
import com.vaadin.flow.theme.lumo.LumoUtility; import com.vaadin.flow.theme.lumo.LumoUtility;
import lombok.extern.slf4j.Slf4j; import lombok.EqualsAndHashCode;
import org.eclipse.hawkbit.mgmt.json.model.MgmtPollStatus; import org.eclipse.hawkbit.mgmt.json.model.MgmtPollStatus;
import org.eclipse.hawkbit.mgmt.json.model.PagedList; import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction; import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionStatus;
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentRequestBody; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentRequestBody;
@@ -78,7 +76,9 @@ import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQueryReq
import org.eclipse.hawkbit.mgmt.json.model.targettype.MgmtTargetType; import org.eclipse.hawkbit.mgmt.json.model.targettype.MgmtTargetType;
import org.eclipse.hawkbit.ui.simple.HawkbitMgmtClient; import org.eclipse.hawkbit.ui.simple.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.simple.MainLayout; import org.eclipse.hawkbit.ui.simple.MainLayout;
import org.eclipse.hawkbit.ui.simple.component.TargetActionsHistory;
import org.eclipse.hawkbit.ui.simple.view.util.Filter; import org.eclipse.hawkbit.ui.simple.view.util.Filter;
import org.eclipse.hawkbit.ui.simple.view.util.LinkedTextArea;
import org.eclipse.hawkbit.ui.simple.view.util.SelectionGrid; import org.eclipse.hawkbit.ui.simple.view.util.SelectionGrid;
import org.eclipse.hawkbit.ui.simple.view.util.TableView; import org.eclipse.hawkbit.ui.simple.view.util.TableView;
import org.eclipse.hawkbit.ui.simple.view.util.Utils; import org.eclipse.hawkbit.ui.simple.view.util.Utils;
@@ -89,33 +89,46 @@ import org.springframework.util.ObjectUtils;
@Route(value = "targets", layout = MainLayout.class) @Route(value = "targets", layout = MainLayout.class)
@RolesAllowed({ "TARGET_READ" }) @RolesAllowed({ "TARGET_READ" })
@Uses(Icon.class) @Uses(Icon.class)
public class TargetView extends TableView<MgmtTarget, String> { public class TargetView extends TableView<TargetView.TargetWithDs, String> {
public static final String STATUS = "Status"; public static final String STATUS = "Status";
public static final String UPDATE = "Sync";
public static final String CONTROLLER_ID = "Controller Id"; public static final String CONTROLLER_ID = "Controller Id";
public static final String FILTER = "Filter";
public static final String TAG = "Tag"; public static final String TAG = "Tag";
public TargetView(final HawkbitMgmtClient hawkbitClient) { public TargetView(final HawkbitMgmtClient hawkbitClient) {
super( super(
new RawFilter(hawkbitClient), new SimpleFilter(hawkbitClient), new RawFilter(hawkbitClient), new SimpleFilter(hawkbitClient),
new SelectionGrid.EntityRepresentation<>(MgmtTarget.class, MgmtTarget::getControllerId) { new SelectionGrid.EntityRepresentation<>(TargetWithDs.class, TargetWithDs::getControllerId) {
@Override @Override
protected void addColumns(final Grid<MgmtTarget> grid) { protected void addColumns(final Grid<TargetWithDs> grid) {
grid.addColumn(new ComponentRenderer<>(TargetStatusCell::new)) grid.addColumn(new ComponentRenderer<>(TargetStatusCell::new))
.setHeader(STATUS) .setHeader(STATUS)
.setAutoWidth(true) .setAutoWidth(true)
.setFlexGrow(0); .setFlexGrow(0).setKey("lastControllerRequestAt").setSortable(true);
grid.addColumn(MgmtTarget::getControllerId).setHeader(CONTROLLER_ID).setAutoWidth(true); grid.addColumn(new ComponentRenderer<>(TargetUpdateStatusCell::new))
grid.addColumn(MgmtTarget::getName).setHeader(Constants.NAME).setAutoWidth(true); .setHeader(UPDATE)
grid.addColumn(MgmtTarget::getTargetTypeName).setHeader(Constants.TYPE).setAutoWidth(true); .setAutoWidth(true)
.setFlexGrow(0).setKey("updateStatus").setSortable(true);
grid.addColumn(MgmtTarget::getControllerId).setHeader(CONTROLLER_ID).setAutoWidth(true).setKey("id").setSortable(true);
grid.addColumn(Utils.localDateTimeRenderer(MgmtTarget::getLastModifiedAt)).setHeader(LAST_MODIFIED_AT).setAutoWidth(
true).setKey("lastModifiedAt").setSortable(true);
grid.addColumn(MgmtTarget::getName).setHeader(Constants.NAME).setAutoWidth(true).setKey("name").setSortable(true);
grid.addColumn(MgmtTarget::getTargetTypeName).setHeader(Constants.TYPE).setAutoWidth(true).setKey("targetType")
.setSortable(true);
grid.addColumn(TargetWithDs::getDsName).setHeader(Constants.DISTRIBUTION_SET).setAutoWidth(true);
grid.addColumn(TargetWithDs::getDsVersion).setHeader(Constants.VERSION).setAutoWidth(true).setKey("installedds")
.setSortable(true);
} }
}, },
(query, filter) -> hawkbitClient.getTargetRestApi() (query, filter) -> hawkbitClient.getTargetRestApi()
.getTargets(filter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC) .getTargets(filter, query.getOffset(), query.getPageSize(), Utils.getSortParam(query.getSortOrders(),
"lastModifiedAt:desc"))
.getBody() .getBody()
.getContent() .getContent()
.stream(), .stream().map(m -> TargetWithDs.from(hawkbitClient, m)),
source -> new RegisterDialog(hawkbitClient).result(), source -> new RegisterDialog(hawkbitClient).result(),
selectionGrid -> { selectionGrid -> {
selectionGrid.getSelectedItems() selectionGrid.getSelectedItems()
@@ -129,8 +142,8 @@ public class TargetView extends TableView<MgmtTarget, String> {
} }
); );
final Function<SelectionGrid<MgmtTarget, String>, CompletionStage<Void>> assignHandler = final Function<SelectionGrid<TargetWithDs, String>, CompletionStage<Void>> assignHandler = source -> new AssignDialog(
source -> new AssignDialog(hawkbitClient, source.getSelectedItems()).result(); hawkbitClient, source.getSelectedItems()).result();
final Button assignBtn = Utils.tooltip(new Button(VaadinIcon.LINK.create()), "Assign"); final Button assignBtn = Utils.tooltip(new Button(VaadinIcon.LINK.create()), "Assign");
assignBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); assignBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
@@ -142,15 +155,15 @@ public class TargetView extends TableView<MgmtTarget, String> {
private final HawkbitMgmtClient hawkbitClient; private final HawkbitMgmtClient hawkbitClient;
private final TextField controllerId; private final TextField textFilter;
private final CheckboxGroup<MgmtTargetType> type; private final CheckboxGroup<MgmtTargetType> type;
private final CheckboxGroup<MgmtTag> tag; private final CheckboxGroup<MgmtTag> tag;
private SimpleFilter(final HawkbitMgmtClient hawkbitClient) { private SimpleFilter(final HawkbitMgmtClient hawkbitClient) {
this.hawkbitClient = hawkbitClient; this.hawkbitClient = hawkbitClient;
controllerId = Utils.textField(CONTROLLER_ID); textFilter = Utils.textField(FILTER);
controllerId.setPlaceholder("<controller id filter>"); textFilter.setPlaceholder("<controller id/name filter>");
type = new CheckboxGroup<>(Constants.TYPE); type = new CheckboxGroup<>(Constants.TYPE);
type.setItemLabelGenerator(MgmtTargetType::getName); type.setItemLabelGenerator(MgmtTargetType::getName);
tag = new CheckboxGroup<>(TAG); tag = new CheckboxGroup<>(TAG);
@@ -160,13 +173,13 @@ public class TargetView extends TableView<MgmtTarget, String> {
@Override @Override
public List<Component> components() { public List<Component> components() {
final List<Component> components = new LinkedList<>(); final List<Component> components = new LinkedList<>();
components.add(controllerId); components.add(textFilter);
type.setItems(hawkbitClient.getTargetTypeRestApi().getTargetTypes(null, 0, 20, Constants.NAME_ASC).getBody().getContent()); type.setItems(hawkbitClient.getTargetTypeRestApi().getTargetTypes(null, 0, 20, Constants.NAME_ASC).getBody().getContent());
if (!type.getValue().isEmpty()) { if (!((ListDataProvider) type.getDataProvider()).getItems().isEmpty()) {
components.add(type); components.add(type);
} }
tag.setItems(hawkbitClient.getTargetTagRestApi().getTargetTags(null, 0, 20, Constants.NAME_ASC).getBody().getContent()); tag.setItems(hawkbitClient.getTargetTagRestApi().getTargetTags(null, 0, 20, Constants.NAME_ASC).getBody().getContent());
if (!tag.isEmpty()) { if (!((ListDataProvider) tag.getDataProvider()).getItems().isEmpty()) {
components.add(tag); components.add(tag);
} }
return components; return components;
@@ -176,15 +189,15 @@ public class TargetView extends TableView<MgmtTarget, String> {
public String filter() { public String filter() {
return Filter.filter( return Filter.filter(
Map.of( Map.of(
"controllerid", controllerId.getOptionalValue(), List.of("controllerid", "name"), textFilter.getOptionalValue().map(s -> "*" + s + "*"),
"targettype.name", type.getSelectedItems().stream().map(MgmtTargetType::getName) "targettype.name", type.getSelectedItems().stream().map(MgmtTargetType::getName)
.toList(), .toList(),
"tag", tag.getSelectedItems())); "tag", tag.getSelectedItems().stream().map(MgmtTag::getName).toList()));
} }
} }
@SuppressWarnings({ "java:S1171", "java:S3599" }) @SuppressWarnings({ "java:S1171", "java:S3599" })
private static class RawFilter implements Filter.Rsql { private static class RawFilter implements Filter.Rsql, Filter.RsqlRw {
private final TextField textFilter = new TextField("Raw Filter", "<raw filter>"); private final TextField textFilter = new TextField("Raw Filter", "<raw filter>");
private final VerticalLayout layout = new VerticalLayout(); private final VerticalLayout layout = new VerticalLayout();
@@ -209,8 +222,8 @@ public class TargetView extends TableView<MgmtTarget, String> {
}); });
savedFilters.setEmptySelectionAllowed(true); savedFilters.setEmptySelectionAllowed(true);
savedFilters.setItems(listFilters(hawkbitClient)); savedFilters.setItems(listFilters(hawkbitClient));
savedFilters.setItemLabelGenerator(query -> savedFilters.setItemLabelGenerator(query -> Optional.ofNullable(query).map(MgmtTargetFilterQuery::getName).orElse(
Optional.ofNullable(query).map(MgmtTargetFilterQuery::getName).orElse("<select saved filter>")); "<select saved filter>"));
savedFilters.setWidthFull(); savedFilters.setWidthFull();
textFilter.setWidthFull(); textFilter.setWidthFull();
@@ -237,25 +250,27 @@ public class TargetView extends TableView<MgmtTarget, String> {
} }
private ComponentEventListener<ClickEvent<Button>> createBtnListener(HawkbitMgmtClient hawkbitClient) { private ComponentEventListener<ClickEvent<Button>> createBtnListener(HawkbitMgmtClient hawkbitClient) {
return e -> return e -> new Utils.BaseDialog<Void>("Create New Filter") {
new Utils.BaseDialog<Void>("Create New Filter") {{
final Button finishBtn = Utils.tooltip(new Button("Save"), "Save (Enter)"); {
final TextField name = Utils.textField(Constants.NAME, e -> finishBtn.setEnabled(!e.getHasValue().isEmpty())); final Button finishBtn = Utils.tooltip(new Button("Save"), "Save (Enter)");
name.focus(); final TextField name = Utils.textField(Constants.NAME, e -> finishBtn.setEnabled(!e.getHasValue().isEmpty()));
finishBtn.addClickShortcut(Key.ENTER); name.focus();
finishBtn.setEnabled(false); finishBtn.addClickShortcut(Key.ENTER);
finishBtn.addClickListener(e -> { finishBtn.setEnabled(false);
final MgmtTargetFilterQueryRequestBody createRequest = new MgmtTargetFilterQueryRequestBody(); finishBtn.addClickListener(e -> {
createRequest.setName(name.getValue()); final MgmtTargetFilterQueryRequestBody createRequest = new MgmtTargetFilterQueryRequestBody();
createRequest.setQuery(textFilter.getValue()); createRequest.setName(name.getValue());
hawkbitClient.getTargetFilterQueryRestApi().createFilter(createRequest); createRequest.setQuery(textFilter.getValue());
savedFilters.setItems(listFilters(hawkbitClient)); hawkbitClient.getTargetFilterQueryRestApi().createFilter(createRequest);
close(); savedFilters.setItems(listFilters(hawkbitClient));
}); close();
getFooter().add(finishBtn); });
add(name); getFooter().add(finishBtn);
open(); add(name);
}}; open();
}
};
} }
private ComponentEventListener<ClickEvent<Button>> updateBtnListener(HawkbitMgmtClient hawkbitClient) { private ComponentEventListener<ClickEvent<Button>> updateBtnListener(HawkbitMgmtClient hawkbitClient) {
@@ -265,34 +280,37 @@ public class TargetView extends TableView<MgmtTarget, String> {
return; return;
} }
new Utils.BaseDialog<Void>("Update Filter") {{ new Utils.BaseDialog<Void>("Update Filter") {
final Button finishBtn = Utils.tooltip(new Button("Update"), "Update (Enter)");
finishBtn.setEnabled(false);
final TextField name = Utils.textField(Constants.NAME, e -> finishBtn.setEnabled(!e.getHasValue().isEmpty())); {
name.focus(); final Button finishBtn = Utils.tooltip(new Button("Update"), "Update (Enter)");
name.setValue(selected.getName()); finishBtn.setEnabled(false);
final TextArea filterValue = new TextArea("Filter Value"); final TextField name = Utils.textField(Constants.NAME, e -> finishBtn.setEnabled(!e.getHasValue().isEmpty()));
filterValue.setReadOnly(true); name.focus();
filterValue.setValue(textFilter.getValue()); name.setValue(selected.getName());
filterValue.setWidthFull();
finishBtn.addClickShortcut(Key.ENTER); final TextArea filterValue = new TextArea("Filter Value");
finishBtn.addClickListener(e -> { filterValue.setReadOnly(true);
final MgmtTargetFilterQueryRequestBody updateRequest = new MgmtTargetFilterQueryRequestBody(); filterValue.setValue(textFilter.getValue());
updateRequest.setName(name.getValue()); filterValue.setWidthFull();
updateRequest.setQuery(textFilter.getValue());
hawkbitClient.getTargetFilterQueryRestApi().updateFilter(selected.getId(), updateRequest);
savedFilters.setItems(listFilters(hawkbitClient));
close();
});
getFooter().add(finishBtn);
add(name); finishBtn.addClickShortcut(Key.ENTER);
add(filterValue); finishBtn.addClickListener(e -> {
open(); final MgmtTargetFilterQueryRequestBody updateRequest = new MgmtTargetFilterQueryRequestBody();
}}; updateRequest.setName(name.getValue());
updateRequest.setQuery(textFilter.getValue());
hawkbitClient.getTargetFilterQueryRestApi().updateFilter(selected.getId(), updateRequest);
savedFilters.setItems(listFilters(hawkbitClient));
close();
});
getFooter().add(finishBtn);
add(name);
add(filterValue);
open();
}
};
}; };
} }
@@ -305,6 +323,11 @@ public class TargetView extends TableView<MgmtTarget, String> {
public String filter() { public String filter() {
return textFilter.getOptionalValue().orElse(null); return textFilter.getOptionalValue().orElse(null);
} }
@Override
public void setFilter(String filter) {
textFilter.setValue(filter);
}
} }
protected static class TargetDetailedView extends TabSheet { protected static class TargetDetailedView extends TabSheet {
@@ -312,26 +335,26 @@ public class TargetView extends TableView<MgmtTarget, String> {
private final TargetDetails targetDetails; private final TargetDetails targetDetails;
private final TargetAssignedInstalled targetAssignedInstalled; private final TargetAssignedInstalled targetAssignedInstalled;
private final TargetTags targetTags; private final TargetTags targetTags;
private final TargetActions targetActions; private final TargetActionsHistoryLayout targetActionsHistoryLayout;
private TargetDetailedView(final HawkbitMgmtClient hawkbitClient) { private TargetDetailedView(final HawkbitMgmtClient hawkbitClient) {
targetDetails = new TargetDetails(hawkbitClient); targetDetails = new TargetDetails(hawkbitClient);
targetAssignedInstalled = new TargetAssignedInstalled(hawkbitClient); targetAssignedInstalled = new TargetAssignedInstalled(hawkbitClient);
targetTags = new TargetTags(hawkbitClient); targetTags = new TargetTags(hawkbitClient);
targetActions = new TargetActions(hawkbitClient); targetActionsHistoryLayout = new TargetActionsHistoryLayout(hawkbitClient);
setWidthFull(); setWidthFull();
add("Details", targetDetails); add("Details", targetDetails);
add("Assigned / Installed", targetAssignedInstalled); add("Assigned / Installed", targetAssignedInstalled);
add("Tags", targetTags); add("Tags", targetTags);
add("Action History", targetActions); add("Action History", targetActionsHistoryLayout);
} }
private void setItem(final MgmtTarget target) { private void setItem(final MgmtTarget target) {
this.targetDetails.setItem(target); this.targetDetails.setItem(target);
this.targetAssignedInstalled.setItem(target); this.targetAssignedInstalled.setItem(target);
this.targetTags.setItem(target); this.targetTags.setItem(target);
this.targetActions.setItem(target); this.targetActionsHistoryLayout.setItem(target);
} }
} }
@@ -346,6 +369,7 @@ public class TargetView extends TableView<MgmtTarget, String> {
private final TextField securityToken = Utils.textField(Constants.SECURITY_TOKEN); private final TextField securityToken = Utils.textField(Constants.SECURITY_TOKEN);
private final TextField lastPoll = Utils.textField(Constants.LAST_POLL); private final TextField lastPoll = Utils.textField(Constants.LAST_POLL);
private final TextField group = Utils.textField(Constants.GROUP); private final TextField group = Utils.textField(Constants.GROUP);
private final TextField targetAddress = Utils.textField(Constants.ADDRESS);
private final TextArea targetAttributes = new TextArea(Constants.ATTRIBUTES); private final TextArea targetAttributes = new TextArea(Constants.ATTRIBUTES);
private transient MgmtTarget target; private transient MgmtTarget target;
@@ -353,11 +377,11 @@ public class TargetView extends TableView<MgmtTarget, String> {
this.hawkbitClient = hawkbitClient; this.hawkbitClient = hawkbitClient;
description.setMinLength(2); description.setMinLength(2);
Stream.of( Stream.of(
description, description,
createdBy, createdAt, createdBy, createdAt,
lastModifiedBy, lastModifiedAt, lastModifiedBy, lastModifiedAt,
securityToken, lastPoll, targetAttributes, group securityToken, lastPoll, group, targetAddress, targetAttributes
) )
.forEach(field -> { .forEach(field -> {
field.setReadOnly(true); field.setReadOnly(true);
add(field); add(field);
@@ -375,14 +399,15 @@ public class TargetView extends TableView<MgmtTarget, String> {
protected void onAttach(final AttachEvent attachEvent) { protected void onAttach(final AttachEvent attachEvent) {
description.setValue(target.getDescription() == null ? "N/A" : target.getDescription()); description.setValue(target.getDescription() == null ? "N/A" : target.getDescription());
createdBy.setValue(target.getCreatedBy()); createdBy.setValue(target.getCreatedBy());
createdAt.setValue(new Date(target.getCreatedAt()).toString()); createdAt.setValue(Utils.localDateTimeFromTs(target.getCreatedAt()));
lastModifiedBy.setValue(target.getLastModifiedBy()); lastModifiedBy.setValue(target.getLastModifiedBy());
lastModifiedAt.setValue(new Date(target.getLastModifiedAt()).toString()); lastModifiedAt.setValue(Utils.localDateTimeFromTs(target.getLastModifiedAt()));
securityToken.setValue(target.getSecurityToken()); securityToken.setValue(Objects.requireNonNullElse(target.getSecurityToken(), ""));
group.setValue(target.getGroup() != null ? target.getGroup() : ""); group.setValue(target.getGroup() != null ? target.getGroup() : "");
targetAddress.setValue(target.getAddress() != null ? target.getAddress() : "");
final MgmtPollStatus pollStatus = target.getPollStatus(); final MgmtPollStatus pollStatus = target.getPollStatus();
lastPoll.setValue(pollStatus == null ? NOT_AVAILABLE_NULL : new Date(pollStatus.getLastRequestAt()).toString()); lastPoll.setValue(pollStatus == null ? NOT_AVAILABLE_NULL : Utils.localDateTimeFromTs(pollStatus.getLastRequestAt()));
final ResponseEntity<MgmtTargetAttributes> response = hawkbitClient.getTargetRestApi().getAttributes(target.getControllerId()); final ResponseEntity<MgmtTargetAttributes> response = hawkbitClient.getTargetRestApi().getAttributes(target.getControllerId());
if (response.getStatusCode().is2xxSuccessful()) { if (response.getStatusCode().is2xxSuccessful()) {
targetAttributes.setValue(Objects.requireNonNullElse(response.getBody(), Collections.emptyMap()).entrySet().stream() targetAttributes.setValue(Objects.requireNonNullElse(response.getBody(), Collections.emptyMap()).entrySet().stream()
@@ -397,14 +422,12 @@ public class TargetView extends TableView<MgmtTarget, String> {
private static class TargetAssignedInstalled extends FormLayout { private static class TargetAssignedInstalled extends FormLayout {
private final transient HawkbitMgmtClient hawkbitClient; private final transient HawkbitMgmtClient hawkbitClient;
private final TextArea assigned = new TextArea("Assigned Distribution Set"); private final LinkedTextArea assigned = new LinkedTextArea("Assigned Distribution Set", "/distribution_sets?");
private final TextArea installed = new TextArea("Installed Distribution Set"); private final LinkedTextArea installed = new LinkedTextArea("Installed Distribution Set", "/distribution_sets?");
private transient MgmtTarget target; private transient MgmtTarget target;
private TargetAssignedInstalled(HawkbitMgmtClient hawkbitClient) { private TargetAssignedInstalled(HawkbitMgmtClient hawkbitClient) {
this.hawkbitClient = hawkbitClient; this.hawkbitClient = hawkbitClient;
assigned.setReadOnly(true);
installed.setReadOnly(true);
assigned.setWidthFull(); assigned.setWidthFull();
installed.setWidthFull(); installed.setWidthFull();
add(assigned, installed); add(assigned, installed);
@@ -421,22 +444,23 @@ public class TargetView extends TableView<MgmtTarget, String> {
updateDistributionSetInfo(() -> hawkbitClient.getTargetRestApi().getAssignedDistributionSet(target.getControllerId()), assigned); updateDistributionSetInfo(() -> hawkbitClient.getTargetRestApi().getAssignedDistributionSet(target.getControllerId()), assigned);
} }
private void updateDistributionSetInfo(Supplier<ResponseEntity<MgmtDistributionSet>> supplier, TextArea textArea) { private void updateDistributionSetInfo(Supplier<ResponseEntity<MgmtDistributionSet>> supplier, LinkedTextArea textArea) {
Optional.ofNullable(supplier.get()) Optional.ofNullable(supplier.get())
.map(ResponseEntity<MgmtDistributionSet>::getBody) .map(ResponseEntity<MgmtDistributionSet>::getBody)
.ifPresent(value -> { .ifPresentOrElse(value -> {
final String description = """ final String description = """
Name: %s Name: %s
Version: %s Version: %s
%s %s
""".replace("\n", System.lineSeparator()); """.replace("\n", System.lineSeparator());
textArea.setValue(description.formatted( textArea.setValueWithLink(description.formatted(
value.getName(), value.getName(),
value.getVersion(), value.getVersion(),
value.getModules().stream().map(module -> module.getTypeName() + ": " + module.getVersion()) value.getModules().stream().map(module -> module.getTypeName() + ": " + module.getVersion())
.collect(Collectors.joining(System.lineSeparator())) .collect(Collectors.joining(System.lineSeparator()))
)); ), "q=id%3D%3D" + value.getId().toString());
}); },
() -> textArea.setValueWithLink("", null));
} }
} }
@@ -466,8 +490,8 @@ public class TargetView extends TableView<MgmtTarget, String> {
private HorizontalLayout buildTagSelectionLayout(HawkbitMgmtClient hawkbitClient) { private HorizontalLayout buildTagSelectionLayout(HawkbitMgmtClient hawkbitClient) {
final Button createTagButton = new Button("Create Tag"); final Button createTagButton = new Button("Create Tag");
createTagButton.addClickListener(event -> createTagButton.addClickListener(event -> new CreateTagDialog(hawkbitClient, () -> tagSelector.setItems(fetchAvailableTags()))
new CreateTagDialog(hawkbitClient, () -> tagSelector.setItems(fetchAvailableTags())).result()); .result());
tagSelector.setWidthFull(); tagSelector.setWidthFull();
tagSelector.setItemLabelGenerator(MgmtTag::getName); tagSelector.setItemLabelGenerator(MgmtTag::getName);
@@ -541,7 +565,7 @@ public class TargetView extends TableView<MgmtTarget, String> {
int offset = 0; int offset = 0;
do { do {
List<MgmtTag> page = Optional.ofNullable( List<MgmtTag> page = Optional.ofNullable(
hawkbitClient.getTargetTagRestApi().getTargetTags(null, offset, 50, Constants.NAME_ASC).getBody()) hawkbitClient.getTargetTagRestApi().getTargetTags(null, offset, 50, Constants.NAME_ASC).getBody())
.map(PagedList::getContent) .map(PagedList::getContent)
.orElse(Collections.emptyList()); .orElse(Collections.emptyList());
tags.addAll(page); tags.addAll(page);
@@ -552,198 +576,105 @@ public class TargetView extends TableView<MgmtTarget, String> {
} }
} }
@Slf4j public static class TargetActionsHistoryLayout extends VerticalLayout {
private static class TargetActions extends Grid<TargetActions.ActionStatusEntry> {
private final transient HawkbitMgmtClient hawkbitClient; private final TargetActionsHistory targetActionsHistory;
private transient MgmtTarget target;
private TargetActions(final HawkbitMgmtClient hawkbitClient) { public TargetActionsHistoryLayout(HawkbitMgmtClient hawkbitMgmtClient) {
this.hawkbitClient = hawkbitClient; ActionStepsGrid actionStepsGrid = new ActionStepsGrid(hawkbitMgmtClient);
setWidthFull(); targetActionsHistory = new TargetActionsHistory(hawkbitMgmtClient, actionStepsGrid);
addColumn(new ComponentRenderer<>(ActionStatusEntry::getStatusIcon)).setHeader(STATUS).setAutoWidth(true).setFlexGrow(0); add(targetActionsHistory);
addColumn(ActionStatusEntry::getDistributionSetName).setHeader("Distribution Set").setAutoWidth(true); add(actionStepsGrid);
addColumn(ActionStatusEntry::getLastModifiedAt)
.setHeader("Last Modified")
.setAutoWidth(true)
.setFlexGrow(0)
.setComparator(ActionStatusEntry::getLastModifiedAt);
addColumn(new ComponentRenderer<>(ActionStatusEntry::getForceTypeIcon)).setHeader("Type").setAutoWidth(true).setFlexGrow(0);
addColumn(new ComponentRenderer<>(ActionStatusEntry::getActionsLayout)).setHeader("Actions").setAutoWidth(true).setFlexGrow(0);
addColumn(new ComponentRenderer<>(ActionStatusEntry::getForceQuitLayout)).setHeader("Force Quit").setAutoWidth(true).setFlexGrow(0);
} }
private void setItem(final MgmtTarget target) { public void setItem(MgmtTarget target) {
this.target = target; targetActionsHistory.setItem(target);
} }
private List<ActionStatusEntry> fetchActions() { public static class ActionStepsGrid extends Grid<ActionStepsGrid.ActionStepEntry> {
return hawkbitClient.getTargetRestApi().getActionHistory(target.getControllerId(), null, 0, 30, null)
.getBody()
.getContent()
.stream()
.map(action -> new ActionStatusEntry(action, () -> setItems(fetchActions())))
.filter(value -> value.action != null)
.toList();
}
@Override private final transient HawkbitMgmtClient hawkbitClient;
protected void onAttach(AttachEvent attachEvent) { private transient MgmtTarget target;
setItems(fetchActions()); private transient Long actionId;
}
private class ActionStatusEntry { private ActionStepsGrid(final HawkbitMgmtClient hawkbitClient) {
final MgmtAction action; this.hawkbitClient = hawkbitClient;
final Runnable onUpdate; setWidthFull();
MgmtDistributionSet distributionSet; addColumn(new ComponentRenderer<>(ActionStepEntry::getStatusIcon)).setHeader(STATUS).setAutoWidth(true)
.setFlexGrow(0);
addColumn(Utils.localDateTimeRenderer(ActionStepEntry::getLastModifiedAt)).setHeader("Time")
.setAutoWidth(true).setFlexGrow(0).setComparator(ActionStepEntry::getLastModifiedAt);
addColumn(new ComponentRenderer<>(ActionStepEntry::getMessage)).setHeader("Message").setAutoWidth(true).setFlexGrow(0);
}
public ActionStatusEntry(final MgmtAction mgmtAction, final Runnable onUpdate) { private List<ActionStepEntry> fetchActionSteps() {
this.action = hawkbitClient.getActionRestApi().getAction(mgmtAction.getId()).getBody(); if (actionId == null) {
this.onUpdate = onUpdate; return new ArrayList<>();
if (action == null) {
log.error("Unable to fetch the action with id : {}", mgmtAction.getId());
return;
} }
this.action.getLink("distributionset").ifPresent(link -> { return hawkbitClient.getTargetRestApi()
try { .getActionStatusList(target.getControllerId(), actionId, 0, 30, null).getBody().getContent()
Long dsId = Long.parseLong(link.getHref().substring(link.getHref().lastIndexOf("/") + 1)); .stream().map(ActionStepEntry::new)
this.distributionSet = hawkbitClient.getDistributionSetRestApi().getDistributionSet(dsId).getBody(); .toList();
} catch (NumberFormatException e) { }
log.error("Error parsing distribution set ID", e);
@Override
protected void onAttach(AttachEvent attachEvent) {
setItems(fetchActionSteps());
}
public void setActionId(Long id) {
actionId = id;
setItems(fetchActionSteps());
}
public void setTarget(MgmtTarget target) {
this.target = target;
actionId = null;
}
private static class ActionStepEntry extends Object {
final MgmtActionStatus status;
public ActionStepEntry(final MgmtActionStatus status) {
this.status = status;
}
public Long getLastModifiedAt() {
return status.getReportedAt();
}
public Component getStatusIcon() {
final HorizontalLayout layout = new HorizontalLayout();
final Icon icon;
switch (status.getType()) {
case FINISHED -> icon = Utils.iconColored(VaadinIcon.CHECK_CIRCLE, "Finished", "green");
case ERROR -> icon = Utils.iconColored(VaadinIcon.CLOSE_CIRCLE, "Error", "red");
case WARNING -> icon = Utils.iconColored(VaadinIcon.WARNING, "Warning", "orange");
case RUNNING -> icon = Utils.iconColored(VaadinIcon.ADJUST, "Running", "green");
case RETRIEVED -> icon = Utils.iconColored(VaadinIcon.CIRCLE_THIN, "Retrieved", "green");
case CANCELED -> icon = Utils.iconColored(VaadinIcon.CLOSE_CIRCLE_O, "Canceled", "gray");
case CANCELING -> icon = Utils.iconColored(VaadinIcon.CLOSE_CIRCLE, "Cancelling", "brown");
case DOWNLOAD -> icon = Utils.iconColored(VaadinIcon.CLOUD_DOWNLOAD_O, "Download", "teal");
case DOWNLOADED -> icon = Utils.iconColored(VaadinIcon.CLOUD_DOWNLOAD, "Downloaded", "purple");
case WAIT_FOR_CONFIRMATION ->
icon = Utils.iconColored(VaadinIcon.QUESTION_CIRCLE, "Wait for confirmation", "coral");
default -> icon = Utils.iconColored(VaadinIcon.CIRCLE_THIN, status.getType().getName().toLowerCase(),
"black");
} }
});
}
private boolean isActive() { icon.addClassNames(LumoUtility.IconSize.SMALL);
return action.getStatus().equals(MgmtAction.ACTION_PENDING); layout.add(icon);
} layout.setWidth(50, Unit.PIXELS);
layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
private boolean isCancelingOrCanceled() { return layout;
return action.getType().equals(MgmtAction.ACTION_CANCEL);
}
public Component getStatusIcon() {
final HorizontalLayout layout = new HorizontalLayout();
final Icon icon;
if (isActive()) {
if (isCancelingOrCanceled()) {
icon = Utils.tooltip(VaadinIcon.ADJUST.create(), "Pending Cancellation");
icon.setColor("red");
} else {
icon = Utils.tooltip(VaadinIcon.ADJUST.create(), "Pending Update");
icon.setColor("orange");
}
} else if (action.getType().equals(MgmtAction.ACTION_UPDATE)) {
icon = Utils.tooltip(VaadinIcon.CHECK_CIRCLE.create(), "Updated");
icon.setColor("green");
} else {
icon = Utils.tooltip(VaadinIcon.CLOSE_CIRCLE.create(), "Canceled");
icon.setColor("red");
} }
icon.addClassNames(LumoUtility.IconSize.SMALL); public VerticalLayout getMessage() {
layout.add(icon); return new VerticalLayout(status.getMessages().stream().map(Span::new).toArray(Span[]::new));
layout.setWidth(50, Unit.PIXELS);
layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
return layout;
}
public String getDistributionSetName() {
return Optional.ofNullable(distributionSet).map(MgmtDistributionSet::getName).orElse("Distribution Set not found");
}
public Instant getLastModifiedAt() {
return Instant.ofEpochMilli(action.getLastModifiedAt());
}
public Icon getForceTypeIcon() {
Icon icon = switch (action.getForceType()) {
case FORCED -> VaadinIcon.BOLT.create();
case TIMEFORCED -> VaadinIcon.USER_CLOCK.create();
case SOFT -> VaadinIcon.USER_CHECK.create();
case DOWNLOAD_ONLY -> VaadinIcon.DOWNLOAD.create();
};
return Utils.tooltip(icon, action.getForceType().getName());
}
public HorizontalLayout getActionsLayout() {
final HorizontalLayout actionsLayout = new HorizontalLayout();
actionsLayout.setSpacing(true);
final Button cancelButton = Utils.tooltip(new Button(VaadinIcon.CLOSE.create()), "Cancel Action");
if (isActive() && !isCancelingOrCanceled()) {
cancelButton.addClickListener(e -> {
String message = "Are you sure you want to cancel the action ?";
promptForConfirmAction(
message, onUpdate,
() -> hawkbitClient.getTargetRestApi().cancelAction(target.getControllerId(), action.getId(), false)).open();
});
} else {
cancelButton.setEnabled(false);
} }
final Button forceButton = Utils.tooltip(new Button(VaadinIcon.BOLT.create()), "Force Action");
if (isActive() && !isCancelingOrCanceled() && action.getForceType() != MgmtActionType.FORCED) {
forceButton.addClickListener(e -> {
String message = "Are you sure you want to force the action ?";
promptForConfirmAction(
message, onUpdate, () -> {
MgmtActionRequestBodyPut setForced = new MgmtActionRequestBodyPut();
setForced.setForceType(MgmtActionType.FORCED);
hawkbitClient.getTargetRestApi().updateAction(target.getControllerId(), action.getId(), setForced);
}
).open();
});
} else {
forceButton.setEnabled(false);
}
actionsLayout.add(cancelButton, forceButton);
return actionsLayout;
}
public HorizontalLayout getForceQuitLayout() {
final HorizontalLayout forceQuitLayout = new HorizontalLayout();
forceQuitLayout.setSpacing(true);
forceQuitLayout.setPadding(true);
forceQuitLayout.setWidthFull();
forceQuitLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
final Button forceQuitButton = Utils.tooltip(new Button(VaadinIcon.CLOSE.create()), "Force Cancel");
forceQuitButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY_INLINE);
if (isActive() && isCancelingOrCanceled()) {
forceQuitButton.addClickListener(e -> {
String message = "Are you sure you want to force cancel the action ?";
promptForConfirmAction(
message, onUpdate,
() -> hawkbitClient.getTargetRestApi().cancelAction(target.getControllerId(), action.getId(), true)).open();
});
} else {
forceQuitButton.setEnabled(false);
}
forceQuitLayout.add(forceQuitButton);
return forceQuitLayout;
}
private static ConfirmDialog promptForConfirmAction(String message, Runnable refreshActions, Runnable actionConsumer) {
final ConfirmDialog dialog = new ConfirmDialog();
dialog.setHeader("Confirm Action");
dialog.setText(message);
dialog.setCancelable(true);
dialog.addCancelListener(event -> dialog.close());
dialog.setConfirmButtonTheme(ButtonVariant.LUMO_ERROR.getVariantName());
dialog.setConfirmText("Confirm");
dialog.addConfirmListener(event -> {
actionConsumer.run();
refreshActions.run();
dialog.close();
});
return dialog;
} }
} }
} }
@@ -772,7 +703,7 @@ public class TargetView extends TableView<MgmtTarget, String> {
type.setWidthFull(); type.setWidthFull();
type.setEmptySelectionAllowed(true); type.setEmptySelectionAllowed(true);
type.setItemLabelGenerator(item -> item == null ? "" : item.getName()); type.setItemLabelGenerator(item -> item == null ? "" : item.getName());
controllerId = Utils.textField(CONTROLLER_ID,e -> register.setEnabled(!e.getHasValue().isEmpty())); controllerId = Utils.textField(FILTER, e -> register.setEnabled(!e.getHasValue().isEmpty()));
controllerId.focus(); controllerId.focus();
name = Utils.textField(Constants.NAME); name = Utils.textField(Constants.NAME);
name.setWidthFull(); name.setWidthFull();
@@ -812,7 +743,7 @@ public class TargetView extends TableView<MgmtTarget, String> {
request.setTargetType(type.getValue().getId()); request.setTargetType(type.getValue().getId());
} }
hawkbitClient.getTargetRestApi().createTargets( hawkbitClient.getTargetRestApi().createTargets(
List.of(request)) List.of(request))
.getBody() .getBody()
.stream() .stream()
.findFirst() .findFirst()
@@ -830,22 +761,21 @@ public class TargetView extends TableView<MgmtTarget, String> {
private final DateTimePicker forceTime = new DateTimePicker("Force Time"); private final DateTimePicker forceTime = new DateTimePicker("Force Time");
private final Button assign = new Button("Assign"); private final Button assign = new Button("Assign");
private AssignDialog(final HawkbitMgmtClient hawkbitClient, Set<MgmtTarget> selectedTargets) { private AssignDialog(final HawkbitMgmtClient hawkbitClient, Set<TargetWithDs> selectedTargets) {
super("Assign Distribution Set"); super("Assign Distribution Set");
distributionSet = new Select<>( distributionSet = new Select<>(
"Distribution Set", "Distribution Set",
this::readyToAssign, this::readyToAssign,
Optional.ofNullable( Optional.ofNullable(
hawkbitClient.getDistributionSetRestApi() hawkbitClient.getDistributionSetRestApi()
.getDistributionSets(null, 0, 30, Constants.NAME_ASC) .getDistributionSets(null, 0, 500, Constants.CREATED_AT_DESC)
.getBody()) .getBody())
.map(body -> body.getContent().toArray(new MgmtDistributionSet[0])) .map(body -> body.getContent().toArray(new MgmtDistributionSet[0]))
.orElseGet(() -> new MgmtDistributionSet[0]) .orElseGet(() -> new MgmtDistributionSet[0])
); );
distributionSet.setRequiredIndicatorVisible(true); distributionSet.setRequiredIndicatorVisible(true);
distributionSet.setItemLabelGenerator(distributionSetO -> distributionSet.setItemLabelGenerator(distributionSetO -> distributionSetO.getName() + ":" + distributionSetO.getVersion());
distributionSetO.getName() + ":" + distributionSetO.getVersion());
distributionSet.setWidthFull(); distributionSet.setWidthFull();
actionType = Utils.actionTypeControls(forceTime); actionType = Utils.actionTypeControls(forceTime);
@@ -874,7 +804,7 @@ public class TargetView extends TableView<MgmtTarget, String> {
} }
} }
private void addAssignClickListener(final HawkbitMgmtClient hawkbitClient, final Set<MgmtTarget> selectedTargets) { private void addAssignClickListener(final HawkbitMgmtClient hawkbitClient, final Set<TargetWithDs> selectedTargets) {
assign.addClickListener(e -> { assign.addClickListener(e -> {
close(); close();
@@ -885,9 +815,7 @@ public class TargetView extends TableView<MgmtTarget, String> {
request.setType(actionType.getValue()); request.setType(actionType.getValue());
if (actionType.getValue() == MgmtActionType.TIMEFORCED) { if (actionType.getValue() == MgmtActionType.TIMEFORCED) {
request.setForcetime( request.setForcetime(
forceTime.isEmpty() ? forceTime.isEmpty() ? System.currentTimeMillis() : forceTime.getValue().toEpochSecond(ZoneOffset.UTC) * 1000);
System.currentTimeMillis() :
forceTime.getValue().toEpochSecond(ZoneOffset.UTC) * 1000);
} }
requests.add(request); requests.add(request);
@@ -945,9 +873,31 @@ public class TargetView extends TableView<MgmtTarget, String> {
private TargetStatusCell(final MgmtTarget target) { private TargetStatusCell(final MgmtTarget target) {
final MgmtPollStatus pollStatus = target.getPollStatus(); final MgmtPollStatus pollStatus = target.getPollStatus();
add(pollStatusIconMapper(pollStatus));
setWidth(25, Unit.PIXELS);
}
private Icon pollStatusIconMapper(MgmtPollStatus pollStatus) {
final Icon pollIcon;
if (pollStatus == null) {
pollIcon = Utils.tooltip(VaadinIcon.QUESTION_CIRCLE.create(), "No Poll Status");
} else if (pollStatus.isOverdue()) {
pollIcon = Utils.tooltip(VaadinIcon.EXCLAMATION_CIRCLE.create(), "Overdue " + Utils.durationFromMillis(pollStatus
.getLastRequestAt()));
} else {
pollIcon = Utils.tooltip(VaadinIcon.CLOCK.create(), "In Time " + Utils.durationFromMillis(pollStatus.getLastRequestAt()));
}
pollIcon.addClassNames(LumoUtility.IconSize.SMALL);
return pollIcon;
}
}
private static class TargetUpdateStatusCell extends HorizontalLayout {
private TargetUpdateStatusCell(final MgmtTarget target) {
final String targetUpdateStatus = Optional.ofNullable(target.getUpdateStatus()).orElse("unknown"); final String targetUpdateStatus = Optional.ofNullable(target.getUpdateStatus()).orElse("unknown");
add(pollStatusIconMapper(pollStatus), targetUpdateStatusMapper(targetUpdateStatus)); add(targetUpdateStatusMapper(targetUpdateStatus));
setWidth(50, Unit.PIXELS); setWidth(25, Unit.PIXELS);
} }
private Icon targetUpdateStatusMapper(final String targetUpdateStatus) { private Icon targetUpdateStatusMapper(final String targetUpdateStatus) {
@@ -967,23 +917,39 @@ public class TargetView extends TableView<MgmtTarget, String> {
default -> "blue"; default -> "blue";
}; };
final Icon statusIcon = Utils.tooltip(icon.create(), targetUpdateStatus); final Icon statusIcon = Utils.tooltip(icon.create(), targetUpdateStatus.replace("_", " "));
statusIcon.setColor(color); statusIcon.setColor(color);
statusIcon.addClassNames(LumoUtility.IconSize.SMALL); statusIcon.addClassNames(LumoUtility.IconSize.SMALL);
return statusIcon; return statusIcon;
} }
}
private Icon pollStatusIconMapper(MgmtPollStatus pollStatus) { // todo change /targets api to reduce api calls ?
final Icon pollIcon; @EqualsAndHashCode(callSuper = true)
if (pollStatus == null) { public static class TargetWithDs extends MgmtTarget {
pollIcon = Utils.tooltip(VaadinIcon.QUESTION_CIRCLE.create(), "No Poll Status");
} else if (pollStatus.isOverdue()) { TargetWithDs() {
pollIcon = Utils.tooltip(VaadinIcon.EXCLAMATION_CIRCLE.create(), "Overdue"); super();
} else { }
pollIcon = Utils.tooltip(VaadinIcon.CLOCK.create(), "In Time");
} Optional<MgmtDistributionSet> ds;
pollIcon.addClassNames(LumoUtility.IconSize.SMALL); static ObjectMapper objectMapper = new ObjectMapper();
return pollIcon;
public static TargetWithDs from(final HawkbitMgmtClient hawkbitClient, MgmtTarget target) {
TargetWithDs targetWithDs = objectMapper.convertValue(target, TargetWithDs.class);
targetWithDs.ds = Optional.ofNullable(hawkbitClient.getTargetRestApi().getInstalledDistributionSet(targetWithDs
.getControllerId())
.getBody());
return targetWithDs;
}
public String getDsVersion() {
return ds.map(MgmtDistributionSet::getVersion).orElse("");
}
public String getDsName() {
return ds.map(MgmtDistributionSet::getName).orElse("");
} }
} }
} }

View File

@@ -28,13 +28,19 @@ import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.theme.lumo.LumoUtility; import com.vaadin.flow.theme.lumo.LumoUtility;
import org.springframework.util.ObjectUtils;
public class Filter extends Div { public class Filter extends Div {
private transient Rsql rsql; private transient Rsql rsql;
private final transient Rsql secondaryRsql;
private final transient Rsql primaryRsql;
private final transient Div filtersDiv;
public Filter(final Consumer<String> changeListener, final Rsql primaryRsql, final Rsql secondaryOptionalRsql) { public Filter(final Consumer<String> changeListener, final Rsql primaryRsql, final Rsql secondaryOptionalRsql) {
rsql = primaryRsql; rsql = primaryRsql;
this.primaryRsql = primaryRsql;
secondaryRsql = secondaryOptionalRsql;
final HorizontalLayout layout = new HorizontalLayout(); final HorizontalLayout layout = new HorizontalLayout();
@@ -42,7 +48,7 @@ public class Filter extends Div {
addClassNames(LumoUtility.Padding.Horizontal.NONE, LumoUtility.Padding.Vertical.SMALL, addClassNames(LumoUtility.Padding.Horizontal.NONE, LumoUtility.Padding.Vertical.SMALL,
LumoUtility.BoxSizing.BORDER); LumoUtility.BoxSizing.BORDER);
final Div filtersDiv = new Div(); filtersDiv = new Div();
filtersDiv.setWidthFull(); filtersDiv.setWidthFull();
filtersDiv.add(primaryRsql.components()); filtersDiv.add(primaryRsql.components());
filtersDiv.addClassName(LumoUtility.Gap.SMALL); filtersDiv.addClassName(LumoUtility.Gap.SMALL);
@@ -70,11 +76,7 @@ public class Filter extends Div {
final Button toggleBtn = Utils.tooltip(new Button(VaadinIcon.FLIP_V.create()), "Toggle Search"); final Button toggleBtn = Utils.tooltip(new Button(VaadinIcon.FLIP_V.create()), "Toggle Search");
toggleBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY); toggleBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
toggleBtn.addClickListener(e -> { toggleBtn.addClickListener(e -> {
filtersDiv.removeAll(); toggle();
synchronized (this) { // toggle
rsql = rsql == primaryRsql ? secondaryOptionalRsql : primaryRsql;
}
filtersDiv.add(rsql.components());
changeListener.accept(primaryRsql.filter()); changeListener.accept(primaryRsql.filter());
}); });
layout.add(toggleBtn); layout.add(toggleBtn);
@@ -84,34 +86,59 @@ public class Filter extends Div {
changeListener.accept(primaryRsql.filter()); changeListener.accept(primaryRsql.filter());
} }
public static String filter(final Map<String, Object> keyToValues) { private void toggle() {
final Map<String, Object> normalized = // toggle
new HashMap<>(keyToValues) filtersDiv.removeAll();
.entrySet() synchronized (this) {
.stream() rsql = rsql == primaryRsql ? secondaryRsql : primaryRsql;
.filter(e -> { }
if (e.getValue() instanceof Optional<?> opt) { filtersDiv.add(rsql.components());
return opt.isPresent(); }
} else {
return e.getValue() != null; public void setFilter(String string, boolean allowToggle) {
} var otherFilter = rsql == primaryRsql ? secondaryRsql : primaryRsql;
}) Stream<Filter.Rsql> rsqlFIlter;
.filter(e -> !(e.getValue() instanceof Collection<?> coll && coll.isEmpty())) // logic to find the filter to use
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); if (allowToggle) {
rsqlFIlter = Stream.of(this.rsql);
} else {
rsqlFIlter = Stream.of(this.rsql, otherFilter);
}
rsqlFIlter.filter(RsqlRw.class::isInstance).findFirst().map(RsqlRw.class::cast).ifPresent(f -> {
if (f == otherFilter) {
toggle();
}
f.setFilter(string);
});
}
public static String filter(final Map<Object, Object> keyToValues) {
final Map<Object, Object> normalized = new HashMap<>(keyToValues)
.entrySet()
.stream()
.map(e -> {
if (e.getValue() instanceof Optional<?> opt) {
e.setValue(opt.orElse(null));
}
return e;
})
.filter(e -> !ObjectUtils.isEmpty(e))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (normalized.isEmpty()) { if (normalized.isEmpty()) {
return null; return null;
} else if (normalized.size() == 1) {
return normalized.entrySet().stream()
.findFirst().map(e -> filter(e.getKey(), e.getValue())).orElse(null); // never return null!
} else { } else {
final StringBuilder sb = new StringBuilder(); final StringBuilder sb = new StringBuilder();
normalized.forEach((k, v) -> { normalized.forEach((k, v) -> {
if (v instanceof Collection<?>) { if (k instanceof Collection<?> keyList) {
sb.append('(').append(filter(k, v)).append(')'); sb.append('(').append(
} else if (v instanceof Optional<?> opt) { keyList.stream().map(subKey -> filter((String) subKey, v))
sb.append(filter(k, opt.get())); .collect(Collectors.joining(" or "))).append(")");
} else { } else if (k instanceof String key) {
sb.append(filter(k, v)); if (v instanceof Collection<?>) {
sb.append('(').append(filter(key, v)).append(')');
} else {
sb.append(filter(key, v));
}
} }
sb.append(';'); sb.append(';');
}); });
@@ -158,4 +185,9 @@ public class Filter extends Div {
String filter(); String filter();
} }
public interface RsqlRw {
void setFilter(String filter);
}
} }

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple.view.util;
import com.vaadin.flow.component.card.Card;
import com.vaadin.flow.component.card.CardVariant;
import com.vaadin.flow.component.html.Anchor;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
public class LinkedTextArea extends Div {
String queryPrefix;
Card card;
public LinkedTextArea(String title, String queryPrefix) {
super();
card = new Card();
card.setTitle(title);
this.queryPrefix = queryPrefix;
}
public void setValueWithLink(String value, String query) {
var span = new Span(value);
span.setWhiteSpace(WhiteSpace.PRE_WRAP);
card.add(span);
card.addThemeVariants(CardVariant.LUMO_ELEVATED);
if (query != null) {
var a = new Anchor(queryPrefix + query, card);
a.addClassName("nocolor");
add(a);
} else {
add(card);
}
}
}

View File

@@ -10,6 +10,7 @@
package org.eclipse.hawkbit.ui.simple.view.util; package org.eclipse.hawkbit.ui.simple.view.util;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.BiFunction; import java.util.function.BiFunction;
@@ -47,7 +48,11 @@ public class SelectionGrid<T, ID> extends Grid<T> {
final Stream<T> fetch = queryFn.apply(query, rsqlFilter); final Stream<T> fetch = queryFn.apply(query, rsqlFilter);
final Set<T> selected = getSelectedItems(); final Set<T> selected = getSelectedItems();
if (selected == null || selected.isEmpty()) { if (selected == null || selected.isEmpty()) {
return fetch; final List<T> fetchList = fetch.toList();
if (fetchList.size() == 1) {
this.setDetailsVisible(fetchList.get(0), true);
}
return fetchList.stream();
} else { } else {
final Set<ID> selectedIds = new HashSet<>(); final Set<ID> selectedIds = new HashSet<>();
selected.forEach(next -> selectedIds.add(entityRepresentation.idFn.apply(next))); selected.forEach(next -> selectedIds.add(entityRepresentation.idFn.apply(next)));
@@ -61,10 +66,11 @@ public class SelectionGrid<T, ID> extends Grid<T> {
} // else externally managed } // else externally managed
} }
public void setRsqlFilter(final String rsqlFilter) { public void setRsqlFilter(final String rsqlFilter, boolean refreshGrid) {
if (!Objects.equals(this.rsqlFilter, rsqlFilter)) { if (!Objects.equals(this.rsqlFilter, rsqlFilter)) {
this.rsqlFilter = rsqlFilter; this.rsqlFilter = rsqlFilter;
refreshGrid(true); if (refreshGrid)
refreshGrid(true);
} }
} }

View File

@@ -15,6 +15,7 @@ import java.util.function.Function;
import java.util.stream.Stream; import java.util.stream.Stream;
import com.vaadin.flow.component.Component; import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Div;
@@ -28,17 +29,21 @@ import com.vaadin.flow.component.splitlayout.SplitLayoutVariant;
import com.vaadin.flow.data.provider.Query; import com.vaadin.flow.data.provider.Query;
import com.vaadin.flow.data.renderer.ComponentRenderer; import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.function.SerializableFunction; import com.vaadin.flow.function.SerializableFunction;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.NavigationTrigger;
import com.vaadin.flow.theme.lumo.LumoUtility; import com.vaadin.flow.theme.lumo.LumoUtility;
import org.eclipse.hawkbit.ui.simple.view.Constants; import org.eclipse.hawkbit.ui.simple.view.Constants;
@SuppressWarnings("java:S119") // better readability @SuppressWarnings("java:S119") // better readability
public class TableView<T, ID> extends Div implements Constants { public class TableView<T, ID> extends Div implements Constants, BeforeEnterObserver {
private static final String COLOR = "color"; private static final String COLOR = "color";
private static final String VAR_LUMO_SECONDARY_TEXT_COLOR = "var(--lumo-secondary-text-color)"; private static final String VAR_LUMO_SECONDARY_TEXT_COLOR = "var(--lumo-secondary-text-color)";
private static final String VAR_LUMO_PRIMARY_COLOR = "var(--lumo-primary-color)"; private static final String VAR_LUMO_PRIMARY_COLOR = "var(--lumo-primary-color)";
private static final int DEFAULT_OPEN_POSITION_SIZE = 50; private static final int DEFAULT_OPEN_POSITION_SIZE = 50;
private static final String QUERY_PARAM_FILTER = "q";
protected SelectionGrid<T, ID> selectionGrid; protected SelectionGrid<T, ID> selectionGrid;
private final Filter filter; private final Filter filter;
@@ -83,8 +88,14 @@ public class TableView<T, ID> extends Div implements Constants {
filter = new Filter( filter = new Filter(
(rsqlFilter) -> { (rsqlFilter) -> {
selectionGrid.setRsqlFilter(rsqlFilter);
closeDetailsPanel(); closeDetailsPanel();
if (rsqlFilter != null) {
var queryParameters = UI.getCurrent().getActiveViewLocation()
.getQueryParameters()
.merging(QUERY_PARAM_FILTER, rsqlFilter);
UI.getCurrent().navigate(this.getClass(), queryParameters);
}
selectionGrid.setRsqlFilter(rsqlFilter, true);
}, rsql, alternativeRsql }, rsql, alternativeRsql
); );
gridLayout = new VerticalLayout(filter, splitLayout); gridLayout = new VerticalLayout(filter, splitLayout);
@@ -147,4 +158,16 @@ public class TableView<T, ID> extends Div implements Constants {
return button; return button;
}; };
} }
@Override
public void beforeEnter(BeforeEnterEvent event) {
var params = event.getLocation().getQueryParameters();
params.getSingleParameter(QUERY_PARAM_FILTER)
.ifPresent(f -> {
var newPage = event.getTrigger() == NavigationTrigger.UI_NAVIGATE;
selectionGrid.setRsqlFilter(f, newPage);
filter.setFilter(f, newPage);
});
}
} }

View File

@@ -9,14 +9,31 @@
*/ */
package org.eclipse.hawkbit.ui.simple.view.util; package org.eclipse.hawkbit.ui.simple.view.util;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage; import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.ToLongFunction;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.IconFactory;
import com.vaadin.flow.data.provider.QuerySortOrder;
import com.vaadin.flow.data.provider.SortDirection;
import com.vaadin.flow.data.renderer.LocalDateTimeRenderer;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
import org.eclipse.hawkbit.ui.simple.view.Constants; import org.eclipse.hawkbit.ui.simple.view.Constants;
@@ -44,6 +61,7 @@ import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.theme.lumo.LumoUtility; import com.vaadin.flow.theme.lumo.LumoUtility;
@Slf4j
public class Utils { public class Utils {
private Utils() { private Utils() {
@@ -117,7 +135,8 @@ public class Utils {
return layout; return layout;
} }
private static <T, ID> ConfirmDialog promptForDeleteConfirmation(Function<SelectionGrid<T, ID>, CompletionStage<Void>> removeHandler, SelectionGrid<T, ID> selectionGrid) { private static <T, ID> ConfirmDialog promptForDeleteConfirmation(Function<SelectionGrid<T, ID>, CompletionStage<Void>> removeHandler,
SelectionGrid<T, ID> selectionGrid) {
final ConfirmDialog dialog = new ConfirmDialog(); final ConfirmDialog dialog = new ConfirmDialog();
dialog.setHeader("Confirm Deletion"); dialog.setHeader("Confirm Deletion");
dialog.setText("Are you sure you want to delete the selected items? This action cannot be undone."); dialog.setText("Are you sure you want to delete the selected items? This action cannot be undone.");
@@ -139,7 +158,7 @@ public class Utils {
public static <T> void remove(final Collection<T> remove, final Set<T> from, final Function<T, ?> idFn) { public static <T> void remove(final Collection<T> remove, final Set<T> from, final Function<T, ?> idFn) {
remove.forEach(toRemove -> { remove.forEach(toRemove -> {
final Object id = idFn.apply(toRemove); final Object id = idFn.apply(toRemove);
for (final Iterator<T> i = from.iterator(); i.hasNext(); ) { for (final Iterator<T> i = from.iterator(); i.hasNext();) {
if (idFn.apply(i.next()).equals(id)) { if (idFn.apply(i.next()).equals(id)) {
i.remove(); i.remove();
} }
@@ -175,27 +194,31 @@ public class Utils {
return component; return component;
} }
public static Icon iconColored(final IconFactory component, final String text, final String color) {
var icon = tooltip(component.create(), text);
icon.setColor(color);
return icon;
}
public static Select<MgmtActionType> actionTypeControls(DateTimePicker forceTime) { public static Select<MgmtActionType> actionTypeControls(DateTimePicker forceTime) {
Select<MgmtActionType> actionType = new Select<>(); Select<MgmtActionType> actionType = new Select<>();
actionType.setLabel(Constants.ACTION_TYPE); actionType.setLabel(Constants.ACTION_TYPE);
actionType.setItems(MgmtActionType.values()); actionType.setItems(MgmtActionType.values());
actionType.setValue(MgmtActionType.FORCED); actionType.setValue(MgmtActionType.FORCED);
final ComponentRenderer<Component, MgmtActionType> actionTypeRenderer = new ComponentRenderer<>(actionTypeO -> final ComponentRenderer<Component, MgmtActionType> actionTypeRenderer = new ComponentRenderer<>(actionTypeO -> switch (actionTypeO) {
switch (actionTypeO) { case SOFT -> new Text(Constants.SOFT);
case SOFT -> new Text(Constants.SOFT); case FORCED -> new Text(Constants.FORCED);
case FORCED -> new Text(Constants.FORCED); case DOWNLOAD_ONLY -> new Text(Constants.DOWNLOAD_ONLY);
case DOWNLOAD_ONLY -> new Text(Constants.DOWNLOAD_ONLY); case TIMEFORCED -> forceTime;
case TIMEFORCED -> forceTime; });
});
actionType.addValueChangeListener(e -> actionType.setRenderer(actionTypeRenderer)); actionType.addValueChangeListener(e -> actionType.setRenderer(actionTypeRenderer));
actionType.setItemLabelGenerator(startTypeO -> actionType.setItemLabelGenerator(startTypeO -> switch (startTypeO) {
switch (startTypeO) { case SOFT -> Constants.SOFT;
case SOFT -> Constants.SOFT; case FORCED -> Constants.FORCED;
case FORCED -> Constants.FORCED; case DOWNLOAD_ONLY -> Constants.DOWNLOAD_ONLY;
case DOWNLOAD_ONLY -> Constants.DOWNLOAD_ONLY; case TIMEFORCED -> "Time Forced at " + (forceTime.isEmpty() ? "" : " " + forceTime.getValue());
case TIMEFORCED -> "Time Forced at " + (forceTime.isEmpty() ? "" : " " + forceTime.getValue()); });
});
actionType.setWidthFull(); actionType.setWidthFull();
return actionType; return actionType;
} }
@@ -235,4 +258,58 @@ public class Utils {
super.close(); super.close();
} }
} }
private static ZoneId getZoneId() {
CompletableFuture<ZoneId> zoneId = new CompletableFuture<>();
UI.getCurrent().getPage().retrieveExtendedClientDetails(details -> {
zoneId.complete(ZoneId.of(details.getTimeZoneId()));
});
try {
return zoneId.get(1, TimeUnit.SECONDS);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
} catch (TimeoutException | ExecutionException ignored) {
log.warn("failed to get zone");
}
return ZoneId.systemDefault();
}
public static <G> LocalDateTimeRenderer<G> localDateTimeRenderer(ToLongFunction<G> f) {
return new LocalDateTimeRenderer<>((e) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(f.applyAsLong(e)), getZoneId()),
() -> DateTimeFormatter.ofLocalizedDateTime(
FormatStyle.SHORT,
FormatStyle.MEDIUM).withLocale(UI.getCurrent().getLocale()));
}
public static String localDateTimeFromTs(long timestamp) {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), getZoneId()).format(DateTimeFormatter.ofLocalizedDateTime(
FormatStyle.SHORT,
FormatStyle.MEDIUM).withLocale(UI.getCurrent().getLocale()));
}
public static String getSortParam(List<QuerySortOrder> querySortOrders) {
return getSortParam(querySortOrders, null);
}
public static String getSortParam(List<QuerySortOrder> querySortOrders, String defaultSort) {
if (!querySortOrders.isEmpty()) {
QuerySortOrder firstSort = querySortOrders.get(0);
String order = firstSort.getDirection() == SortDirection.ASCENDING ? "asc" : "desc";
return String.format("%s:%s", firstSort.getSorted(), order);
}
return defaultSort;
}
public static String durationFromMillis(Long time) {
var duration = Duration.between(Instant.ofEpochMilli(time), Instant.now());
var day = duration.toDaysPart();
if (day > 2) {
return day + "d";
}
return duration.withNanos(0).toString()
.substring(2)
.replaceFirst("(^\\d+[HMS]\\d*M*)", "$1")
.toLowerCase();
}
} }

View File

@@ -26,5 +26,7 @@ spring.mustache.check-template-location=false
vaadin.launch-browser=true vaadin.launch-browser=true
# To improve the performance during development. # To improve the performance during development.
# For more information https://vaadin.com/docs/flow/spring/tutorial-spring-configuration.html#special-configuration-parameters # For more information https://vaadin.com/docs/flow/spring/tutorial-spring-configuration.html#special-configuration-parameters
vaadin.whitelisted-packages=com.vaadin,org.vaadin,dev.hilla,org.eclipse.hawkbit vaadin.allowed-packages=com.vaadin,org.vaadin,dev.hilla,org.eclipse.hawkbit
spring.application.name=Simple-UI
server.servlet.session.persistent=false
### Vaadin end ### ### Vaadin end ###

View File

@@ -368,6 +368,13 @@
<groupId>org.jacoco</groupId> <groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId> <artifactId>jacoco-maven-plugin</artifactId>
</plugin> </plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins> </plugins>
<pluginManagement> <pluginManagement>

View File

@@ -41,19 +41,17 @@ hawkBit optionally supports configuring multiple static users through the applic
and password Spring security properties are ignored. and password Spring security properties are ignored.
An example configuration is given below. An example configuration is given below.
hawkbit.server.im.users[0].username=admin hawkbit.security.user.admin.password={noop}admin
hawkbit.server.im.users[0].password={noop}admin hawkbit.security.user.admin.firstname=Test
hawkbit.server.im.users[0].firstname=Test hawkbit.security.user.admin.lastname=Admin
hawkbit.server.im.users[0].lastname=Admin hawkbit.security.user.admin.email=admin@test.de
hawkbit.server.im.users[0].email=admin@test.de hawkbit.security.user.admin.permissions=ALL
hawkbit.server.im.users[0].permissions=ALL
hawkbit.server.im.users[1].username=test hawkbit.security.user.test.password={noop}test
hawkbit.server.im.users[1].password={noop}test hawkbit.security.user.test.firstname=Test
hawkbit.server.im.users[1].firstname=Test hawkbit.security.user.test.lastname=Tester
hawkbit.server.im.users[1].lastname=Tester hawkbit.security.user.test.email=test@tester.com
hawkbit.server.im.users[1].email=test@tester.com hawkbit.security.user.test.permissions=READ_TARGET,UPDATE_TARGET,CREATE_TARGET,DELETE_TARGET
hawkbit.server.im.users[1].permissions=READ_TARGET,UPDATE_TARGET,CREATE_TARGET,DELETE_TARGET
A permissions value of `ALL` will provide that user with all possible permissions. Passwords need to be specified with A permissions value of `ALL` will provide that user with all possible permissions. Passwords need to be specified with
the used password encoder in brackets. In this example, `noop` is used as the plaintext encoder. For production use, it the used password encoder in brackets. In this example, `noop` is used as the plaintext encoder. For production use, it