Remove 'Simple' from Simple UI (#2809)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-11-17 09:03:27 +02:00
committed by GitHub
parent c5ea265e0f
commit bcf62f39e7
47 changed files with 134 additions and 127 deletions

10
hawkbit-ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
bundles
src/main/frontend/*
!src/main/frontend/themes/
node_modules
package.json
package-lock.json
tsconfig.json
types.d.ts
vite.config.ts
vite.generated.ts

129
hawkbit-ui/pom.xml Normal file
View File

@@ -0,0 +1,129 @@
<!--
Copyright (c) 2023 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
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-parent</artifactId>
<version>${revision}</version>
</parent>
<artifactId>hawkbit-ui</artifactId>
<version>${revision}</version>
<packaging>jar</packaging>
<name>hawkBit :: UI</name>
<properties>
<vaadin.version>24.9.5</vaadin.version>
</properties>
<repositories>
<repository>
<id>Vaadin Repo</id>
<url>https://maven.vaadin.com/vaadin-addons</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-bom</artifactId>
<version>${vaadin.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Override vulnerable feign-form-spring dependency on commons-text -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>${commons-text.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-sdk-commons</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.eclipse.hawkbit</groupId>
<artifactId>hawkbit-mgmt-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-core</artifactId>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<defaultGoal>spring-boot:run</defaultGoal>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
<finalName>hawkBit-UI</finalName>
<mainClass>org.eclipse.hawkbit.ui.HawkbitUiApp</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-maven-plugin</artifactId>
<version>${vaadin.version}</version>
<executions>
<execution>
<goals>
<goal>prepare-frontend</goal>
<goal>build-frontend</goal>
</goals>
<phase>compile</phase>
</execution>
</executions>
<configuration>
<forceProductionBuild>true</forceProductionBuild>
<frontendHotdeploy>false</frontendHotdeploy>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,7 @@
a.nocolor:link {
color: inherit;
}
a.nocolor:visited {
color: inherit;
}

View File

@@ -0,0 +1,3 @@
{
"lumoImports": ["badge"]
}

View File

@@ -0,0 +1,101 @@
/**
* Copyright (c) 2023 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;
import java.util.function.Supplier;
import feign.FeignException;
import lombok.Getter;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtActionRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetTagRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetTypeRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRolloutRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftwareModuleRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftwareModuleTypeRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetFilterQueryRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetTagRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetTypeRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTenantManagementRestApi;
import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.Tenant;
import org.springframework.http.ResponseEntity;
@Getter
public class HawkbitMgmtClient {
private final Tenant tenant;
private final HawkbitClient hawkbitClient;
private final MgmtSoftwareModuleRestApi softwareModuleRestApi;
private final MgmtSoftwareModuleTypeRestApi softwareModuleTypeRestApi;
private final MgmtDistributionSetRestApi distributionSetRestApi;
private final MgmtDistributionSetTypeRestApi distributionSetTypeRestApi;
private final MgmtDistributionSetTagRestApi distributionSetTagRestApi;
private final MgmtTargetRestApi targetRestApi;
private final MgmtTargetTypeRestApi targetTypeRestApi;
private final MgmtTargetTagRestApi targetTagRestApi;
private final MgmtTargetFilterQueryRestApi targetFilterQueryRestApi;
private final MgmtRolloutRestApi rolloutRestApi;
private final MgmtTenantManagementRestApi tenantManagementRestApi;
private final MgmtActionRestApi actionRestApi;
public HawkbitMgmtClient(final Tenant tenant, final HawkbitClient hawkbitClient) {
this.tenant = tenant;
this.hawkbitClient = hawkbitClient;
softwareModuleRestApi = service(MgmtSoftwareModuleRestApi.class);
softwareModuleTypeRestApi = service(MgmtSoftwareModuleTypeRestApi.class);
distributionSetRestApi = service(MgmtDistributionSetRestApi.class);
distributionSetTypeRestApi = service(MgmtDistributionSetTypeRestApi.class);
distributionSetTagRestApi = service(MgmtDistributionSetTagRestApi.class);
targetRestApi = service(MgmtTargetRestApi.class);
targetTypeRestApi = service(MgmtTargetTypeRestApi.class);
targetTagRestApi = service(MgmtTargetTagRestApi.class);
targetFilterQueryRestApi = service(MgmtTargetFilterQueryRestApi.class);
rolloutRestApi = service(MgmtRolloutRestApi.class);
tenantManagementRestApi = service(MgmtTenantManagementRestApi.class);
actionRestApi = service(MgmtActionRestApi.class);
}
public boolean hasSoftwareModulesRead() {
return hasRead(() -> softwareModuleRestApi.getSoftwareModule(-1L));
}
public boolean hasRolloutRead() {
return hasRead(() -> rolloutRestApi.getRollout(-1L));
}
public boolean hasDistributionSetRead() {
return hasRead(() -> distributionSetRestApi.getDistributionSet(-1L));
}
public boolean hasTargetRead() {
return hasRead(() -> targetRestApi.getTarget("_#ETE$ER"));
}
public boolean hasConfigRead() {
return hasRead(() -> tenantManagementRestApi.getTenantConfigurationValue("_#ETE$ER"));
}
private boolean hasRead(final Supplier<ResponseEntity<?>> doCall) {
try {
final int statusCode = doCall.get().getStatusCode().value();
return statusCode != 401 && statusCode != 403;
} catch (final FeignException e) {
return !(e instanceof FeignException.Unauthorized) && !(e instanceof FeignException.Forbidden);
}
}
private <T> T service(final Class<T> serviceType) {
return hawkbitClient.mgmtService(serviceType, tenant);
}
}

View File

@@ -0,0 +1,141 @@
/**
* Copyright (c) 2023 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;
import com.vaadin.flow.component.page.AppShellConfigurator;
import com.vaadin.flow.server.PWA;
import com.vaadin.flow.theme.Theme;
import feign.Contract;
import feign.RequestInterceptor;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.HawkbitServer;
import org.eclipse.hawkbit.sdk.Tenant;
import org.eclipse.hawkbit.ui.view.util.Utils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Base64;
import java.util.Collections;
import java.util.Objects;
import static feign.Util.ISO_8859_1;
@Slf4j
@Theme("hawkbit")
@PWA(name = "hawkBit UI", shortName = "hawkBit UI")
@EnableCaching
@EnableScheduling
@SpringBootApplication
@Import(FeignClientsConfiguration.class)
public class HawkbitUiApp implements AppShellConfigurator {
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final RequestInterceptor AUTHORIZATION = requestTemplate -> {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.getPrincipal() instanceof OidcUser oidcUser) {
requestTemplate.header(AUTHORIZATION_HEADER, "Bearer " + oidcUser.getIdToken().getTokenValue());
} else {
requestTemplate.header(
AUTHORIZATION_HEADER, "Basic " + Base64.getEncoder().encodeToString(
(Objects.requireNonNull(authentication.getPrincipal(), "User is null!") + ":" + Objects.requireNonNull(
authentication.getCredentials(), "Password is not available!")).getBytes(ISO_8859_1))
);
}
};
private static final ErrorDecoder DEFAULT_ERROR_DECODER = new ErrorDecoder.Default();
private static final ErrorDecoder ERROR_DECODER = (methodKey, response) -> {
final Exception e = DEFAULT_ERROR_DECODER.decode(methodKey, response);
Utils.errorNotification(e);
return e;
};
public static void main(String[] args) {
SpringApplication.run(HawkbitUiApp.class, args);
}
@Bean
HawkbitClient hawkbitClient(
final HawkbitServer hawkBitServer,
final Encoder encoder,
final Decoder decoder,
final Contract contract
) {
return new HawkbitClient(
hawkBitServer, encoder, decoder, contract,
ERROR_DECODER,
(tenant, controller) -> controller == null
? AUTHORIZATION
: HawkbitClient.DEFAULT_REQUEST_INTERCEPTOR_FN.apply(tenant, controller)
);
}
@Bean
HawkbitMgmtClient hawkbitMgmtClient(final Tenant tenant, final HawkbitClient hawkbitClient) {
return new HawkbitMgmtClient(tenant, hawkbitClient);
}
// accepts all user / pass, just delegating them to the feign client
@Bean
AuthenticationManager authenticationManager(final HawkbitMgmtClient hawkbitClient, final HawkbitServer server) {
return authentication -> {
final String username = authentication.getName();
final String password = authentication.getCredentials().toString();
// make simple check in order not to be logged in as not real user.
if (!isAuthenticated(username, password, server.getMgmtUrl())) {
throw new BadCredentialsException("Incorrect username or password!");
}
return new UsernamePasswordAuthenticationToken(username, password, Collections.emptyList()) {
@Override
public void eraseCredentials() {
// don't erase credentials because they will be used
// to authenticate to the hawkBit update server / mgmt server
}
};
};
}
public static boolean isAuthenticated(String username, String password, String mgmtUrl) {
try {
final URL url = new URL(mgmtUrl + "/rest/v1/rollouts");
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
final String auth = username + ":" + password;
final String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes());
conn.setRequestProperty(AUTHORIZATION_HEADER, "Basic " + encodedAuth);
return conn.getResponseCode() != 401;
} catch (final Exception ex) {
log.error("Failed to authenticate user {} .Reason : ", username, ex);
return false;
}
}
}

View File

@@ -0,0 +1,165 @@
/**
* Copyright (c) 2023 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;
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;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.contextmenu.MenuItem;
import com.vaadin.flow.component.html.Anchor;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Footer;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Header;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.menubar.MenuBar;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.Scroller;
import com.vaadin.flow.component.sidenav.SideNav;
import com.vaadin.flow.component.sidenav.SideNavItem;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.server.auth.AccessAnnotationChecker;
import com.vaadin.flow.theme.lumo.LumoUtility;
import org.eclipse.hawkbit.ui.security.AuthenticatedUser;
import org.eclipse.hawkbit.ui.view.AboutView;
import org.eclipse.hawkbit.ui.view.ConfigView;
import org.eclipse.hawkbit.ui.view.DistributionSetView;
import org.eclipse.hawkbit.ui.view.RolloutView;
import org.eclipse.hawkbit.ui.view.SoftwareModuleView;
import org.eclipse.hawkbit.ui.view.TargetView;
/**
* The main view is a top-level placeholder for other views.
*/
public class MainLayout extends AppLayout {
static final List<Class<? extends Component>> DEFAULT_VIEW_PRIORITY = List.of(TargetView.class, DistributionSetView.class,
SoftwareModuleView.class, RolloutView.class);
private final transient AuthenticatedUser authenticatedUser;
private final AccessAnnotationChecker accessChecker;
private H2 viewTitle;
private transient Optional<Class<? extends Component>> defaultView;
public MainLayout(final AuthenticatedUser authenticatedUser, final AccessAnnotationChecker accessChecker) {
this.authenticatedUser = authenticatedUser;
this.accessChecker = accessChecker;
setPrimarySection(Section.DRAWER);
addDrawerContent();
setDrawerOpened(true);
addHeaderContent();
}
@Override
protected void afterNavigation() {
super.afterNavigation();
viewTitle.setText(
Optional.ofNullable(getContent())
.map(c -> c.getClass().getAnnotation(PageTitle.class))
.map(PageTitle::value)
.orElse(""));
if (UI.getCurrent().getActiveViewLocation().getPath().isEmpty()) {
defaultView.ifPresent(c -> UI.getCurrent().navigate(c));
}
}
private void addHeaderContent() {
final DrawerToggle toggle = new DrawerToggle();
toggle.setAriaLabel("Menu toggle");
viewTitle = new H2();
viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE);
addToNavbar(false, toggle, viewTitle);
}
private void addDrawerContent() {
final H1 appName = new H1("hawkBit UI");
final HorizontalLayout layout = new HorizontalLayout();
layout.setPadding(true);
layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
final Image icon = new Image("images/header_icon.png", "hawkBit icon");
icon.setMaxHeight(24, Unit.PIXELS);
icon.setMaxWidth(24, Unit.PIXELS);
appName.addClassNames(LumoUtility.AlignItems.BASELINE, LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE);
layout.add(icon, appName);
final Header header = new Header(layout);
final Scroller scroller = new Scroller(createNavigation());
addToDrawer(header, scroller, createFooter());
}
private SideNav createNavigation() {
final SideNav nav = new SideNav();
if (accessChecker.hasAccess(TargetView.class)) {
nav.addItem(new SideNavItem("Targets", TargetView.class, VaadinIcon.FILTER.create()));
}
if (accessChecker.hasAccess(RolloutView.class)) {
nav.addItem(new SideNavItem("Rollouts", RolloutView.class, VaadinIcon.COGS.create()));
}
if (accessChecker.hasAccess(DistributionSetView.class)) {
nav.addItem(new SideNavItem("Distribution Sets", DistributionSetView.class, VaadinIcon.FILE_TREE.create()));
}
if (accessChecker.hasAccess(SoftwareModuleView.class)) {
nav.addItem(new SideNavItem("Software Modules", SoftwareModuleView.class, VaadinIcon.FILE.create()));
}
if (accessChecker.hasAccess(ConfigView.class)) {
nav.addItem(new SideNavItem("Config", ConfigView.class, VaadinIcon.COG.create()));
}
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;
}
private Footer createFooter() {
final Footer layout = new Footer();
final Optional<String> maybeUser = authenticatedUser.getName();
if (maybeUser.isPresent()) {
final String user = maybeUser.get();
final Avatar avatar = new Avatar(user);
final MenuBar userMenu = new MenuBar();
userMenu.setThemeName("tertiary-inline contrast");
final MenuItem userName = userMenu.addItem("");
final Div div = new Div();
div.add(avatar);
div.add(user);
div.add(new Icon("lumo", "dropdown"));
div.getElement().getStyle().set("display", "flex");
div.getElement().getStyle().set("align-items", "center");
div.getElement().getStyle().set("gap", "var(--lumo-space-s)");
userName.add(div);
userName.getSubMenu().addItem("Sign out", e -> authenticatedUser.logout());
layout.add(userMenu);
} else {
final Anchor loginLink = new Anchor("login", "Sign in");
layout.add(loginLink);
}
return layout;
}
}

View File

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

View File

@@ -0,0 +1,28 @@
/**
* 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;
import com.vaadin.flow.server.ServiceInitEvent;
import com.vaadin.flow.server.VaadinServiceInitListener;
import com.vaadin.flow.spring.annotation.SpringComponent;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@SpringComponent
public class VaadinServiceInit implements VaadinServiceInitListener {
@Override
public void serviceInit(ServiceInitEvent event) {
// cache zoneId of client as soon as possible
event.getSource().addUIInitListener(uiEvent ->
uiEvent.getUI().getPage().retrieveExtendedClientDetails(details -> {}));
}
}

View File

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

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2023 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.security;
import java.util.Optional;
import com.vaadin.flow.spring.security.AuthenticationContext;
import org.springframework.stereotype.Component;
@Component
public class AuthenticatedUser {
private final AuthenticationContext authenticationContext;
private final GrantedAuthoritiesService grantedAuthoritiesService;
public AuthenticatedUser(final AuthenticationContext authenticationContext, GrantedAuthoritiesService grantedAuthoritiesService) {
this.authenticationContext = authenticationContext;
this.grantedAuthoritiesService = grantedAuthoritiesService;
}
public Optional<String> getName() {
return authenticationContext.getPrincipalName();
}
public void logout() {
this.getName().ifPresent(grantedAuthoritiesService::evictUserFromCache);
authenticationContext.logout();
}
}

View File

@@ -0,0 +1,65 @@
/**
* 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.security;
import java.util.LinkedList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.ui.HawkbitMgmtClient;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
@Service
@AllArgsConstructor
@Slf4j
public class GrantedAuthoritiesService {
public static final String USER_CACHE = "userDetails";
private HawkbitMgmtClient hawkbitClient;
@Cacheable(cacheNames = "userDetails", key = "#authentication.getName()")
public List<SimpleGrantedAuthority> getGrantedAuthorities(Authentication authentication) {
final List<String> roles = new LinkedList<>();
roles.add("ANONYMOUS");
if (hawkbitClient.hasSoftwareModulesRead()) {
roles.add("SOFTWARE_MODULE_READ");
}
if (hawkbitClient.hasRolloutRead()) {
roles.add("ROLLOUT_READ");
}
if (hawkbitClient.hasDistributionSetRead()) {
roles.add("DISTRIBUTION_SET_READ");
}
if (hawkbitClient.hasTargetRead()) {
roles.add("TARGET_READ");
}
if (hawkbitClient.hasConfigRead()) {
roles.add("CONFIG_READ");
}
return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList();
}
@Scheduled(fixedRateString = "${caching.spring.userDetailsTTL:1h}")
@CacheEvict(cacheNames = USER_CACHE, allEntries = true)
public void emptyCache() {
log.debug("emptying userDetails cache");
}
@CacheEvict(cacheNames = "userDetails")
public void evictUserFromCache(String principalName) {
log.debug("remove user from {} cache", principalName);
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2023 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.security;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
@Configuration
public class Oauth2ClientConfig {
@Bean(name = "hawkbitOAuth2ClientCustomizer")
@ConditionalOnProperty(prefix = "hawkbit.server.security.oauth2.client", name = "enabled")
@ConditionalOnMissingBean(name = "hawkbitOAuth2ClientCustomizer")
Customizer<OAuth2LoginConfigurer<HttpSecurity>> defaultOAuth2ClientCustomizer() {
return Customizer.withDefaults();
}
}

View File

@@ -0,0 +1,34 @@
/**
* 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.security;
import lombok.Data;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ToString
@ConfigurationProperties("hawkbit.server.security")
public class OidcClientProperties {
private final OidcClientProperties.Oauth2 oauth2 = new OidcClientProperties.Oauth2();
@Data
public static class Oauth2 {
private final OidcClientProperties.Oauth2.Client client = new OidcClientProperties.Oauth2.Client();
@Data
public static class Client {
private boolean enabled = false;
}
}
}

View File

@@ -0,0 +1,83 @@
/**
* Copyright (c) 2023 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.security;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import com.vaadin.flow.spring.security.VaadinAwareSecurityContextHolderStrategyConfiguration;
import com.vaadin.flow.spring.security.VaadinSecurityConfigurer;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.ui.view.LoginView;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
@EnableWebSecurity
@Configuration
@EnableConfigurationProperties(OidcClientProperties.class)
@Slf4j
@Import(VaadinAwareSecurityContextHolderStrategyConfiguration.class)
public class SecurityConfiguration {
private Customizer<OAuth2LoginConfigurer<HttpSecurity>> oAuth2LoginConfigurerCustomizer;
@Autowired(required = false)
public void setOAuth2LoginConfigurerCustomizer(
@Qualifier("hawkbitOAuth2ClientCustomizer") final Customizer<OAuth2LoginConfigurer<HttpSecurity>> oauth2LoginConfigurerCustomizer) {
this.oAuth2LoginConfigurerCustomizer = oauth2LoginConfigurerCustomizer;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationFailureHandler customFailureHandler() {
// Redirect back to login with your message
return (request, response, exception) -> response.sendRedirect("/login?error=" + URLEncoder.encode(exception.getMessage(),
StandardCharsets.UTF_8));
}
@Bean
SecurityFilterChain securityFilterChain(
final HttpSecurity http,
final UserDetailsSetter userDetailsSetter,
@Autowired(required = false) InMemoryClientRegistrationRepository clientRegistrationRepository) throws Exception {
http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/images/*.png").permitAll());
http.addFilterAfter(userDetailsSetter, AuthorizationFilter.class);
return http.with(VaadinSecurityConfigurer.vaadin(), configurer -> {
if (oAuth2LoginConfigurerCustomizer == null) {
configurer.loginView(LoginView.class);
} else {
var defaultClientRegistration = clientRegistrationRepository.iterator().next();
configurer.oauth2LoginPage(
"/oauth2/authorization/" + defaultClientRegistration.getRegistrationId(),
"{baseUrl}/"
);
}
}).build();
}
}

View File

@@ -0,0 +1,73 @@
/**
* 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.security;
import java.io.IOException;
import java.time.Instant;
import java.util.Collection;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@RequiredArgsConstructor
@Component
@Slf4j
class UserDetailsSetter extends OncePerRequestFilter {
private final SecurityContextHolderStrategy securityContextHolderStrategy;
private final GrantedAuthoritiesService grantedAuthoritiesService;
@SuppressWarnings("java:S1066") // java:S1066 - readability preferred
@Override
protected void doFilterInternal(@NotNull final HttpServletRequest request, @NotNull final HttpServletResponse response,
@NotNull final FilterChain filterChain)
throws ServletException, IOException {
Authentication authentication = securityContextHolderStrategy.getContext().getAuthentication();
Authentication newAuthentication;
if (!(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated()) {
Collection<? extends GrantedAuthority> grantedAuthorities = grantedAuthoritiesService.getGrantedAuthorities(authentication);
if (authentication instanceof OAuth2AuthenticationToken oAuth2AuthenticationToken) {
newAuthentication = new OAuth2AuthenticationToken(oAuth2AuthenticationToken.getPrincipal(), grantedAuthorities,
oAuth2AuthenticationToken.getAuthorizedClientRegistrationId());
if (authentication.getPrincipal() instanceof OidcUser user) {
// if there is no refresh token and the access token is expired then relogin is required
if (user.getIdToken().getExpiresAt() != null && Instant.now().isAfter(user.getIdToken().getExpiresAt())) {
throw new AccountExpiredException("Token expired");
}
}
} else {
newAuthentication = new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(),
grantedAuthorities);
}
securityContextHolderStrategy.getContext().setAuthentication(newAuthentication);
}
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2023 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.view;
import jakarta.annotation.security.RolesAllowed;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.RouteAlias;
import com.vaadin.flow.theme.lumo.LumoUtility.Margin;
import org.eclipse.hawkbit.ui.MainLayout;
@PageTitle("About")
@Route(value = "about", layout = MainLayout.class)
@RouteAlias(value = "", layout = MainLayout.class)
@RolesAllowed({ "ANONYMOUS" })
public class AboutView extends VerticalLayout {
public AboutView() {
setSpacing(false);
final Image img = new Image("images/about_image.png", "hawkBit");
img.setWidth("200px");
add(img);
final H2 header = new H2("Eclipse hawkBit UI");
header.addClassNames(Margin.Top.XLARGE, Margin.Bottom.MEDIUM);
setSizeFull();
setJustifyContentMode(JustifyContentMode.CENTER);
setDefaultHorizontalComponentAlignment(Alignment.CENTER);
getStyle().set("text-align", "center");
}
}

View File

@@ -0,0 +1,89 @@
/**
* Copyright (c) 2023 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.view;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import jakarta.annotation.security.RolesAllowed;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.NumberField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.mgmt.json.model.system.MgmtSystemTenantConfigurationValueRequest;
import org.eclipse.hawkbit.ui.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.MainLayout;
@PageTitle("Config")
@Route(value = "config", layout = MainLayout.class)
@RolesAllowed({ "CONFIG_READ" })
@Slf4j
public class ConfigView extends VerticalLayout {
private static final String WIDTH = "width";
private static final String PX_300 = "300px";
private final Map<String, MgmtSystemTenantConfigurationValueRequest> configValue = new HashMap<>();
public ConfigView(final HawkbitMgmtClient hawkbitClient) {
setSpacing(false);
final Button saveButton = new Button("Save");
Optional.ofNullable(
hawkbitClient.getTenantManagementRestApi().getTenantConfiguration().getBody()).ifPresent(config ->
config.forEach((k, v) -> {
if (v.getValue() instanceof String strValue) {
final TextField tf = new TextField(k, strValue, event -> {
final MgmtSystemTenantConfigurationValueRequest vre = new MgmtSystemTenantConfigurationValueRequest();
vre.setValue(event.getValue());
configValue.put(k, vre);
});
tf.getElement().getStyle().set(WIDTH, PX_300);
add(tf);
} else if (v.getValue() instanceof Boolean boolValue) {
add(new Checkbox(k, boolValue, event -> {
final MgmtSystemTenantConfigurationValueRequest vre = new MgmtSystemTenantConfigurationValueRequest();
vre.setValue(event.getValue());
configValue.put(k, vre);
}));
} else if (v.getValue() instanceof Long longValue) {
final NumberField nf = new NumberField(k, (double) longValue, event -> {
final MgmtSystemTenantConfigurationValueRequest vre = new MgmtSystemTenantConfigurationValueRequest();
vre.setValue(event.getValue());
configValue.put(k, vre);
});
nf.getElement().getStyle().set(WIDTH, PX_300);
add(nf);
} else if (v.getValue() instanceof Integer intValue) {
final NumberField nf = new NumberField(k, (double) intValue, event -> {
MgmtSystemTenantConfigurationValueRequest vre = new MgmtSystemTenantConfigurationValueRequest();
vre.setValue(event.getValue());
configValue.put(k, vre);
});
nf.getElement().getStyle().set(WIDTH, PX_300);
add(nf);
} else {
log.debug("Unexpected value type: {} -> {} (class: {})",
k, v.getValue(), v.getValue() == null ? "null" : v.getValue().getClass());
}
}));
saveButton.addClickListener(click -> configValue.forEach(
(key, value) -> hawkbitClient.getTenantManagementRestApi().updateTenantConfigurationValue(key, value)));
saveButton.addClickShortcut(Key.ENTER);
add(saveButton);
}
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2023 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.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
@SuppressWarnings("java:S1214")
public interface Constants {
// properties
String ID = "Id";
String ADDRESS = "Address";
String NAME = "Name";
String DESCRIPTION = "Description";
String VERSION = "Version";
String VENDOR = "Vendor";
String TYPE = "Type";
String GROUP = "Group";
String CREATED_BY = "Created by";
String CREATED_AT = "Created at";
String LAST_MODIFIED_BY = "Last modified by";
String LAST_MODIFIED_AT = "Last modified at";
String LAST_POLL = "Last Poll";
String SECURITY_TOKEN = "Security Token";
String ATTRIBUTES = "Attributes";
// rollout
String GROUP_COUNT = "Group Count";
String TARGET_COUNT = "Target Count";
String STATS = "Stats";
String STATUS = "Status";
String ACTIONS = "Actions";
// create rollout
String TARGET_FILTER = "Target Filter";
String DISTRIBUTION_SET = "Distribution Set";
String ACTION_TYPE = "Action Type";
String START_AT = "Start At";
String SOFT = "Soft";
String FORCED = "Forced";
String DOWNLOAD_ONLY = "Download Only";
String START_TYPE = "Start Type";
String MANUAL = "Manual";
String AUTO = "Auto";
String DYNAMIC = "Dynamic";
// dialog
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)";
}

View File

@@ -0,0 +1,385 @@
/**
* Copyright (c) 2023 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.view;
import java.util.Collection;
import java.util.Collections;
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;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.checkbox.CheckboxGroup;
import com.vaadin.flow.component.dependency.Uses;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPost;
import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetType;
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule;
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleAssignment;
import org.eclipse.hawkbit.mgmt.json.model.tag.MgmtTag;
import org.eclipse.hawkbit.ui.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.MainLayout;
import org.eclipse.hawkbit.ui.view.util.Filter;
import org.eclipse.hawkbit.ui.view.util.SelectionGrid;
import org.eclipse.hawkbit.ui.view.util.TableView;
import org.eclipse.hawkbit.ui.view.util.Utils;
@PageTitle("Distribution Sets")
@Route(value = "distribution_sets", layout = MainLayout.class)
@RolesAllowed({ "DISTRIBUTION_SET_READ" })
@Uses(Icon.class)
public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
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<MgmtDistributionSet> grid) {
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(), Utils.getSortParam(query
.getSortOrders()))
.getBody())
.stream().flatMap(body -> body.getContent().stream()),
e -> new CreateDialog(hawkbitClient).result(),
selectionGrid -> {
selectionGrid.getSelectedItems().forEach(
distributionSet -> hawkbitClient.getDistributionSetRestApi()
.deleteDistributionSet(distributionSet.getId()));
return CompletableFuture.completedFuture(null);
}, null);
}
private static SelectionGrid<MgmtSoftwareModule, Long> selectSoftwareModuleGrid() {
return new SelectionGrid<>(
new SelectionGrid.EntityRepresentation<>(
MgmtSoftwareModule.class, MgmtSoftwareModule::getId) {
@Override
protected void addColumns(Grid<MgmtSoftwareModule> grid) {
grid.addColumn(MgmtSoftwareModule::getId).setHeader(Constants.ID).setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getName).setHeader(Constants.NAME).setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getVersion).setHeader(Constants.VERSION).setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getTypeName).setHeader(Constants.TYPE).setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getVendor).setHeader(Constants.VENDOR).setAutoWidth(true);
}
});
}
private static class DistributionSetRawFilter implements Filter.Rsql, Filter.RsqlRw {
private final TextField name = Utils.textField("Name");
private DistributionSetRawFilter() {
name.setPlaceholder("<rsql filter>");
}
@Override
public List<Component> components() {
return List.of(name);
}
@Override
public String filter() {
return name.getOptionalValue().orElse(null);
}
@Override
public void setFilter(String filter) {
name.setValue(filter);
}
}
private static class DistributionSetFilter implements Filter.Rsql {
private final TextField textFilter = Utils.textField("Filter");
private final CheckboxGroup<MgmtDistributionSetType> type = new CheckboxGroup<>("Type");
private final CheckboxGroup<MgmtTag> tag = new CheckboxGroup<>("Tag");
private DistributionSetFilter(final HawkbitMgmtClient hawkbitClient) {
textFilter.setPlaceholder("<name/version filter>");
type.setItemLabelGenerator(MgmtDistributionSetType::getName);
type.setItems(Optional.ofNullable(
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())
.map(PagedList::getContent)
.orElseGet(Collections::emptyList));
}
@Override
public List<Component> components() {
return List.of(textFilter, type);
}
@Override
public String filter() {
return Filter.filter(
Map.of(
List.of("version", "name"), textFilter.getOptionalValue().map(s -> "*" + s + "*"),
"type", type.getSelectedItems().stream().map(MgmtDistributionSetType::getKey).toList(),
"tag", tag.getSelectedItems().stream().map(MgmtTag::getName).toList()));
}
}
private static class DistributionSetDetails extends FormLayout {
private final transient HawkbitMgmtClient hawkbitClient;
private final TextArea description = new TextArea("Description");
private final TextField createdBy = Utils.textField("Created by");
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<MgmtSoftwareModule, Long> softwareModulesGrid = selectSoftwareModuleGrid();
private DistributionSetDetails(final HawkbitMgmtClient hawkbitClient) {
this.hawkbitClient = hawkbitClient;
description.setMinLength(2);
Stream.of(
description,
createdBy, createdAt,
lastModifiedBy, lastModifiedAt, metadata)
.forEach(field -> {
field.setReadOnly(true);
add(field);
});
add(softwareModulesGrid);
setResponsiveSteps(new ResponsiveStep("0", 2));
setColspan(description, 2);
setColspan(softwareModulesGrid, 2);
}
private void setItem(final MgmtDistributionSet distributionSet) {
description.setValue(distributionSet.getDescription());
createdBy.setValue(distributionSet.getCreatedBy());
createdAt.setValue(Utils.localDateTimeFromTs(distributionSet.getCreatedAt()));
lastModifiedBy.setValue(distributionSet.getLastModifiedBy());
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()
.getAssignedSoftwareModules(
distributionSet.getId(),
query.getOffset(), query.getLimit(), Constants.NAME_ASC)
.getBody()).stream().flatMap(body -> body.getContent().stream()));
softwareModulesGrid.setSelectionMode(Grid.SelectionMode.NONE);
}
}
private static class CreateDialog extends Utils.BaseDialog<Void> {
private final transient HawkbitMgmtClient hawkbitClient;
private final Select<MgmtDistributionSetType> type;
private final TextField name;
private final TextField version;
private final TextArea description;
private final Checkbox requiredMigrationStep;
private final Button create;
private CreateDialog(final HawkbitMgmtClient hawkbitClient) {
super("Create Distribution Set");
this.hawkbitClient = hawkbitClient;
type = new Select<>(
"Type",
this::readyToCreate,
Optional.ofNullable(
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();
type.setWidthFull();
type.setRequiredIndicatorVisible(true);
type.setItemLabelGenerator(MgmtDistributionSetType::getName);
name = Utils.textField(Constants.NAME, this::readyToCreate);
version = Utils.textField(Constants.VERSION, this::readyToCreate);
final TextField vendor = Utils.textField(Constants.VENDOR);
description = new TextArea(Constants.DESCRIPTION);
description.setWidthFull();
description.setMinLength(2);
requiredMigrationStep = new Checkbox("Required Migration Step");
create = Utils.tooltip(new Button("Create"), "Create (Enter)");
create.setEnabled(false);
addCreateClickListener();
create.addClickShortcut(Key.ENTER);
final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC);
cancel.addClickListener(e -> close());
create.addClickShortcut(Key.ESCAPE);
create.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
getFooter().add(cancel);
getFooter().add(create);
final VerticalLayout layout = new VerticalLayout();
layout.setSizeFull();
layout.setPadding(true);
layout.setSpacing(false);
layout.add(type, name, version, vendor, description, requiredMigrationStep);
add(layout);
open();
}
private void readyToCreate(final Object v) {
final boolean createEnabled = !type.isEmpty() && !name.isEmpty() && !version.isEmpty();
if (create.isEnabled() != createEnabled) {
create.setEnabled(createEnabled);
}
}
private void addCreateClickListener() {
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())
.stream()
.flatMap(Collection::stream)
.findFirst()
.orElseThrow()
.getId();
new AddSoftwareModulesDialog(distributionSetId, hawkbitClient).open();
});
}
}
@SuppressWarnings({ "java:S1171", "java:S3599" })
private static class AddSoftwareModulesDialog extends Utils.BaseDialog<Void> {
private final transient Set<MgmtSoftwareModule> softwareModules = Collections.synchronizedSet(new HashSet<>());
private AddSoftwareModulesDialog(final long distributionSetId, final HawkbitMgmtClient hawkbitClient) {
super("Add Software Modules");
final SelectionGrid<MgmtSoftwareModule, Long> softwareModulesGrid = selectSoftwareModuleGrid();
softwareModulesGrid.setItems(query -> {
query.getOffset(); // to keep vaadin contract
return softwareModules.stream().limit(query.getLimit());
});
final Component addRemoveControls = Utils.addRemoveControls(
v -> new Utils.BaseDialog<Void>("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);
return CompletableFuture.completedFuture(null);
},
softwareModulesGrid, true);
final Button finishBtn = Utils.tooltip(new Button("Finish"), "Finish (Esc)");
finishBtn.addClickListener(e -> {
hawkbitClient.getDistributionSetRestApi().assignSoftwareModules(
distributionSetId, softwareModules.stream().map(softwareModule -> {
final MgmtSoftwareModuleAssignment assignment = new MgmtSoftwareModuleAssignment();
assignment.setId(softwareModule.getId());
return assignment;
}).toList());
close();
});
finishBtn.addClickShortcut(Key.ENTER);
finishBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
getFooter().add(finishBtn);
final HorizontalLayout addRemove = new HorizontalLayout(addRemoveControls);
addRemove.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
addRemove.setWidthFull();
final VerticalLayout layout = new VerticalLayout();
layout.setSizeFull();
layout.setSpacing(false);
layout.add(softwareModulesGrid, addRemove);
add(layout);
}
}
}

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) 2023 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.view;
import com.vaadin.flow.component.login.LoginI18n;
import com.vaadin.flow.component.login.LoginOverlay;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.internal.RouteUtil;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import org.eclipse.hawkbit.ui.security.AuthenticatedUser;
@AnonymousAllowed
@PageTitle("Login")
@Route(value = "login")
public class LoginView extends LoginOverlay implements BeforeEnterObserver {
private final transient AuthenticatedUser authenticatedUser;
public LoginView(final AuthenticatedUser authenticatedUser) {
this.authenticatedUser = authenticatedUser;
setAction(RouteUtil.getRoutePath(VaadinService.getCurrent().getContext(), getClass()));
final LoginI18n i18n = LoginI18n.createDefault();
i18n.setHeader(new LoginI18n.Header());
i18n.getHeader().setTitle("hawkBit");
i18n.getHeader().setDescription("Login in hawkBit");
i18n.setAdditionalInformation(null);
setI18n(i18n);
setForgotPasswordButtonVisible(false);
setOpened(true);
}
@Override
public void beforeEnter(final BeforeEnterEvent event) {
if (authenticatedUser.getName().isPresent()) { // already logged in
setOpened(false);
event.forwardTo("");
}
setError(event.getLocation().getQueryParameters().getParameters().containsKey("error"));
}
}

View File

@@ -0,0 +1,440 @@
/**
* Copyright (c) 2023 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.view;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import jakarta.annotation.security.RolesAllowed;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.Text;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.datetimepicker.DateTimePicker;
import com.vaadin.flow.component.dependency.Uses;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.component.textfield.NumberField;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
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.rollout.MgmtRolloutCondition;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutErrorAction;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutResponseBody;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPost;
import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroupResponseBody;
import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQuery;
import org.eclipse.hawkbit.ui.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.MainLayout;
import org.eclipse.hawkbit.ui.view.util.Filter;
import org.eclipse.hawkbit.ui.view.util.SelectionGrid;
import org.eclipse.hawkbit.ui.view.util.TableView;
import org.eclipse.hawkbit.ui.view.util.Utils;
import org.springframework.util.ObjectUtils;
@PageTitle("Rollouts")
@Route(value = "rollouts", layout = MainLayout.class)
@RolesAllowed({ "ROLLOUT_READ" })
@Uses(Icon.class)
@SuppressWarnings({ "java:S1171", "java:S3599" })
public class RolloutView extends TableView<MgmtRolloutResponseBody, Long> {
public RolloutView(final HawkbitMgmtClient hawkbitClient) {
super(
new RolloutFilter(),
new SelectionGrid.EntityRepresentation<>(
MgmtRolloutResponseBody.class, MgmtRolloutResponseBody::getId) {
private final RolloutDetails details = new RolloutDetails(hawkbitClient);
@Override
protected void addColumns(final Grid<MgmtRolloutResponseBody> grid) {
grid.addColumn(MgmtRolloutResponseBody::getId).setHeader(Constants.ID).setAutoWidth(true);
grid.addColumn(MgmtRolloutResponseBody::getName).setHeader(Constants.NAME).setAutoWidth(true);
grid.addColumn(MgmtRolloutResponseBody::getTotalGroups).setHeader(Constants.GROUP_COUNT).setAutoWidth(true);
grid.addColumn(MgmtRolloutResponseBody::getTotalTargets).setHeader(Constants.TARGET_COUNT).setAutoWidth(true);
grid.addColumn(MgmtRolloutResponseBody::getTotalTargetsPerStatus).setHeader(Constants.STATS).setAutoWidth(true);
grid.addColumn(MgmtRolloutResponseBody::getStatus).setHeader(Constants.STATUS).setAutoWidth(true);
grid.addComponentColumn(rollout -> new Actions(rollout, grid, hawkbitClient)).setHeader(
Constants.ACTIONS).setAutoWidth(true);
grid.setItemDetailsRenderer(new ComponentRenderer<>(
() -> details, RolloutDetails::setItem));
}
},
(query, rsqlFilter) -> Optional.ofNullable(
hawkbitClient.getRolloutRestApi()
.getRollouts(
rsqlFilter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC, "full")
.getBody()).stream().flatMap(page -> page.getContent().stream()),
selectionGrid -> new CreateDialog(hawkbitClient).result(),
selectionGrid -> {
selectionGrid.getSelectedItems().forEach(
rollout -> hawkbitClient.getRolloutRestApi().delete(rollout.getId()));
selectionGrid.refreshGrid(false);
return CompletableFuture.completedFuture(null);
});
}
private static class Actions extends HorizontalLayout {
private final long rolloutId;
private final Grid<MgmtRolloutResponseBody> grid;
private final transient HawkbitMgmtClient hawkbitClient;
private Actions(final MgmtRolloutResponseBody rollout, final Grid<MgmtRolloutResponseBody> grid,
final HawkbitMgmtClient hawkbitClient) {
this.rolloutId = rollout.getId();
this.grid = grid;
this.hawkbitClient = hawkbitClient;
init(rollout);
}
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"));
} else if ("RUNNING".equalsIgnoreCase(rollout.getStatus())) {
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.TRASH.create()) {
{
addClickListener(v -> {
hawkbitClient.getRolloutRestApi().delete(rollout.getId());
grid.getDataProvider().refreshAll();
});
}
}, "Cancel and Remove"));
}
private void refresh() {
removeAll();
final MgmtRolloutResponseBody body = hawkbitClient.getRolloutRestApi().getRollout(rolloutId).getBody();
if (body != null) {
init(body);
}
}
}
private static class RolloutFilter implements Filter.Rsql {
private final TextField name = Utils.textField(Constants.NAME);
private RolloutFilter() {
name.setPlaceholder("<name filter>");
}
@Override
public List<Component> components() {
return List.of(name);
}
@Override
public String filter() {
return Filter.filter(Map.of("name", name.getOptionalValue()));
}
}
private static class RolloutDetails extends FormLayout {
private final transient HawkbitMgmtClient hawkbitClient;
private final TextArea description = new TextArea(Constants.DESCRIPTION);
private final TextField createdBy = Utils.textField(Constants.CREATED_BY);
private final TextField createdAt = Utils.textField(Constants.CREATED_AT);
private final TextField lastModifiedBy = Utils.textField(Constants.LAST_MODIFIED_BY);
private final TextField lastModifiedAt = Utils.textField(Constants.LAST_MODIFIED_AT);
private final TextField targetFilter = Utils.textField(Constants.TARGET_FILTER);
private final TextField distributionSet = Utils.textField(Constants.DISTRIBUTION_SET);
private final TextField actonType = Utils.textField(Constants.ACTION_TYPE);
private final TextField startAt = Utils.textField(Constants.START_AT);
private final Checkbox dynamic = new Checkbox(Constants.DYNAMIC);
private final SelectionGrid<MgmtRolloutGroupResponseBody, Long> groupGrid;
private RolloutDetails(final HawkbitMgmtClient hawkbitClient) {
this.hawkbitClient = hawkbitClient;
description.setMinLength(2);
groupGrid = createGroupGrid();
Stream.of(
description,
createdBy, createdAt,
lastModifiedBy, lastModifiedAt,
targetFilter, distributionSet,
actonType, startAt)
.forEach(field -> {
field.setReadOnly(true);
add(field);
});
dynamic.setReadOnly(true);
dynamic.setEnabled(false);
add(dynamic);
add(groupGrid);
setResponsiveSteps(new ResponsiveStep("0", 2));
setColspan(description, 2);
setColspan(groupGrid, 2);
}
private void setItem(final MgmtRolloutResponseBody rollout) {
description.setValue(rollout.getDescription());
createdBy.setValue(rollout.getCreatedBy());
createdAt.setValue(Utils.localDateTimeFromTs(rollout.getCreatedAt()));
lastModifiedBy.setValue(rollout.getLastModifiedBy());
lastModifiedAt.setValue(Utils.localDateTimeFromTs(rollout.getLastModifiedAt()));
targetFilter.setValue(rollout.getTargetFilterQuery());
final MgmtDistributionSet distributionSetMgmt = hawkbitClient.getDistributionSetRestApi()
.getDistributionSet(rollout.getDistributionSetId()).getBody();
distributionSet.setValue(distributionSetMgmt == null
? NOT_AVAILABLE_NULL // should not be the case
: distributionSetMgmt.getName() + ":" + distributionSetMgmt.getVersion());
actonType.setValue(switch (rollout.getType()) {
case SOFT -> Constants.SOFT;
case FORCED -> Constants.FORCED;
case DOWNLOAD_ONLY -> Constants.DOWNLOAD_ONLY;
case TIMEFORCED -> "Scheduled at " + Utils.localDateTimeFromTs(rollout.getForcetime());
});
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())
.stream().flatMap(body -> body.getContent().stream())
.skip(query.getOffset())
.limit(query.getPageSize()));
groupGrid.setSelectionMode(Grid.SelectionMode.NONE);
}
private static SelectionGrid<MgmtRolloutGroupResponseBody, Long> createGroupGrid() {
return new SelectionGrid<>(
new SelectionGrid.EntityRepresentation<>(MgmtRolloutGroupResponseBody.class, MgmtRolloutGroupResponseBody::getId) {
@Override
protected void addColumns(final Grid<MgmtRolloutGroupResponseBody> grid) {
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::getStatus).setHeader(Constants.STATUS).setAutoWidth(true);
}
});
}
}
private static class CreateDialog extends Utils.BaseDialog<Void> {
private final TextField name;
private final Select<MgmtDistributionSet> distributionSet;
private final Select<MgmtTargetFilterQuery> targetFilter;
private final TextArea description;
private final Select<MgmtActionType> actionType;
private final DateTimePicker forceTime = new DateTimePicker("Force Time");
private final Select<StartType> startType;
private final DateTimePicker startAt = new DateTimePicker(Constants.START_AT);
private final NumberField groupNumber;
private final NumberField triggerThreshold;
private final NumberField errorThreshold;
private final Checkbox dynamic = new Checkbox(Constants.DYNAMIC);
private final Button create = new Button("Create");
private CreateDialog(final HawkbitMgmtClient hawkbitClient) {
super("Create Rollout");
name = Utils.textField("Name", this::readyToCreate);
name.focus();
distributionSet = new Select<>(
"Distribution Set",
this::readyToCreate,
Optional.ofNullable(
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.setWidthFull();
targetFilter = new Select<>(
"Target Filter",
this::readyToCreate,
Optional.ofNullable(
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);
targetFilter.setItemLabelGenerator(MgmtTargetFilterQuery::getName);
targetFilter.setWidthFull();
description = new TextArea(Constants.DESCRIPTION);
description.setMinLength(2);
description.setWidthFull();
actionType = Utils.actionTypeControls(forceTime);
startType = new Select<>();
startType.setValue(StartType.MANUAL);
startType.setLabel(Constants.START_TYPE);
startType.setItems(StartType.values());
startType.setValue(StartType.MANUAL);
final ComponentRenderer<Component, StartType> 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.setWidthFull();
final Div percentSuffix = new Div();
percentSuffix.setText("%");
groupNumber = Utils.numberField("Group number", this::readyToCreate);
groupNumber.setMin(1);
groupNumber.setValue(1.0);
triggerThreshold = Utils.numberField("Trigger Threshold", this::readyToCreate);
triggerThreshold.setMin(0);
triggerThreshold.setMax(100);
triggerThreshold.setValue(100.0);
triggerThreshold.setSuffixComponent(percentSuffix);
errorThreshold = Utils.numberField("Error Threshold", this::readyToCreate);
errorThreshold.setMin(1);
errorThreshold.setMax(100);
errorThreshold.setValue(10.0);
errorThreshold.setSuffixComponent(percentSuffix);
create.setEnabled(false);
create.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
addCreateClickListener(hawkbitClient);
final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC);
cancel.addClickListener(e -> close());
cancel.addClickShortcut(Key.ESCAPE);
getFooter().add(cancel);
getFooter().add(create);
final VerticalLayout layout = new VerticalLayout();
layout.setSizeFull();
layout.setSpacing(false);
layout.add(
name, distributionSet, targetFilter, description,
actionType, startType,
groupNumber, triggerThreshold, errorThreshold,
dynamic);
add(layout);
open();
}
private void readyToCreate(final Object v) {
final boolean createEnabled = !name.isEmpty() && !distributionSet.isEmpty() && !targetFilter.isEmpty() && !groupNumber
.isEmpty() && !triggerThreshold.isEmpty() && !errorThreshold.isEmpty();
if (create.isEnabled() != createEnabled) {
create.setEnabled(createEnabled);
}
}
private void addCreateClickListener(final HawkbitMgmtClient hawkbitClient) {
create.addClickListener(e -> {
close();
final MgmtRolloutRestRequestBodyPost request = new MgmtRolloutRestRequestBodyPost();
request.setName(name.getValue());
request.setDistributionSetId(distributionSet.getValue().getId());
request.setTargetFilterQuery(targetFilter.getValue().getQuery());
request.setDescription(description.getValue());
request.setType(actionType.getValue());
if (actionType.getValue() == MgmtActionType.TIMEFORCED) {
request.setForcetime(
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);
case MANUAL -> {
// do nothing, will be started manually
}
}
request.setAmountGroups(groupNumber.getValue().intValue());
request.setSuccessCondition(
new MgmtRolloutCondition(
MgmtRolloutCondition.Condition.THRESHOLD,
triggerThreshold.getValue().intValue() + ""));
request.setErrorCondition(
new MgmtRolloutCondition(
MgmtRolloutCondition.Condition.THRESHOLD,
errorThreshold.getValue().intValue() + ""));
request.setErrorAction(
new MgmtRolloutErrorAction(
MgmtRolloutErrorAction.ErrorAction.PAUSE, ""));
request.setDynamic(dynamic.getValue());
hawkbitClient.getRolloutRestApi().create(request).getBody();
});
}
private enum StartType {
MANUAL, AUTO, SCHEDULED
}
}
}

View File

@@ -0,0 +1,432 @@
/**
* Copyright (c) 2023 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.view;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
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.Stream;
import jakarta.annotation.security.RolesAllowed;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.checkbox.CheckboxGroup;
import com.vaadin.flow.component.dependency.Uses;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.upload.Upload;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.streams.UploadEvent;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.artifact.MgmtArtifact;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPost;
import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetType;
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule;
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleAssignment;
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleRequestBodyPost;
import org.eclipse.hawkbit.mgmt.json.model.softwaremoduletype.MgmtSoftwareModuleType;
import org.eclipse.hawkbit.ui.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.MainLayout;
import org.eclipse.hawkbit.ui.view.util.Filter;
import org.eclipse.hawkbit.ui.view.util.SelectionGrid;
import org.eclipse.hawkbit.ui.view.util.TableView;
import org.eclipse.hawkbit.ui.view.util.Utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.multipart.MultipartFile;
@PageTitle("Software Modules")
@Route(value = "software_modules", layout = MainLayout.class)
@RolesAllowed({ "SOFTWARE_MODULE_READ" })
@Uses(Icon.class)
@Slf4j
public class SoftwareModuleView extends TableView<MgmtSoftwareModule, Long> {
@Autowired
public SoftwareModuleView(final HawkbitMgmtClient hawkbitClient) {
this(true, hawkbitClient);
}
public SoftwareModuleView(final boolean isParent, final HawkbitMgmtClient hawkbitClient) {
super(
new SoftwareModuleFilter(hawkbitClient),
new SelectionGrid.EntityRepresentation<>(MgmtSoftwareModule.class, MgmtSoftwareModule::getId) {
private final SoftwareModuleDetails details = new SoftwareModuleDetails(hawkbitClient);
@Override
protected void addColumns(final Grid<MgmtSoftwareModule> grid) {
grid.addColumn(MgmtSoftwareModule::getId).setHeader(Constants.ID).setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getName).setHeader(Constants.NAME).setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getVersion).setHeader(Constants.VERSION).setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getTypeName).setHeader(Constants.TYPE).setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getVendor).setHeader(Constants.VENDOR).setAutoWidth(true);
grid.setItemDetailsRenderer(new ComponentRenderer<>(() -> details, SoftwareModuleDetails::setItem));
}
},
(query, rsqlFilter) -> Optional.ofNullable(
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 -> {
selectionGrid.getSelectedItems().forEach(
module -> hawkbitClient.getSoftwareModuleRestApi().deleteSoftwareModule(module.getId()));
selectionGrid.refreshGrid(false);
return CompletableFuture.completedFuture(null);
} : null);
}
public Set<MgmtSoftwareModule> getSelection() {
return selectionGrid.getSelectedItems();
}
private static SelectionGrid<MgmtArtifact, Long> createArtifactGrid() {
return new SelectionGrid<>(
new SelectionGrid.EntityRepresentation<>(MgmtArtifact.class, MgmtArtifact::getId) {
@Override
protected void addColumns(final Grid<MgmtArtifact> grid) {
grid.addColumn(MgmtArtifact::getId).setHeader(Constants.ID).setAutoWidth(true);
grid.addColumn(MgmtArtifact::getProvidedFilename).setHeader(Constants.NAME).setAutoWidth(true);
grid.addColumn(MgmtArtifact::getSize).setHeader("Size").setAutoWidth(true);
}
});
}
private static class SoftwareModuleFilter implements Filter.Rsql {
private final TextField name = Utils.textField(Constants.NAME);
private final CheckboxGroup<MgmtSoftwareModuleType> type = new CheckboxGroup<>(Constants.TYPE);
private SoftwareModuleFilter(final HawkbitMgmtClient hawkbitClient) {
name.setPlaceholder("<name filter>");
type.setItemLabelGenerator(MgmtSoftwareModuleType::getName);
type.setItems(Optional.ofNullable(
hawkbitClient.getSoftwareModuleTypeRestApi()
.getTypes(null, 0, 20, Constants.NAME_ASC)
.getBody())
.map(PagedList::getContent)
.orElseGet(Collections::emptyList));
}
@Override
public List<Component> components() {
return List.of(name, type);
}
@Override
public String filter() {
return Filter.filter(
Map.of(
"name", name.getOptionalValue(),
"type", type.getSelectedItems().stream().map(MgmtSoftwareModuleType::getKey).toList()
));
}
}
private static class SoftwareModuleDetails extends FormLayout {
private final transient HawkbitMgmtClient hawkbitClient;
private final TextArea description = new TextArea(Constants.DESCRIPTION);
private final TextField createdBy = Utils.textField(Constants.CREATED_BY);
private final TextField createdAt = Utils.textField(Constants.CREATED_AT);
private final TextField lastModifiedBy = Utils.textField(Constants.LAST_MODIFIED_BY);
private final TextField lastModifiedAt = Utils.textField(Constants.LAST_MODIFIED_AT);
private final SelectionGrid<MgmtArtifact, Long> artifactGrid;
private SoftwareModuleDetails(final HawkbitMgmtClient hawkbitClient) {
this.hawkbitClient = hawkbitClient;
description.setMinLength(2);
artifactGrid = createArtifactGrid();
Stream.of(description, createdBy, createdAt, lastModifiedBy, lastModifiedAt).forEach(field -> {
field.setReadOnly(true);
add(field);
});
add(artifactGrid);
setResponsiveSteps(new ResponsiveStep("0", 2));
setColspan(description, 2);
setColspan(artifactGrid, 2);
}
private void setItem(final MgmtSoftwareModule softwareModule) {
description.setValue(softwareModule.getDescription());
createdBy.setValue(softwareModule.getCreatedBy());
createdAt.setValue(Utils.localDateTimeFromTs(softwareModule.getCreatedAt()));
lastModifiedBy.setValue(softwareModule.getLastModifiedBy());
lastModifiedAt.setValue(Utils.localDateTimeFromTs(softwareModule.getLastModifiedAt()));
artifactGrid.setItems(query -> Optional.ofNullable(
hawkbitClient.getSoftwareModuleRestApi()
.getArtifacts(softwareModule.getId(), null, null)
.getBody())
.stream()
.flatMap(Collection::stream)
.skip(query.getOffset())
.limit(query.getPageSize()));
artifactGrid.setSelectionMode(Grid.SelectionMode.NONE);
}
}
private static class CreateDialog extends Utils.BaseDialog<Void> {
private final Select<MgmtSoftwareModuleType> type;
private final TextField name;
private final TextField version;
private final TextField vendor;
private final TextArea description;
private final Checkbox enableArtifactEncryption;
private final Checkbox createDistributionSet;
private final Select<MgmtDistributionSetType> distType;
private final Checkbox distRequiredMigrationStep;
private final Button create;
private CreateDialog(final HawkbitMgmtClient hawkbitClient) {
super("Create Software Module");
type = new Select<>(
Constants.TYPE,
this::readyToCreate,
Optional.ofNullable(
hawkbitClient.getSoftwareModuleTypeRestApi()
.getTypes(null, 0, 30, Constants.NAME_ASC)
.getBody())
.map(body -> body.getContent().toArray(new MgmtSoftwareModuleType[0]))
.orElseGet(() -> new MgmtSoftwareModuleType[0]));
type.setWidthFull();
type.setRequiredIndicatorVisible(true);
type.setItemLabelGenerator(MgmtSoftwareModuleType::getName);
type.focus();
name = Utils.textField(Constants.NAME, this::readyToCreate);
version = Utils.textField(Constants.VERSION, this::readyToCreate);
vendor = Utils.textField(Constants.VENDOR);
description = new TextArea(Constants.DESCRIPTION);
description.setWidthFull();
description.setMinLength(2);
enableArtifactEncryption = new Checkbox("Enable artifact encryption");
distType = new Select<>("Distribution Set Type", this::readyToCreate);
distType.setWidthFull();
distType.setRequiredIndicatorVisible(true);
distType.setItemLabelGenerator(MgmtDistributionSetType::getName);
distType.setVisible(false);
distRequiredMigrationStep = new Checkbox("Required Migration Step");
distRequiredMigrationStep.setVisible(false);
createDistributionSet = new Checkbox("Create single software module distribution set");
createDistributionSet.setHelperText("Create single software module distribution set with this software module");
createDistributionSet.addValueChangeListener(e -> {
if (Boolean.TRUE.equals(createDistributionSet.getValue()) && distType.isEmpty()) {
distType.setItems(
Optional.ofNullable(
hawkbitClient.getDistributionSetTypeRestApi()
.getDistributionSetTypes(null, 0, 30, Constants.NAME_ASC)
.getBody())
.map(body -> body.getContent().toArray(new MgmtDistributionSetType[0]))
.orElseGet(() -> new MgmtDistributionSetType[0]));
}
distType.setVisible(createDistributionSet.getValue());
distRequiredMigrationStep.setVisible(createDistributionSet.getValue());
readyToCreate(e);
});
create = Utils.tooltip(new Button("Create"), "Create (Enter)");
create.setEnabled(false);
addCreateClickListener(hawkbitClient);
create.addClickShortcut(Key.ENTER);
create.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC);
cancel.addClickListener(e -> close());
cancel.addClickShortcut(Key.ESCAPE);
getFooter().add(cancel);
getFooter().add(create);
final VerticalLayout layout = new VerticalLayout();
layout.setSizeFull();
layout.setSpacing(false);
layout.add(
type, name, version, vendor, description, enableArtifactEncryption,
createDistributionSet, distType, distRequiredMigrationStep);
add(layout);
open();
}
private void readyToCreate(final Object v) {
final boolean createEnabled = !type.isEmpty() && !name.isEmpty() && !version.isEmpty() && (!createDistributionSet
.getValue() || !distType.isEmpty());
if (create.isEnabled() != createEnabled) {
create.setEnabled(createEnabled);
}
}
private void addCreateClickListener(final HawkbitMgmtClient hawkbitClient) {
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())
.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())
.stream()
.flatMap(Collection::stream)
.findFirst()
.orElseThrow()
.getId();
hawkbitClient.getDistributionSetRestApi().assignSoftwareModules(
distributionSetId, List.of((MgmtSoftwareModuleAssignment) new MgmtSoftwareModuleAssignment()
.setId(softwareModuleId))).getBody();
}
new AddArtifactsDialog(softwareModuleId, hawkbitClient).open();
});
}
}
private static class AddArtifactsDialog extends Utils.BaseDialog<Void> {
private final transient Set<MgmtArtifact> artifacts = Collections.synchronizedSet(new HashSet<>());
private AddArtifactsDialog(
final long softwareModuleId,
final HawkbitMgmtClient hawkbitClient) {
super("Add Artifacts");
final SelectionGrid<MgmtArtifact, Long> artifactGrid = createArtifactGrid();
artifactGrid.setItems(query -> {
query.getOffset(); // to keep vaadin contract
return artifacts.stream().limit(query.getLimit());
});
artifactGrid.setSelectionMode(Grid.SelectionMode.NONE);
final Upload uploadBtn = new Upload(uploadEvent -> {
final MgmtArtifact artifact = hawkbitClient.getSoftwareModuleRestApi()
.uploadArtifact(
softwareModuleId,
new MultipartFileImpl(uploadEvent),
uploadEvent.getFileName(), null, null, null)
.getBody();
artifacts.add(artifact);
artifactGrid.refreshGrid(false);
});
uploadBtn.setMaxFiles(10);
uploadBtn.setWidthFull();
uploadBtn.setDropAllowed(true);
final Button finishBtn = Utils.tooltip(new Button("Finish"), "Finish (Enter)");
finishBtn.addClickListener(e -> close());
finishBtn.addClickShortcut(Key.ENTER);
finishBtn.setHeightFull();
finishBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
getFooter().add(finishBtn);
final VerticalLayout layout = new VerticalLayout(artifactGrid, uploadBtn);
layout.setSizeFull();
layout.setSpacing(false);
add(layout);
}
private static class MultipartFileImpl implements MultipartFile {
private final UploadEvent uploadEvent;
public MultipartFileImpl(final UploadEvent uploadEvent) {
this.uploadEvent = uploadEvent;
}
@Override
public String getName() {
return uploadEvent.getFileName();
}
@Override
public String getOriginalFilename() {
return getName();
}
@Override
public String getContentType() {
return uploadEvent.getContentType();
}
@Override
public boolean isEmpty() {
return uploadEvent.getFileSize() == 0;
}
@Override
public long getSize() {
return uploadEvent.getFileSize();
}
@Override
public byte[] getBytes() throws IOException {
log.warn("Multipart file getBytes() is called. Whole input stream is loaded into the memory!");
try (final InputStream is = getInputStream()) {
return is.readAllBytes();
}
}
@Override
public InputStream getInputStream() {
return uploadEvent.getInputStream();
}
@Override
public void transferTo(final File dest) throws IllegalStateException {
throw new UnsupportedOperationException();
}
}
}
}

View File

@@ -0,0 +1,963 @@
/**
* Copyright (c) 2023 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.view;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;
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;
import com.vaadin.flow.component.ClickEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.Key;
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.checkbox.CheckboxGroup;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.datetimepicker.DateTimePicker;
import com.vaadin.flow.component.dependency.Uses;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Input;
import com.vaadin.flow.component.html.Span;
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.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.component.tabs.TabSheet;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.renderer.ComponentRenderer;
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.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.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;
import org.eclipse.hawkbit.mgmt.json.model.tag.MgmtTag;
import org.eclipse.hawkbit.mgmt.json.model.tag.MgmtTagRequestBodyPut;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetAttributes;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetRequestBody;
import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQuery;
import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQueryRequestBody;
import org.eclipse.hawkbit.mgmt.json.model.targettype.MgmtTargetType;
import org.eclipse.hawkbit.ui.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.MainLayout;
import org.eclipse.hawkbit.ui.component.TargetActionsHistory;
import org.eclipse.hawkbit.ui.view.util.Filter;
import org.eclipse.hawkbit.ui.view.util.LinkedTextArea;
import org.eclipse.hawkbit.ui.view.util.SelectionGrid;
import org.eclipse.hawkbit.ui.view.util.TableView;
import org.eclipse.hawkbit.ui.view.util.Utils;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ObjectUtils;
@PageTitle("Targets")
@Route(value = "targets", layout = MainLayout.class)
@RolesAllowed({ "TARGET_READ" })
@Uses(Icon.class)
public class TargetView extends TableView<TargetView.TargetWithDs, String> {
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 static final String GREEN = "green";
public static final String RED = "red";
public static final String ORANGE = "orange";
public static final String GRAY = "gray";
public static final String BROWN = "brown";
public static final String TEAL = "teal";
public static final String PURPLE = "purple";
public static final String CORAL = "coral";
public static final String BLACK = "black";
public TargetView(final HawkbitMgmtClient hawkbitClient) {
super(
new RawFilter(hawkbitClient), new SimpleFilter(hawkbitClient),
new SelectionGrid.EntityRepresentation<>(TargetWithDs.class, TargetWithDs::getControllerId) {
@Override
protected void addColumns(final Grid<TargetWithDs> grid) {
grid.addColumn(new ComponentRenderer<>(TargetStatusCell::new))
.setHeader(STATUS)
.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(), Utils.getSortParam(query.getSortOrders(),
"lastModifiedAt:desc"))
.getBody()
.getContent()
.stream().map(m -> TargetWithDs.from(hawkbitClient, m)),
source -> new RegisterDialog(hawkbitClient).result(),
selectionGrid -> {
selectionGrid.getSelectedItems()
.forEach(toDelete -> hawkbitClient.getTargetRestApi().deleteTarget(toDelete.getControllerId()));
return CompletableFuture.completedFuture(null);
},
target -> {
final TargetDetailedView targetDetailedView = new TargetDetailedView(hawkbitClient);
targetDetailedView.setItem(target);
return targetDetailedView;
}
);
final Function<SelectionGrid<TargetWithDs, String>, CompletionStage<Void>> assignHandler = source -> new AssignDialog(
hawkbitClient, source.getSelectedItems()).result();
final Button assignBtn = Utils.tooltip(new Button(VaadinIcon.LINK.create()), "Assign");
assignBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
assignBtn.addClickListener(e -> assignHandler.apply(selectionGrid).thenAccept(v -> selectionGrid.refreshGrid(false)));
controlsLayout.addComponentAtIndex(0, assignBtn);
}
private static class SimpleFilter implements Filter.Rsql {
private final HawkbitMgmtClient hawkbitClient;
private final TextField textFilter;
private final CheckboxGroup<MgmtTargetType> type;
private final CheckboxGroup<MgmtTag> tag;
private SimpleFilter(final HawkbitMgmtClient hawkbitClient) {
this.hawkbitClient = hawkbitClient;
textFilter = Utils.textField(FILTER);
textFilter.setPlaceholder("<controller id/name filter>");
type = new CheckboxGroup<>(Constants.TYPE);
type.setItemLabelGenerator(MgmtTargetType::getName);
tag = new CheckboxGroup<>(TAG);
tag.setItemLabelGenerator(MgmtTag::getName);
}
@Override
public List<Component> components() {
final List<Component> components = new LinkedList<>();
components.add(textFilter);
type.setItems(hawkbitClient.getTargetTypeRestApi().getTargetTypes(null, 0, 20, Constants.NAME_ASC).getBody().getContent());
if (!((ListDataProvider) type.getDataProvider()).getItems().isEmpty()) {
components.add(type);
}
tag.setItems(hawkbitClient.getTargetTagRestApi().getTargetTags(null, 0, 20, Constants.NAME_ASC).getBody().getContent());
if (!((ListDataProvider) tag.getDataProvider()).getItems().isEmpty()) {
components.add(tag);
}
return components;
}
@Override
public String filter() {
return Filter.filter(
Map.of(
List.of("controllerid", "name"), textFilter.getOptionalValue().map(s -> "*" + s + "*"),
"targettype.name", type.getSelectedItems().stream().map(MgmtTargetType::getName)
.toList(),
"tag", tag.getSelectedItems().stream().map(MgmtTag::getName).toList()));
}
}
@SuppressWarnings({ "java:S1171", "java:S3599" })
private static class RawFilter implements Filter.Rsql, Filter.RsqlRw {
private final TextField textFilter = new TextField("Raw Filter", "<raw filter>");
private final VerticalLayout layout = new VerticalLayout();
private final Select<MgmtTargetFilterQuery> savedFilters = new Select<>();
private RawFilter(final HawkbitMgmtClient hawkbitClient) {
final Button createBtn = Utils.tooltip(new Button("Save", VaadinIcon.PLUS.create()), "Save");
final Button updateBtn = Utils.tooltip(new Button(VaadinIcon.HARDDRIVE.create()), "Update");
updateBtn.setEnabled(false);
savedFilters.setLabel("Saved Filters");
savedFilters.addValueChangeListener(e -> {
if (e.getValue() != null) {
textFilter.setValue(e.getValue().getQuery());
updateBtn.setEnabled(true);
createBtn.setText("Save as");
} else {
textFilter.clear();
updateBtn.setEnabled(false);
createBtn.setText("Save");
}
});
savedFilters.setEmptySelectionAllowed(true);
savedFilters.setItems(listFilters(hawkbitClient));
savedFilters.setItemLabelGenerator(query -> Optional.ofNullable(query).map(MgmtTargetFilterQuery::getName).orElse(
"<select saved filter>"));
savedFilters.setWidthFull();
textFilter.setWidthFull();
createBtn.addClickListener(createBtnListener(hawkbitClient));
updateBtn.addClickListener(updateBtnListener(hawkbitClient));
layout.setSpacing(false);
layout.setPadding(false);
final HorizontalLayout textSaveLayout = new HorizontalLayout(textFilter, createBtn, updateBtn);
textSaveLayout.setAlignItems(FlexComponent.Alignment.BASELINE);
textSaveLayout.setWidthFull();
layout.add(savedFilters, textSaveLayout);
layout.addClassNames(LumoUtility.Gap.SMALL);
}
private static List<MgmtTargetFilterQuery> listFilters(HawkbitMgmtClient hawkbitClient) {
return Optional
.ofNullable(
hawkbitClient.getTargetFilterQueryRestApi()
.getFilters(null, 0, 30, null, null)
.getBody()
.getContent())
.orElse(Collections.emptyList());
}
private ComponentEventListener<ClickEvent<Button>> createBtnListener(HawkbitMgmtClient hawkbitClient) {
return e -> new Utils.BaseDialog<Void>("Create New Filter") {
{
final Button finishBtn = Utils.tooltip(new Button("Save"), "Save (Enter)");
final TextField name = Utils.textField(Constants.NAME, e -> finishBtn.setEnabled(!e.getHasValue().isEmpty()));
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<ClickEvent<Button>> updateBtnListener(HawkbitMgmtClient hawkbitClient) {
return e -> {
final MgmtTargetFilterQuery selected = savedFilters.getValue();
if (selected == null) {
return;
}
new Utils.BaseDialog<Void>("Update Filter") {
{
final Button finishBtn = Utils.tooltip(new Button("Update"), "Update (Enter)");
finishBtn.setEnabled(false);
final TextField name = Utils.textField(Constants.NAME, e -> finishBtn.setEnabled(!e.getHasValue().isEmpty()));
name.focus();
name.setValue(selected.getName());
final TextArea filterValue = new TextArea("Filter Value");
filterValue.setReadOnly(true);
filterValue.setValue(textFilter.getValue());
filterValue.setWidthFull();
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();
}
};
};
}
@Override
public List<Component> components() {
return List.of(layout);
}
@Override
public String filter() {
return textFilter.getOptionalValue().orElse(null);
}
@Override
public void setFilter(String filter) {
textFilter.setValue(filter);
}
}
protected static class TargetDetailedView extends TabSheet {
private final TargetDetails targetDetails;
private final TargetAssignedInstalled targetAssignedInstalled;
private final TargetTags targetTags;
private final TargetActionsHistoryLayout targetActionsHistoryLayout;
private TargetDetailedView(final HawkbitMgmtClient hawkbitClient) {
targetDetails = new TargetDetails(hawkbitClient);
targetAssignedInstalled = new TargetAssignedInstalled(hawkbitClient);
targetTags = new TargetTags(hawkbitClient);
targetActionsHistoryLayout = new TargetActionsHistoryLayout(hawkbitClient);
setWidthFull();
add("Details", targetDetails);
add("Assigned / Installed", targetAssignedInstalled);
add("Tags", targetTags);
add("Action History", targetActionsHistoryLayout);
}
private void setItem(final MgmtTarget target) {
this.targetDetails.setItem(target);
this.targetAssignedInstalled.setItem(target);
this.targetTags.setItem(target);
this.targetActionsHistoryLayout.setItem(target);
}
}
private static class TargetDetails extends FormLayout {
private final transient HawkbitMgmtClient hawkbitClient;
private final TextArea description = new TextArea(Constants.DESCRIPTION);
private final TextField createdBy = Utils.textField(Constants.CREATED_BY);
private final TextField createdAt = Utils.textField(Constants.CREATED_AT);
private final TextField lastModifiedBy = Utils.textField(Constants.LAST_MODIFIED_BY);
private final TextField lastModifiedAt = Utils.textField(Constants.LAST_MODIFIED_AT);
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;
private TargetDetails(final HawkbitMgmtClient hawkbitClient) {
this.hawkbitClient = hawkbitClient;
description.setMinLength(2);
Stream.of(
description,
createdBy, createdAt,
lastModifiedBy, lastModifiedAt,
securityToken, lastPoll, group, targetAddress, targetAttributes
)
.forEach(field -> {
field.setReadOnly(true);
add(field);
});
setResponsiveSteps(new ResponsiveStep("0", 2));
setColspan(description, 2);
}
private void setItem(final MgmtTarget target) {
this.target = target;
}
@Override
protected void onAttach(final AttachEvent attachEvent) {
description.setValue(target.getDescription() == null ? "N/A" : target.getDescription());
createdBy.setValue(target.getCreatedBy());
createdAt.setValue(Utils.localDateTimeFromTs(target.getCreatedAt()));
lastModifiedBy.setValue(target.getLastModifiedBy());
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 : Utils.localDateTimeFromTs(pollStatus.getLastRequestAt()));
final ResponseEntity<MgmtTargetAttributes> response = hawkbitClient.getTargetRestApi().getAttributes(target.getControllerId());
if (response.getStatusCode().is2xxSuccessful()) {
targetAttributes.setValue(Objects.requireNonNullElse(response.getBody(), Collections.emptyMap()).entrySet().stream()
.map(entry -> entry.getKey() + ": " + entry.getValue())
.collect(Collectors.joining("\n")));
} else {
targetAttributes.setValue("Error occurred fetching attributes from server: " + response.getStatusCode());
}
}
}
private static class TargetAssignedInstalled extends FormLayout {
private final transient HawkbitMgmtClient hawkbitClient;
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.setWidthFull();
installed.setWidthFull();
add(assigned, installed);
setResponsiveSteps(new FormLayout.ResponsiveStep("0", 2));
}
private void setItem(final MgmtTarget target) {
this.target = target;
}
@Override
protected void onAttach(AttachEvent attachEvent) {
updateDistributionSetInfo(() -> hawkbitClient.getTargetRestApi().getInstalledDistributionSet(target.getControllerId()), installed);
updateDistributionSetInfo(() -> hawkbitClient.getTargetRestApi().getAssignedDistributionSet(target.getControllerId()), assigned);
}
private void updateDistributionSetInfo(Supplier<ResponseEntity<MgmtDistributionSet>> supplier, LinkedTextArea textArea) {
Optional.ofNullable(supplier.get())
.map(ResponseEntity<MgmtDistributionSet>::getBody)
.ifPresentOrElse(value -> {
final String description = """
Name: %s
Version: %s
%s
""".replace("\n", System.lineSeparator());
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));
}
}
private static class TargetTags extends VerticalLayout {
private final transient HawkbitMgmtClient hawkbitClient;
private final ComboBox<MgmtTag> tagSelector = new ComboBox<>(TAG);
private final HorizontalLayout tagsArea = new HorizontalLayout();
private Registration changeListener;
private transient MgmtTarget target;
private TargetTags(HawkbitMgmtClient hawkbitClient) {
this.hawkbitClient = hawkbitClient;
setWidthFull();
setPadding(false);
setSpacing(true);
setAlignItems(FlexComponent.Alignment.STRETCH);
setJustifyContentMode(JustifyContentMode.CENTER);
final HorizontalLayout tagSelectorLayout = buildTagSelectionLayout(hawkbitClient);
add(tagSelectorLayout);
tagsArea.setWrap(true);
tagsArea.setWidthFull();
add(tagsArea);
}
private HorizontalLayout buildTagSelectionLayout(HawkbitMgmtClient hawkbitClient) {
final Button createTagButton = new Button("Create Tag");
createTagButton.addClickListener(event -> new CreateTagDialog(hawkbitClient, () -> tagSelector.setItems(fetchAvailableTags()))
.result());
tagSelector.setWidthFull();
tagSelector.setItemLabelGenerator(MgmtTag::getName);
tagSelector.setClearButtonVisible(true);
final HorizontalLayout tagSelectorLayout = new HorizontalLayout(tagSelector, createTagButton);
tagSelectorLayout.setWidthFull();
tagSelectorLayout.setSpacing(true);
tagSelectorLayout.setAlignItems(Alignment.END);
tagSelectorLayout.setJustifyContentMode(JustifyContentMode.CENTER);
return tagSelectorLayout;
}
private void setItem(final MgmtTarget target) {
this.target = target;
}
@Override
protected void onAttach(AttachEvent attachEvent) {
tagSelector.setItems(fetchAvailableTags());
if (changeListener != null) {
changeListener.remove();
}
changeListener = tagSelector.addValueChangeListener(event -> {
if (event.getValue() != null) {
hawkbitClient.getTargetTagRestApi().assignTarget(event.getValue().getId(), target.getControllerId());
refreshTargetTagsList(target);
tagSelector.clear();
}
});
refreshTargetTagsList(target);
}
private void refreshTargetTagsList(MgmtTarget target) {
tagsArea.removeAll();
final List<MgmtTag> tagList = Optional.ofNullable(hawkbitClient.getTargetRestApi().getTags(target.getControllerId()).getBody())
.orElse(Collections.emptyList());
for (MgmtTag tag : tagList) {
tagsArea.add(buildTargetTagBadge(tag, target.getControllerId()));
}
}
private Span buildTargetTagBadge(MgmtTag tag, String controllerId) {
Button clearButton = new Button(VaadinIcon.CLOSE_SMALL.create());
clearButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST, ButtonVariant.LUMO_TERTIARY_INLINE);
clearButton.getStyle().set("margin-inline-start", "var(--lumo-space-xs)");
clearButton.getElement().setAttribute("aria-label", "Clear filter: " + tag.getName());
clearButton.getElement().setAttribute("title", "Clear filter: " + tag.getName());
Span pill = new Span();
pill.getElement().getThemeList().add("badge pill");
pill.getStyle().set("background-color", tag.getColour());
pill.getStyle().set("margin-inline-end", "var(--lumo-space-xs)");
Span badge = new Span(pill, new Span(tag.getName()), clearButton);
badge.getElement().getThemeList().add("badge contrast pill");
clearButton.addClickListener(event -> {
hawkbitClient.getTargetTagRestApi().unassignTarget(tag.getId(), controllerId);
badge.getElement().removeFromParent();
});
return badge;
}
private List<MgmtTag> fetchAvailableTags() {
List<MgmtTag> tags = new ArrayList<>();
int fetched = 0;
int offset = 0;
do {
List<MgmtTag> page = Optional.ofNullable(
hawkbitClient.getTargetTagRestApi().getTargetTags(null, offset, 50, Constants.NAME_ASC).getBody())
.map(PagedList::getContent)
.orElse(Collections.emptyList());
tags.addAll(page);
fetched = page.size();
offset += fetched;
} while (fetched > 0);
return tags;
}
}
public static class TargetActionsHistoryLayout extends VerticalLayout {
private final TargetActionsHistory targetActionsHistory;
public TargetActionsHistoryLayout(HawkbitMgmtClient hawkbitMgmtClient) {
ActionStepsGrid actionStepsGrid = new ActionStepsGrid(hawkbitMgmtClient);
targetActionsHistory = new TargetActionsHistory(hawkbitMgmtClient, actionStepsGrid);
add(targetActionsHistory);
add(actionStepsGrid);
}
public void setItem(MgmtTarget target) {
targetActionsHistory.setItem(target);
}
public static class ActionStepsGrid extends Grid<ActionStepsGrid.ActionStepEntry> {
private final transient HawkbitMgmtClient hawkbitClient;
private transient MgmtTarget target;
private transient Long actionId;
private ActionStepsGrid(final HawkbitMgmtClient hawkbitClient) {
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);
}
private List<ActionStepEntry> fetchActionSteps() {
if (actionId == null) {
return new ArrayList<>();
}
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);
}
icon.addClassNames(LumoUtility.IconSize.SMALL);
layout.add(icon);
layout.setWidth(50, Unit.PIXELS);
layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
return layout;
}
public VerticalLayout getMessage() {
return new VerticalLayout(status.getMessages().stream().map(Span::new).toArray(Span[]::new));
}
}
}
}
private static class RegisterDialog extends Utils.BaseDialog<Void> {
private final Select<MgmtTargetType> type;
private final TextField controllerId;
private final TextField name;
private final TextArea description;
private final TextField group;
private RegisterDialog(final HawkbitMgmtClient hawkbitClient) {
super("Register Target");
final Button register = Utils.tooltip(new Button("Register"), "Register (Enter)");
type = new Select<>(
"Type",
e -> {},
hawkbitClient.getTargetTypeRestApi()
.getTargetTypes(null, 0, 30, Constants.NAME_ASC)
.getBody()
.getContent()
.toArray(new MgmtTargetType[0])
);
type.setWidthFull();
type.setEmptySelectionAllowed(true);
type.setItemLabelGenerator(item -> item == null ? "" : item.getName());
controllerId = Utils.textField(CONTROLLER_ID, e -> register.setEnabled(!e.getHasValue().isEmpty()));
controllerId.focus();
name = Utils.textField(Constants.NAME);
name.setWidthFull();
description = new TextArea(Constants.DESCRIPTION);
description.setMinLength(2);
description.setWidthFull();
group = Utils.textField(Constants.GROUP);
group.setWidthFull();
addCreateClickListener(register, hawkbitClient);
register.setEnabled(false);
register.addClickShortcut(Key.ENTER);
register.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC);
cancel.addClickListener(e -> close());
cancel.addClickShortcut(Key.ESCAPE);
getFooter().add(cancel);
getFooter().add(register);
final VerticalLayout layout = new VerticalLayout();
layout.setSizeFull();
layout.setPadding(true);
layout.setSpacing(false);
layout.add(type, controllerId, name, description, group);
add(layout);
open();
}
private void addCreateClickListener(final Button register, final HawkbitMgmtClient hawkbitClient) {
register.addClickListener(e -> {
final MgmtTargetRequestBody request = new MgmtTargetRequestBody()
.setControllerId(controllerId.getValue())
.setName(name.getValue())
.setDescription(description.getValue())
.setGroup(group.getValue());
if (!ObjectUtils.isEmpty(type.getValue())) {
request.setTargetType(type.getValue().getId());
}
hawkbitClient.getTargetRestApi().createTargets(
List.of(request))
.getBody()
.stream()
.findFirst()
.orElseThrow()
.getControllerId();
close();
});
}
}
private static class AssignDialog extends Utils.BaseDialog<Void> {
private final Select<MgmtDistributionSet> distributionSet;
private final Select<MgmtActionType> actionType;
private final DateTimePicker forceTime = new DateTimePicker("Force Time");
private final Button assign = new Button("Assign");
private AssignDialog(final HawkbitMgmtClient hawkbitClient, Set<TargetWithDs> selectedTargets) {
super("Assign Distribution Set");
distributionSet = new Select<>(
"Distribution Set",
this::readyToAssign,
Optional.ofNullable(
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.setWidthFull();
actionType = Utils.actionTypeControls(forceTime);
assign.setEnabled(false);
assign.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
addAssignClickListener(hawkbitClient, selectedTargets);
final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC);
cancel.addClickListener(e -> close());
cancel.addClickShortcut(Key.ESCAPE);
getFooter().add(cancel);
getFooter().add(assign);
final VerticalLayout layout = new VerticalLayout();
layout.setSizeFull();
layout.setSpacing(false);
layout.add(distributionSet, actionType);
add(layout);
open();
}
private void readyToAssign(final Object v) {
final boolean assignEnabled = !distributionSet.isEmpty();
if (assign.isEnabled() != assignEnabled) {
assign.setEnabled(assignEnabled);
}
}
private void addAssignClickListener(final HawkbitMgmtClient hawkbitClient, final Set<TargetWithDs> selectedTargets) {
assign.addClickListener(e -> {
close();
final List<MgmtTargetAssignmentRequestBody> requests = new LinkedList<>();
for (final MgmtTarget target : selectedTargets) {
final MgmtTargetAssignmentRequestBody request = new MgmtTargetAssignmentRequestBody(target.getControllerId());
request.setType(actionType.getValue());
if (actionType.getValue() == MgmtActionType.TIMEFORCED) {
request.setForcetime(
forceTime.isEmpty() ? System.currentTimeMillis() : forceTime.getValue().toEpochSecond(ZoneOffset.UTC) * 1000);
}
requests.add(request);
}
hawkbitClient.getDistributionSetRestApi().createAssignedTarget(distributionSet.getValue().getId(), requests, null).getBody();
});
}
}
private static class CreateTagDialog extends Utils.BaseDialog<Void> {
private final TextField name;
private final TextArea description = new TextArea(Constants.DESCRIPTION);
private CreateTagDialog(final HawkbitMgmtClient hawkbitClient, Runnable onSuccess) {
super("Create Tag");
final FormLayout formLayout = new FormLayout();
formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
final Button create = Utils.tooltip(new Button("Create"), "Create (Enter)");
final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC);
final Input colorInput = new Input();
colorInput.setType("color");
name = Utils.textField("Tag Name", e -> create.setEnabled(!e.getHasValue().isEmpty()));
formLayout.add(name);
formLayout.add(description);
formLayout.addFormItem(colorInput, "Color Selection");
add(formLayout);
create.setEnabled(false);
create.addClickShortcut(Key.ENTER);
create.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
create.addClickListener(e -> {
hawkbitClient.getTargetTagRestApi().createTargetTags(List.of(
new MgmtTagRequestBodyPut()
.setName(name.getValue())
.setDescription(description.getValue())
.setColour(colorInput.getValue())));
onSuccess.run();
close();
});
cancel.addClickListener(e -> close());
cancel.addClickShortcut(Key.ESCAPE);
getFooter().add(cancel);
getFooter().add(create);
open();
}
}
private static class TargetStatusCell extends HorizontalLayout {
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(targetUpdateStatusMapper(targetUpdateStatus));
setWidth(25, Unit.PIXELS);
}
private Icon targetUpdateStatusMapper(final String targetUpdateStatus) {
final VaadinIcon icon = switch (targetUpdateStatus) {
case "error" -> VaadinIcon.EXCLAMATION_CIRCLE;
case "in_sync" -> VaadinIcon.CHECK_CIRCLE;
case "pending" -> VaadinIcon.ADJUST;
case "registered" -> VaadinIcon.DOT_CIRCLE;
default -> VaadinIcon.QUESTION_CIRCLE;
};
final String color = switch (targetUpdateStatus) {
case "error" -> RED;
case "in_sync" -> GREEN;
case "pending" -> ORANGE;
case "registered" -> "lightblue";
default -> "blue";
};
final Icon statusIcon = Utils.tooltip(icon.create(), targetUpdateStatus.replace("_", " "));
statusIcon.setColor(color);
statusIcon.addClassNames(LumoUtility.IconSize.SMALL);
return statusIcon;
}
}
// todo change /targets api to reduce api calls ?
@EqualsAndHashCode(callSuper = true)
public static class TargetWithDs extends MgmtTarget {
TargetWithDs() {
super();
}
Optional<MgmtDistributionSet> 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("");
}
}
}

View File

@@ -0,0 +1,187 @@
/**
* Copyright (c) 2023 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.view.util;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasValue;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.html.Div;
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<String> changeListener, final Rsql primaryRsql, final Rsql secondaryOptionalRsql) {
rsql = primaryRsql;
this.primaryRsql = primaryRsql;
secondaryRsql = secondaryOptionalRsql;
final HorizontalLayout layout = new HorizontalLayout();
setWidthFull();
addClassNames(LumoUtility.Padding.Horizontal.NONE, LumoUtility.Padding.Vertical.SMALL,
LumoUtility.BoxSizing.BORDER);
filtersDiv = new Div();
filtersDiv.setWidthFull();
filtersDiv.add(primaryRsql.components());
filtersDiv.addClassName(LumoUtility.Gap.SMALL);
final Button searchBtn = Utils.tooltip(new Button(VaadinIcon.SEARCH.create()), "Search (Enter)");
searchBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
searchBtn.addClickListener(e -> changeListener.accept(rsql.filter()));
searchBtn.addClickShortcut(Key.ENTER);
final Button resetBtn = Utils.tooltip(new Button(VaadinIcon.REFRESH.create()), "Reset");
resetBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
resetBtn.addClickListener(e -> {
clear(layout.getChildren());
changeListener.accept(primaryRsql.filter());
});
final HorizontalLayout actions = new HorizontalLayout(searchBtn, resetBtn);
actions.addClassNames(LumoUtility.Gap.SMALL);
layout.setPadding(true);
layout.setAlignItems(FlexComponent.Alignment.BASELINE);
layout.add(filtersDiv);
layout.add(actions);
if (secondaryOptionalRsql != null) {
final Button toggleBtn = Utils.tooltip(new Button(VaadinIcon.FLIP_V.create()), "Toggle Search");
toggleBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
toggleBtn.addClickListener(e -> {
toggle();
changeListener.accept(primaryRsql.filter());
});
layout.add(toggleBtn);
}
add(layout);
changeListener.accept(primaryRsql.filter());
}
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<Filter.Rsql> 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<Object, Object> keyToValues) {
final Map<Object, Object> normalized = new HashMap<>(keyToValues)
.entrySet()
.stream()
.filter(e -> !ObjectUtils.isEmpty(e.getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (normalized.isEmpty()) {
return null;
} else {
final StringBuilder sb = new StringBuilder();
normalized.forEach((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(';');
});
return sb.substring(0, sb.length() - 1);
}
}
private static String filter(final String key, final Object value) {
if (value == null || (value instanceof Collection<?> coll && coll.isEmpty())) {
return null;
}
if (value instanceof Collection<?> coll) {
final StringBuilder sb = new StringBuilder();
coll.stream().forEach(next -> sb.append(key).append("==").append(next).append(','));
return sb.substring(0, sb.length() - 1);
} else if (value instanceof Optional<?> opt) {
if (opt.isEmpty()) {
return null;
} else {
return key + "==" + opt.get();
}
} else {
return key + "==" + value;
}
}
private static void clear(final Stream<Component> components) {
if (components == null) {
return;
}
components.forEach(component -> {
if (component instanceof HasValue<?, ?> hasValue) {
hasValue.clear();
}
clear(component.getChildren());
});
}
public interface Rsql {
List<Component> components();
String filter();
}
public interface RsqlRw {
void setFilter(String filter);
}
}

View File

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

View File

@@ -0,0 +1,102 @@
/**
* Copyright (c) 2023 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.view.util;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Stream;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.grid.GridVariant;
import com.vaadin.flow.data.provider.Query;
import com.vaadin.flow.theme.lumo.LumoUtility;
// id type shall have proper equals and hashCode - i.e. eligible hash set element
@SuppressWarnings("java:S119") // better readability
public class SelectionGrid<T, ID> extends Grid<T> {
private volatile String rsqlFilter;
public SelectionGrid(
final EntityRepresentation<T, ID> entityRepresentation) {
this(entityRepresentation, null);
}
public SelectionGrid(
final EntityRepresentation<T, ID> entityRepresentation,
final BiFunction<Query<T, Void>, String, Stream<T>> queryFn) {
super(entityRepresentation.beanType, false);
addThemeVariants(GridVariant.LUMO_NO_BORDER);
addClassNames(LumoUtility.Border.TOP, LumoUtility.BorderColor.CONTRAST_10);
setSelectionMode(Grid.SelectionMode.MULTI);
entityRepresentation.addColumns(this);
if (queryFn != null) {
setItems(query -> {
final Stream<T> fetch = queryFn.apply(query, rsqlFilter);
final Set<T> selected = getSelectedItems();
if (selected == null || selected.isEmpty()) {
final List<T> fetchList = fetch.toList();
if (fetchList.size() == 1) {
this.setDetailsVisible(fetchList.get(0), true);
}
return fetchList.stream();
} else {
final Set<ID> selectedIds = new HashSet<>();
selected.forEach(next -> selectedIds.add(entityRepresentation.idFn.apply(next)));
// if matching keeps old entries instead of new the new ones in order to
// select them in case refresh is made with keepSelection
// this however means that if they are changed the old state will be shown!!!
return Stream.concat(selected.stream(),
fetch.filter(next -> !selectedIds.contains(entityRepresentation.idFn.apply(next))));
}
});
} // else externally managed
}
public void setRsqlFilter(final String rsqlFilter, boolean refreshGrid) {
if (!Objects.equals(this.rsqlFilter, rsqlFilter)) {
this.rsqlFilter = rsqlFilter;
if (refreshGrid)
refreshGrid(true);
}
}
public void refreshGrid(final boolean keepSelection) {
if (keepSelection) {
final Set<T> selected = getSelectedItems();
getDataProvider().refreshAll();
if (selected != null && !selected.isEmpty()) {
selected.forEach(this::select);
}
} else {
deselectAll();
getDataProvider().refreshAll();
}
}
public abstract static class EntityRepresentation<T, ID> {
private final Class<T> beanType;
private final Function<T, ID> idFn;
protected EntityRepresentation(final Class<T> beanType, final Function<T, ID> idFn) {
this.beanType = beanType;
this.idFn = idFn;
}
protected abstract void addColumns(final Grid<T> grid);
}
}

View File

@@ -0,0 +1,173 @@
/**
* Copyright (c) 2023 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.view.util;
import java.util.concurrent.CompletionStage;
import java.util.function.BiFunction;
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;
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.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.splitlayout.SplitLayout;
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.view.Constants;
@SuppressWarnings("java:S119") // better readability
public class TableView<T, ID> 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<T, ID> selectionGrid;
private final Filter filter;
private final VerticalLayout gridLayout;
protected final HorizontalLayout controlsLayout;
private final SplitLayout splitLayout;
private final Div detailsPanel;
private Button currentSelectionButton;
public TableView(
final Filter.Rsql rsql,
final SelectionGrid.EntityRepresentation<T, ID> entityRepresentation,
final BiFunction<Query<T, Void>, String, Stream<T>> queryFn,
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> addHandler,
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> removeHandler) {
this(rsql, null, entityRepresentation, queryFn, addHandler, removeHandler, null);
}
public TableView(
final Filter.Rsql rsql, final Filter.Rsql alternativeRsql,
final SelectionGrid.EntityRepresentation<T, ID> entityRepresentation,
final BiFunction<Query<T, Void>, String, Stream<T>> queryFn,
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> addHandler,
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> removeHandler,
final Function<T, Component> detailsButtonHandler) {
selectionGrid = new SelectionGrid<>(entityRepresentation, queryFn);
selectionGrid.setSizeFull();
setSizeFull();
splitLayout = new SplitLayout();
splitLayout.setSizeFull();
splitLayout.setOrientation(SplitLayout.Orientation.HORIZONTAL);
splitLayout.addThemeVariants(SplitLayoutVariant.LUMO_SMALL);
splitLayout.setSplitterPosition(100);
splitLayout.addToPrimary(selectionGrid);
detailsPanel = new Div();
if (detailsButtonHandler != null) {
ComponentRenderer<Button, T> renderer = new ComponentRenderer<>(renderDetailsButton(detailsButtonHandler));
selectionGrid.addColumn(renderer).setHeader("Details").setAutoWidth(true).setFlexGrow(0);
}
filter = new Filter(
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);
gridLayout.setSizeFull();
gridLayout.setPadding(false);
gridLayout.setSpacing(false);
if (addHandler != null || removeHandler != null) {
controlsLayout = Utils.addRemoveControls(addHandler, removeHandler, selectionGrid, false);
} else {
controlsLayout = new HorizontalLayout();
controlsLayout.setWidthFull();
controlsLayout.addClassNames(
LumoUtility.Padding.Horizontal.XLARGE, LumoUtility.Padding.Vertical.SMALL, LumoUtility.BoxSizing.BORDER);
controlsLayout.setAlignItems(FlexComponent.Alignment.BASELINE);
}
gridLayout.add(controlsLayout);
add(gridLayout);
}
private void closeDetailsPanel() {
detailsPanel.removeAll();
if (splitLayout.getSecondaryComponent() != null) {
splitLayout.remove(detailsPanel);
}
splitLayout.setSplitterPosition(100);
currentSelectionButton = null;
}
private SerializableFunction<T, Button> renderDetailsButton(final Function<T, Component> selectionHandler) {
return selectedItem -> {
final Button button = new Button(VaadinIcon.EYE.create());
button.getStyle().set(COLOR, VAR_LUMO_SECONDARY_TEXT_COLOR);
button.addClickListener(event -> {
final Icon eyeIcon = VaadinIcon.EYE.create();
final Icon closeIcon = VaadinIcon.CLOSE_SMALL.create();
if (button == currentSelectionButton) {
button.setIcon(eyeIcon);
button.getStyle().set(COLOR, VAR_LUMO_SECONDARY_TEXT_COLOR);
closeDetailsPanel();
} else {
button.setIcon(closeIcon);
button.getStyle().set(COLOR, VAR_LUMO_PRIMARY_COLOR);
if (currentSelectionButton == null) {
splitLayout.addToSecondary(detailsPanel);
} else {
currentSelectionButton.setIcon(eyeIcon);
currentSelectionButton.getStyle().set(COLOR, VAR_LUMO_SECONDARY_TEXT_COLOR);
}
detailsPanel.removeAll();
splitLayout.setSplitterPosition(DEFAULT_OPEN_POSITION_SIZE);
detailsPanel.add(selectionHandler.apply(selectedItem));
currentSelectionButton = button;
}
});
button.setTooltipText("Show details");
button.addThemeVariants(ButtonVariant.LUMO_SMALL);
return button;
};
}
@Override
public void beforeEnter(BeforeEnterEvent event) {
var params = event.getLocation().getQueryParameters();
params.getSingleParameter(QUERY_PARAM_FILTER)
.ifPresent(f -> {
var newPage = event.getTrigger() == NavigationTrigger.UI_NAVIGATE;
selectionGrid.setRsqlFilter(f, newPage);
filter.setFilter(f, newPage);
});
}
}

View File

@@ -0,0 +1,313 @@
/**
* Copyright (c) 2023 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.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.view.Constants;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasValue;
import com.vaadin.flow.component.Text;
import com.vaadin.flow.component.UI;
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.datetimepicker.DateTimePicker;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.component.shared.Tooltip;
import com.vaadin.flow.component.textfield.NumberField;
import com.vaadin.flow.component.textfield.TextField;
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() {
// prevent initialization
}
public static TextField textField(final String label) {
return textField(label, null);
}
public static TextField textField(final String label, final Consumer<HasValue.ValueChangeEvent<String>> changeListener) {
final TextField textField = new TextField(label);
textField.setWidthFull();
if (changeListener != null) {
textField.setRequired(true);
textField.addValueChangeListener(changeListener::accept);
textField.setValueChangeMode(ValueChangeMode.TIMEOUT);
textField.setValueChangeTimeout(200);
}
return textField;
}
public static NumberField numberField(final String label) {
return numberField(label, null);
}
public static NumberField numberField(final String label, final Consumer<HasValue.ValueChangeEvent<Double>> changeListener) {
final NumberField numberField = new NumberField(label);
numberField.setWidthFull();
if (changeListener != null) {
numberField.setRequired(true);
numberField.addValueChangeListener(changeListener::accept);
numberField.setValueChangeMode(ValueChangeMode.TIMEOUT);
numberField.setValueChangeTimeout(200);
}
return numberField;
}
@SuppressWarnings("java:S119") // better readability
public static <T, ID> HorizontalLayout addRemoveControls(
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> addHandler,
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> removeHandler,
final SelectionGrid<T, ID> selectionGrid, final boolean noPadding) {
if (addHandler == null && removeHandler == null) {
throw new IllegalArgumentException("At least one of add or remove handlers must not be null!");
}
final HorizontalLayout layout = new HorizontalLayout();
layout.setWidthFull();
if (!noPadding) {
layout.addClassNames(LumoUtility.Padding.Horizontal.XLARGE, LumoUtility.Padding.Vertical.SMALL, LumoUtility.BoxSizing.BORDER);
}
layout.setAlignItems(FlexComponent.Alignment.BASELINE);
layout.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
if (addHandler != null) {
final Button addBtn = tooltip(new Button(VaadinIcon.PLUS.create()), "Add");
addBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
addBtn.addClickListener(e -> addHandler
.apply(selectionGrid)
.thenAccept(v -> selectionGrid.refreshGrid(true)));
layout.add(addBtn);
}
if (removeHandler != null) {
final ConfirmDialog dialog = promptForDeleteConfirmation(removeHandler, selectionGrid);
final Button removeBtn = tooltip(new Button(VaadinIcon.MINUS.create()), "Remove");
removeBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_CONTRAST);
removeBtn.addClickListener(e -> dialog.open());
layout.add(removeBtn);
}
return layout;
}
private static <T, ID> ConfirmDialog promptForDeleteConfirmation(Function<SelectionGrid<T, ID>, CompletionStage<Void>> removeHandler,
SelectionGrid<T, ID> 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.");
dialog.setCancelable(true);
dialog.addCancelListener(event -> dialog.close());
dialog.setConfirmButtonTheme(ButtonVariant.LUMO_ERROR.getVariantName());
dialog.setConfirmText("Delete");
dialog.addConfirmListener(event -> {
removeHandler
.apply(selectionGrid)
.thenAccept(v -> selectionGrid.refreshGrid(false));
dialog.close();
});
return dialog;
}
public static <T> void remove(final Collection<T> remove, final Set<T> from, final Function<T, ?> idFn) {
remove.forEach(toRemove -> {
final Object id = idFn.apply(toRemove);
for (final Iterator<T> i = from.iterator(); i.hasNext();) {
if (idFn.apply(i.next()).equals(id)) {
i.remove();
}
}
});
}
public static void errorNotification(final Throwable t) {
if (UI.getCurrent() == null) {
return;
}
final Notification notification = new Notification();
notification.addThemeVariants(NotificationVariant.LUMO_ERROR);
final Div error = new Div(new Text("Error: " + t.getMessage()));
final Button closeButton = tooltip(new Button(VaadinIcon.CLOSE.create()), "Close");
closeButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE);
closeButton.addClickListener(event -> notification.close());
final HorizontalLayout layout = new HorizontalLayout(error, closeButton);
layout.setAlignItems(FlexComponent.Alignment.CENTER);
notification.add(layout);
notification.open();
}
public static <T extends Component> T tooltip(final T component, final String text) {
Tooltip.forComponent(component)
.withText(text)
.withPosition(Tooltip.TooltipPosition.TOP_START);
return component;
}
public static Icon iconColored(final IconFactory component, final String text, final String color) {
var icon = tooltip(component.create(), text);
icon.setColor(color);
return icon;
}
public static Select<MgmtActionType> actionTypeControls(DateTimePicker forceTime) {
Select<MgmtActionType> actionType = new Select<>();
actionType.setLabel(Constants.ACTION_TYPE);
actionType.setItems(MgmtActionType.values());
actionType.setValue(MgmtActionType.FORCED);
final ComponentRenderer<Component, MgmtActionType> 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.setWidthFull();
return actionType;
}
public static class BaseDialog<T> extends Dialog {
protected final transient CompletableFuture<T> result = new CompletableFuture<>();
protected BaseDialog(final String headerTitle) {
setHeaderTitle(headerTitle);
setMinWidth(640, Unit.PIXELS);
setModal(true);
setDraggable(true);
setResizable(true);
setCloseOnEsc(true);
setCloseOnOutsideClick(true);
final Button closeBtn = tooltip(new Button(VaadinIcon.CLOSE.create()), "Close");
closeBtn.addClickListener(e -> {
result.complete(null);
super.close();
});
getHeader().add(closeBtn);
}
public CompletionStage<T> result() {
return result;
}
@Override
public void close() {
if (!result.isDone()) {
result.complete(null);
}
super.close();
}
}
private static ZoneId getZoneId() {
CompletableFuture<ZoneId> zoneId = new CompletableFuture<>();
UI.getCurrent().getPage().retrieveExtendedClientDetails(details -> zoneId.complete(ZoneId.of(details.getTimeZoneId())));
try {
return zoneId.get(1, TimeUnit.SECONDS);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
} catch (TimeoutException | ExecutionException ignored) {
log.warn("failed to get zone");
}
return ZoneId.systemDefault();
}
public static <G> LocalDateTimeRenderer<G> localDateTimeRenderer(ToLongFunction<G> f) {
return new LocalDateTimeRenderer<>((e) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(f.applyAsLong(e)), getZoneId()),
() -> DateTimeFormatter.ofLocalizedDateTime(
FormatStyle.SHORT,
FormatStyle.MEDIUM).withLocale(UI.getCurrent().getLocale()));
}
public static String localDateTimeFromTs(long timestamp) {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), getZoneId()).format(DateTimeFormatter.ofLocalizedDateTime(
FormatStyle.SHORT,
FormatStyle.MEDIUM).withLocale(UI.getCurrent().getLocale()));
}
public static String getSortParam(List<QuerySortOrder> querySortOrders) {
return getSortParam(querySortOrders, null);
}
public static String getSortParam(List<QuerySortOrder> querySortOrders, String defaultSort) {
if (!querySortOrders.isEmpty()) {
QuerySortOrder firstSort = querySortOrders.get(0);
String order = firstSort.getDirection() == SortDirection.ASCENDING ? "asc" : "desc";
return String.format("%s:%s", firstSort.getSorted(), order);
}
return defaultSort;
}
public static String durationFromMillis(Long time) {
var duration = Duration.between(Instant.ofEpochMilli(time), Instant.now());
var day = duration.toDaysPart();
if (day > 2) {
return day + "d";
}
return duration.withNanos(0).toString()
.substring(2)
.replaceFirst("(^\\d+[HMS]\\d*M*)", "$1")
.toLowerCase();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,32 @@
#
# Copyright (c) 2023 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
#
# Spring configuration
server.port=${PORT:8088}
# Logging configuration
logging.level.org.springframework.boot.actuate.audit.listener.AuditListener=WARN
# logging pattern
logging.pattern.console=%clr(%d{${logging.pattern.dateformat:yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${logging.pattern.level:%5p}) %clr(${PID:}){magenta} %clr(---){faint} %clr([${spring.application.name}] [%X{tenant}:%X{user}] [%15.15t]){faint} %clr(${logging.pattern.correlation:}){faint}%clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${logging.exception-conversion-word:%wEx}
### Vaadin start ###
# build with mvn vaadin:build-frontend to enable / disable
vaadin.productionMode=true
vaadin.frontend.hotdeploy=false
logging.level.org.atmosphere=warn
spring.mustache.check-template-location=false
# Launch the default browser when starting the application in development mode
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.allowed-packages=com.vaadin,org.vaadin,dev.hilla,org.eclipse.hawkbit
spring.application.name=hawkBit-UI
server.servlet.session.persistent=false
### Vaadin end ###

View File

@@ -0,0 +1,18 @@
______ _ _ _ _ ____ _ _
| ____| | (_) | | | | | _ \(_) |
| |__ ___| |_ _ __ ___ ___ | |__ __ ___ _| | _| |_) |_| |_
| __| / __| | | '_ \/ __|/ _ \ | '_ \ / _` \ \ /\ / / |/ / _ <| | __|
| |___| (__| | | |_) \__ \ __/ | | | | (_| |\ V V /| <| |_) | | |_
|______\___|_|_| .__/|___/\___|_|_| |_|\__,_|_\_/\_/ |_|\_\____/|_|\__|
/ ____(_)| | | | | | | |_ _|
| (___ _ |_|_ ___ _ __ | | ___ | | | | | |
\___ \| | '_ ` _ \| '_ \| |/ _ \ | | | | | |
____) | | | | | | | |_) | | __/ | |__| |_| |_
|_____/|_|_| |_| |_| .__/|_|\___| \____/|_____|
| |
|_|
Eclipse hawkBit UI ${application.formatted-version}
using Spring Boot ${spring-boot.formatted-version}
Go to https://www.eclipse.org/hawkbit for more information.