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-ddi-server</module>
|
||||||
<module>hawkbit-dmf-server</module>
|
<module>hawkbit-dmf-server</module>
|
||||||
<module>hawkbit-mgmt-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>
|
<module>hawkbit-update-server</module>
|
||||||
</modules>
|
</modules>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
Reference in New Issue
Block a user