Initial draft of Simple UI (#1499)
A very simple UI based on Vaadin 24.
This commit is contained in:
1
hawkbit-runtime/hawkbit-simple-ui/.gitignore
vendored
Normal file
1
hawkbit-runtime/hawkbit-simple-ui/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
frontend
|
||||
156
hawkbit-runtime/hawkbit-simple-ui/pom.xml
Normal file
156
hawkbit-runtime/hawkbit-simple-ui/pom.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(""));
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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 ###
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user