Initial draft of Simple UI (#1499)

A very simple UI based on Vaadin 24.
This commit is contained in:
Avgustin Marinov
2023-12-04 15:00:18 +02:00
committed by GitHub
parent d0bb7b61d8
commit 3b66281c08
22 changed files with 2691 additions and 1 deletions

View File

@@ -0,0 +1 @@
frontend

View File

@@ -0,0 +1,156 @@
<!--
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="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
</parent>
<groupId>com.eclipse.hawkbit</groupId>
<artifactId>hawkbit-simple-ui</artifactId>
<name>hawkBit :: Runtime :: Simple UI</name>
<version>0.4.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<java.version>17</java.version>
<vaadin.version>24.2.2</vaadin.version>
<spring-cloud-starter-openfeign.version>4.0.4</spring-cloud-starter-openfeign.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-mgmt-api</artifactId>
<version>0.4.0-SNAPSHOT</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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>${spring-cloud-starter-openfeign.version}</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
<version>13.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</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>
<!-- TODO - to check license - remove after the parent become a hawkBit pom -->
<plugin>
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
<version>2.11</version>
<configuration>
<header>licenses/LICENSE_HEADER_TEMPLATE.txt</header>
<excludes>
<exclude>**/banner.txt</exclude>
<exclude>**/README.md</exclude>
<exclude>**/frontend/**</exclude>
</excludes>
<mapping>
<scss>JAVADOC_STYLE</scss>
</mapping>
</configuration>
</plugin>
</plugins>
</build>
<!-- TODO - to specify license - remove after the parent become a hawkBit pom -->
<licenses>
<license>
<name>EPL-2.0</name>
<url>https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt</url>
<comments>Eclipse Public License - Version 2.0</comments>
</license>
</licenses>
</project>

View File

@@ -0,0 +1,122 @@
/**
* 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 com.eclipse.hawkbit.ui;
import com.eclipse.hawkbit.ui.view.util.Utils;
import com.vaadin.flow.component.UI;
import feign.Client;
import feign.Contract;
import feign.Feign;
import feign.FeignException;
import feign.RequestInterceptor;
import feign.Response;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
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.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Base64;
import java.util.Objects;
import java.util.function.Supplier;
import static feign.Util.ISO_8859_1;
@Getter
public class HawkbitClient {
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 final String hawkbitUrl;
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;
HawkbitClient(final String hawkbitUrl,
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
this.hawkbitUrl = hawkbitUrl;
softwareModuleRestApi = service(MgmtSoftwareModuleRestApi .class, client, encoder, decoder, contract);
softwareModuleTypeRestApi = service(MgmtSoftwareModuleTypeRestApi.class, client, encoder, decoder, contract);
distributionSetRestApi = service(MgmtDistributionSetRestApi.class, client, encoder, decoder, contract);
distributionSetTypeRestApi = service(MgmtDistributionSetTypeRestApi.class, client, encoder, decoder, contract);
distributionSetTagRestApi = service(MgmtDistributionSetTagRestApi.class, client, encoder, decoder, contract);
targetRestApi = service(MgmtTargetRestApi.class, client, encoder, decoder, contract);
targetTypeRestApi = service(MgmtTargetTypeRestApi.class, client, encoder, decoder, contract);
targetTagRestApi = service(MgmtTargetTagRestApi.class, client, encoder, decoder, contract);
targetFilterQueryRestApi = service(MgmtTargetFilterQueryRestApi.class, client, encoder, decoder, contract);
rolloutRestApi = service(MgmtRolloutRestApi.class, client, encoder, decoder, contract);
}
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"));
}
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 static final ErrorDecoder DEFAULT_ERROR_DECODER = new ErrorDecoder.Default();
private <T> T service(final Class<T> serviceType,
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
return Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.errorDecoder((methodKey, response) -> {
final Exception e = DEFAULT_ERROR_DECODER.decode(methodKey, response);
Utils.errorNotification(e);
return e;
})
.contract(contract)
.requestInterceptor(AUTHORIZATION)
.target(serviceType, hawkbitUrl);
}
}

View File

@@ -0,0 +1,150 @@
/**
* 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 com.eclipse.hawkbit.ui;
import com.eclipse.hawkbit.ui.view.RolloutView;
import com.eclipse.hawkbit.ui.view.TargetView;
import com.eclipse.hawkbit.ui.security.AuthenticatedUser;
import com.eclipse.hawkbit.ui.view.AboutView;
import com.eclipse.hawkbit.ui.view.DistributionSetView;
import com.eclipse.hawkbit.ui.view.SoftwareModuleView;
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 java.util.Optional;
/**
* The main view is a top-level placeholder for other views.
*/
public class MainLayout extends AppLayout {
private H2 viewTitle;
private final AuthenticatedUser authenticatedUser;
private final AccessAnnotationChecker accessChecker;
public MainLayout(final AuthenticatedUser authenticatedUser, final AccessAnnotationChecker accessChecker) {
this.authenticatedUser = authenticatedUser;
this.accessChecker = accessChecker;
setPrimarySection(Section.DRAWER);
addDrawerContent();
setDrawerOpened(true);
addHeaderContent();
}
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(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;
}
@Override
protected void afterNavigation() {
super.afterNavigation();
viewTitle.setText(
Optional.ofNullable(getContent().getClass().getAnnotation(PageTitle.class))
.map(PageTitle::value)
.orElse(""));
}
}

View File

@@ -0,0 +1,90 @@
/**
* 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 com.eclipse.hawkbit.ui;
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.codec.Decoder;
import feign.codec.Encoder;
import org.springframework.beans.factory.annotation.Value;
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.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.LinkedList;
import java.util.List;
@Theme(themeClass = Lumo.class)
@PWA(name="hawkBit UI", shortName="hawkBit UI")
@SpringBootApplication
@Import(FeignClientsConfiguration.class)
public class SimpleUIApp implements AppShellConfigurator {
public static void main(String[] args) {
SpringApplication.run(SimpleUIApp.class, args);
}
@Bean
HawkbitClient hawkbitClient(
@Value("${hawkbit.url:http://localhost:8080}")
final String hawkbitUrl,
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
return new HawkbitClient(hawkbitUrl, client, encoder, decoder, contract);
}
// accepts all user / pass, just delegating them to the feign client
@Bean
AuthenticationManager authenticationManager(final HawkbitClient 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");
}
} finally {
SecurityContextHolder.setContext(currentContext);
}
return new UsernamePasswordAuthenticationToken(
username, password,
roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList()) {
public void eraseCredentials() {}
};
};
}
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package com.eclipse.hawkbit.ui.security;
import com.vaadin.flow.spring.security.AuthenticationContext;
import org.springframework.stereotype.Component;
import java.util.Optional;
@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 com.eclipse.hawkbit.ui.security;
import com.eclipse.hawkbit.ui.view.LoginView;
import com.vaadin.flow.spring.security.VaadinWebSecurity;
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,43 @@
/**
* 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 com.eclipse.hawkbit.ui.view;
import com.eclipse.hawkbit.ui.MainLayout;
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 jakarta.annotation.security.RolesAllowed;
@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,333 @@
/**
* 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 com.eclipse.hawkbit.ui.view;
import com.eclipse.hawkbit.ui.HawkbitClient;
import com.eclipse.hawkbit.ui.view.util.Filter;
import com.eclipse.hawkbit.ui.MainLayout;
import com.eclipse.hawkbit.ui.view.util.SelectionGrid;
import com.eclipse.hawkbit.ui.view.util.TableView;
import com.eclipse.hawkbit.ui.view.util.Utils;
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 jakarta.annotation.security.RolesAllowed;
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 java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
@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 HawkbitClient 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("Id").setAutoWidth(true);
grid.addColumn(MgmtDistributionSet::getName).setHeader("Name").setAutoWidth(true);
grid.addColumn(MgmtDistributionSet::getVersion).setHeader("Version").setAutoWidth(true);
grid.addColumn(MgmtDistributionSet::getTypeName).setHeader("Type").setAutoWidth(true);
grid.setItemDetailsRenderer(new ComponentRenderer<>(
() -> details, DistributionSetDetails::setItem));
}
},
(query, rsqlFilter) -> hawkbitClient.getDistributionSetRestApi()
.getDistributionSets(
query.getOffset(), query.getPageSize(), "name:asc", rsqlFilter)
.getBody()
.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("Id").setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getVersion).setHeader("Version").setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getTypeName).setHeader("Name").setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getVendor).setHeader("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 HawkbitClient hawkbitClient) {
name.setPlaceholder("<name filter>");
type.setItemLabelGenerator(MgmtDistributionSetType::getName);
type.setItems(
hawkbitClient.getDistributionSetTypeRestApi()
.getDistributionSetTypes(0, 20, "name:asc", null)
.getBody()
.getContent());
tag.setItemLabelGenerator(MgmtTag::getName);
tag.setItems(
hawkbitClient.getDistributionSetTagRestApi()
.getDistributionSetTags(0, 20, "name:asc", null)
.getBody()
.getContent());
}
@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 HawkbitClient 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 HawkbitClient 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 ->
hawkbitClient.getDistributionSetRestApi()
.getAssignedSoftwareModules(
distributionSet.getDsId(),
query.getOffset(), query.getLimit(), "name:asc")
.getBody()
.getContent()
.stream());
softwareModulesGrid.setSelectionMode(Grid.SelectionMode.NONE);
}
}
private static class CreateDialog extends Utils.BaseDialog<Void> {
private final HawkbitClient 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 HawkbitClient hawkbitClient) {
super("Create Distribution Set");
this.hawkbitClient = hawkbitClient;
type = new Select<>(
"Type",
this::readyToCreate,
hawkbitClient.getDistributionSetTypeRestApi()
.getDistributionSetTypes(0, 30, "name:asc", null)
.getBody()
.getContent()
.toArray(new MgmtDistributionSetType[0]));
type.focus();
type.setWidthFull();
type.setRequiredIndicatorVisible(true);
type.setItemLabelGenerator(MgmtDistributionSetType::getName);
name = Utils.textField("Name", this::readyToCreate);
version = Utils.textField("Version", this::readyToCreate);
final TextField vendor = Utils.textField("Vendor");
description = new TextArea("Description");
description.setWidthFull();
description.setMinLength(2);
requiredMigrationStep = new Checkbox("Reguired 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 = 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()
.findFirst()
.orElseThrow()
.getDsId();
new AddSoftwareModulesDialog(distributionSetId, hawkbitClient).open();
});
}
}
private static class AddSoftwareModulesDialog extends Utils.BaseDialog<Void> {
private final Set<MgmtSoftwareModule> softwareModules = Collections.synchronizedSet(new HashSet<>());
private AddSoftwareModulesDialog(final long distributionSetId, final HawkbitClient 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).get();
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 com.eclipse.hawkbit.ui.view;
import com.eclipse.hawkbit.ui.security.AuthenticatedUser;
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;
@AnonymousAllowed
@PageTitle("Login")
@Route(value = "login")
public class LoginView extends LoginOverlay implements BeforeEnterObserver {
private final 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,427 @@
/**
* 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 com.eclipse.hawkbit.ui.view;
import com.eclipse.hawkbit.ui.HawkbitClient;
import com.eclipse.hawkbit.ui.view.util.Filter;
import com.eclipse.hawkbit.ui.MainLayout;
import com.eclipse.hawkbit.ui.view.util.SelectionGrid;
import com.eclipse.hawkbit.ui.view.util.TableView;
import com.eclipse.hawkbit.ui.view.util.Utils;
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.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 jakarta.annotation.security.RolesAllowed;
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.MgmtRolloutRestRequestBody;
import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroupResponseBody;
import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQuery;
import org.springframework.util.ObjectUtils;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
@PageTitle("Rollouts")
@Route(value = "rollouts", layout = MainLayout.class)
@RolesAllowed({"ROLLOUT_READ"})
@Uses(Icon.class)
public class RolloutView extends TableView<MgmtRolloutResponseBody, Long> {
public RolloutView(final HawkbitClient hawkbitClient) {
super(
new RolloutFilter(hawkbitClient),
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("Id").setAutoWidth(true);
grid.addColumn(MgmtRolloutResponseBody::getName).setHeader("Name").setAutoWidth(true);
grid.addColumn(MgmtRolloutResponseBody::getTotalGroups).setHeader("Group Count").setAutoWidth(true);
grid.addColumn(MgmtRolloutResponseBody::getTotalTargets).setHeader("Target Count").setAutoWidth(true);
grid.addColumn(MgmtRolloutResponseBody::getTotalTargetsPerStatus).setHeader("Stats").setAutoWidth(true);
grid.addColumn(MgmtRolloutResponseBody::getStatus).setHeader("Status").setAutoWidth(true);
grid.addComponentColumn(rollout -> new Actions(rollout, grid, hawkbitClient)).setHeader("Actions").setAutoWidth(true);
grid.setItemDetailsRenderer(new ComponentRenderer<>(
() -> details, RolloutDetails::setItem));
}
},
(query, rsqlFilter) -> hawkbitClient.getRolloutRestApi()
.getRollouts(
query.getOffset(), query.getPageSize(), "name:asc", rsqlFilter, null)
.getBody()
.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("Id").setAutoWidth(true);
grid.addColumn(MgmtRolloutGroupResponseBody::getName).setHeader("Name").setAutoWidth(true);
grid.addColumn(MgmtRolloutGroupResponseBody::getTotalTargets).setHeader("Target Count").setAutoWidth(true);
grid.addColumn(MgmtRolloutGroupResponseBody::getTotalTargetsPerStatus).setHeader("Stats").setAutoWidth(true);
grid.addColumn(MgmtRolloutGroupResponseBody::getStatus).setHeader("Status").setAutoWidth(true);
}
});
}
private static class Actions extends HorizontalLayout {
private final long rolloutId;
private final Grid<MgmtRolloutResponseBody> grid;
private final HawkbitClient hawkbitClient;
private Actions(final MgmtRolloutResponseBody rollout, final Grid<MgmtRolloutResponseBody> grid, final HawkbitClient 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();
init(hawkbitClient.getRolloutRestApi().getRollout(rolloutId).getBody());
}
}
private static class RolloutFilter implements Filter.Rsql {
private final TextField name = Utils.textField("Name");
private RolloutFilter(final HawkbitClient hawkbitClient) {
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 HawkbitClient 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 TextField targetFilter = Utils.textField("Target Filter");
private final TextField distributionSet = Utils.textField("Distribution Set");
private final TextField actonType = Utils.textField("Action Type");
private final TextField startAt = Utils.textField("Start At");
private final SelectionGrid<MgmtRolloutGroupResponseBody, Long> groupGrid;
private RolloutDetails(final HawkbitClient 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);
});
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();
distributionSet.setValue(distributionSetMgmt.getName() + ":" + distributionSetMgmt.getVersion());
actonType.setValue(switch (rollout.getType()) {
case SOFT -> "Soft";
case FORCED -> "Forced";
case DOWNLOAD_ONLY -> "Download Only";
case TIMEFORCED -> "Scheduled at " + new Date(rollout.getForcetime());
});
startAt.setValue(ObjectUtils.isEmpty(rollout.getStartAt()) ? "" : new Date(rollout.getStartAt()).toString());
groupGrid.setItems(query ->
hawkbitClient.getRolloutRestApi()
.getRolloutGroups(
rollout.getRolloutId(),
query.getOffset(), query.getPageSize(),
null, null, null)
.getBody().getContent().stream()
.skip(query.getOffset())
.limit(query.getPageSize()));
groupGrid.setSelectionMode(Grid.SelectionMode.NONE);
}
}
private static class CreateDialog extends Utils.BaseDialog<Void> {
private enum StartType {
MANUAL, AUTO, SCHEDULED
}
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("Start At");
private final NumberField groupNumber;
private final NumberField triggerThreshold;
private final NumberField errorThreshold;
private final Button create = new Button("Create");
private CreateDialog(final HawkbitClient hawkbitClient) {
super("Create Rollout");
name = Utils.textField("Name", this::readyToCreate);
name.focus();
distributionSet = new Select<>(
"Distribution Set",
this::readyToCreate,
hawkbitClient.getDistributionSetRestApi()
.getDistributionSets(0, 30, "name:asc", null)
.getBody()
.getContent()
.toArray(new MgmtDistributionSet[0]));
distributionSet.setRequiredIndicatorVisible(true);
distributionSet.setItemLabelGenerator(distributionSet ->
distributionSet.getName() + ":" + distributionSet.getVersion());
distributionSet.setWidthFull();
targetFilter = new Select<>(
"Target Filter",
this::readyToCreate,
hawkbitClient.getTargetFilterQueryRestApi()
.getFilters(0, 30, "name:asc", null, null)
.getBody()
.getContent()
.toArray(new MgmtTargetFilterQuery[0]));
targetFilter.setRequiredIndicatorVisible(true);
targetFilter.setItemLabelGenerator(MgmtTargetFilterQuery::getName);
targetFilter.setWidthFull();
description = new TextArea("Description");
description.setMinLength(2);
description.setWidthFull();
actionType = new Select<>();
actionType.setLabel("Action Type");
actionType.setItems(MgmtActionType.values());
actionType.setValue(MgmtActionType.FORCED);
final ComponentRenderer<Component, MgmtActionType> actionTypeRenderer = new ComponentRenderer<>(actionType ->
switch (actionType) {
case SOFT -> new Text("Soft");
case FORCED -> new Text("Forced");
case DOWNLOAD_ONLY -> new Text("Download Only");
case TIMEFORCED -> forceTime;
});
actionType.addValueChangeListener(e -> actionType.setRenderer(actionTypeRenderer));
actionType.setItemLabelGenerator(startType ->
switch (startType) {
case SOFT -> "Soft";
case FORCED -> "Forced";
case DOWNLOAD_ONLY -> "Download Only";
case TIMEFORCED -> "Time Forced at " + (forceTime.isEmpty() ? "" : " " + forceTime.getValue());
});
actionType.setWidthFull();
startType = new Select<>();
startType.setValue(StartType.MANUAL);
startType.setLabel("Start Type");
startType.setItems(StartType.values());
startType.setValue(StartType.MANUAL);
final ComponentRenderer<Component, StartType> startTypeRenderer = new ComponentRenderer<>(startType ->
switch (startType) {
case MANUAL -> new Text("Manual");
case AUTO -> new Text("Auto");
case SCHEDULED -> startAt;
});
startType.setRenderer(startTypeRenderer);
startType.addValueChangeListener(e -> startType.setRenderer(startTypeRenderer));
startType.setItemLabelGenerator(startType ->
switch (startType) {
case MANUAL -> "Manual";
case AUTO -> "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,
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 HawkbitClient hawkbitClient) {
create.addClickListener(e -> {
close();
final MgmtRolloutRestRequestBody request = new MgmtRolloutRestRequestBody();
request.setName(name.getValue());
request.setDistributionSetId(distributionSet.getValue().getDsId());
request.setTargetFilterQuery(targetFilter.getValue().getName());
request.setDescription(description.getValue());
request.setType(actionType.getValue());
if (actionType.getValue() == MgmtActionType.FORCED) {
request.setForcetime(forceTime.getValue().toEpochSecond(ZoneOffset.UTC) * 1000);
}
switch (startType.getValue()) {
case AUTO -> request.setStartAt(System.currentTimeMillis());
case SCHEDULED -> request.setStartAt(startAt.getValue().toEpochSecond(ZoneOffset.UTC) * 1000);
} // else - manual, do not start
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, ""));
hawkbitClient.getRolloutRestApi().create(request).getBody();
});
}
}
}

View File

@@ -0,0 +1,378 @@
/**
* 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 com.eclipse.hawkbit.ui.view;
import com.eclipse.hawkbit.ui.HawkbitClient;
import com.eclipse.hawkbit.ui.view.util.Filter;
import com.eclipse.hawkbit.ui.view.util.SelectionGrid;
import com.eclipse.hawkbit.ui.MainLayout;
import com.eclipse.hawkbit.ui.view.util.TableView;
import com.eclipse.hawkbit.ui.view.util.Utils;
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 jakarta.annotation.security.RolesAllowed;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
@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 HawkbitClient hawkbitClient) {
this(true, hawkbitClient);
}
public SoftwareModuleView(final boolean isParent, final HawkbitClient 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("Id").setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getName).setHeader("Name").setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getVersion).setHeader("Version").setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getTypeName).setHeader("Type").setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getVendor).setHeader("Vendor").setAutoWidth(true);
grid.setItemDetailsRenderer(new ComponentRenderer<>(
() -> details, SoftwareModuleDetails::setItem));
}
},
(query, rsqlFilter) -> hawkbitClient.getSoftwareModuleRestApi()
.getSoftwareModules(
query.getOffset(), query.getPageSize(), "name:asc", rsqlFilter)
.getBody()
.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("Id").setAutoWidth(true);
grid.addColumn(MgmtArtifact::getProvidedFilename).setHeader("Name").setAutoWidth(true);
grid.addColumn(MgmtArtifact::getSize).setHeader("Size").setAutoWidth(true);
grid.addColumn(MgmtArtifact::getHashes).setHeader("Hashes").setAutoWidth(true);
}
});
}
private static class SoftwareModuleFilter implements Filter.Rsql {
private final TextField name = Utils.textField("Name");
private final CheckboxGroup<MgmtSoftwareModuleType> type = new CheckboxGroup<>("Type");
private SoftwareModuleFilter(final HawkbitClient hawkbitClient) {
name.setPlaceholder("<name filter>");
type.setItemLabelGenerator(MgmtSoftwareModuleType::getName);
type.setItems(
hawkbitClient.getSoftwareModuleTypeRestApi()
.getTypes(0, 20, "name:asc", null)
.getBody()
.getContent());
}
@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 HawkbitClient 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<MgmtArtifact, Long> artifactGrid;
private SoftwareModuleDetails(final HawkbitClient 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 ->
hawkbitClient.getSoftwareModuleRestApi()
.getArtifacts(
softwareModule.getModuleId(), null, null)
.getBody().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 HawkbitClient hawkbitClient) {
super("Create Software Module");
type = new Select<>(
"Type",
this::readyToCreate,
hawkbitClient.getSoftwareModuleTypeRestApi()
.getTypes(0, 30, "name:asc", null)
.getBody()
.getContent()
.toArray(new MgmtSoftwareModuleType[0]));
type.setWidthFull();
type.setRequiredIndicatorVisible(true);
type.setItemLabelGenerator(MgmtSoftwareModuleType::getName);
type.focus();
name = Utils.textField("Name", this::readyToCreate);
version = Utils.textField("Version", this::readyToCreate);
vendor = Utils.textField("Vendor");
description = new TextArea("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 HawkbitClient hawkbitClient) {
create.addClickListener(e -> {
close();
final long softwareModuleId = 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()
.findFirst()
.orElseThrow()
.getModuleId();
new AddArtifactsDialog(softwareModuleId, hawkbitClient).open();
});
}
}
private static class AddArtifactsDialog extends Utils.BaseDialog<Void> {
private final Set<MgmtArtifact> artifacts = Collections.synchronizedSet(new HashSet<>());
private AddArtifactsDialog(
final long softwareModuleId,
final HawkbitClient 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 {
return getInputStream().readAllBytes();
}
@Override
public InputStream getInputStream() throws IOException {
return fileBuffer.getInputStream();
}
@Override
public void transferTo(final File dest) throws IllegalStateException {
throw new UnsupportedOperationException();
}
}
}
}

View File

@@ -0,0 +1,329 @@
/**
* 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 com.eclipse.hawkbit.ui.view;
import com.eclipse.hawkbit.ui.HawkbitClient;
import com.eclipse.hawkbit.ui.view.util.Filter;
import com.eclipse.hawkbit.ui.MainLayout;
import com.eclipse.hawkbit.ui.view.util.SelectionGrid;
import com.eclipse.hawkbit.ui.view.util.TableView;
import com.eclipse.hawkbit.ui.view.util.Utils;
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 jakarta.annotation.security.RolesAllowed;
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.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.springframework.util.ObjectUtils;
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;
@PageTitle("Targets")
@Route(value = "targets", layout = MainLayout.class)
@RolesAllowed({"TARGET_READ"})
@Uses(Icon.class)
public class TargetView extends TableView<MgmtTarget, String> {
private final HawkbitClient hawkbitClient;
private final MgmtTargetRestApi targetRestApi;
private final MgmtTargetTypeRestApi targetTypeRestApi;
private final MgmtTargetTagRestApi targetTagRestApi;
private final MgmtTargetFilterQueryRestApi targetFilterQueryRestApi;
public TargetView(final HawkbitClient 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("Name").setAutoWidth(true);
grid.addColumn(MgmtTarget::getTargetTypeName).setHeader("Type").setAutoWidth(true);
grid.setItemDetailsRenderer(new ComponentRenderer<>(
TargetDetails::new, TargetDetails::setItem));
}
},
(query, filter) -> hawkbitClient.getTargetRestApi()
.getTargets(
query.getOffset(), query.getPageSize(), "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);
});
this.hawkbitClient = hawkbitClient;
this.targetRestApi = hawkbitClient.getTargetRestApi();
this.targetTypeRestApi = hawkbitClient.getTargetTypeRestApi();
this.targetTagRestApi = hawkbitClient.getTargetTagRestApi();
this.targetFilterQueryRestApi = hawkbitClient.getTargetFilterQueryRestApi();
}
private static class SimpleFilter implements Filter.Rsql {
private final HawkbitClient hawkbitClient;
private final TextField controllerId;
private final CheckboxGroup<MgmtTargetType> type;
private final CheckboxGroup<MgmtTag> tag;
private SimpleFilter(final HawkbitClient hawkbitClient) {
this.hawkbitClient = hawkbitClient;
controllerId = Utils.textField("Controller Id");
controllerId.setPlaceholder("<controller id filter>");
type = new CheckboxGroup<>("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, "name:asc", null).getBody().getContent());
if (!type.getValue().isEmpty()) {
components.add(type);
}
tag.setItems(hawkbitClient.getTargetTagRestApi().getTargetTags(0, 20, "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 HawkbitClient hawkbitClient;
private final TextField textFilter = new TextField("Raw Filter");
private final VerticalLayout layout = new VerticalLayout();
private RawFilter(final HawkbitClient hawkbitClient) {
this.hawkbitClient = 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("Save Filter") {{
setHeight("40%");
final Button finishBtn = Utils.tooltip(new Button("Save"), "Save (Enter)");
final TextField name = Utils.textField(
"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("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 TargetDetails() {
description.setMinLength(2);
Stream.of(
description,
createdBy, createdAt,
lastModifiedBy, lastModifiedAt)
.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());
createdBy.setValue(target.getCreatedBy());
createdAt.setValue(new Date(target.getCreatedAt()).toString());
lastModifiedBy.setValue(target.getLastModifiedBy());
lastModifiedAt.setValue(new Date(target.getLastModifiedAt()).toString());
}
}
private static class RegisterDialog extends Utils.BaseDialog {
private final Select<MgmtTargetType> type;
private final TextField controllerId;
private final TextField name;
private final TextArea description;
private RegisterDialog(final HawkbitClient hawkbitClient) {
super("Register Target");
final Button register = Utils.tooltip(new Button("Register"), "Register (Enter)");
type = new Select<>(
"Type",
e -> {},
hawkbitClient.getTargetTypeRestApi()
.getTargetTypes(0, 30, "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("Name");
name.setWidthFull();
description = new TextArea("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 HawkbitClient 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,155 @@
/**
* 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 com.eclipse.hawkbit.ui.view.util;
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;
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;
public class Filter extends Div {
private 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())).get();
} 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) {
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,94 @@
/**
* 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 com.eclipse.hawkbit.ui.view.util;
import com.google.common.collect.Streams;
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;
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;
// 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.stream().forEach(next -> selectedIds.add(entityRepresentation.idFn.apply(next)));
return Streams.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,64 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package com.eclipse.hawkbit.ui.view.util;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.data.provider.Query;
import java.util.concurrent.CompletionStage;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Stream;
public class TableView<T, ID> extends Div {
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);
Utils.addRemoveControls(addHandler, removeHandler, selectionGrid, false).ifPresent(layout::add);
add(layout);
}
}

View File

@@ -0,0 +1,180 @@
/**
* 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 com.eclipse.hawkbit.ui.view.util;
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;
import java.util.Collection;
import java.util.Iterator;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
import java.util.function.Function;
public class Utils {
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> Optional<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) {
return Optional.empty();
}
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 Optional.of(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 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,24 @@
#
# 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
#
server.port=${PORT:8088}
spring.cloud.openfeign.httpclient.hc5.enabled=true
### 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,com.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.

View File

@@ -111,8 +111,9 @@
<module>hawkbit-ddi-server</module>
<module>hawkbit-dmf-server</module>
<module>hawkbit-mgmt-server</module>
<module>hawkbit-vv8-ui</module>
<module>hawkbit-simple-ui</module>
<module>hawkbit-vv8-ui</module>
<module>hawkbit-update-server</module>
</modules>
</project>