From d2b8e740565796704a7bbb1634b09c5b18fdeadf Mon Sep 17 00:00:00 2001 From: Florian BEZANNIER <48728684+flobz@users.noreply.github.com> Date: Mon, 28 Jul 2025 15:07:25 +0200 Subject: [PATCH] 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 --- docker/build/build_dev.sh | 4 +- eclipse_codeformatter.xml | 2 +- .../repository/DistributionSetFields.java | 3 + .../json/model/action/MgmtActionStatus.java | 79 ++- .../resource/mapper/MgmtTargetMapper.java | 4 +- hawkbit-simple-ui/pom.xml | 16 +- .../main/frontend/themes/hawkbit/styles.css | 8 +- .../eclipse/hawkbit/ui/simple/MainLayout.java | 12 +- .../hawkbit/ui/simple/SimpleI18NProvider.java | 24 + .../hawkbit/ui/simple/VaadinServiceInit.java | 29 + .../component/TargetActionsHistory.java | 250 ++++++++ .../hawkbit/ui/simple/view/Constants.java | 6 +- .../ui/simple/view/DistributionSetView.java | 144 +++-- .../hawkbit/ui/simple/view/RolloutView.java | 149 ++--- .../ui/simple/view/SoftwareModuleView.java | 75 ++- .../hawkbit/ui/simple/view/TargetView.java | 562 ++++++++---------- .../hawkbit/ui/simple/view/util/Filter.java | 90 ++- .../ui/simple/view/util/LinkedTextArea.java | 43 ++ .../ui/simple/view/util/SelectionGrid.java | 12 +- .../ui/simple/view/util/TableView.java | 27 +- .../hawkbit/ui/simple/view/util/Utils.java | 109 +++- .../src/main/resources/application.properties | 4 +- pom.xml | 7 + site/content/concepts/authorization.md | 22 +- 24 files changed, 1143 insertions(+), 538 deletions(-) create mode 100644 hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/SimpleI18NProvider.java create mode 100644 hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/VaadinServiceInit.java create mode 100644 hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/component/TargetActionsHistory.java create mode 100644 hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/LinkedTextArea.java diff --git a/docker/build/build_dev.sh b/docker/build/build_dev.sh index 42d7915e9..caf6bd0b8 100755 --- a/docker/build/build_dev.sh +++ b/docker/build/build_dev.sh @@ -9,7 +9,7 @@ # SPDX-License-Identifier: EPL-2.0 # -#set -xe +set -xe # Usage: builds all docker images. Use: # -v to pass version @@ -20,6 +20,8 @@ VERSION=0-SNAPSHOT MVN_REPO=~/.m2/repository TAG=latest +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +cd "${SCRIPT_DIR}" while getopts v:r:t: option do diff --git a/eclipse_codeformatter.xml b/eclipse_codeformatter.xml index f087b31eb..a985b3a23 100644 --- a/eclipse_codeformatter.xml +++ b/eclipse_codeformatter.xml @@ -497,7 +497,7 @@ - + diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFields.java index 1ca1a7925..fb70d123c 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFields.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/DistributionSetFields.java @@ -20,6 +20,7 @@ import lombok.Getter; @Getter public enum DistributionSetFields implements RsqlQueryField { +// @formatter:off ID("id"), TYPE("type", "key"), NAME("name"), @@ -30,8 +31,10 @@ public enum DistributionSetFields implements RsqlQueryField { COMPLETE("complete"), MODULE("modules", SoftwareModuleFields.ID.getJpaEntityFieldName(), SoftwareModuleFields.NAME.getJpaEntityFieldName()), TAG("tags", "name"), + TYPENAME("typeName"), METADATA("metadata"), VALID("valid"); +// @formatter:on private final String jpaEntityFieldName; private final List subEntityAttributes; diff --git a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtActionStatus.java b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtActionStatus.java index adff564f3..d8b5b963b 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtActionStatus.java +++ b/hawkbit-mgmt/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/action/MgmtActionStatus.java @@ -11,9 +11,11 @@ package org.eclipse.hawkbit.mgmt.json.model.action; import java.util.List; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonValue; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.experimental.Accessors; @@ -31,7 +33,7 @@ public class MgmtActionStatus { private Long id; @Schema(example = "running") - private String type; + private StatusType type; private List messages; @@ -43,4 +45,79 @@ public class MgmtActionStatus { @Schema(example = "200") 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()); + } + } } \ No newline at end of file diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtTargetMapper.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtTargetMapper.java index 717eaac7e..05df491cd 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtTargetMapper.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/mapper/MgmtTargetMapper.java @@ -207,7 +207,7 @@ public final class MgmtTargetMapper { return actionStatus.stream() .map(status -> toResponse(status, 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())) .toList(); } @@ -346,7 +346,7 @@ public final class MgmtTargetMapper { result.setReportedAt(actionStatus.getCreatedAt()); result.setTimestamp(actionStatus.getOccurredAt()); result.setId(actionStatus.getId()); - result.setType(actionStatus.getStatus().name().toLowerCase()); + result.setType(MgmtActionStatus.StatusType.forValue(actionStatus.getStatus().name())); actionStatus.getCode().ifPresent(result::setCode); return result; diff --git a/hawkbit-simple-ui/pom.xml b/hawkbit-simple-ui/pom.xml index 973fab2aa..155e0d91b 100644 --- a/hawkbit-simple-ui/pom.xml +++ b/hawkbit-simple-ui/pom.xml @@ -65,12 +65,6 @@ com.vaadin vaadin-core - - - com.vaadin - vaadin-dev - - com.vaadin @@ -85,6 +79,11 @@ org.springframework.boot spring-boot-starter-oauth2-client + + org.springframework.boot + spring-boot-devtools + true + @@ -93,6 +92,11 @@ org.springframework.boot spring-boot-maven-plugin + + false + Simple-UI + org.eclipse.hawkbit.ui.simple.SimpleUIApp + diff --git a/hawkbit-simple-ui/src/main/frontend/themes/hawkbit/styles.css b/hawkbit-simple-ui/src/main/frontend/themes/hawkbit/styles.css index fdae9c548..144131688 100644 --- a/hawkbit-simple-ui/src/main/frontend/themes/hawkbit/styles.css +++ b/hawkbit-simple-ui/src/main/frontend/themes/hawkbit/styles.css @@ -1 +1,7 @@ -/* Import your application global css files here or add the styles directly to this file */ \ No newline at end of file +a.nocolor:link { + color: inherit; +} + +a.nocolor:visited { + color: inherit; +} \ No newline at end of file diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/MainLayout.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/MainLayout.java index 9f22500ad..6848a0e19 100644 --- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/MainLayout.java +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/MainLayout.java @@ -9,8 +9,11 @@ */ package org.eclipse.hawkbit.ui.simple; +import java.util.List; 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.applayout.AppLayout; import com.vaadin.flow.component.applayout.DrawerToggle; @@ -47,9 +50,12 @@ import org.eclipse.hawkbit.ui.simple.view.TargetView; */ public class MainLayout extends AppLayout { + static final List> DEFAULT_VIEW_PRIORITY = List.of(TargetView.class, DistributionSetView.class, + SoftwareModuleView.class, RolloutView.class); private final transient AuthenticatedUser authenticatedUser; private final AccessAnnotationChecker accessChecker; private H2 viewTitle; + private transient Optional> defaultView; public MainLayout(final AuthenticatedUser authenticatedUser, final AccessAnnotationChecker accessChecker) { this.authenticatedUser = authenticatedUser; @@ -68,6 +74,9 @@ public class MainLayout extends AppLayout { Optional.ofNullable(getContent().getClass().getAnnotation(PageTitle.class)) .map(PageTitle::value) .orElse("")); + if (UI.getCurrent().getActiveViewLocation().getPath().isEmpty()) { + defaultView.ifPresent(c -> UI.getCurrent().navigate(c)); + } } private void addHeaderContent() { @@ -81,7 +90,7 @@ public class MainLayout extends AppLayout { } private void addDrawerContent() { - final H1 appName = new H1("hawkBit UI (Experimental!)"); + final H1 appName = new H1("hawkBit UI"); final HorizontalLayout layout = new HorizontalLayout(); layout.setPadding(true); layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); @@ -117,6 +126,7 @@ public class MainLayout extends AppLayout { if (accessChecker.hasAccess(AboutView.class)) { nav.addItem(new SideNavItem("About", AboutView.class, VaadinIcon.INFO_CIRCLE.create())); } + defaultView = DEFAULT_VIEW_PRIORITY.stream().filter(accessChecker::hasAccess).findFirst(); return nav; } diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/SimpleI18NProvider.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/SimpleI18NProvider.java new file mode 100644 index 000000000..aa95f8d39 --- /dev/null +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/SimpleI18NProvider.java @@ -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()); + } +} diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/VaadinServiceInit.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/VaadinServiceInit.java new file mode 100644 index 000000000..55941eac6 --- /dev/null +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/VaadinServiceInit.java @@ -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 -> {}); + }); + } +} diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/component/TargetActionsHistory.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/component/TargetActionsHistory.java new file mode 100644 index 000000000..9db88d091 --- /dev/null +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/component/TargetActionsHistory.java @@ -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 { + + 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 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 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; + } + } +} diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/Constants.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/Constants.java index a18f02270..9b99f4c9b 100644 --- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/Constants.java +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/Constants.java @@ -10,12 +10,13 @@ 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 -// which is not not better +// which is not not better @SuppressWarnings("java:S1214") public interface Constants { // properties String ID = "Id"; + String ADDRESS = "Address"; String NAME = "Name"; String DESCRIPTION = "Description"; String VERSION = "Version"; @@ -54,7 +55,10 @@ public interface Constants { String CANCEL = "Cancel"; String CANCEL_ESC = "Cancel (Esc)"; + String CREATED_AT_DESC = "createdAt:desc"; + String NAME_ASC = "name:asc"; + String NAME_DESC = "name:desc"; String NOT_AVAILABLE_NULL = "n/a (null)"; } \ No newline at end of file diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/DistributionSetView.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/DistributionSetView.java index 057ba438b..013541d35 100644 --- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/DistributionSetView.java +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/DistributionSetView.java @@ -11,15 +11,17 @@ package org.eclipse.hawkbit.ui.simple.view; import java.util.Collection; import java.util.Collections; -import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; 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 com.vaadin.flow.component.Component; @@ -64,25 +66,32 @@ public class DistributionSetView extends TableView { public DistributionSetView(final HawkbitMgmtClient hawkbitClient) { super( new DistributionSetFilter(hawkbitClient), + new DistributionSetRawFilter(), new SelectionGrid.EntityRepresentation<>(MgmtDistributionSet.class, MgmtDistributionSet::getId) { private final DistributionSetDetails details = new DistributionSetDetails(hawkbitClient); @Override protected void addColumns(Grid grid) { - grid.addColumn(MgmtDistributionSet::getId).setHeader(Constants.ID).setAutoWidth(true); - grid.addColumn(MgmtDistributionSet::getName).setHeader(Constants.NAME).setAutoWidth(true); - grid.addColumn(MgmtDistributionSet::getVersion).setHeader(Constants.VERSION).setAutoWidth(true); - grid.addColumn(MgmtDistributionSet::getTypeName).setHeader(Constants.TYPE).setAutoWidth(true); + var createdAtCol = grid.addColumn(Utils.localDateTimeRenderer(MgmtDistributionSet::getCreatedAt)).setHeader( + Constants.CREATED_AT).setAutoWidth(true).setKey("createdAt").setSortable(true); + grid.addColumn(MgmtDistributionSet::getName).setHeader(Constants.NAME).setAutoWidth(true).setKey("name").setSortable( + 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<>( () -> details, DistributionSetDetails::setItem)); } }, (query, rsqlFilter) -> Optional.ofNullable( - hawkbitClient.getDistributionSetRestApi() - .getDistributionSets(rsqlFilter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC) - .getBody()) + hawkbitClient.getDistributionSetRestApi() + .getDistributionSets(rsqlFilter, query.getOffset(), query.getPageSize(), Utils.getSortParam(query + .getSortOrders())) + .getBody()) .stream().flatMap(body -> body.getContent().stream()), e -> new CreateDialog(hawkbitClient).result(), selectionGrid -> { @@ -90,7 +99,7 @@ public class DistributionSetView extends TableView { distributionSet -> hawkbitClient.getDistributionSetRestApi() .deleteDistributionSet(distributionSet.getId())); return CompletableFuture.completedFuture(null); - }); + }, null); } private static SelectionGrid selectSoftwareModuleGrid() { @@ -109,42 +118,66 @@ public class DistributionSetView extends TableView { }); } - private static class DistributionSetFilter implements Filter.Rsql { + private static class DistributionSetRawFilter implements Filter.Rsql, Filter.RsqlRw { private final TextField name = Utils.textField("Name"); + + private DistributionSetRawFilter() { + name.setPlaceholder(""); + } + + @Override + public List 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 type = new CheckboxGroup<>("Type"); private final CheckboxGroup tag = new CheckboxGroup<>("Tag"); private DistributionSetFilter(final HawkbitMgmtClient hawkbitClient) { - name.setPlaceholder(""); + textFilter.setPlaceholder(""); type.setItemLabelGenerator(MgmtDistributionSetType::getName); type.setItems(Optional.ofNullable( - hawkbitClient.getDistributionSetTypeRestApi() - .getDistributionSetTypes(null, 0, 20, Constants.NAME_ASC) - .getBody()) + hawkbitClient.getDistributionSetTypeRestApi() + .getDistributionSetTypes(null, 0, 20, Constants.NAME_ASC) + .getBody()) .map(PagedList::getContent) .orElseGet(Collections::emptyList)); tag.setItemLabelGenerator(MgmtTag::getName); tag.setItems(Optional.ofNullable( - hawkbitClient.getDistributionSetTagRestApi() - .getDistributionSetTags(null, 0, 20, Constants.NAME_ASC) - .getBody()) + hawkbitClient.getDistributionSetTagRestApi() + .getDistributionSetTags(null, 0, 20, Constants.NAME_ASC) + .getBody()) .map(PagedList::getContent) .orElseGet(Collections::emptyList)); } @Override public List components() { - return List.of(name, type); + return List.of(textFilter, type); } @Override public String filter() { return Filter.filter( Map.of( - "name", name.getOptionalValue(), + List.of("version", "name"), textFilter.getOptionalValue().map(s -> "*" + s + "*"), "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 { private final TextField createdAt = Utils.textField("Created at"); private final TextField lastModifiedBy = Utils.textField("Last modified by"); private final TextField lastModifiedAt = Utils.textField("Last modified at"); + private final TextArea metadata = new TextArea("Metadata"); private final SelectionGrid softwareModulesGrid = selectSoftwareModuleGrid(); private DistributionSetDetails(final HawkbitMgmtClient hawkbitClient) { @@ -164,9 +198,9 @@ public class DistributionSetView extends TableView { description.setMinLength(2); Stream.of( - description, - createdBy, createdAt, - lastModifiedBy, lastModifiedAt) + description, + createdBy, createdAt, + lastModifiedBy, lastModifiedAt, metadata) .forEach(field -> { field.setReadOnly(true); add(field); @@ -181,9 +215,14 @@ public class DistributionSetView extends TableView { private void setItem(final MgmtDistributionSet distributionSet) { description.setValue(distributionSet.getDescription()); createdBy.setValue(distributionSet.getCreatedBy()); - createdAt.setValue(new Date(distributionSet.getCreatedAt()).toString()); + createdAt.setValue(Utils.localDateTimeFromTs(distributionSet.getCreatedAt())); 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( hawkbitClient.getDistributionSetRestApi() @@ -214,9 +253,9 @@ public class DistributionSetView extends TableView { "Type", this::readyToCreate, Optional.ofNullable( - hawkbitClient.getDistributionSetTypeRestApi() - .getDistributionSetTypes(null, 0, 30, Constants.NAME_ASC) - .getBody()) + hawkbitClient.getDistributionSetTypeRestApi() + .getDistributionSetTypes(null, 0, 30, Constants.CREATED_AT_DESC) + .getBody()) .map(body -> body.getContent().toArray(new MgmtDistributionSetType[0])) .orElseGet(() -> new MgmtDistributionSetType[0])); type.focus(); @@ -262,15 +301,15 @@ public class DistributionSetView extends TableView { create.addClickListener(e -> { close(); final long distributionSetId = Optional.ofNullable( - hawkbitClient.getDistributionSetRestApi() - .createDistributionSets( - List.of((MgmtDistributionSetRequestBodyPost) new MgmtDistributionSetRequestBodyPost() - .setType(type.getValue().getKey()) - .setName(name.getValue()) - .setVersion(version.getValue()) - .setDescription(description.getValue()) - .setRequiredMigrationStep(requiredMigrationStep.getValue()))) - .getBody()) + hawkbitClient.getDistributionSetRestApi() + .createDistributionSets( + List.of((MgmtDistributionSetRequestBodyPost) new MgmtDistributionSetRequestBodyPost() + .setType(type.getValue().getKey()) + .setName(name.getValue()) + .setVersion(version.getValue()) + .setDescription(description.getValue()) + .setRequiredMigrationStep(requiredMigrationStep.getValue()))) + .getBody()) .stream() .flatMap(Collection::stream) .findFirst() @@ -281,7 +320,7 @@ public class DistributionSetView extends TableView { } } - @SuppressWarnings({"java:S1171", "java:S3599"}) + @SuppressWarnings({ "java:S1171", "java:S3599" }) private static class AddSoftwareModulesDialog extends Utils.BaseDialog { private final transient Set softwareModules = Collections.synchronizedSet(new HashSet<>()); @@ -296,19 +335,22 @@ public class DistributionSetView extends TableView { }); final Component addRemoveControls = Utils.addRemoveControls( - v -> new Utils.BaseDialog("Add Software Modules") {{ - final SoftwareModuleView softwareModulesView = new SoftwareModuleView(false, hawkbitClient); - add(softwareModulesView); - final Button addBtn = new Button("Add"); - addBtn.addClickListener(e -> { - softwareModules.addAll(softwareModulesView.getSelection()); - softwareModulesGrid.refreshGrid(false); - close(); - }); - addBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - getFooter().add(addBtn); - open(); - }}.result(), + v -> new Utils.BaseDialog("Add Software Modules") { + + { + final SoftwareModuleView softwareModulesView = new SoftwareModuleView(false, hawkbitClient); + add(softwareModulesView); + final Button addBtn = new Button("Add"); + addBtn.addClickListener(e -> { + softwareModules.addAll(softwareModulesView.getSelection()); + softwareModulesGrid.refreshGrid(false); + close(); + }); + addBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + getFooter().add(addBtn); + open(); + } + }.result(), v -> { Utils.remove(softwareModulesGrid.getSelectedItems(), softwareModules, MgmtSoftwareModule::getId); softwareModulesGrid.refreshGrid(false); diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/RolloutView.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/RolloutView.java index fd4843ddd..955d6cdd6 100644 --- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/RolloutView.java +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/RolloutView.java @@ -10,7 +10,6 @@ package org.eclipse.hawkbit.ui.simple.view; import java.time.ZoneOffset; -import java.util.Date; import java.util.List; import java.util.Map; import java.util.Optional; @@ -61,7 +60,7 @@ import org.springframework.util.ObjectUtils; @Route(value = "rollouts", layout = MainLayout.class) @RolesAllowed({ "ROLLOUT_READ" }) @Uses(Icon.class) -@SuppressWarnings({"java:S1171", "java:S3599"}) +@SuppressWarnings({ "java:S1171", "java:S3599" }) public class RolloutView extends TableView { public RolloutView(final HawkbitMgmtClient hawkbitClient) { @@ -118,33 +117,45 @@ public class RolloutView extends TableView { private void init(final MgmtRolloutResponseBody rollout) { if ("READY".equalsIgnoreCase(rollout.getStatus())) { - add(Utils.tooltip(new Button(VaadinIcon.START_COG.create()) {{ - addClickListener(v -> { - hawkbitClient.getRolloutRestApi().start(rollout.getId()); - refresh(); - }); - }}, "Start")); + add(Utils.tooltip(new Button(VaadinIcon.START_COG.create()) { + + { + addClickListener(v -> { + hawkbitClient.getRolloutRestApi().start(rollout.getId()); + refresh(); + }); + } + }, "Start")); } else if ("RUNNING".equalsIgnoreCase(rollout.getStatus())) { - add(Utils.tooltip(new Button(VaadinIcon.PAUSE.create()) {{ - addClickListener(v -> { - hawkbitClient.getRolloutRestApi().pause(rollout.getId()); - refresh(); - }); - }}, "Pause")); + add(Utils.tooltip(new Button(VaadinIcon.PAUSE.create()) { + + { + addClickListener(v -> { + hawkbitClient.getRolloutRestApi().pause(rollout.getId()); + refresh(); + }); + } + }, "Pause")); } else if ("PAUSED".equalsIgnoreCase(rollout.getStatus())) { - add(Utils.tooltip(new Button(VaadinIcon.START_COG.create()) {{ - addClickListener(v -> { - hawkbitClient.getRolloutRestApi().resume(rollout.getId()); - refresh(); - }); - }}, "Resume")); + add(Utils.tooltip(new Button(VaadinIcon.START_COG.create()) { + + { + addClickListener(v -> { + hawkbitClient.getRolloutRestApi().resume(rollout.getId()); + refresh(); + }); + } + }, "Resume")); } - add(Utils.tooltip(new Button(VaadinIcon.TRASH.create()) {{ - addClickListener(v -> { - hawkbitClient.getRolloutRestApi().delete(rollout.getId()); - grid.getDataProvider().refreshAll(); - }); - }}, "Cancel and Remove")); + add(Utils.tooltip(new Button(VaadinIcon.TRASH.create()) { + + { + addClickListener(v -> { + hawkbitClient.getRolloutRestApi().delete(rollout.getId()); + grid.getDataProvider().refreshAll(); + }); + } + }, "Cancel and Remove")); } private void refresh() { @@ -197,11 +208,11 @@ public class RolloutView extends TableView { description.setMinLength(2); groupGrid = createGroupGrid(); Stream.of( - description, - createdBy, createdAt, - lastModifiedBy, lastModifiedAt, - targetFilter, distributionSet, - actonType, startAt) + description, + createdBy, createdAt, + lastModifiedBy, lastModifiedAt, + targetFilter, distributionSet, + actonType, startAt) .forEach(field -> { field.setReadOnly(true); add(field); @@ -219,9 +230,9 @@ public class RolloutView extends TableView { private void setItem(final MgmtRolloutResponseBody rollout) { description.setValue(rollout.getDescription()); createdBy.setValue(rollout.getCreatedBy()); - createdAt.setValue(new Date(rollout.getCreatedAt()).toString()); + createdAt.setValue(Utils.localDateTimeFromTs(rollout.getCreatedAt())); lastModifiedBy.setValue(rollout.getLastModifiedBy()); - lastModifiedAt.setValue(new Date(rollout.getLastModifiedAt()).toString()); + lastModifiedAt.setValue(Utils.localDateTimeFromTs(rollout.getLastModifiedAt())); targetFilter.setValue(rollout.getTargetFilterQuery()); final MgmtDistributionSet distributionSetMgmt = hawkbitClient.getDistributionSetRestApi() .getDistributionSet(rollout.getDistributionSetId()).getBody(); @@ -232,18 +243,18 @@ public class RolloutView extends TableView { case SOFT -> Constants.SOFT; case FORCED -> Constants.FORCED; 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()); groupGrid.setItems(query -> Optional.ofNullable( - hawkbitClient.getRolloutRestApi() - .getRolloutGroups( - rollout.getId(), - null, query.getOffset(), query.getPageSize(), - null, "full") - .getBody()) + hawkbitClient.getRolloutRestApi() + .getRolloutGroups( + rollout.getId(), + null, query.getOffset(), query.getPageSize(), + null, "full") + .getBody()) .stream().flatMap(body -> body.getContent().stream()) .skip(query.getOffset()) .limit(query.getPageSize())); @@ -259,7 +270,8 @@ public class RolloutView extends TableView { grid.addColumn(MgmtRolloutGroupResponseBody::getId).setHeader(Constants.ID).setAutoWidth(true); grid.addColumn(MgmtRolloutGroupResponseBody::getName).setHeader(Constants.NAME).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); } }); @@ -291,22 +303,21 @@ public class RolloutView extends TableView { "Distribution Set", this::readyToCreate, Optional.ofNullable( - hawkbitClient.getDistributionSetRestApi() - .getDistributionSets(null, 0, 30, Constants.NAME_ASC) - .getBody()) + hawkbitClient.getDistributionSetRestApi() + .getDistributionSets(null, 0, 30, Constants.CREATED_AT_DESC) + .getBody()) .map(body -> body.getContent().toArray(new MgmtDistributionSet[0])) .orElseGet(() -> new MgmtDistributionSet[0])); distributionSet.setRequiredIndicatorVisible(true); - distributionSet.setItemLabelGenerator(distributionSetO -> - distributionSetO.getName() + ":" + distributionSetO.getVersion()); + distributionSet.setItemLabelGenerator(distributionSetO -> distributionSetO.getName() + ":" + distributionSetO.getVersion()); distributionSet.setWidthFull(); targetFilter = new Select<>( "Target Filter", this::readyToCreate, Optional.ofNullable( - hawkbitClient.getTargetFilterQueryRestApi() - .getFilters(null, 0, 30, Constants.NAME_ASC, null) - .getBody()) + hawkbitClient.getTargetFilterQueryRestApi() + .getFilters(null, 0, 30, Constants.NAME_ASC, null) + .getBody()) .map(body -> body.getContent().toArray(new MgmtTargetFilterQuery[0])) .orElseGet(() -> new MgmtTargetFilterQuery[0])); targetFilter.setRequiredIndicatorVisible(true); @@ -323,20 +334,18 @@ public class RolloutView extends TableView { startType.setLabel(Constants.START_TYPE); startType.setItems(StartType.values()); startType.setValue(StartType.MANUAL); - final ComponentRenderer startTypeRenderer = new ComponentRenderer<>(startTypeO -> - switch (startTypeO) { - case MANUAL -> new Text(Constants.MANUAL); - case AUTO -> new Text(Constants.AUTO); - case SCHEDULED -> startAt; - }); + final ComponentRenderer startTypeRenderer = new ComponentRenderer<>(startTypeO -> switch (startTypeO) { + case MANUAL -> new Text(Constants.MANUAL); + case AUTO -> new Text(Constants.AUTO); + case SCHEDULED -> startAt; + }); startType.setRenderer(startTypeRenderer); startType.addValueChangeListener(e -> startType.setRenderer(startTypeRenderer)); - startType.setItemLabelGenerator(startTypeO -> - switch (startTypeO) { - case MANUAL -> Constants.MANUAL; - case AUTO -> Constants.AUTO; - case SCHEDULED -> "Scheduled" + (startAt.isEmpty() ? "" : " at " + startAt.getValue()); - }); + startType.setItemLabelGenerator(startTypeO -> switch (startTypeO) { + case MANUAL -> Constants.MANUAL; + case AUTO -> Constants.AUTO; + case SCHEDULED -> "Scheduled" + (startAt.isEmpty() ? "" : " at " + startAt.getValue()); + }); startType.setWidthFull(); final Div percentSuffix = new Div(); @@ -377,12 +386,8 @@ public class RolloutView extends TableView { } private void readyToCreate(final Object v) { - final boolean createEnabled = !name.isEmpty() && - !distributionSet.isEmpty() && - !targetFilter.isEmpty() && - !groupNumber.isEmpty() && - !triggerThreshold.isEmpty() && - !errorThreshold.isEmpty(); + final boolean createEnabled = !name.isEmpty() && !distributionSet.isEmpty() && !targetFilter.isEmpty() && !groupNumber + .isEmpty() && !triggerThreshold.isEmpty() && !errorThreshold.isEmpty(); if (create.isEnabled() != createEnabled) { create.setEnabled(createEnabled); } @@ -400,16 +405,12 @@ public class RolloutView extends TableView { request.setType(actionType.getValue()); if (actionType.getValue() == MgmtActionType.TIMEFORCED) { request.setForcetime( - forceTime.isEmpty() ? - System.currentTimeMillis() : - forceTime.getValue().toEpochSecond(ZoneOffset.UTC) * 1000); + forceTime.isEmpty() ? System.currentTimeMillis() : forceTime.getValue().toEpochSecond(ZoneOffset.UTC) * 1000); } switch (startType.getValue()) { case AUTO -> request.setStartAt(System.currentTimeMillis()); case SCHEDULED -> request.setStartAt( - startAt.isEmpty() ? - System.currentTimeMillis() : - startAt.getValue().toEpochSecond(ZoneOffset.UTC) * 1000); + startAt.isEmpty() ? System.currentTimeMillis() : startAt.getValue().toEpochSecond(ZoneOffset.UTC) * 1000); case MANUAL -> { // do nothing, will be started manually } diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/SoftwareModuleView.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/SoftwareModuleView.java index 60fb902d8..4db027b1f 100644 --- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/SoftwareModuleView.java +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/SoftwareModuleView.java @@ -14,7 +14,6 @@ import java.io.IOException; import java.io.InputStream; import java.util.Collection; import java.util.Collections; -import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -93,9 +92,9 @@ public class SoftwareModuleView extends TableView { } }, (query, rsqlFilter) -> Optional.ofNullable( - hawkbitClient.getSoftwareModuleRestApi() - .getSoftwareModules(rsqlFilter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC) - .getBody()) + hawkbitClient.getSoftwareModuleRestApi() + .getSoftwareModules(rsqlFilter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC) + .getBody()) .stream().map(PagedList::getContent).flatMap(List::stream), isParent ? v -> new CreateDialog(hawkbitClient).result() : null, isParent ? selectionGrid -> { @@ -132,9 +131,9 @@ public class SoftwareModuleView extends TableView { name.setPlaceholder(""); type.setItemLabelGenerator(MgmtSoftwareModuleType::getName); type.setItems(Optional.ofNullable( - hawkbitClient.getSoftwareModuleTypeRestApi() - .getTypes(null, 0, 20, Constants.NAME_ASC) - .getBody()) + hawkbitClient.getSoftwareModuleTypeRestApi() + .getTypes(null, 0, 20, Constants.NAME_ASC) + .getBody()) .map(PagedList::getContent) .orElseGet(Collections::emptyList)); } @@ -184,14 +183,14 @@ public class SoftwareModuleView extends TableView { private void setItem(final MgmtSoftwareModule softwareModule) { description.setValue(softwareModule.getDescription()); createdBy.setValue(softwareModule.getCreatedBy()); - createdAt.setValue(new Date(softwareModule.getCreatedAt()).toString()); + createdAt.setValue(Utils.localDateTimeFromTs(softwareModule.getCreatedAt())); lastModifiedBy.setValue(softwareModule.getLastModifiedBy()); - lastModifiedAt.setValue(new Date(softwareModule.getLastModifiedAt()).toString()); + lastModifiedAt.setValue(Utils.localDateTimeFromTs(softwareModule.getLastModifiedAt())); artifactGrid.setItems(query -> Optional.ofNullable( - hawkbitClient.getSoftwareModuleRestApi() - .getArtifacts(softwareModule.getId(), null, null) - .getBody()) + hawkbitClient.getSoftwareModuleRestApi() + .getArtifacts(softwareModule.getId(), null, null) + .getBody()) .stream() .flatMap(Collection::stream) .skip(query.getOffset()) @@ -220,9 +219,9 @@ public class SoftwareModuleView extends TableView { Constants.TYPE, this::readyToCreate, Optional.ofNullable( - hawkbitClient.getSoftwareModuleTypeRestApi() - .getTypes(null, 0, 30, Constants.NAME_ASC) - .getBody()) + hawkbitClient.getSoftwareModuleTypeRestApi() + .getTypes(null, 0, 30, Constants.NAME_ASC) + .getBody()) .map(body -> body.getContent().toArray(new MgmtSoftwareModuleType[0])) .orElseGet(() -> new MgmtSoftwareModuleType[0])); type.setWidthFull(); @@ -253,9 +252,9 @@ public class SoftwareModuleView extends TableView { if (Boolean.TRUE.equals(createDistributionSet.getValue()) && distType.isEmpty()) { distType.setItems( Optional.ofNullable( - hawkbitClient.getDistributionSetTypeRestApi() - .getDistributionSetTypes(null, 0, 30, Constants.NAME_ASC) - .getBody()) + hawkbitClient.getDistributionSetTypeRestApi() + .getDistributionSetTypes(null, 0, 30, Constants.NAME_ASC) + .getBody()) .map(body -> body.getContent().toArray(new MgmtDistributionSetType[0])) .orElseGet(() -> new MgmtDistributionSetType[0])); } @@ -286,8 +285,8 @@ public class SoftwareModuleView extends TableView { } private void readyToCreate(final Object v) { - final boolean createEnabled = !type.isEmpty() && !name.isEmpty() && !version.isEmpty() && - (!createDistributionSet.getValue() || !distType.isEmpty()); + final boolean createEnabled = !type.isEmpty() && !name.isEmpty() && !version.isEmpty() && (!createDistributionSet + .getValue() || !distType.isEmpty()); if (create.isEnabled() != createEnabled) { create.setEnabled(createEnabled); } @@ -297,30 +296,30 @@ public class SoftwareModuleView extends TableView { create.addClickListener(e -> { close(); final long softwareModuleId = Optional.ofNullable( - hawkbitClient.getSoftwareModuleRestApi().createSoftwareModules( - List.of(new MgmtSoftwareModuleRequestBodyPost() - .setType(type.getValue().getKey()) - .setName(name.getValue()) - .setVersion(version.getValue()) - .setVendor(vendor.getValue()) - .setDescription(description.getValue()) - .setEncrypted(enableArtifactEncryption.getValue()))) - .getBody()) + hawkbitClient.getSoftwareModuleRestApi().createSoftwareModules( + List.of(new MgmtSoftwareModuleRequestBodyPost() + .setType(type.getValue().getKey()) + .setName(name.getValue()) + .setVersion(version.getValue()) + .setVendor(vendor.getValue()) + .setDescription(description.getValue()) + .setEncrypted(enableArtifactEncryption.getValue()))) + .getBody()) .stream().flatMap(Collection::stream) .findFirst() .orElseThrow() .getId(); if (Boolean.TRUE.equals(createDistributionSet.getValue())) { final long distributionSetId = Optional.ofNullable( - hawkbitClient.getDistributionSetRestApi() - .createDistributionSets( - List.of((MgmtDistributionSetRequestBodyPost) new MgmtDistributionSetRequestBodyPost() - .setType(distType.getValue().getKey()) - .setName(name.getValue()) - .setVersion(version.getValue()) - .setDescription(description.getValue()) - .setRequiredMigrationStep(distRequiredMigrationStep.getValue()))) - .getBody()) + hawkbitClient.getDistributionSetRestApi() + .createDistributionSets( + List.of((MgmtDistributionSetRequestBodyPost) new MgmtDistributionSetRequestBodyPost() + .setType(distType.getValue().getKey()) + .setName(name.getValue()) + .setVersion(version.getValue()) + .setDescription(description.getValue()) + .setRequiredMigrationStep(distRequiredMigrationStep.getValue()))) + .getBody()) .stream() .flatMap(Collection::stream) .findFirst() diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/TargetView.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/TargetView.java index 934d9526a..62757d59b 100644 --- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/TargetView.java +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/TargetView.java @@ -9,11 +9,9 @@ */ package org.eclipse.hawkbit.ui.simple.view; -import java.time.Instant; import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Collections; -import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -27,6 +25,8 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vaadin.flow.data.provider.ListDataProvider; import jakarta.annotation.security.RolesAllowed; 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.checkbox.CheckboxGroup; 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.dependency.Uses; 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.shared.Registration; 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.PagedList; -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.action.MgmtActionStatus; 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.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.ui.simple.HawkbitMgmtClient; 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.LinkedTextArea; 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.Utils; @@ -89,33 +89,46 @@ import org.springframework.util.ObjectUtils; @Route(value = "targets", layout = MainLayout.class) @RolesAllowed({ "TARGET_READ" }) @Uses(Icon.class) -public class TargetView extends TableView { +public class TargetView extends TableView { public static final String STATUS = "Status"; + public static final String UPDATE = "Sync"; public static final String CONTROLLER_ID = "Controller Id"; + public static final String FILTER = "Filter"; public static final String TAG = "Tag"; public TargetView(final HawkbitMgmtClient hawkbitClient) { super( new RawFilter(hawkbitClient), new SimpleFilter(hawkbitClient), - new SelectionGrid.EntityRepresentation<>(MgmtTarget.class, MgmtTarget::getControllerId) { + new SelectionGrid.EntityRepresentation<>(TargetWithDs.class, TargetWithDs::getControllerId) { @Override - protected void addColumns(final Grid grid) { + protected void addColumns(final Grid grid) { grid.addColumn(new ComponentRenderer<>(TargetStatusCell::new)) .setHeader(STATUS) .setAutoWidth(true) - .setFlexGrow(0); - grid.addColumn(MgmtTarget::getControllerId).setHeader(CONTROLLER_ID).setAutoWidth(true); - grid.addColumn(MgmtTarget::getName).setHeader(Constants.NAME).setAutoWidth(true); - grid.addColumn(MgmtTarget::getTargetTypeName).setHeader(Constants.TYPE).setAutoWidth(true); + .setFlexGrow(0).setKey("lastControllerRequestAt").setSortable(true); + grid.addColumn(new ComponentRenderer<>(TargetUpdateStatusCell::new)) + .setHeader(UPDATE) + .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() - .getTargets(filter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC) + .getTargets(filter, query.getOffset(), query.getPageSize(), Utils.getSortParam(query.getSortOrders(), + "lastModifiedAt:desc")) .getBody() .getContent() - .stream(), + .stream().map(m -> TargetWithDs.from(hawkbitClient, m)), source -> new RegisterDialog(hawkbitClient).result(), selectionGrid -> { selectionGrid.getSelectedItems() @@ -129,8 +142,8 @@ public class TargetView extends TableView { } ); - final Function, CompletionStage> assignHandler = - source -> new AssignDialog(hawkbitClient, source.getSelectedItems()).result(); + final Function, CompletionStage> assignHandler = source -> new AssignDialog( + hawkbitClient, source.getSelectedItems()).result(); final Button assignBtn = Utils.tooltip(new Button(VaadinIcon.LINK.create()), "Assign"); assignBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); @@ -142,15 +155,15 @@ public class TargetView extends TableView { private final HawkbitMgmtClient hawkbitClient; - private final TextField controllerId; + private final TextField textFilter; private final CheckboxGroup type; private final CheckboxGroup tag; private SimpleFilter(final HawkbitMgmtClient hawkbitClient) { this.hawkbitClient = hawkbitClient; - controllerId = Utils.textField(CONTROLLER_ID); - controllerId.setPlaceholder(""); + textFilter = Utils.textField(FILTER); + textFilter.setPlaceholder(""); type = new CheckboxGroup<>(Constants.TYPE); type.setItemLabelGenerator(MgmtTargetType::getName); tag = new CheckboxGroup<>(TAG); @@ -160,13 +173,13 @@ public class TargetView extends TableView { @Override public List components() { final List components = new LinkedList<>(); - components.add(controllerId); + components.add(textFilter); 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); } 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); } return components; @@ -176,15 +189,15 @@ public class TargetView extends TableView { public String filter() { return Filter.filter( Map.of( - "controllerid", controllerId.getOptionalValue(), + List.of("controllerid", "name"), textFilter.getOptionalValue().map(s -> "*" + s + "*"), "targettype.name", type.getSelectedItems().stream().map(MgmtTargetType::getName) .toList(), - "tag", tag.getSelectedItems())); + "tag", tag.getSelectedItems().stream().map(MgmtTag::getName).toList())); } } @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", ""); private final VerticalLayout layout = new VerticalLayout(); @@ -209,8 +222,8 @@ public class TargetView extends TableView { }); savedFilters.setEmptySelectionAllowed(true); savedFilters.setItems(listFilters(hawkbitClient)); - savedFilters.setItemLabelGenerator(query -> - Optional.ofNullable(query).map(MgmtTargetFilterQuery::getName).orElse("")); savedFilters.setWidthFull(); textFilter.setWidthFull(); @@ -237,25 +250,27 @@ public class TargetView extends TableView { } private ComponentEventListener> createBtnListener(HawkbitMgmtClient hawkbitClient) { - return e -> - new Utils.BaseDialog("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())); - name.focus(); - finishBtn.addClickShortcut(Key.ENTER); - finishBtn.setEnabled(false); - finishBtn.addClickListener(e -> { - final MgmtTargetFilterQueryRequestBody createRequest = new MgmtTargetFilterQueryRequestBody(); - createRequest.setName(name.getValue()); - createRequest.setQuery(textFilter.getValue()); - hawkbitClient.getTargetFilterQueryRestApi().createFilter(createRequest); - savedFilters.setItems(listFilters(hawkbitClient)); - close(); - }); - getFooter().add(finishBtn); - add(name); - open(); - }}; + return e -> new Utils.BaseDialog("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())); + name.focus(); + finishBtn.addClickShortcut(Key.ENTER); + finishBtn.setEnabled(false); + finishBtn.addClickListener(e -> { + final MgmtTargetFilterQueryRequestBody createRequest = new MgmtTargetFilterQueryRequestBody(); + createRequest.setName(name.getValue()); + createRequest.setQuery(textFilter.getValue()); + hawkbitClient.getTargetFilterQueryRestApi().createFilter(createRequest); + savedFilters.setItems(listFilters(hawkbitClient)); + close(); + }); + getFooter().add(finishBtn); + add(name); + open(); + } + }; } private ComponentEventListener> updateBtnListener(HawkbitMgmtClient hawkbitClient) { @@ -265,34 +280,37 @@ public class TargetView extends TableView { return; } - new Utils.BaseDialog("Update Filter") {{ - final Button finishBtn = Utils.tooltip(new Button("Update"), "Update (Enter)"); - finishBtn.setEnabled(false); + new Utils.BaseDialog("Update Filter") { - final TextField name = Utils.textField(Constants.NAME, e -> finishBtn.setEnabled(!e.getHasValue().isEmpty())); - name.focus(); - name.setValue(selected.getName()); + { + final Button finishBtn = Utils.tooltip(new Button("Update"), "Update (Enter)"); + finishBtn.setEnabled(false); - final TextArea filterValue = new TextArea("Filter Value"); - filterValue.setReadOnly(true); - filterValue.setValue(textFilter.getValue()); - filterValue.setWidthFull(); + final TextField name = Utils.textField(Constants.NAME, e -> finishBtn.setEnabled(!e.getHasValue().isEmpty())); + name.focus(); + name.setValue(selected.getName()); - finishBtn.addClickShortcut(Key.ENTER); - finishBtn.addClickListener(e -> { - 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); + final TextArea filterValue = new TextArea("Filter Value"); + filterValue.setReadOnly(true); + filterValue.setValue(textFilter.getValue()); + filterValue.setWidthFull(); - add(name); - add(filterValue); - open(); - }}; + finishBtn.addClickShortcut(Key.ENTER); + finishBtn.addClickListener(e -> { + 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 { public String filter() { return textFilter.getOptionalValue().orElse(null); } + + @Override + public void setFilter(String filter) { + textFilter.setValue(filter); + } } protected static class TargetDetailedView extends TabSheet { @@ -312,26 +335,26 @@ public class TargetView extends TableView { private final TargetDetails targetDetails; private final TargetAssignedInstalled targetAssignedInstalled; private final TargetTags targetTags; - private final TargetActions targetActions; + private final TargetActionsHistoryLayout targetActionsHistoryLayout; private TargetDetailedView(final HawkbitMgmtClient hawkbitClient) { targetDetails = new TargetDetails(hawkbitClient); targetAssignedInstalled = new TargetAssignedInstalled(hawkbitClient); targetTags = new TargetTags(hawkbitClient); - targetActions = new TargetActions(hawkbitClient); + targetActionsHistoryLayout = new TargetActionsHistoryLayout(hawkbitClient); setWidthFull(); add("Details", targetDetails); add("Assigned / Installed", targetAssignedInstalled); add("Tags", targetTags); - add("Action History", targetActions); + add("Action History", targetActionsHistoryLayout); } private void setItem(final MgmtTarget target) { this.targetDetails.setItem(target); this.targetAssignedInstalled.setItem(target); this.targetTags.setItem(target); - this.targetActions.setItem(target); + this.targetActionsHistoryLayout.setItem(target); } } @@ -346,6 +369,7 @@ public class TargetView extends TableView { private final TextField securityToken = Utils.textField(Constants.SECURITY_TOKEN); private final TextField lastPoll = Utils.textField(Constants.LAST_POLL); 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 transient MgmtTarget target; @@ -353,11 +377,11 @@ public class TargetView extends TableView { this.hawkbitClient = hawkbitClient; description.setMinLength(2); Stream.of( - description, - createdBy, createdAt, - lastModifiedBy, lastModifiedAt, - securityToken, lastPoll, targetAttributes, group - ) + description, + createdBy, createdAt, + lastModifiedBy, lastModifiedAt, + securityToken, lastPoll, group, targetAddress, targetAttributes + ) .forEach(field -> { field.setReadOnly(true); add(field); @@ -375,14 +399,15 @@ public class TargetView extends TableView { protected void onAttach(final AttachEvent attachEvent) { description.setValue(target.getDescription() == null ? "N/A" : target.getDescription()); createdBy.setValue(target.getCreatedBy()); - createdAt.setValue(new Date(target.getCreatedAt()).toString()); + createdAt.setValue(Utils.localDateTimeFromTs(target.getCreatedAt())); lastModifiedBy.setValue(target.getLastModifiedBy()); - lastModifiedAt.setValue(new Date(target.getLastModifiedAt()).toString()); - securityToken.setValue(target.getSecurityToken()); + lastModifiedAt.setValue(Utils.localDateTimeFromTs(target.getLastModifiedAt())); + securityToken.setValue(Objects.requireNonNullElse(target.getSecurityToken(), "")); group.setValue(target.getGroup() != null ? target.getGroup() : ""); + targetAddress.setValue(target.getAddress() != null ? target.getAddress() : ""); 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 response = hawkbitClient.getTargetRestApi().getAttributes(target.getControllerId()); if (response.getStatusCode().is2xxSuccessful()) { targetAttributes.setValue(Objects.requireNonNullElse(response.getBody(), Collections.emptyMap()).entrySet().stream() @@ -397,14 +422,12 @@ public class TargetView extends TableView { private static class TargetAssignedInstalled extends FormLayout { private final transient HawkbitMgmtClient hawkbitClient; - private final TextArea assigned = new TextArea("Assigned Distribution Set"); - private final TextArea installed = new TextArea("Installed Distribution Set"); + private final LinkedTextArea assigned = new LinkedTextArea("Assigned Distribution Set", "/distribution_sets?"); + private final LinkedTextArea installed = new LinkedTextArea("Installed Distribution Set", "/distribution_sets?"); private transient MgmtTarget target; private TargetAssignedInstalled(HawkbitMgmtClient hawkbitClient) { this.hawkbitClient = hawkbitClient; - assigned.setReadOnly(true); - installed.setReadOnly(true); assigned.setWidthFull(); installed.setWidthFull(); add(assigned, installed); @@ -421,22 +444,23 @@ public class TargetView extends TableView { updateDistributionSetInfo(() -> hawkbitClient.getTargetRestApi().getAssignedDistributionSet(target.getControllerId()), assigned); } - private void updateDistributionSetInfo(Supplier> supplier, TextArea textArea) { + private void updateDistributionSetInfo(Supplier> supplier, LinkedTextArea textArea) { Optional.ofNullable(supplier.get()) .map(ResponseEntity::getBody) - .ifPresent(value -> { + .ifPresentOrElse(value -> { final String description = """ Name: %s Version: %s %s """.replace("\n", System.lineSeparator()); - textArea.setValue(description.formatted( + textArea.setValueWithLink(description.formatted( value.getName(), value.getVersion(), value.getModules().stream().map(module -> module.getTypeName() + ": " + module.getVersion()) .collect(Collectors.joining(System.lineSeparator())) - )); - }); + ), "q=id%3D%3D" + value.getId().toString()); + }, + () -> textArea.setValueWithLink("", null)); } } @@ -466,8 +490,8 @@ public class TargetView extends TableView { private HorizontalLayout buildTagSelectionLayout(HawkbitMgmtClient hawkbitClient) { final Button createTagButton = new Button("Create Tag"); - createTagButton.addClickListener(event -> - new CreateTagDialog(hawkbitClient, () -> tagSelector.setItems(fetchAvailableTags())).result()); + createTagButton.addClickListener(event -> new CreateTagDialog(hawkbitClient, () -> tagSelector.setItems(fetchAvailableTags())) + .result()); tagSelector.setWidthFull(); tagSelector.setItemLabelGenerator(MgmtTag::getName); @@ -541,7 +565,7 @@ public class TargetView extends TableView { int offset = 0; do { List 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) .orElse(Collections.emptyList()); tags.addAll(page); @@ -552,198 +576,105 @@ public class TargetView extends TableView { } } - @Slf4j - private static class TargetActions extends Grid { + public static class TargetActionsHistoryLayout extends VerticalLayout { - private final transient HawkbitMgmtClient hawkbitClient; - private transient MgmtTarget target; + private final TargetActionsHistory targetActionsHistory; - private TargetActions(final HawkbitMgmtClient hawkbitClient) { - this.hawkbitClient = hawkbitClient; - setWidthFull(); - addColumn(new ComponentRenderer<>(ActionStatusEntry::getStatusIcon)).setHeader(STATUS).setAutoWidth(true).setFlexGrow(0); - addColumn(ActionStatusEntry::getDistributionSetName).setHeader("Distribution Set").setAutoWidth(true); - 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); + public TargetActionsHistoryLayout(HawkbitMgmtClient hawkbitMgmtClient) { + ActionStepsGrid actionStepsGrid = new ActionStepsGrid(hawkbitMgmtClient); + targetActionsHistory = new TargetActionsHistory(hawkbitMgmtClient, actionStepsGrid); + add(targetActionsHistory); + add(actionStepsGrid); } - private void setItem(final MgmtTarget target) { - this.target = target; + public void setItem(MgmtTarget target) { + targetActionsHistory.setItem(target); } - private List 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(); - } + public static class ActionStepsGrid extends Grid { - @Override - protected void onAttach(AttachEvent attachEvent) { - setItems(fetchActions()); - } + private final transient HawkbitMgmtClient hawkbitClient; + private transient MgmtTarget target; + private transient Long actionId; - private class ActionStatusEntry { + private ActionStepsGrid(final HawkbitMgmtClient hawkbitClient) { - final MgmtAction action; - final Runnable onUpdate; - MgmtDistributionSet distributionSet; + this.hawkbitClient = hawkbitClient; + setWidthFull(); + 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) { - 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; + private List fetchActionSteps() { + if (actionId == null) { + return new ArrayList<>(); } - 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); + return hawkbitClient.getTargetRestApi() + .getActionStatusList(target.getControllerId(), actionId, 0, 30, null).getBody().getContent() + .stream().map(ActionStepEntry::new) + .toList(); + } + + @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() { - 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"); - } - } 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); + layout.add(icon); + layout.setWidth(50, Unit.PIXELS); + layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); + return layout; } - 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(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); + public VerticalLayout getMessage() { + return new VerticalLayout(status.getMessages().stream().map(Span::new).toArray(Span[]::new)); } - - 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 { type.setWidthFull(); type.setEmptySelectionAllowed(true); 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(); name = Utils.textField(Constants.NAME); name.setWidthFull(); @@ -812,7 +743,7 @@ public class TargetView extends TableView { request.setTargetType(type.getValue().getId()); } hawkbitClient.getTargetRestApi().createTargets( - List.of(request)) + List.of(request)) .getBody() .stream() .findFirst() @@ -830,22 +761,21 @@ public class TargetView extends TableView { private final DateTimePicker forceTime = new DateTimePicker("Force Time"); private final Button assign = new Button("Assign"); - private AssignDialog(final HawkbitMgmtClient hawkbitClient, Set selectedTargets) { + private AssignDialog(final HawkbitMgmtClient hawkbitClient, Set selectedTargets) { super("Assign Distribution Set"); distributionSet = new Select<>( "Distribution Set", this::readyToAssign, Optional.ofNullable( - hawkbitClient.getDistributionSetRestApi() - .getDistributionSets(null, 0, 30, Constants.NAME_ASC) - .getBody()) + hawkbitClient.getDistributionSetRestApi() + .getDistributionSets(null, 0, 500, Constants.CREATED_AT_DESC) + .getBody()) .map(body -> body.getContent().toArray(new MgmtDistributionSet[0])) .orElseGet(() -> new MgmtDistributionSet[0]) ); distributionSet.setRequiredIndicatorVisible(true); - distributionSet.setItemLabelGenerator(distributionSetO -> - distributionSetO.getName() + ":" + distributionSetO.getVersion()); + distributionSet.setItemLabelGenerator(distributionSetO -> distributionSetO.getName() + ":" + distributionSetO.getVersion()); distributionSet.setWidthFull(); actionType = Utils.actionTypeControls(forceTime); @@ -874,7 +804,7 @@ public class TargetView extends TableView { } } - private void addAssignClickListener(final HawkbitMgmtClient hawkbitClient, final Set selectedTargets) { + private void addAssignClickListener(final HawkbitMgmtClient hawkbitClient, final Set selectedTargets) { assign.addClickListener(e -> { close(); @@ -885,9 +815,7 @@ public class TargetView extends TableView { request.setType(actionType.getValue()); if (actionType.getValue() == MgmtActionType.TIMEFORCED) { request.setForcetime( - forceTime.isEmpty() ? - System.currentTimeMillis() : - forceTime.getValue().toEpochSecond(ZoneOffset.UTC) * 1000); + forceTime.isEmpty() ? System.currentTimeMillis() : forceTime.getValue().toEpochSecond(ZoneOffset.UTC) * 1000); } requests.add(request); @@ -945,9 +873,31 @@ public class TargetView extends TableView { private TargetStatusCell(final MgmtTarget target) { 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"); - add(pollStatusIconMapper(pollStatus), targetUpdateStatusMapper(targetUpdateStatus)); - setWidth(50, Unit.PIXELS); + add(targetUpdateStatusMapper(targetUpdateStatus)); + setWidth(25, Unit.PIXELS); } private Icon targetUpdateStatusMapper(final String targetUpdateStatus) { @@ -967,23 +917,39 @@ public class TargetView extends TableView { default -> "blue"; }; - final Icon statusIcon = Utils.tooltip(icon.create(), targetUpdateStatus); + final Icon statusIcon = Utils.tooltip(icon.create(), targetUpdateStatus.replace("_", " ")); statusIcon.setColor(color); statusIcon.addClassNames(LumoUtility.IconSize.SMALL); return statusIcon; } + } - 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"); - } else { - pollIcon = Utils.tooltip(VaadinIcon.CLOCK.create(), "In Time"); - } - pollIcon.addClassNames(LumoUtility.IconSize.SMALL); - return pollIcon; + // todo change /targets api to reduce api calls ? + @EqualsAndHashCode(callSuper = true) + public static class TargetWithDs extends MgmtTarget { + + TargetWithDs() { + super(); + } + + Optional ds; + static ObjectMapper objectMapper = new ObjectMapper(); + + 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(""); } } } \ No newline at end of file diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/Filter.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/Filter.java index 2cc866ded..e1bcdd697 100644 --- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/Filter.java +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/Filter.java @@ -28,13 +28,19 @@ 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.theme.lumo.LumoUtility; +import org.springframework.util.ObjectUtils; public class Filter extends Div { private transient Rsql rsql; + private final transient Rsql secondaryRsql; + private final transient Rsql primaryRsql; + private final transient Div filtersDiv; public Filter(final Consumer changeListener, final Rsql primaryRsql, final Rsql secondaryOptionalRsql) { rsql = primaryRsql; + this.primaryRsql = primaryRsql; + secondaryRsql = secondaryOptionalRsql; final HorizontalLayout layout = new HorizontalLayout(); @@ -42,7 +48,7 @@ public class Filter extends Div { addClassNames(LumoUtility.Padding.Horizontal.NONE, LumoUtility.Padding.Vertical.SMALL, LumoUtility.BoxSizing.BORDER); - final Div filtersDiv = new Div(); + filtersDiv = new Div(); filtersDiv.setWidthFull(); filtersDiv.add(primaryRsql.components()); 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"); toggleBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY); toggleBtn.addClickListener(e -> { - filtersDiv.removeAll(); - synchronized (this) { // toggle - rsql = rsql == primaryRsql ? secondaryOptionalRsql : primaryRsql; - } - filtersDiv.add(rsql.components()); + toggle(); changeListener.accept(primaryRsql.filter()); }); layout.add(toggleBtn); @@ -84,34 +86,59 @@ public class Filter extends Div { changeListener.accept(primaryRsql.filter()); } - public static String filter(final Map keyToValues) { - final Map normalized = - new HashMap<>(keyToValues) - .entrySet() - .stream() - .filter(e -> { - if (e.getValue() instanceof Optional opt) { - return opt.isPresent(); - } else { - return e.getValue() != null; - } - }) - .filter(e -> !(e.getValue() instanceof Collection coll && coll.isEmpty())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + private void toggle() { + // toggle + filtersDiv.removeAll(); + synchronized (this) { + rsql = rsql == primaryRsql ? secondaryRsql : primaryRsql; + } + filtersDiv.add(rsql.components()); + } + + public void setFilter(String string, boolean allowToggle) { + var otherFilter = rsql == primaryRsql ? secondaryRsql : primaryRsql; + Stream rsqlFIlter; + // logic to find the filter to use + 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 keyToValues) { + final Map 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()) { 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 { final StringBuilder sb = new StringBuilder(); normalized.forEach((k, v) -> { - if (v instanceof Collection) { - sb.append('(').append(filter(k, v)).append(')'); - } else if (v instanceof Optional opt) { - sb.append(filter(k, opt.get())); - } else { - sb.append(filter(k, v)); + if (k instanceof Collection keyList) { + sb.append('(').append( + keyList.stream().map(subKey -> filter((String) subKey, v)) + .collect(Collectors.joining(" or "))).append(")"); + } else if (k instanceof String key) { + if (v instanceof Collection) { + sb.append('(').append(filter(key, v)).append(')'); + } else { + sb.append(filter(key, v)); + } } sb.append(';'); }); @@ -158,4 +185,9 @@ public class Filter extends Div { String filter(); } + + public interface RsqlRw { + + void setFilter(String filter); + } } diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/LinkedTextArea.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/LinkedTextArea.java new file mode 100644 index 000000000..2f4a1b9c6 --- /dev/null +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/LinkedTextArea.java @@ -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); + } + } +} diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/SelectionGrid.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/SelectionGrid.java index 57bbd0342..60e0064aa 100644 --- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/SelectionGrid.java +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/SelectionGrid.java @@ -10,6 +10,7 @@ package org.eclipse.hawkbit.ui.simple.view.util; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.function.BiFunction; @@ -47,7 +48,11 @@ public class SelectionGrid extends Grid { final Stream fetch = queryFn.apply(query, rsqlFilter); final Set selected = getSelectedItems(); if (selected == null || selected.isEmpty()) { - return fetch; + final List fetchList = fetch.toList(); + if (fetchList.size() == 1) { + this.setDetailsVisible(fetchList.get(0), true); + } + return fetchList.stream(); } else { final Set selectedIds = new HashSet<>(); selected.forEach(next -> selectedIds.add(entityRepresentation.idFn.apply(next))); @@ -61,10 +66,11 @@ public class SelectionGrid extends Grid { } // else externally managed } - public void setRsqlFilter(final String rsqlFilter) { + public void setRsqlFilter(final String rsqlFilter, boolean refreshGrid) { if (!Objects.equals(this.rsqlFilter, rsqlFilter)) { this.rsqlFilter = rsqlFilter; - refreshGrid(true); + if (refreshGrid) + refreshGrid(true); } } diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/TableView.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/TableView.java index 00c675bb8..d492365f4 100644 --- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/TableView.java +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/TableView.java @@ -15,6 +15,7 @@ import java.util.function.Function; import java.util.stream.Stream; 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.ButtonVariant; 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.renderer.ComponentRenderer; 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 org.eclipse.hawkbit.ui.simple.view.Constants; @SuppressWarnings("java:S119") // better readability -public class TableView extends Div implements Constants { +public class TableView extends Div implements Constants, BeforeEnterObserver { 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_PRIMARY_COLOR = "var(--lumo-primary-color)"; private static final int DEFAULT_OPEN_POSITION_SIZE = 50; + private static final String QUERY_PARAM_FILTER = "q"; protected SelectionGrid selectionGrid; private final Filter filter; @@ -83,8 +88,14 @@ public class TableView extends Div implements Constants { filter = new Filter( (rsqlFilter) -> { - selectionGrid.setRsqlFilter(rsqlFilter); 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 ); gridLayout = new VerticalLayout(filter, splitLayout); @@ -147,4 +158,16 @@ public class TableView extends Div implements Constants { 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); + }); + + } } \ No newline at end of file diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/Utils.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/Utils.java index feccd14a3..ec06a6b1e 100644 --- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/Utils.java +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/util/Utils.java @@ -9,14 +9,31 @@ */ 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.Iterator; +import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; 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.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.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.theme.lumo.LumoUtility; +@Slf4j public class Utils { private Utils() { @@ -117,7 +135,8 @@ public class Utils { return layout; } - private static ConfirmDialog promptForDeleteConfirmation(Function, CompletionStage> removeHandler, SelectionGrid selectionGrid) { + private static ConfirmDialog promptForDeleteConfirmation(Function, CompletionStage> removeHandler, + SelectionGrid selectionGrid) { final ConfirmDialog dialog = new ConfirmDialog(); dialog.setHeader("Confirm Deletion"); 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 void remove(final Collection remove, final Set from, final Function idFn) { remove.forEach(toRemove -> { final Object id = idFn.apply(toRemove); - for (final Iterator i = from.iterator(); i.hasNext(); ) { + for (final Iterator i = from.iterator(); i.hasNext();) { if (idFn.apply(i.next()).equals(id)) { i.remove(); } @@ -175,27 +194,31 @@ public class Utils { 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 actionTypeControls(DateTimePicker forceTime) { Select actionType = new Select<>(); actionType.setLabel(Constants.ACTION_TYPE); actionType.setItems(MgmtActionType.values()); actionType.setValue(MgmtActionType.FORCED); - final ComponentRenderer actionTypeRenderer = new ComponentRenderer<>(actionTypeO -> - switch (actionTypeO) { - case SOFT -> new Text(Constants.SOFT); - case FORCED -> new Text(Constants.FORCED); - case DOWNLOAD_ONLY -> new Text(Constants.DOWNLOAD_ONLY); - case TIMEFORCED -> forceTime; - }); + final ComponentRenderer actionTypeRenderer = new ComponentRenderer<>(actionTypeO -> switch (actionTypeO) { + case SOFT -> new Text(Constants.SOFT); + case FORCED -> new Text(Constants.FORCED); + case DOWNLOAD_ONLY -> new Text(Constants.DOWNLOAD_ONLY); + case TIMEFORCED -> forceTime; + }); actionType.addValueChangeListener(e -> actionType.setRenderer(actionTypeRenderer)); - actionType.setItemLabelGenerator(startTypeO -> - switch (startTypeO) { - case SOFT -> Constants.SOFT; - case FORCED -> Constants.FORCED; - case DOWNLOAD_ONLY -> Constants.DOWNLOAD_ONLY; - case TIMEFORCED -> "Time Forced at " + (forceTime.isEmpty() ? "" : " " + forceTime.getValue()); - }); + actionType.setItemLabelGenerator(startTypeO -> switch (startTypeO) { + case SOFT -> Constants.SOFT; + case FORCED -> Constants.FORCED; + case DOWNLOAD_ONLY -> Constants.DOWNLOAD_ONLY; + case TIMEFORCED -> "Time Forced at " + (forceTime.isEmpty() ? "" : " " + forceTime.getValue()); + }); actionType.setWidthFull(); return actionType; } @@ -235,4 +258,58 @@ public class Utils { super.close(); } } + + private static ZoneId getZoneId() { + CompletableFuture 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 LocalDateTimeRenderer localDateTimeRenderer(ToLongFunction 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 querySortOrders) { + return getSortParam(querySortOrders, null); + } + + public static String getSortParam(List 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(); + } } diff --git a/hawkbit-simple-ui/src/main/resources/application.properties b/hawkbit-simple-ui/src/main/resources/application.properties index 86f29fe8b..60074fcc6 100644 --- a/hawkbit-simple-ui/src/main/resources/application.properties +++ b/hawkbit-simple-ui/src/main/resources/application.properties @@ -26,5 +26,7 @@ spring.mustache.check-template-location=false vaadin.launch-browser=true # To improve the performance during development. # 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 ### \ No newline at end of file diff --git a/pom.xml b/pom.xml index 56c7f492a..b6108ae87 100644 --- a/pom.xml +++ b/pom.xml @@ -368,6 +368,13 @@ org.jacoco jacoco-maven-plugin + + org.springframework.boot + spring-boot-maven-plugin + + true + + diff --git a/site/content/concepts/authorization.md b/site/content/concepts/authorization.md index 64ff4e6d0..9ecfb0d5e 100644 --- a/site/content/concepts/authorization.md +++ b/site/content/concepts/authorization.md @@ -41,19 +41,17 @@ hawkBit optionally supports configuring multiple static users through the applic and password Spring security properties are ignored. An example configuration is given below. - hawkbit.server.im.users[0].username=admin - hawkbit.server.im.users[0].password={noop}admin - hawkbit.server.im.users[0].firstname=Test - hawkbit.server.im.users[0].lastname=Admin - hawkbit.server.im.users[0].email=admin@test.de - hawkbit.server.im.users[0].permissions=ALL + hawkbit.security.user.admin.password={noop}admin + hawkbit.security.user.admin.firstname=Test + hawkbit.security.user.admin.lastname=Admin + hawkbit.security.user.admin.email=admin@test.de + hawkbit.security.user.admin.permissions=ALL - hawkbit.server.im.users[1].username=test - hawkbit.server.im.users[1].password={noop}test - hawkbit.server.im.users[1].firstname=Test - hawkbit.server.im.users[1].lastname=Tester - hawkbit.server.im.users[1].email=test@tester.com - hawkbit.server.im.users[1].permissions=READ_TARGET,UPDATE_TARGET,CREATE_TARGET,DELETE_TARGET + hawkbit.security.user.test.password={noop}test + hawkbit.security.user.test.firstname=Test + hawkbit.security.user.test.lastname=Tester + hawkbit.security.user.test.email=test@tester.com + hawkbit.security.user.test.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 the used password encoder in brackets. In this example, `noop` is used as the plaintext encoder. For production use, it