Move Simple UI at root level (#2005)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2024-11-11 16:13:08 +02:00
committed by GitHub
parent 4cc83cacbd
commit ef216840a7
25 changed files with 7 additions and 3 deletions

1
hawkbit-simple-ui/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
frontend

109
hawkbit-simple-ui/pom.xml Normal file
View File

@@ -0,0 +1,109 @@
<!--
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-simple-ui</artifactId>
<version>${revision}</version>
<packaging>jar</packaging>
<name>hawkBit :: Runtime :: Simple UI</name>
<properties>
<vaadin.version>24.3.7</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>
<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>
<exclusions>
<exclusion>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-dev</artifactId>
</exclusion>
</exclusions>
</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>
</dependencies>
<build>
<defaultGoal>spring-boot:run</defaultGoal>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-maven-plugin</artifactId>
<version>${vaadin.version}</version>
<executions>
<execution>
<goals>
<goal>build-frontend</goal>
</goals>
<phase>compile</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,98 @@
/**
* 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.simple;
import java.util.function.Supplier;
import feign.FeignException;
import lombok.Getter;
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;
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);
}
boolean hasSoftwareModulesRead() {
return hasRead(() -> softwareModuleRestApi.getSoftwareModule(-1L));
}
boolean hasRolloutRead() {
return hasRead(() -> rolloutRestApi.getRollout(-1L));
}
boolean hasDistributionSetRead() {
return hasRead(() -> distributionSetRestApi.getDistributionSet(-1L));
}
boolean hasTargetRead() {
return hasRead(() -> targetRestApi.getTarget("_#ETE$ER"));
}
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,154 @@
/**
* 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.simple;
import java.util.Optional;
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.simple.security.AuthenticatedUser;
import org.eclipse.hawkbit.ui.simple.view.AboutView;
import org.eclipse.hawkbit.ui.simple.view.ConfigView;
import org.eclipse.hawkbit.ui.simple.view.DistributionSetView;
import org.eclipse.hawkbit.ui.simple.view.RolloutView;
import org.eclipse.hawkbit.ui.simple.view.SoftwareModuleView;
import org.eclipse.hawkbit.ui.simple.view.TargetView;
/**
* The main view is a top-level placeholder for other views.
*/
public class MainLayout extends AppLayout {
private final transient AuthenticatedUser authenticatedUser;
private final AccessAnnotationChecker accessChecker;
private H2 viewTitle;
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().getClass().getAnnotation(PageTitle.class))
.map(PageTitle::value)
.orElse(""));
}
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 (Experimental!)");
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()));
}
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,131 @@
/**
* 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.simple;
import static feign.Util.ISO_8859_1;
import java.util.Base64;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import com.vaadin.flow.component.page.AppShellConfigurator;
import com.vaadin.flow.server.PWA;
import com.vaadin.flow.theme.Theme;
import com.vaadin.flow.theme.lumo.Lumo;
import feign.Client;
import feign.Contract;
import feign.RequestInterceptor;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.HawkbitServer;
import org.eclipse.hawkbit.sdk.Tenant;
import org.eclipse.hawkbit.ui.simple.view.util.Utils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
@Theme(themeClass = Lumo.class)
@PWA(name = "hawkBit UI", shortName = "hawkBit UI")
@SpringBootApplication
@Import(FeignClientsConfiguration.class)
public class SimpleUIApp implements AppShellConfigurator {
private static final RequestInterceptor AUTHORIZATION = requestTemplate -> {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
requestTemplate.header("Authorization", "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(SimpleUIApp.class, args);
}
@Bean
HawkbitClient hawkbitClient(
final HawkbitServer hawkBitServer,
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
return new HawkbitClient(
hawkBitServer, client, 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) {
return authentication -> {
final String username = authentication.getName();
final String password = authentication.getCredentials().toString();
final List<String> roles = new LinkedList<>();
roles.add("ANONYMOUS");
final SecurityContext unauthorizedContext = SecurityContextHolder.createEmptyContext();
unauthorizedContext.setAuthentication(
new UsernamePasswordAuthenticationToken(username, password));
final SecurityContext currentContext = SecurityContextHolder.getContext();
try {
SecurityContextHolder.setContext(unauthorizedContext);
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");
}
} finally {
SecurityContextHolder.setContext(currentContext);
}
return new UsernamePasswordAuthenticationToken(
username, password,
roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList()) {
@Override
public void eraseCredentials() {
// don't erase credentials because they will be used
// to authenticate to the hawkBit update server / mgmt server
}
};
};
}
}

View File

@@ -0,0 +1,33 @@
/**
* 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.simple.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;
public AuthenticatedUser(final AuthenticationContext authenticationContext) {
this.authenticationContext = authenticationContext;
}
public Optional<String> getName() {
return authenticationContext.getPrincipalName();
}
public void logout() {
authenticationContext.logout();
}
}

View File

@@ -0,0 +1,39 @@
/**
* 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.simple.security;
import com.vaadin.flow.spring.security.VaadinWebSecurity;
import org.eclipse.hawkbit.ui.simple.view.LoginView;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends VaadinWebSecurity {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(final HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
authorize -> authorize.requestMatchers(new AntPathRequestMatcher("/images/*.png")).permitAll());
super.configure(http);
setLoginView(http, LoginView.class);
}
}

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.simple.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.simple.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.simple.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.simple.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.simple.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,48 @@
/**
* 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.simple.view;
public interface Constants {
// properties
String ID = "Id";
String NAME = "Name";
String DESCRIPTION = "Description";
String VERSION = "Version";
String VENDOR = "Vendor";
String TYPE = "Type";
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 SECURITY_TOKEN = "Security Token";
// 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";
String NAME_ASC = "name:asc";
}

View File

@@ -0,0 +1,341 @@
/**
* 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.simple.view;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.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.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.MgmtSoftwareModuleAssigment;
import org.eclipse.hawkbit.mgmt.json.model.tag.MgmtTag;
import org.eclipse.hawkbit.ui.simple.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.simple.MainLayout;
import org.eclipse.hawkbit.ui.simple.view.util.Filter;
import org.eclipse.hawkbit.ui.simple.view.util.SelectionGrid;
import org.eclipse.hawkbit.ui.simple.view.util.TableView;
import org.eclipse.hawkbit.ui.simple.view.util.Utils;
@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 SelectionGrid.EntityRepresentation<>(MgmtDistributionSet.class, MgmtDistributionSet::getDsId) {
private final DistributionSetDetails details = new DistributionSetDetails(hawkbitClient);
@Override
protected void addColumns(Grid<MgmtDistributionSet> grid) {
grid.addColumn(MgmtDistributionSet::getDsId).setHeader(Constants.ID).setAutoWidth(true);
grid.addColumn(MgmtDistributionSet::getName).setHeader(Constants.NAME).setAutoWidth(true);
grid.addColumn(MgmtDistributionSet::getVersion).setHeader(Constants.VERSION).setAutoWidth(true);
grid.addColumn(MgmtDistributionSet::getTypeName).setHeader(Constants.TYPE).setAutoWidth(true);
grid.setItemDetailsRenderer(new ComponentRenderer<>(
() -> details, DistributionSetDetails::setItem));
}
},
(query, rsqlFilter) -> Optional.ofNullable(
hawkbitClient.getDistributionSetRestApi()
.getDistributionSets(query.getOffset(), query.getPageSize(), Constants.NAME_ASC, rsqlFilter)
.getBody())
.stream().flatMap(body -> body.getContent().stream()),
e -> new CreateDialog(hawkbitClient).result(),
selectionGrid -> {
selectionGrid.getSelectedItems().forEach(
distributionSet -> hawkbitClient.getDistributionSetRestApi()
.deleteDistributionSet(distributionSet.getDsId()));
return CompletableFuture.completedFuture(null);
});
}
private static SelectionGrid<MgmtSoftwareModule, Long> selectSoftwareModuleGrid() {
return new SelectionGrid<>(
new SelectionGrid.EntityRepresentation<>(
MgmtSoftwareModule.class, MgmtSoftwareModule::getModuleId) {
@Override
protected void addColumns(Grid<MgmtSoftwareModule> grid) {
grid.addColumn(MgmtSoftwareModule::getModuleId).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 DistributionSetFilter implements Filter.Rsql {
private final TextField name = Utils.textField("Name");
private final CheckboxGroup<MgmtDistributionSetType> type = new CheckboxGroup<>("Type");
private final CheckboxGroup<MgmtTag> tag = new CheckboxGroup<>("Tag");
private DistributionSetFilter(final HawkbitMgmtClient hawkbitClient) {
name.setPlaceholder("<name filter>");
type.setItemLabelGenerator(MgmtDistributionSetType::getName);
type.setItems(Optional.ofNullable(
hawkbitClient.getDistributionSetTypeRestApi()
.getDistributionSetTypes(0, 20, Constants.NAME_ASC, null)
.getBody())
.map(PagedList::getContent)
.orElseGet(Collections::emptyList));
tag.setItemLabelGenerator(MgmtTag::getName);
tag.setItems(Optional.ofNullable(
hawkbitClient.getDistributionSetTagRestApi()
.getDistributionSetTags(0, 20, Constants.NAME_ASC, null)
.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(MgmtDistributionSetType::getKey)
.toList(),
"tag", tag.getSelectedItems()));
}
}
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 SelectionGrid<MgmtSoftwareModule, Long> softwareModulesGrid = selectSoftwareModuleGrid();
private DistributionSetDetails(final HawkbitMgmtClient hawkbitClient) {
this.hawkbitClient = hawkbitClient;
description.setMinLength(2);
Stream.of(
description,
createdBy, createdAt,
lastModifiedBy, lastModifiedAt)
.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(new Date(distributionSet.getCreatedAt()).toString());
lastModifiedBy.setValue(distributionSet.getLastModifiedBy());
lastModifiedAt.setValue(new Date(distributionSet.getLastModifiedAt()).toString());
softwareModulesGrid.setItems(query -> Optional.ofNullable(
hawkbitClient.getDistributionSetRestApi()
.getAssignedSoftwareModules(
distributionSet.getDsId(),
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(0, 30, Constants.NAME_ASC, null)
.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);
final HorizontalLayout actions = new HorizontalLayout(create, cancel);
actions.setSizeFull();
actions.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
final VerticalLayout layout = new VerticalLayout();
layout.setSizeFull();
layout.setPadding(true);
layout.setSpacing(false);
layout.add(type, name, version, vendor, description, requiredMigrationStep, actions);
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()
.getDsId();
new AddSoftwareModulesDialog(distributionSetId, hawkbitClient).open();
});
}
}
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();
});
add(addBtn);
open();
}}.result(),
v -> {
Utils.remove(softwareModulesGrid.getSelectedItems(), softwareModules, MgmtSoftwareModule::getModuleId);
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 MgmtSoftwareModuleAssigment assignment = new MgmtSoftwareModuleAssigment();
assignment.setId(softwareModule.getModuleId());
return assignment;
}).toList());
close();
});
finishBtn.addClickShortcut(Key.ENTER);
final HorizontalLayout finish = new HorizontalLayout(finishBtn);
finish.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
finish.setWidthFull();
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, finish);
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.simple.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.simple.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,459 @@
/**
* 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.simple.view;
import java.time.ZoneOffset;
import java.util.Date;
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.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.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.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.simple.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.simple.MainLayout;
import org.eclipse.hawkbit.ui.simple.view.util.Filter;
import org.eclipse.hawkbit.ui.simple.view.util.SelectionGrid;
import org.eclipse.hawkbit.ui.simple.view.util.TableView;
import org.eclipse.hawkbit.ui.simple.view.util.Utils;
import org.springframework.util.ObjectUtils;
@PageTitle("Rollouts")
@Route(value = "rollouts", layout = MainLayout.class)
@RolesAllowed({ "ROLLOUT_READ" })
@Uses(Icon.class)
public class RolloutView extends TableView<MgmtRolloutResponseBody, Long> {
public RolloutView(final HawkbitMgmtClient hawkbitClient) {
super(
new RolloutFilter(),
new SelectionGrid.EntityRepresentation<>(
MgmtRolloutResponseBody.class, MgmtRolloutResponseBody::getRolloutId) {
private final RolloutDetails details = new RolloutDetails(hawkbitClient);
@Override
protected void addColumns(final Grid<MgmtRolloutResponseBody> grid) {
grid.addColumn(MgmtRolloutResponseBody::getRolloutId).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(
query.getOffset(), query.getPageSize(), Constants.NAME_ASC, rsqlFilter, "full")
.getBody()).stream().flatMap(page -> page.getContent().stream()),
selectionGrid -> new CreateDialog(hawkbitClient).result(),
selectionGrid -> {
selectionGrid.getSelectedItems().forEach(
rollout -> hawkbitClient.getRolloutRestApi().delete(rollout.getRolloutId()));
selectionGrid.refreshGrid(false);
return CompletableFuture.completedFuture(null);
});
}
private static SelectionGrid<MgmtRolloutGroupResponseBody, Long> createGroupGrid() {
return new SelectionGrid<>(
new SelectionGrid.EntityRepresentation<>(MgmtRolloutGroupResponseBody.class, MgmtRolloutGroupResponseBody::getRolloutGroupId) {
@Override
protected void addColumns(final Grid<MgmtRolloutGroupResponseBody> grid) {
grid.addColumn(MgmtRolloutGroupResponseBody::getRolloutGroupId).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 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.getRolloutId();
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.getRolloutId());
refresh();
});
}}, "Start"));
} else if ("RUNNING".equalsIgnoreCase(rollout.getStatus())) {
add(Utils.tooltip(new Button(VaadinIcon.PAUSE.create()) {{
addClickListener(v -> {
hawkbitClient.getRolloutRestApi().pause(rollout.getRolloutId());
refresh();
});
}}, "Pause"));
} else if ("PAUSED".equalsIgnoreCase(rollout.getStatus())) {
add(Utils.tooltip(new Button(VaadinIcon.START_COG.create()) {{
addClickListener(v -> {
hawkbitClient.getRolloutRestApi().resume(rollout.getRolloutId());
refresh();
});
}}, "Resume"));
}
add(Utils.tooltip(new Button(VaadinIcon.TRASH.create()) {{
addClickListener(v -> {
hawkbitClient.getRolloutRestApi().delete(rollout.getRolloutId());
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(new Date(rollout.getCreatedAt()).toString());
lastModifiedBy.setValue(rollout.getLastModifiedBy());
lastModifiedAt.setValue(new Date(rollout.getLastModifiedAt()).toString());
targetFilter.setValue(rollout.getTargetFilterQuery());
final MgmtDistributionSet distributionSetMgmt = hawkbitClient.getDistributionSetRestApi()
.getDistributionSet(rollout.getDistributionSetId()).getBody();
if (distributionSetMgmt == null) { // should not be here
distributionSet.setValue("n/a (null)");
} else {
distributionSet.setValue(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 " + new Date(rollout.getForcetime());
});
startAt.setValue(ObjectUtils.isEmpty(rollout.getStartAt()) ? "" : new Date(rollout.getStartAt()).toString());
dynamic.setValue(rollout.isDynamic());
groupGrid.setItems(query -> Optional.ofNullable(
hawkbitClient.getRolloutRestApi()
.getRolloutGroups(
rollout.getRolloutId(),
query.getOffset(), query.getPageSize(),
null, null, "full")
.getBody())
.stream().flatMap(body -> body.getContent().stream())
.skip(query.getOffset())
.limit(query.getPageSize()));
groupGrid.setSelectionMode(Grid.SelectionMode.NONE);
}
}
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(0, 30, Constants.NAME_ASC, null)
.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(0, 30, Constants.NAME_ASC, null, 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 = 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();
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);
addCreateClickListener(hawkbitClient);
final Button cancel = Utils.tooltip(new Button("Cancel"), "Cancel (Esc)");
cancel.addClickListener(e -> close());
cancel.addClickShortcut(Key.ESCAPE);
final HorizontalLayout actions = new HorizontalLayout(create, cancel);
actions.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
actions.setSizeFull();
final VerticalLayout layout = new VerticalLayout();
layout.setSizeFull();
layout.setSpacing(false);
layout.add(
name, distributionSet, targetFilter, description,
actionType, startType,
groupNumber, triggerThreshold, errorThreshold,
dynamic,
actions);
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().getDsId());
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,386 @@
/**
* 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.simple.view;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
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.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.component.upload.Upload;
import com.vaadin.flow.component.upload.receivers.FileBuffer;
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.artifact.MgmtArtifact;
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule;
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleRequestBodyPost;
import org.eclipse.hawkbit.mgmt.json.model.softwaremoduletype.MgmtSoftwareModuleType;
import org.eclipse.hawkbit.ui.simple.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.simple.MainLayout;
import org.eclipse.hawkbit.ui.simple.view.util.Filter;
import org.eclipse.hawkbit.ui.simple.view.util.SelectionGrid;
import org.eclipse.hawkbit.ui.simple.view.util.TableView;
import org.eclipse.hawkbit.ui.simple.view.util.Utils;
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)
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::getModuleId) {
private final SoftwareModuleDetails details = new SoftwareModuleDetails(hawkbitClient);
@Override
protected void addColumns(final Grid<MgmtSoftwareModule> grid) {
grid.addColumn(MgmtSoftwareModule::getModuleId).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(
query.getOffset(), query.getPageSize(), Constants.NAME_ASC, rsqlFilter)
.getBody())
.stream().flatMap(body -> body.getContent().stream()),
isParent ? v -> new CreateDialog(hawkbitClient).result() : null,
isParent ? selectionGrid -> {
selectionGrid.getSelectedItems().forEach(
module -> hawkbitClient.getSoftwareModuleRestApi().deleteSoftwareModule(module.getModuleId()));
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::getArtifactId) {
@Override
protected void addColumns(final Grid<MgmtArtifact> grid) {
grid.addColumn(MgmtArtifact::getArtifactId).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(0, 20, Constants.NAME_ASC, null)
.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(new Date(softwareModule.getCreatedAt()).toString());
lastModifiedBy.setValue(softwareModule.getLastModifiedBy());
lastModifiedAt.setValue(new Date(softwareModule.getLastModifiedAt()).toString());
artifactGrid.setItems(query -> Optional.ofNullable(
hawkbitClient.getSoftwareModuleRestApi()
.getArtifacts(
softwareModule.getModuleId(), 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 Button create;
private CreateDialog(final HawkbitMgmtClient hawkbitClient) {
super("Create Software Module");
type = new Select<>(
Constants.TYPE,
this::readyToCreate,
Optional.ofNullable(
hawkbitClient.getSoftwareModuleTypeRestApi()
.getTypes(0, 30, Constants.NAME_ASC, null)
.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");
create = Utils.tooltip(new Button("Create"), "Create (Enter)");
create.setEnabled(false);
addCreateClickListener(hawkbitClient);
create.addClickShortcut(Key.ENTER);
final Button cancel = Utils.tooltip(new Button("Cancel"), "Cancel (Esc)");
cancel.addClickListener(e -> close());
cancel.addClickShortcut(Key.ESCAPE);
final HorizontalLayout actions = new HorizontalLayout(create, cancel);
actions.setSizeFull();
actions.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
final VerticalLayout layout = new VerticalLayout();
layout.setSizeFull();
layout.setSpacing(false);
layout.add(type, name, version, vendor, description, enableArtifactEncryption, actions);
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(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()
.getModuleId();
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 FileBuffer fileBuffer = new FileBuffer();
final Upload uploadBtn = new Upload(fileBuffer);
uploadBtn.setMaxFiles(10);
uploadBtn.setWidthFull();
uploadBtn.setDropAllowed(true);
uploadBtn.addSucceededListener(e -> {
final MgmtArtifact artifact = hawkbitClient.getSoftwareModuleRestApi()
.uploadArtifact(softwareModuleId,
new MultipartFileImpl(fileBuffer, e.getContentLength(), e.getMIMEType()), fileBuffer.getFileName(), null, null,
null).getBody();
artifacts.add(artifact);
artifactGrid.refreshGrid(false);
});
final Button finishBtn = Utils.tooltip(new Button("Finish"), "Finish (Enter)");
finishBtn.addClickListener(e -> close());
finishBtn.addClickShortcut(Key.ENTER);
finishBtn.setHeightFull();
final HorizontalLayout finish = new HorizontalLayout(finishBtn);
finish.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
finish.setWidthFull();
final VerticalLayout layout = new VerticalLayout(artifactGrid, uploadBtn, finish);
layout.setSizeFull();
layout.setSpacing(false);
add(layout);
}
private static class MultipartFileImpl implements MultipartFile {
private final FileBuffer fileBuffer;
private final String mimeType;
private final long contentLength;
public MultipartFileImpl(final FileBuffer fileBuffer, final long contentLength, final String mimeType) {
this.fileBuffer = fileBuffer;
this.contentLength = contentLength;
this.mimeType = mimeType;
}
@Override
public String getName() {
return fileBuffer.getFileName();
}
@Override
public String getOriginalFilename() {
return getName();
}
@Override
public String getContentType() {
return mimeType;
}
@Override
public boolean isEmpty() {
return contentLength == 0;
}
@Override
public long getSize() {
return contentLength;
}
@Override
public byte[] getBytes() throws IOException {
try (final InputStream is = getInputStream()) {
return is.readAllBytes();
}
}
@Override
public InputStream getInputStream() {
return fileBuffer.getInputStream();
}
@Override
public void transferTo(final File dest) throws IllegalStateException {
throw new UnsupportedOperationException();
}
}
}
}

View File

@@ -0,0 +1,315 @@
/**
* 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.simple.view;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
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.button.Button;
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.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.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.tag.MgmtTag;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
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.simple.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.simple.MainLayout;
import org.eclipse.hawkbit.ui.simple.view.util.Filter;
import org.eclipse.hawkbit.ui.simple.view.util.SelectionGrid;
import org.eclipse.hawkbit.ui.simple.view.util.TableView;
import org.eclipse.hawkbit.ui.simple.view.util.Utils;
import org.springframework.util.ObjectUtils;
@PageTitle("Targets")
@Route(value = "targets", layout = MainLayout.class)
@RolesAllowed({ "TARGET_READ" })
@Uses(Icon.class)
public class TargetView extends TableView<MgmtTarget, String> {
public static final String CONTROLLER_ID = "Controller Id";
public static final String TAG = "Tag";
public TargetView(final HawkbitMgmtClient hawkbitClient) {
super(
new RawFilter(hawkbitClient), new SimpleFilter(hawkbitClient),
new SelectionGrid.EntityRepresentation<>(MgmtTarget.class, MgmtTarget::getControllerId) {
@Override
protected void addColumns(final Grid<MgmtTarget> grid) {
grid.addColumn(MgmtTarget::getControllerId).setHeader(CONTROLLER_ID).setAutoWidth(true);
grid.addColumn(MgmtTarget::getName).setHeader(Constants.NAME).setAutoWidth(true);
grid.addColumn(MgmtTarget::getTargetTypeName).setHeader(Constants.TYPE).setAutoWidth(true);
grid.setItemDetailsRenderer(new ComponentRenderer<>(
TargetDetails::new, TargetDetails::setItem));
}
},
(query, filter) -> hawkbitClient.getTargetRestApi()
.getTargets(
query.getOffset(), query.getPageSize(), Constants.NAME_ASC,
filter)
.getBody()
.getContent()
.stream(),
source -> new RegisterDialog(hawkbitClient).result(),
selectionGrid -> {
selectionGrid.getSelectedItems().forEach(toDelete ->
hawkbitClient.getTargetRestApi().deleteTarget(toDelete.getControllerId()));
return CompletableFuture.completedFuture(null);
});
}
private static class SimpleFilter implements Filter.Rsql {
private final HawkbitMgmtClient hawkbitClient;
private final TextField controllerId;
private final CheckboxGroup<MgmtTargetType> type;
private final CheckboxGroup<MgmtTag> tag;
private SimpleFilter(final HawkbitMgmtClient hawkbitClient) {
this.hawkbitClient = hawkbitClient;
controllerId = Utils.textField(CONTROLLER_ID);
controllerId.setPlaceholder("<controller id 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(controllerId);
type.setItems(hawkbitClient.getTargetTypeRestApi().getTargetTypes(0, 20, Constants.NAME_ASC, null).getBody().getContent());
if (!type.getValue().isEmpty()) {
components.add(type);
}
tag.setItems(hawkbitClient.getTargetTagRestApi().getTargetTags(0, 20, Constants.NAME_ASC, null).getBody().getContent());
if (!tag.isEmpty()) {
components.add(tag);
}
return components;
}
@Override
public String filter() {
return Filter.filter(
Map.of(
"controllerid", controllerId.getOptionalValue(),
"targettype.name", type.getSelectedItems().stream().map(MgmtTargetType::getName)
.toList(),
"tag", tag.getSelectedItems()));
}
}
private static class RawFilter implements Filter.Rsql {
private final TextField textFilter = new TextField("Raw Filter");
private final VerticalLayout layout = new VerticalLayout();
private RawFilter(final HawkbitMgmtClient hawkbitClient) {
textFilter.setPlaceholder("<raw filter>");
final Select<MgmtTargetFilterQuery> savedFilters = new Select<>(
"Saved Filters",
e -> {
if (e.getValue() != null) {
textFilter.setValue(e.getValue().getQuery());
}
});
savedFilters.setEmptySelectionAllowed(true);
savedFilters.setItems(
Optional.ofNullable(
hawkbitClient.getTargetFilterQueryRestApi()
.getFilters(0, 30, null, null, null)
.getBody().getContent())
.orElse(Collections.emptyList()));
savedFilters.setItemLabelGenerator(
query -> Optional.ofNullable(query).map(MgmtTargetFilterQuery::getName).orElse("<select saved filter>"));
savedFilters.setWidthFull();
textFilter.setWidthFull();
final Button saveBtn = Utils.tooltip(new Button(VaadinIcon.ARCHIVE.create()), "Save (Enter)");
saveBtn.addClickListener(e ->
new Utils.BaseDialog<Void>("Save Filter") {{
setHeight("40%");
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(
hawkbitClient.getTargetFilterQueryRestApi()
.getFilters(0, 30, null, null, null).getBody().getContent());
close();
});
add(name, finishBtn);
open();
}});
saveBtn.addClickShortcut(Key.ENTER);
layout.setSpacing(false);
final HorizontalLayout textSaveLayout = new HorizontalLayout(textFilter, saveBtn);
textSaveLayout.setAlignItems(FlexComponent.Alignment.BASELINE);
textSaveLayout.setWidthFull();
layout.add(savedFilters, textSaveLayout);
}
@Override
public List<Component> components() {
return List.of(layout);
}
@Override
public String filter() {
return textFilter.getOptionalValue().orElse(null);
}
}
private static class TargetDetails extends FormLayout {
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 TargetDetails() {
description.setMinLength(2);
Stream.of(
description,
createdBy, createdAt,
lastModifiedBy, lastModifiedAt,
securityToken)
.forEach(field -> {
field.setReadOnly(true);
add(field);
});
setResponsiveSteps(new ResponsiveStep("0", 2));
setColspan(description, 2);
}
private void setItem(final MgmtTarget target) {
description.setValue(target.getDescription() == null ? "N/A" : target.getDescription());
createdBy.setValue(target.getCreatedBy());
createdAt.setValue(new Date(target.getCreatedAt()).toString());
lastModifiedBy.setValue(target.getLastModifiedBy());
lastModifiedAt.setValue(new Date(target.getLastModifiedAt()).toString());
securityToken.setValue(target.getSecurityToken());
}
}
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 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(0, 30, Constants.NAME_ASC, null)
.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();
addCreateClickListener(register, hawkbitClient);
register.setEnabled(false);
register.addClickShortcut(Key.ENTER);
final Button cancel = Utils.tooltip(new Button("Cancel"), "Cancel (Esc)");
cancel.addClickListener(e -> close());
register.addClickShortcut(Key.ESCAPE);
final HorizontalLayout actions = new HorizontalLayout(register, cancel);
actions.setSizeFull();
actions.setPadding(true);
actions.setSpacing(true);
final VerticalLayout layout = new VerticalLayout();
layout.setSizeFull();
layout.setPadding(true);
layout.setSpacing(false);
layout.add(type, controllerId, name, description, actions);
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());
if (!ObjectUtils.isEmpty(type.getValue())) {
request.setTargetType(type.getValue().getTypeId());
}
hawkbitClient.getTargetRestApi().createTargets(
List.of(request))
.getBody()
.stream()
.findFirst()
.orElseThrow()
.getControllerId();
close();
});
}
}
}

View File

@@ -0,0 +1,160 @@
/**
* 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.simple.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.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;
public class Filter extends Div {
private transient Rsql rsql;
public Filter(final Consumer<String> changeListener, final Rsql primaryRsql, final Rsql secondaryOptionalRsql) {
rsql = primaryRsql;
final HorizontalLayout layout = new HorizontalLayout();
setWidthFull();
addClassNames(LumoUtility.Padding.Horizontal.NONE, LumoUtility.Padding.Vertical.SMALL,
LumoUtility.BoxSizing.BORDER);
final Div filtersDiv = new Div();
filtersDiv.setWidthFull();
filtersDiv.add(primaryRsql.components());
filtersDiv.addClassName(LumoUtility.Gap.SMALL);
final Button searchBtn = Utils.tooltip(new Button(VaadinIcon.REFRESH.create()), "Search");
searchBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
searchBtn.addClickListener(e -> changeListener.accept(rsql.filter()));
final Button resetBtn = Utils.tooltip(new Button(VaadinIcon.ERASER.create()), "Reset");
resetBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
resetBtn.addClickListener(e -> {
clear(layout.getChildren());
changeListener.accept(primaryRsql.filter());
});
final Div actionDiv = new Div();
actionDiv.add(searchBtn, resetBtn);
actionDiv.addClassNames(LumoUtility.Gap.SMALL);
layout.setPadding(true);
layout.setAlignItems(FlexComponent.Alignment.BASELINE);
layout.add(filtersDiv);
layout.add(actionDiv);
if (secondaryOptionalRsql != null) {
final Button toggleBtn = Utils.tooltip(new Button(VaadinIcon.FLIP_V.create()), "Toggle Search");
toggleBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
toggleBtn.addClickListener(e -> {
filtersDiv.removeAll();
synchronized (this) { // toggle
rsql = rsql == primaryRsql ? secondaryOptionalRsql : primaryRsql;
}
filtersDiv.add(rsql.components());
changeListener.accept(primaryRsql.filter());
});
layout.add(toggleBtn);
}
add(layout);
changeListener.accept(primaryRsql.filter());
}
public static String filter(final Map<String, Object> keyToValues) {
final Map<String, Object> normalized =
new HashMap<>(keyToValues)
.entrySet()
.stream()
.filter(e -> {
if (e.getValue() instanceof Optional<?> opt) {
return opt.isPresent();
} else {
return e.getValue() != null;
}
})
.filter(e -> !(e.getValue() instanceof Collection<?> coll && coll.isEmpty()))
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
if (normalized.isEmpty()) {
return null;
} else if (normalized.size() == 1) {
return normalized.entrySet().stream()
.findFirst().map(e -> filter(e.getKey(), e.getValue())).orElse(null); // never return null!
} else {
final StringBuilder sb = new StringBuilder();
normalized.forEach((k, v) -> {
if (v instanceof Collection<?>) {
sb.append('(').append(filter(k, v)).append(')');
} else if (v instanceof Optional<?> opt) {
sb.append(filter(k, opt.get()));
} else {
sb.append(filter(k, v));
}
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();
}
}

View File

@@ -0,0 +1,95 @@
/**
* 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.simple.view.util;
import java.util.HashSet;
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
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()) {
return fetch;
} 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) {
if (!Objects.equals(this.rsqlFilter, rsqlFilter)) {
this.rsqlFilter = rsqlFilter;
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 static abstract 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,70 @@
/**
* 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.simple.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.html.Div;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.data.provider.Query;
import org.eclipse.hawkbit.ui.simple.view.Constants;
public class TableView<T, ID> extends Div implements Constants {
protected SelectionGrid<T, ID> selectionGrid;
private final Filter filter;
public TableView(
final Filter.Rsql rsql,
final SelectionGrid.EntityRepresentation<T, ID> entityRepresentation,
final BiFunction<Query<T, Void>, String, Stream<T>> queryFn) {
this(rsql, null, entityRepresentation, queryFn, null, 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) {
this(rsql, alternativeRsql, entityRepresentation, queryFn, null, null);
}
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);
}
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) {
selectionGrid = new SelectionGrid<>(entityRepresentation, queryFn);
filter = new Filter(selectionGrid::setRsqlFilter, rsql, alternativeRsql);
setSizeFull();
final VerticalLayout layout = new VerticalLayout(filter, selectionGrid);
layout.setSizeFull();
layout.setPadding(false);
layout.setSpacing(false);
if (addHandler != null || removeHandler != null) {
layout.add(Utils.addRemoveControls(addHandler, removeHandler, selectionGrid, false));
}
add(layout);
}
}

View File

@@ -0,0 +1,185 @@
/**
* 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.simple.view.util;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
import java.util.function.Function;
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.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
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.shared.Tooltip;
import com.vaadin.flow.component.textfield.NumberField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.theme.lumo.LumoUtility;
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;
}
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 Button removeBtn = tooltip(new Button(VaadinIcon.MINUS.create()), "Remove");
removeBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_CONTRAST);
removeBtn.addClickListener(e -> removeHandler
.apply(selectionGrid)
.thenAccept(v -> selectionGrid.refreshGrid(false)));
layout.add(removeBtn);
}
return layout;
}
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 class BaseDialog<T> extends Dialog {
protected final transient CompletableFuture<T> result = new CompletableFuture<>();
protected BaseDialog(final String headerTitle) {
setHeaderTitle(headerTitle);
setHeightFull();
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();
}
}
}

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,29 @@
#
# 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.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.whitelisted-packages=com.vaadin,org.vaadin,dev.hilla,org.eclipse.hawkbit
### 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.