Remove 'Simple' from Simple UI (#2809)
Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
10
hawkbit-ui/.gitignore
vendored
Normal file
10
hawkbit-ui/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
bundles
|
||||
src/main/frontend/*
|
||||
!src/main/frontend/themes/
|
||||
node_modules
|
||||
package.json
|
||||
package-lock.json
|
||||
tsconfig.json
|
||||
types.d.ts
|
||||
vite.config.ts
|
||||
vite.generated.ts
|
||||
129
hawkbit-ui/pom.xml
Normal file
129
hawkbit-ui/pom.xml
Normal file
@@ -0,0 +1,129 @@
|
||||
<!--
|
||||
|
||||
Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
|
||||
This program and the accompanying materials are made
|
||||
available under the terms of the Eclipse Public License 2.0
|
||||
which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
|
||||
SPDX-License-Identifier: EPL-2.0
|
||||
|
||||
-->
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hawkbit-ui</artifactId>
|
||||
<version>${revision}</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>hawkBit :: UI</name>
|
||||
|
||||
<properties>
|
||||
<vaadin.version>24.9.5</vaadin.version>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>Vaadin Repo</id>
|
||||
<url>https://maven.vaadin.com/vaadin-addons</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-bom</artifactId>
|
||||
<version>${vaadin.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Override vulnerable feign-form-spring dependency on commons-text -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-text</artifactId>
|
||||
<version>${commons-text.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-sdk-commons</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.hawkbit</groupId>
|
||||
<artifactId>hawkbit-mgmt-api</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<defaultGoal>spring-boot:run</defaultGoal>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
<finalName>hawkBit-UI</finalName>
|
||||
<mainClass>org.eclipse.hawkbit.ui.HawkbitUiApp</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-maven-plugin</artifactId>
|
||||
<version>${vaadin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>prepare-frontend</goal>
|
||||
<goal>build-frontend</goal>
|
||||
</goals>
|
||||
<phase>compile</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<forceProductionBuild>true</forceProductionBuild>
|
||||
<frontendHotdeploy>false</frontendHotdeploy>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
7
hawkbit-ui/src/main/frontend/themes/hawkbit/styles.css
Normal file
7
hawkbit-ui/src/main/frontend/themes/hawkbit/styles.css
Normal file
@@ -0,0 +1,7 @@
|
||||
a.nocolor:link {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a.nocolor:visited {
|
||||
color: inherit;
|
||||
}
|
||||
3
hawkbit-ui/src/main/frontend/themes/hawkbit/theme.json
Normal file
3
hawkbit-ui/src/main/frontend/themes/hawkbit/theme.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"lumoImports": ["badge"]
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import feign.FeignException;
|
||||
import lombok.Getter;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtActionRestApi;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetRestApi;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetTagRestApi;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetTypeRestApi;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRolloutRestApi;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftwareModuleRestApi;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftwareModuleTypeRestApi;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetFilterQueryRestApi;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetRestApi;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetTagRestApi;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetTypeRestApi;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTenantManagementRestApi;
|
||||
import org.eclipse.hawkbit.sdk.HawkbitClient;
|
||||
import org.eclipse.hawkbit.sdk.Tenant;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
@Getter
|
||||
public class HawkbitMgmtClient {
|
||||
|
||||
private final Tenant tenant;
|
||||
private final HawkbitClient hawkbitClient;
|
||||
|
||||
private final MgmtSoftwareModuleRestApi softwareModuleRestApi;
|
||||
private final MgmtSoftwareModuleTypeRestApi softwareModuleTypeRestApi;
|
||||
private final MgmtDistributionSetRestApi distributionSetRestApi;
|
||||
private final MgmtDistributionSetTypeRestApi distributionSetTypeRestApi;
|
||||
private final MgmtDistributionSetTagRestApi distributionSetTagRestApi;
|
||||
private final MgmtTargetRestApi targetRestApi;
|
||||
private final MgmtTargetTypeRestApi targetTypeRestApi;
|
||||
private final MgmtTargetTagRestApi targetTagRestApi;
|
||||
private final MgmtTargetFilterQueryRestApi targetFilterQueryRestApi;
|
||||
private final MgmtRolloutRestApi rolloutRestApi;
|
||||
private final MgmtTenantManagementRestApi tenantManagementRestApi;
|
||||
private final MgmtActionRestApi actionRestApi;
|
||||
|
||||
public HawkbitMgmtClient(final Tenant tenant, final HawkbitClient hawkbitClient) {
|
||||
this.tenant = tenant;
|
||||
this.hawkbitClient = hawkbitClient;
|
||||
|
||||
softwareModuleRestApi = service(MgmtSoftwareModuleRestApi.class);
|
||||
softwareModuleTypeRestApi = service(MgmtSoftwareModuleTypeRestApi.class);
|
||||
distributionSetRestApi = service(MgmtDistributionSetRestApi.class);
|
||||
distributionSetTypeRestApi = service(MgmtDistributionSetTypeRestApi.class);
|
||||
distributionSetTagRestApi = service(MgmtDistributionSetTagRestApi.class);
|
||||
targetRestApi = service(MgmtTargetRestApi.class);
|
||||
targetTypeRestApi = service(MgmtTargetTypeRestApi.class);
|
||||
targetTagRestApi = service(MgmtTargetTagRestApi.class);
|
||||
targetFilterQueryRestApi = service(MgmtTargetFilterQueryRestApi.class);
|
||||
rolloutRestApi = service(MgmtRolloutRestApi.class);
|
||||
tenantManagementRestApi = service(MgmtTenantManagementRestApi.class);
|
||||
actionRestApi = service(MgmtActionRestApi.class);
|
||||
}
|
||||
|
||||
public boolean hasSoftwareModulesRead() {
|
||||
return hasRead(() -> softwareModuleRestApi.getSoftwareModule(-1L));
|
||||
}
|
||||
|
||||
public boolean hasRolloutRead() {
|
||||
return hasRead(() -> rolloutRestApi.getRollout(-1L));
|
||||
}
|
||||
|
||||
public boolean hasDistributionSetRead() {
|
||||
return hasRead(() -> distributionSetRestApi.getDistributionSet(-1L));
|
||||
}
|
||||
|
||||
public boolean hasTargetRead() {
|
||||
return hasRead(() -> targetRestApi.getTarget("_#ETE$ER"));
|
||||
}
|
||||
|
||||
public boolean hasConfigRead() {
|
||||
return hasRead(() -> tenantManagementRestApi.getTenantConfigurationValue("_#ETE$ER"));
|
||||
}
|
||||
|
||||
private boolean hasRead(final Supplier<ResponseEntity<?>> doCall) {
|
||||
try {
|
||||
final int statusCode = doCall.get().getStatusCode().value();
|
||||
return statusCode != 401 && statusCode != 403;
|
||||
} catch (final FeignException e) {
|
||||
return !(e instanceof FeignException.Unauthorized) && !(e instanceof FeignException.Forbidden);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T service(final Class<T> serviceType) {
|
||||
return hawkbitClient.mgmtService(serviceType, tenant);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui;
|
||||
|
||||
import com.vaadin.flow.component.page.AppShellConfigurator;
|
||||
import com.vaadin.flow.server.PWA;
|
||||
import com.vaadin.flow.theme.Theme;
|
||||
import feign.Contract;
|
||||
import feign.RequestInterceptor;
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import feign.codec.ErrorDecoder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.sdk.HawkbitClient;
|
||||
import org.eclipse.hawkbit.sdk.HawkbitServer;
|
||||
import org.eclipse.hawkbit.sdk.Tenant;
|
||||
import org.eclipse.hawkbit.ui.view.util.Utils;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
|
||||
import static feign.Util.ISO_8859_1;
|
||||
|
||||
@Slf4j
|
||||
@Theme("hawkbit")
|
||||
@PWA(name = "hawkBit UI", shortName = "hawkBit UI")
|
||||
@EnableCaching
|
||||
@EnableScheduling
|
||||
@SpringBootApplication
|
||||
@Import(FeignClientsConfiguration.class)
|
||||
public class HawkbitUiApp implements AppShellConfigurator {
|
||||
|
||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
private static final RequestInterceptor AUTHORIZATION = requestTemplate -> {
|
||||
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication.getPrincipal() instanceof OidcUser oidcUser) {
|
||||
requestTemplate.header(AUTHORIZATION_HEADER, "Bearer " + oidcUser.getIdToken().getTokenValue());
|
||||
} else {
|
||||
requestTemplate.header(
|
||||
AUTHORIZATION_HEADER, "Basic " + Base64.getEncoder().encodeToString(
|
||||
(Objects.requireNonNull(authentication.getPrincipal(), "User is null!") + ":" + Objects.requireNonNull(
|
||||
authentication.getCredentials(), "Password is not available!")).getBytes(ISO_8859_1))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private static final ErrorDecoder DEFAULT_ERROR_DECODER = new ErrorDecoder.Default();
|
||||
private static final ErrorDecoder ERROR_DECODER = (methodKey, response) -> {
|
||||
final Exception e = DEFAULT_ERROR_DECODER.decode(methodKey, response);
|
||||
Utils.errorNotification(e);
|
||||
return e;
|
||||
};
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(HawkbitUiApp.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
HawkbitClient hawkbitClient(
|
||||
final HawkbitServer hawkBitServer,
|
||||
final Encoder encoder,
|
||||
final Decoder decoder,
|
||||
final Contract contract
|
||||
) {
|
||||
return new HawkbitClient(
|
||||
hawkBitServer, encoder, decoder, contract,
|
||||
ERROR_DECODER,
|
||||
(tenant, controller) -> controller == null
|
||||
? AUTHORIZATION
|
||||
: HawkbitClient.DEFAULT_REQUEST_INTERCEPTOR_FN.apply(tenant, controller)
|
||||
);
|
||||
}
|
||||
|
||||
@Bean
|
||||
HawkbitMgmtClient hawkbitMgmtClient(final Tenant tenant, final HawkbitClient hawkbitClient) {
|
||||
return new HawkbitMgmtClient(tenant, hawkbitClient);
|
||||
}
|
||||
|
||||
// accepts all user / pass, just delegating them to the feign client
|
||||
@Bean
|
||||
AuthenticationManager authenticationManager(final HawkbitMgmtClient hawkbitClient, final HawkbitServer server) {
|
||||
return authentication -> {
|
||||
final String username = authentication.getName();
|
||||
final String password = authentication.getCredentials().toString();
|
||||
|
||||
// make simple check in order not to be logged in as not real user.
|
||||
if (!isAuthenticated(username, password, server.getMgmtUrl())) {
|
||||
throw new BadCredentialsException("Incorrect username or password!");
|
||||
}
|
||||
|
||||
return new UsernamePasswordAuthenticationToken(username, password, Collections.emptyList()) {
|
||||
|
||||
@Override
|
||||
public void eraseCredentials() {
|
||||
// don't erase credentials because they will be used
|
||||
// to authenticate to the hawkBit update server / mgmt server
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isAuthenticated(String username, String password, String mgmtUrl) {
|
||||
try {
|
||||
final URL url = new URL(mgmtUrl + "/rest/v1/rollouts");
|
||||
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
|
||||
final String auth = username + ":" + password;
|
||||
final String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes());
|
||||
conn.setRequestProperty(AUTHORIZATION_HEADER, "Basic " + encodedAuth);
|
||||
|
||||
return conn.getResponseCode() != 401;
|
||||
} catch (final Exception ex) {
|
||||
log.error("Failed to authenticate user {} .Reason : ", username, ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
165
hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/MainLayout.java
Normal file
165
hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/MainLayout.java
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.UI;
|
||||
import com.vaadin.flow.component.Unit;
|
||||
import com.vaadin.flow.component.applayout.AppLayout;
|
||||
import com.vaadin.flow.component.applayout.DrawerToggle;
|
||||
import com.vaadin.flow.component.avatar.Avatar;
|
||||
import com.vaadin.flow.component.contextmenu.MenuItem;
|
||||
import com.vaadin.flow.component.html.Anchor;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.Footer;
|
||||
import com.vaadin.flow.component.html.H1;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.html.Header;
|
||||
import com.vaadin.flow.component.html.Image;
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.menubar.MenuBar;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.Scroller;
|
||||
import com.vaadin.flow.component.sidenav.SideNav;
|
||||
import com.vaadin.flow.component.sidenav.SideNavItem;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.server.auth.AccessAnnotationChecker;
|
||||
import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||
import org.eclipse.hawkbit.ui.security.AuthenticatedUser;
|
||||
import org.eclipse.hawkbit.ui.view.AboutView;
|
||||
import org.eclipse.hawkbit.ui.view.ConfigView;
|
||||
import org.eclipse.hawkbit.ui.view.DistributionSetView;
|
||||
import org.eclipse.hawkbit.ui.view.RolloutView;
|
||||
import org.eclipse.hawkbit.ui.view.SoftwareModuleView;
|
||||
import org.eclipse.hawkbit.ui.view.TargetView;
|
||||
|
||||
/**
|
||||
* The main view is a top-level placeholder for other views.
|
||||
*/
|
||||
public class MainLayout extends AppLayout {
|
||||
|
||||
static final List<Class<? extends Component>> DEFAULT_VIEW_PRIORITY = List.of(TargetView.class, DistributionSetView.class,
|
||||
SoftwareModuleView.class, RolloutView.class);
|
||||
private final transient AuthenticatedUser authenticatedUser;
|
||||
private final AccessAnnotationChecker accessChecker;
|
||||
private H2 viewTitle;
|
||||
private transient Optional<Class<? extends Component>> defaultView;
|
||||
|
||||
public MainLayout(final AuthenticatedUser authenticatedUser, final AccessAnnotationChecker accessChecker) {
|
||||
this.authenticatedUser = authenticatedUser;
|
||||
this.accessChecker = accessChecker;
|
||||
|
||||
setPrimarySection(Section.DRAWER);
|
||||
addDrawerContent();
|
||||
setDrawerOpened(true);
|
||||
addHeaderContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterNavigation() {
|
||||
super.afterNavigation();
|
||||
viewTitle.setText(
|
||||
Optional.ofNullable(getContent())
|
||||
.map(c -> c.getClass().getAnnotation(PageTitle.class))
|
||||
.map(PageTitle::value)
|
||||
.orElse(""));
|
||||
if (UI.getCurrent().getActiveViewLocation().getPath().isEmpty()) {
|
||||
defaultView.ifPresent(c -> UI.getCurrent().navigate(c));
|
||||
}
|
||||
}
|
||||
|
||||
private void addHeaderContent() {
|
||||
final DrawerToggle toggle = new DrawerToggle();
|
||||
toggle.setAriaLabel("Menu toggle");
|
||||
|
||||
viewTitle = new H2();
|
||||
viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE);
|
||||
|
||||
addToNavbar(false, toggle, viewTitle);
|
||||
}
|
||||
|
||||
private void addDrawerContent() {
|
||||
final H1 appName = new H1("hawkBit UI");
|
||||
final HorizontalLayout layout = new HorizontalLayout();
|
||||
layout.setPadding(true);
|
||||
layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
|
||||
final Image icon = new Image("images/header_icon.png", "hawkBit icon");
|
||||
icon.setMaxHeight(24, Unit.PIXELS);
|
||||
icon.setMaxWidth(24, Unit.PIXELS);
|
||||
appName.addClassNames(LumoUtility.AlignItems.BASELINE, LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE);
|
||||
layout.add(icon, appName);
|
||||
final Header header = new Header(layout);
|
||||
|
||||
final Scroller scroller = new Scroller(createNavigation());
|
||||
|
||||
addToDrawer(header, scroller, createFooter());
|
||||
}
|
||||
|
||||
private SideNav createNavigation() {
|
||||
final SideNav nav = new SideNav();
|
||||
if (accessChecker.hasAccess(TargetView.class)) {
|
||||
nav.addItem(new SideNavItem("Targets", TargetView.class, VaadinIcon.FILTER.create()));
|
||||
}
|
||||
if (accessChecker.hasAccess(RolloutView.class)) {
|
||||
nav.addItem(new SideNavItem("Rollouts", RolloutView.class, VaadinIcon.COGS.create()));
|
||||
}
|
||||
if (accessChecker.hasAccess(DistributionSetView.class)) {
|
||||
nav.addItem(new SideNavItem("Distribution Sets", DistributionSetView.class, VaadinIcon.FILE_TREE.create()));
|
||||
}
|
||||
if (accessChecker.hasAccess(SoftwareModuleView.class)) {
|
||||
nav.addItem(new SideNavItem("Software Modules", SoftwareModuleView.class, VaadinIcon.FILE.create()));
|
||||
}
|
||||
if (accessChecker.hasAccess(ConfigView.class)) {
|
||||
nav.addItem(new SideNavItem("Config", ConfigView.class, VaadinIcon.COG.create()));
|
||||
}
|
||||
if (accessChecker.hasAccess(AboutView.class)) {
|
||||
nav.addItem(new SideNavItem("About", AboutView.class, VaadinIcon.INFO_CIRCLE.create()));
|
||||
}
|
||||
defaultView = DEFAULT_VIEW_PRIORITY.stream().filter(accessChecker::hasAccess).findFirst();
|
||||
return nav;
|
||||
}
|
||||
|
||||
private Footer createFooter() {
|
||||
final Footer layout = new Footer();
|
||||
|
||||
final Optional<String> maybeUser = authenticatedUser.getName();
|
||||
if (maybeUser.isPresent()) {
|
||||
final String user = maybeUser.get();
|
||||
|
||||
final Avatar avatar = new Avatar(user);
|
||||
|
||||
final MenuBar userMenu = new MenuBar();
|
||||
userMenu.setThemeName("tertiary-inline contrast");
|
||||
|
||||
final MenuItem userName = userMenu.addItem("");
|
||||
final Div div = new Div();
|
||||
div.add(avatar);
|
||||
div.add(user);
|
||||
div.add(new Icon("lumo", "dropdown"));
|
||||
div.getElement().getStyle().set("display", "flex");
|
||||
div.getElement().getStyle().set("align-items", "center");
|
||||
div.getElement().getStyle().set("gap", "var(--lumo-space-s)");
|
||||
userName.add(div);
|
||||
userName.getSubMenu().addItem("Sign out", e -> authenticatedUser.logout());
|
||||
|
||||
layout.add(userMenu);
|
||||
} else {
|
||||
final Anchor loginLink = new Anchor("login", "Sign in");
|
||||
layout.add(loginLink);
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.eclipse.hawkbit.ui;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import com.vaadin.flow.i18n.DefaultI18NProvider;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SimpleI18NProvider extends DefaultI18NProvider {
|
||||
|
||||
SimpleI18NProvider() {
|
||||
super(Arrays.stream(Locale.getAvailableLocales()).toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.eclipse.hawkbit.ui;
|
||||
|
||||
import com.vaadin.flow.server.ServiceInitEvent;
|
||||
import com.vaadin.flow.server.VaadinServiceInitListener;
|
||||
import com.vaadin.flow.spring.annotation.SpringComponent;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@SpringComponent
|
||||
public class VaadinServiceInit implements VaadinServiceInitListener {
|
||||
|
||||
@Override
|
||||
public void serviceInit(ServiceInitEvent event) {
|
||||
// cache zoneId of client as soon as possible
|
||||
event.getSource().addUIInitListener(uiEvent ->
|
||||
uiEvent.getUI().getPage().retrieveExtendedClientDetails(details -> {}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
|
||||
package org.eclipse.hawkbit.ui.component;
|
||||
|
||||
import static org.eclipse.hawkbit.ui.view.Constants.STATUS;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.vaadin.flow.component.AttachEvent;
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.Unit;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.confirmdialog.ConfirmDialog;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.data.renderer.ComponentRenderer;
|
||||
import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
|
||||
import org.eclipse.hawkbit.ui.HawkbitMgmtClient;
|
||||
import org.eclipse.hawkbit.ui.view.TargetView;
|
||||
import org.eclipse.hawkbit.ui.view.util.Utils;
|
||||
|
||||
@Slf4j
|
||||
public class TargetActionsHistory extends Grid<TargetActionsHistory.ActionStatusEntry> {
|
||||
|
||||
private final transient HawkbitMgmtClient hawkbitClient;
|
||||
private transient MgmtTarget target;
|
||||
private final TargetView.TargetActionsHistoryLayout.ActionStepsGrid actionStepsGrid;
|
||||
|
||||
public TargetActionsHistory(final HawkbitMgmtClient hawkbitClient, TargetView.TargetActionsHistoryLayout.ActionStepsGrid actionStepsGrid) {
|
||||
this.hawkbitClient = hawkbitClient;
|
||||
setWidthFull();
|
||||
addColumn(new ComponentRenderer<>(ActionStatusEntry::getStatusIcon)).setHeader(STATUS).setAutoWidth(true).setFlexGrow(0);
|
||||
addColumn(ActionStatusEntry::getDistributionSetName).setHeader("Distribution Set").setAutoWidth(true);
|
||||
addColumn(Utils.localDateTimeRenderer(ActionStatusEntry::getLastModifiedAt))
|
||||
.setHeader("Last Modified")
|
||||
.setAutoWidth(true)
|
||||
.setFlexGrow(0)
|
||||
.setComparator(ActionStatusEntry::getLastModifiedAt);
|
||||
|
||||
addColumn(new ComponentRenderer<>(ActionStatusEntry::getForceTypeIcon)).setHeader("Type").setAutoWidth(true).setFlexGrow(0);
|
||||
addColumn(new ComponentRenderer<>(ActionStatusEntry::getActionsLayout)).setHeader("Actions").setAutoWidth(true).setFlexGrow(0);
|
||||
addColumn(new ComponentRenderer<>(ActionStatusEntry::getForceQuitLayout)).setHeader("Force Quit").setAutoWidth(true)
|
||||
.setFlexGrow(0);
|
||||
addItemClickListener(e -> actionStepsGrid.setActionId(e.getItem().action.getId()));
|
||||
this.actionStepsGrid = actionStepsGrid;
|
||||
}
|
||||
|
||||
public void setItem(final MgmtTarget target) {
|
||||
this.target = target;
|
||||
this.actionStepsGrid.setTarget(target);
|
||||
}
|
||||
|
||||
private List<ActionStatusEntry> fetchActions() {
|
||||
return hawkbitClient.getTargetRestApi().getActionHistory(target.getControllerId(), null, 0, 30, null)
|
||||
.getBody()
|
||||
.getContent()
|
||||
.stream()
|
||||
.map(action -> new ActionStatusEntry(action, () -> setItems(fetchActions())))
|
||||
.filter(value -> value.action != null)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttach(AttachEvent attachEvent) {
|
||||
List<ActionStatusEntry> actionStatusEntries = fetchActions();
|
||||
setItems(actionStatusEntries);
|
||||
actionStatusEntries.stream().findFirst().ifPresentOrElse(e -> {
|
||||
// select first action in the list by default
|
||||
asSingleSelect().setValue(e);
|
||||
actionStepsGrid.setActionId(e.action.getId());
|
||||
}, () -> actionStepsGrid.setActionId(null));
|
||||
}
|
||||
|
||||
protected class ActionStatusEntry {
|
||||
|
||||
final MgmtAction action;
|
||||
final Runnable onUpdate;
|
||||
MgmtDistributionSet distributionSet;
|
||||
|
||||
public ActionStatusEntry(final MgmtAction mgmtAction, final Runnable onUpdate) {
|
||||
this.action = hawkbitClient.getActionRestApi().getAction(mgmtAction.getId()).getBody();
|
||||
this.onUpdate = onUpdate;
|
||||
if (action == null) {
|
||||
log.error("Unable to fetch the action with id : {}", mgmtAction.getId());
|
||||
return;
|
||||
}
|
||||
this.action.getLink("distributionset").ifPresent(link -> {
|
||||
try {
|
||||
Long dsId = Long.parseLong(link.getHref().substring(link.getHref().lastIndexOf("/") + 1));
|
||||
this.distributionSet = hawkbitClient.getDistributionSetRestApi().getDistributionSet(dsId).getBody();
|
||||
} catch (NumberFormatException e) {
|
||||
log.error("Error parsing distribution set ID", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isActive() {
|
||||
return action.isActive();
|
||||
}
|
||||
|
||||
private boolean isCancelingOrCanceled() {
|
||||
return action.getType().equals(MgmtAction.ACTION_CANCEL);
|
||||
}
|
||||
|
||||
public Component getStatusIcon() {
|
||||
final HorizontalLayout layout = new HorizontalLayout();
|
||||
final Icon icon;
|
||||
if (isActive()) {
|
||||
if (isCancelingOrCanceled()) {
|
||||
icon = Utils.tooltip(VaadinIcon.ADJUST.create(), "Pending Cancellation");
|
||||
icon.setColor("red");
|
||||
} else {
|
||||
icon = Utils.tooltip(VaadinIcon.ADJUST.create(), "Pending Update");
|
||||
icon.setColor("orange");
|
||||
}// todo getDetailStatus should return an enum from src/main/java/org/eclipse/hawkbit/repository/model/Action.java
|
||||
} else if (action.getType().equals(MgmtAction.ACTION_UPDATE) && action.getStatus().equals("finished")) {
|
||||
icon = Utils.tooltip(VaadinIcon.CHECK_CIRCLE.create(), "Updated");
|
||||
icon.setColor("green");
|
||||
} else {
|
||||
icon = Utils.tooltip(VaadinIcon.CLOSE_CIRCLE.create(), "Canceled");
|
||||
icon.setColor("red");
|
||||
}
|
||||
|
||||
icon.addClassNames(LumoUtility.IconSize.SMALL);
|
||||
layout.add(icon);
|
||||
layout.setWidth(50, Unit.PIXELS);
|
||||
layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
|
||||
return layout;
|
||||
}
|
||||
|
||||
public String getDistributionSetName() {
|
||||
return Optional.ofNullable(distributionSet).map(d -> d.getName() + ":" + d.getVersion()).orElse(
|
||||
"Distribution Set not found");
|
||||
}
|
||||
|
||||
public Long getLastModifiedAt() {
|
||||
return action.getLastModifiedAt();
|
||||
}
|
||||
|
||||
public Icon getForceTypeIcon() {
|
||||
Icon icon = switch (action.getForceType()) {
|
||||
case FORCED -> VaadinIcon.BOLT.create();
|
||||
case TIMEFORCED -> VaadinIcon.USER_CLOCK.create();
|
||||
case SOFT -> VaadinIcon.USER_CHECK.create();
|
||||
case DOWNLOAD_ONLY -> VaadinIcon.DOWNLOAD.create();
|
||||
};
|
||||
return Utils.tooltip(icon, action.getForceType().getName());
|
||||
}
|
||||
|
||||
public HorizontalLayout getActionsLayout() {
|
||||
final HorizontalLayout actionsLayout = new HorizontalLayout();
|
||||
actionsLayout.setSpacing(true);
|
||||
|
||||
final Button cancelButton = Utils.tooltip(new Button(VaadinIcon.CLOSE.create()), "Cancel Action");
|
||||
if (isActive() && !isCancelingOrCanceled()) {
|
||||
cancelButton.addClickListener(e -> {
|
||||
String message = "Are you sure you want to cancel the action ?";
|
||||
promptForConfirmAction(
|
||||
message, onUpdate,
|
||||
() -> hawkbitClient.getTargetRestApi().cancelAction(target.getControllerId(), action.getId(), false))
|
||||
.open();
|
||||
});
|
||||
} else {
|
||||
cancelButton.setEnabled(false);
|
||||
}
|
||||
|
||||
final Button forceButton = Utils.tooltip(new Button(VaadinIcon.BOLT.create()), "Force Action");
|
||||
if (isActive() && !isCancelingOrCanceled() && action.getForceType() != MgmtActionType.FORCED) {
|
||||
forceButton.addClickListener(e -> {
|
||||
String message = "Are you sure you want to force the action ?";
|
||||
promptForConfirmAction(
|
||||
message, onUpdate, () -> {
|
||||
MgmtActionRequestBodyPut setForced = new MgmtActionRequestBodyPut();
|
||||
setForced.setForceType(MgmtActionType.FORCED);
|
||||
hawkbitClient.getTargetRestApi()
|
||||
.updateAction(target.getControllerId(), action.getId(), setForced);
|
||||
}
|
||||
).open();
|
||||
});
|
||||
} else {
|
||||
forceButton.setEnabled(false);
|
||||
}
|
||||
|
||||
actionsLayout.add(cancelButton, forceButton);
|
||||
return actionsLayout;
|
||||
}
|
||||
|
||||
public HorizontalLayout getForceQuitLayout() {
|
||||
final HorizontalLayout forceQuitLayout = new HorizontalLayout();
|
||||
forceQuitLayout.setSpacing(true);
|
||||
forceQuitLayout.setPadding(true);
|
||||
forceQuitLayout.setWidthFull();
|
||||
forceQuitLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
|
||||
|
||||
final Button forceQuitButton = Utils.tooltip(new Button(VaadinIcon.CLOSE.create()), "Force Cancel");
|
||||
forceQuitButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY_INLINE);
|
||||
|
||||
if (isActive() && isCancelingOrCanceled()) {
|
||||
forceQuitButton.addClickListener(e -> {
|
||||
String message = "Are you sure you want to force cancel the action ?";
|
||||
promptForConfirmAction(
|
||||
message, onUpdate,
|
||||
() -> hawkbitClient.getTargetRestApi().cancelAction(target.getControllerId(), action.getId(), true)).open();
|
||||
});
|
||||
} else {
|
||||
forceQuitButton.setEnabled(false);
|
||||
}
|
||||
|
||||
forceQuitLayout.add(forceQuitButton);
|
||||
return forceQuitLayout;
|
||||
}
|
||||
|
||||
private static ConfirmDialog promptForConfirmAction(String message, Runnable refreshActions, Runnable actionConsumer) {
|
||||
final ConfirmDialog dialog = new ConfirmDialog();
|
||||
dialog.setHeader("Confirm Action");
|
||||
dialog.setText(message);
|
||||
|
||||
dialog.setCancelable(true);
|
||||
dialog.addCancelListener(event -> dialog.close());
|
||||
|
||||
dialog.setConfirmButtonTheme(ButtonVariant.LUMO_ERROR.getVariantName());
|
||||
dialog.setConfirmText("Confirm");
|
||||
dialog.addConfirmListener(event -> {
|
||||
actionConsumer.run();
|
||||
refreshActions.run();
|
||||
dialog.close();
|
||||
});
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.security;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.vaadin.flow.spring.security.AuthenticationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AuthenticatedUser {
|
||||
|
||||
private final AuthenticationContext authenticationContext;
|
||||
private final GrantedAuthoritiesService grantedAuthoritiesService;
|
||||
|
||||
public AuthenticatedUser(final AuthenticationContext authenticationContext, GrantedAuthoritiesService grantedAuthoritiesService) {
|
||||
this.authenticationContext = authenticationContext;
|
||||
this.grantedAuthoritiesService = grantedAuthoritiesService;
|
||||
}
|
||||
|
||||
public Optional<String> getName() {
|
||||
return authenticationContext.getPrincipalName();
|
||||
}
|
||||
|
||||
public void logout() {
|
||||
this.getName().ifPresent(grantedAuthoritiesService::evictUserFromCache);
|
||||
authenticationContext.logout();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.security;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.ui.HawkbitMgmtClient;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class GrantedAuthoritiesService {
|
||||
|
||||
public static final String USER_CACHE = "userDetails";
|
||||
private HawkbitMgmtClient hawkbitClient;
|
||||
|
||||
@Cacheable(cacheNames = "userDetails", key = "#authentication.getName()")
|
||||
public List<SimpleGrantedAuthority> getGrantedAuthorities(Authentication authentication) {
|
||||
final List<String> roles = new LinkedList<>();
|
||||
roles.add("ANONYMOUS");
|
||||
if (hawkbitClient.hasSoftwareModulesRead()) {
|
||||
roles.add("SOFTWARE_MODULE_READ");
|
||||
}
|
||||
if (hawkbitClient.hasRolloutRead()) {
|
||||
roles.add("ROLLOUT_READ");
|
||||
}
|
||||
if (hawkbitClient.hasDistributionSetRead()) {
|
||||
roles.add("DISTRIBUTION_SET_READ");
|
||||
}
|
||||
if (hawkbitClient.hasTargetRead()) {
|
||||
roles.add("TARGET_READ");
|
||||
}
|
||||
if (hawkbitClient.hasConfigRead()) {
|
||||
roles.add("CONFIG_READ");
|
||||
}
|
||||
return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList();
|
||||
}
|
||||
|
||||
@Scheduled(fixedRateString = "${caching.spring.userDetailsTTL:1h}")
|
||||
@CacheEvict(cacheNames = USER_CACHE, allEntries = true)
|
||||
public void emptyCache() {
|
||||
log.debug("emptying userDetails cache");
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = "userDetails")
|
||||
public void evictUserFromCache(String principalName) {
|
||||
log.debug("remove user from {} cache", principalName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.security;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class Oauth2ClientConfig {
|
||||
@Bean(name = "hawkbitOAuth2ClientCustomizer")
|
||||
@ConditionalOnProperty(prefix = "hawkbit.server.security.oauth2.client", name = "enabled")
|
||||
@ConditionalOnMissingBean(name = "hawkbitOAuth2ClientCustomizer")
|
||||
Customizer<OAuth2LoginConfigurer<HttpSecurity>> defaultOAuth2ClientCustomizer() {
|
||||
return Customizer.withDefaults();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.security;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@Data
|
||||
@ToString
|
||||
@ConfigurationProperties("hawkbit.server.security")
|
||||
public class OidcClientProperties {
|
||||
|
||||
private final OidcClientProperties.Oauth2 oauth2 = new OidcClientProperties.Oauth2();
|
||||
|
||||
@Data
|
||||
public static class Oauth2 {
|
||||
|
||||
private final OidcClientProperties.Oauth2.Client client = new OidcClientProperties.Oauth2.Client();
|
||||
|
||||
@Data
|
||||
public static class Client {
|
||||
|
||||
private boolean enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.security;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import com.vaadin.flow.spring.security.VaadinAwareSecurityContextHolderStrategyConfiguration;
|
||||
import com.vaadin.flow.spring.security.VaadinSecurityConfigurer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.ui.view.LoginView;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(OidcClientProperties.class)
|
||||
@Slf4j
|
||||
@Import(VaadinAwareSecurityContextHolderStrategyConfiguration.class)
|
||||
public class SecurityConfiguration {
|
||||
|
||||
private Customizer<OAuth2LoginConfigurer<HttpSecurity>> oAuth2LoginConfigurerCustomizer;
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setOAuth2LoginConfigurerCustomizer(
|
||||
@Qualifier("hawkbitOAuth2ClientCustomizer") final Customizer<OAuth2LoginConfigurer<HttpSecurity>> oauth2LoginConfigurerCustomizer) {
|
||||
this.oAuth2LoginConfigurerCustomizer = oauth2LoginConfigurerCustomizer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationFailureHandler customFailureHandler() {
|
||||
// Redirect back to login with your message
|
||||
return (request, response, exception) -> response.sendRedirect("/login?error=" + URLEncoder.encode(exception.getMessage(),
|
||||
StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain securityFilterChain(
|
||||
final HttpSecurity http,
|
||||
final UserDetailsSetter userDetailsSetter,
|
||||
@Autowired(required = false) InMemoryClientRegistrationRepository clientRegistrationRepository) throws Exception {
|
||||
http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/images/*.png").permitAll());
|
||||
http.addFilterAfter(userDetailsSetter, AuthorizationFilter.class);
|
||||
return http.with(VaadinSecurityConfigurer.vaadin(), configurer -> {
|
||||
if (oAuth2LoginConfigurerCustomizer == null) {
|
||||
configurer.loginView(LoginView.class);
|
||||
|
||||
} else {
|
||||
var defaultClientRegistration = clientRegistrationRepository.iterator().next();
|
||||
configurer.oauth2LoginPage(
|
||||
"/oauth2/authorization/" + defaultClientRegistration.getRegistrationId(),
|
||||
"{baseUrl}/"
|
||||
);
|
||||
}
|
||||
}).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.security.authentication.AccountExpiredException;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@Slf4j
|
||||
class UserDetailsSetter extends OncePerRequestFilter {
|
||||
|
||||
private final SecurityContextHolderStrategy securityContextHolderStrategy;
|
||||
private final GrantedAuthoritiesService grantedAuthoritiesService;
|
||||
|
||||
@SuppressWarnings("java:S1066") // java:S1066 - readability preferred
|
||||
@Override
|
||||
protected void doFilterInternal(@NotNull final HttpServletRequest request, @NotNull final HttpServletResponse response,
|
||||
@NotNull final FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
Authentication authentication = securityContextHolderStrategy.getContext().getAuthentication();
|
||||
Authentication newAuthentication;
|
||||
|
||||
if (!(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated()) {
|
||||
Collection<? extends GrantedAuthority> grantedAuthorities = grantedAuthoritiesService.getGrantedAuthorities(authentication);
|
||||
|
||||
if (authentication instanceof OAuth2AuthenticationToken oAuth2AuthenticationToken) {
|
||||
newAuthentication = new OAuth2AuthenticationToken(oAuth2AuthenticationToken.getPrincipal(), grantedAuthorities,
|
||||
oAuth2AuthenticationToken.getAuthorizedClientRegistrationId());
|
||||
if (authentication.getPrincipal() instanceof OidcUser user) {
|
||||
// if there is no refresh token and the access token is expired then relogin is required
|
||||
if (user.getIdToken().getExpiresAt() != null && Instant.now().isAfter(user.getIdToken().getExpiresAt())) {
|
||||
throw new AccountExpiredException("Token expired");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newAuthentication = new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(),
|
||||
grantedAuthorities);
|
||||
}
|
||||
|
||||
securityContextHolderStrategy.getContext().setAuthentication(newAuthentication);
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.view;
|
||||
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.html.Image;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.router.RouteAlias;
|
||||
import com.vaadin.flow.theme.lumo.LumoUtility.Margin;
|
||||
import org.eclipse.hawkbit.ui.MainLayout;
|
||||
|
||||
@PageTitle("About")
|
||||
@Route(value = "about", layout = MainLayout.class)
|
||||
@RouteAlias(value = "", layout = MainLayout.class)
|
||||
@RolesAllowed({ "ANONYMOUS" })
|
||||
public class AboutView extends VerticalLayout {
|
||||
|
||||
public AboutView() {
|
||||
setSpacing(false);
|
||||
|
||||
final Image img = new Image("images/about_image.png", "hawkBit");
|
||||
img.setWidth("200px");
|
||||
add(img);
|
||||
|
||||
final H2 header = new H2("Eclipse hawkBit UI");
|
||||
header.addClassNames(Margin.Top.XLARGE, Margin.Bottom.MEDIUM);
|
||||
|
||||
setSizeFull();
|
||||
setJustifyContentMode(JustifyContentMode.CENTER);
|
||||
setDefaultHorizontalComponentAlignment(Alignment.CENTER);
|
||||
getStyle().set("text-align", "center");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.view;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
|
||||
import com.vaadin.flow.component.Key;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.textfield.NumberField;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.system.MgmtSystemTenantConfigurationValueRequest;
|
||||
import org.eclipse.hawkbit.ui.HawkbitMgmtClient;
|
||||
import org.eclipse.hawkbit.ui.MainLayout;
|
||||
|
||||
@PageTitle("Config")
|
||||
@Route(value = "config", layout = MainLayout.class)
|
||||
@RolesAllowed({ "CONFIG_READ" })
|
||||
@Slf4j
|
||||
public class ConfigView extends VerticalLayout {
|
||||
|
||||
private static final String WIDTH = "width";
|
||||
private static final String PX_300 = "300px";
|
||||
|
||||
private final Map<String, MgmtSystemTenantConfigurationValueRequest> configValue = new HashMap<>();
|
||||
|
||||
public ConfigView(final HawkbitMgmtClient hawkbitClient) {
|
||||
setSpacing(false);
|
||||
final Button saveButton = new Button("Save");
|
||||
Optional.ofNullable(
|
||||
hawkbitClient.getTenantManagementRestApi().getTenantConfiguration().getBody()).ifPresent(config ->
|
||||
config.forEach((k, v) -> {
|
||||
if (v.getValue() instanceof String strValue) {
|
||||
final TextField tf = new TextField(k, strValue, event -> {
|
||||
final MgmtSystemTenantConfigurationValueRequest vre = new MgmtSystemTenantConfigurationValueRequest();
|
||||
vre.setValue(event.getValue());
|
||||
configValue.put(k, vre);
|
||||
});
|
||||
tf.getElement().getStyle().set(WIDTH, PX_300);
|
||||
add(tf);
|
||||
} else if (v.getValue() instanceof Boolean boolValue) {
|
||||
add(new Checkbox(k, boolValue, event -> {
|
||||
final MgmtSystemTenantConfigurationValueRequest vre = new MgmtSystemTenantConfigurationValueRequest();
|
||||
vre.setValue(event.getValue());
|
||||
configValue.put(k, vre);
|
||||
}));
|
||||
} else if (v.getValue() instanceof Long longValue) {
|
||||
final NumberField nf = new NumberField(k, (double) longValue, event -> {
|
||||
final MgmtSystemTenantConfigurationValueRequest vre = new MgmtSystemTenantConfigurationValueRequest();
|
||||
vre.setValue(event.getValue());
|
||||
configValue.put(k, vre);
|
||||
});
|
||||
nf.getElement().getStyle().set(WIDTH, PX_300);
|
||||
add(nf);
|
||||
} else if (v.getValue() instanceof Integer intValue) {
|
||||
final NumberField nf = new NumberField(k, (double) intValue, event -> {
|
||||
MgmtSystemTenantConfigurationValueRequest vre = new MgmtSystemTenantConfigurationValueRequest();
|
||||
vre.setValue(event.getValue());
|
||||
configValue.put(k, vre);
|
||||
});
|
||||
nf.getElement().getStyle().set(WIDTH, PX_300);
|
||||
add(nf);
|
||||
} else {
|
||||
log.debug("Unexpected value type: {} -> {} (class: {})",
|
||||
k, v.getValue(), v.getValue() == null ? "null" : v.getValue().getClass());
|
||||
}
|
||||
}));
|
||||
|
||||
saveButton.addClickListener(click -> configValue.forEach(
|
||||
(key, value) -> hawkbitClient.getTenantManagementRestApi().updateTenantConfigurationValue(key, value)));
|
||||
saveButton.addClickShortcut(Key.ENTER);
|
||||
add(saveButton);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.view;
|
||||
|
||||
// java:S1214 - implementations of Constants interface extends other classes, so if make this class we shall go for static imports
|
||||
// which is not not better
|
||||
@SuppressWarnings("java:S1214")
|
||||
public interface Constants {
|
||||
|
||||
// properties
|
||||
String ID = "Id";
|
||||
String ADDRESS = "Address";
|
||||
String NAME = "Name";
|
||||
String DESCRIPTION = "Description";
|
||||
String VERSION = "Version";
|
||||
String VENDOR = "Vendor";
|
||||
String TYPE = "Type";
|
||||
String GROUP = "Group";
|
||||
String CREATED_BY = "Created by";
|
||||
String CREATED_AT = "Created at";
|
||||
String LAST_MODIFIED_BY = "Last modified by";
|
||||
String LAST_MODIFIED_AT = "Last modified at";
|
||||
String LAST_POLL = "Last Poll";
|
||||
String SECURITY_TOKEN = "Security Token";
|
||||
String ATTRIBUTES = "Attributes";
|
||||
|
||||
// rollout
|
||||
String GROUP_COUNT = "Group Count";
|
||||
String TARGET_COUNT = "Target Count";
|
||||
String STATS = "Stats";
|
||||
String STATUS = "Status";
|
||||
String ACTIONS = "Actions";
|
||||
|
||||
// create rollout
|
||||
String TARGET_FILTER = "Target Filter";
|
||||
String DISTRIBUTION_SET = "Distribution Set";
|
||||
String ACTION_TYPE = "Action Type";
|
||||
String START_AT = "Start At";
|
||||
String SOFT = "Soft";
|
||||
String FORCED = "Forced";
|
||||
String DOWNLOAD_ONLY = "Download Only";
|
||||
String START_TYPE = "Start Type";
|
||||
String MANUAL = "Manual";
|
||||
String AUTO = "Auto";
|
||||
String DYNAMIC = "Dynamic";
|
||||
|
||||
// dialog
|
||||
String CANCEL = "Cancel";
|
||||
String CANCEL_ESC = "Cancel (Esc)";
|
||||
|
||||
String CREATED_AT_DESC = "createdAt:desc";
|
||||
|
||||
String NAME_ASC = "name:asc";
|
||||
String NAME_DESC = "name:desc";
|
||||
|
||||
String NOT_AVAILABLE_NULL = "n/a (null)";
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.view;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.vaadin.flow.component.grid.GridSortOrder;
|
||||
import com.vaadin.flow.data.provider.SortDirection;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.Key;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.checkbox.CheckboxGroup;
|
||||
import com.vaadin.flow.component.dependency.Uses;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.select.Select;
|
||||
import com.vaadin.flow.component.textfield.TextArea;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.data.renderer.ComponentRenderer;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPost;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetType;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleAssignment;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.tag.MgmtTag;
|
||||
import org.eclipse.hawkbit.ui.HawkbitMgmtClient;
|
||||
import org.eclipse.hawkbit.ui.MainLayout;
|
||||
import org.eclipse.hawkbit.ui.view.util.Filter;
|
||||
import org.eclipse.hawkbit.ui.view.util.SelectionGrid;
|
||||
import org.eclipse.hawkbit.ui.view.util.TableView;
|
||||
import org.eclipse.hawkbit.ui.view.util.Utils;
|
||||
|
||||
@PageTitle("Distribution Sets")
|
||||
@Route(value = "distribution_sets", layout = MainLayout.class)
|
||||
@RolesAllowed({ "DISTRIBUTION_SET_READ" })
|
||||
@Uses(Icon.class)
|
||||
public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
|
||||
|
||||
public DistributionSetView(final HawkbitMgmtClient hawkbitClient) {
|
||||
super(
|
||||
new DistributionSetFilter(hawkbitClient),
|
||||
new DistributionSetRawFilter(),
|
||||
new SelectionGrid.EntityRepresentation<>(MgmtDistributionSet.class, MgmtDistributionSet::getId) {
|
||||
|
||||
private final DistributionSetDetails details = new DistributionSetDetails(hawkbitClient);
|
||||
|
||||
@Override
|
||||
protected void addColumns(Grid<MgmtDistributionSet> grid) {
|
||||
var createdAtCol = grid.addColumn(Utils.localDateTimeRenderer(MgmtDistributionSet::getCreatedAt)).setHeader(
|
||||
Constants.CREATED_AT).setAutoWidth(true).setKey("createdAt").setSortable(true);
|
||||
grid.addColumn(MgmtDistributionSet::getName).setHeader(Constants.NAME).setAutoWidth(true).setKey("name").setSortable(
|
||||
true);
|
||||
grid.addColumn(MgmtDistributionSet::getVersion).setHeader(Constants.VERSION).setAutoWidth(true).setKey("version")
|
||||
.setSortable(true);
|
||||
grid.addColumn(MgmtDistributionSet::getTypeName).setHeader(Constants.TYPE).setAutoWidth(true).setKey("typename")
|
||||
.setSortable(true);
|
||||
grid.sort(List.of(new GridSortOrder<>(createdAtCol, SortDirection.DESCENDING)));
|
||||
|
||||
grid.setItemDetailsRenderer(new ComponentRenderer<>(
|
||||
() -> details, DistributionSetDetails::setItem));
|
||||
}
|
||||
},
|
||||
(query, rsqlFilter) -> Optional.ofNullable(
|
||||
hawkbitClient.getDistributionSetRestApi()
|
||||
.getDistributionSets(rsqlFilter, query.getOffset(), query.getPageSize(), Utils.getSortParam(query
|
||||
.getSortOrders()))
|
||||
.getBody())
|
||||
.stream().flatMap(body -> body.getContent().stream()),
|
||||
e -> new CreateDialog(hawkbitClient).result(),
|
||||
selectionGrid -> {
|
||||
selectionGrid.getSelectedItems().forEach(
|
||||
distributionSet -> hawkbitClient.getDistributionSetRestApi()
|
||||
.deleteDistributionSet(distributionSet.getId()));
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}, null);
|
||||
}
|
||||
|
||||
private static SelectionGrid<MgmtSoftwareModule, Long> selectSoftwareModuleGrid() {
|
||||
return new SelectionGrid<>(
|
||||
new SelectionGrid.EntityRepresentation<>(
|
||||
MgmtSoftwareModule.class, MgmtSoftwareModule::getId) {
|
||||
|
||||
@Override
|
||||
protected void addColumns(Grid<MgmtSoftwareModule> grid) {
|
||||
grid.addColumn(MgmtSoftwareModule::getId).setHeader(Constants.ID).setAutoWidth(true);
|
||||
grid.addColumn(MgmtSoftwareModule::getName).setHeader(Constants.NAME).setAutoWidth(true);
|
||||
grid.addColumn(MgmtSoftwareModule::getVersion).setHeader(Constants.VERSION).setAutoWidth(true);
|
||||
grid.addColumn(MgmtSoftwareModule::getTypeName).setHeader(Constants.TYPE).setAutoWidth(true);
|
||||
grid.addColumn(MgmtSoftwareModule::getVendor).setHeader(Constants.VENDOR).setAutoWidth(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class DistributionSetRawFilter implements Filter.Rsql, Filter.RsqlRw {
|
||||
|
||||
private final TextField name = Utils.textField("Name");
|
||||
|
||||
private DistributionSetRawFilter() {
|
||||
name.setPlaceholder("<rsql filter>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Component> components() {
|
||||
return List.of(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String filter() {
|
||||
return name.getOptionalValue().orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFilter(String filter) {
|
||||
name.setValue(filter);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DistributionSetFilter implements Filter.Rsql {
|
||||
|
||||
private final TextField textFilter = Utils.textField("Filter");
|
||||
private final CheckboxGroup<MgmtDistributionSetType> type = new CheckboxGroup<>("Type");
|
||||
private final CheckboxGroup<MgmtTag> tag = new CheckboxGroup<>("Tag");
|
||||
|
||||
private DistributionSetFilter(final HawkbitMgmtClient hawkbitClient) {
|
||||
textFilter.setPlaceholder("<name/version filter>");
|
||||
type.setItemLabelGenerator(MgmtDistributionSetType::getName);
|
||||
type.setItems(Optional.ofNullable(
|
||||
hawkbitClient.getDistributionSetTypeRestApi()
|
||||
.getDistributionSetTypes(null, 0, 20, Constants.NAME_ASC)
|
||||
.getBody())
|
||||
.map(PagedList::getContent)
|
||||
.orElseGet(Collections::emptyList));
|
||||
tag.setItemLabelGenerator(MgmtTag::getName);
|
||||
tag.setItems(Optional.ofNullable(
|
||||
hawkbitClient.getDistributionSetTagRestApi()
|
||||
.getDistributionSetTags(null, 0, 20, Constants.NAME_ASC)
|
||||
.getBody())
|
||||
.map(PagedList::getContent)
|
||||
.orElseGet(Collections::emptyList));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Component> components() {
|
||||
return List.of(textFilter, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String filter() {
|
||||
return Filter.filter(
|
||||
Map.of(
|
||||
List.of("version", "name"), textFilter.getOptionalValue().map(s -> "*" + s + "*"),
|
||||
"type", type.getSelectedItems().stream().map(MgmtDistributionSetType::getKey).toList(),
|
||||
"tag", tag.getSelectedItems().stream().map(MgmtTag::getName).toList()));
|
||||
}
|
||||
}
|
||||
|
||||
private static class DistributionSetDetails extends FormLayout {
|
||||
|
||||
private final transient HawkbitMgmtClient hawkbitClient;
|
||||
|
||||
private final TextArea description = new TextArea("Description");
|
||||
private final TextField createdBy = Utils.textField("Created by");
|
||||
private final TextField createdAt = Utils.textField("Created at");
|
||||
private final TextField lastModifiedBy = Utils.textField("Last modified by");
|
||||
private final TextField lastModifiedAt = Utils.textField("Last modified at");
|
||||
private final TextArea metadata = new TextArea("Metadata");
|
||||
private final SelectionGrid<MgmtSoftwareModule, Long> softwareModulesGrid = selectSoftwareModuleGrid();
|
||||
|
||||
private DistributionSetDetails(final HawkbitMgmtClient hawkbitClient) {
|
||||
this.hawkbitClient = hawkbitClient;
|
||||
|
||||
description.setMinLength(2);
|
||||
Stream.of(
|
||||
description,
|
||||
createdBy, createdAt,
|
||||
lastModifiedBy, lastModifiedAt, metadata)
|
||||
.forEach(field -> {
|
||||
field.setReadOnly(true);
|
||||
add(field);
|
||||
});
|
||||
add(softwareModulesGrid);
|
||||
|
||||
setResponsiveSteps(new ResponsiveStep("0", 2));
|
||||
setColspan(description, 2);
|
||||
setColspan(softwareModulesGrid, 2);
|
||||
}
|
||||
|
||||
private void setItem(final MgmtDistributionSet distributionSet) {
|
||||
description.setValue(distributionSet.getDescription());
|
||||
createdBy.setValue(distributionSet.getCreatedBy());
|
||||
createdAt.setValue(Utils.localDateTimeFromTs(distributionSet.getCreatedAt()));
|
||||
lastModifiedBy.setValue(distributionSet.getLastModifiedBy());
|
||||
lastModifiedAt.setValue(Utils.localDateTimeFromTs(distributionSet.getLastModifiedAt()));
|
||||
metadata.setValue(Optional.ofNullable(
|
||||
hawkbitClient.getDistributionSetRestApi().getMetadata(distributionSet.getId()).getBody())
|
||||
.map(body -> body.getContent().stream()
|
||||
.map(b -> String.format("%s: %s\n", b.getKey(), b.getValue())).collect(
|
||||
Collectors.joining())).orElse(""));
|
||||
|
||||
softwareModulesGrid.setItems(query -> Optional.ofNullable(
|
||||
hawkbitClient.getDistributionSetRestApi()
|
||||
.getAssignedSoftwareModules(
|
||||
distributionSet.getId(),
|
||||
query.getOffset(), query.getLimit(), Constants.NAME_ASC)
|
||||
.getBody()).stream().flatMap(body -> body.getContent().stream()));
|
||||
softwareModulesGrid.setSelectionMode(Grid.SelectionMode.NONE);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CreateDialog extends Utils.BaseDialog<Void> {
|
||||
|
||||
private final transient HawkbitMgmtClient hawkbitClient;
|
||||
|
||||
private final Select<MgmtDistributionSetType> type;
|
||||
private final TextField name;
|
||||
private final TextField version;
|
||||
private final TextArea description;
|
||||
private final Checkbox requiredMigrationStep;
|
||||
private final Button create;
|
||||
|
||||
private CreateDialog(final HawkbitMgmtClient hawkbitClient) {
|
||||
super("Create Distribution Set");
|
||||
this.hawkbitClient = hawkbitClient;
|
||||
|
||||
type = new Select<>(
|
||||
"Type",
|
||||
this::readyToCreate,
|
||||
Optional.ofNullable(
|
||||
hawkbitClient.getDistributionSetTypeRestApi()
|
||||
.getDistributionSetTypes(null, 0, 30, Constants.CREATED_AT_DESC)
|
||||
.getBody())
|
||||
.map(body -> body.getContent().toArray(new MgmtDistributionSetType[0]))
|
||||
.orElseGet(() -> new MgmtDistributionSetType[0]));
|
||||
type.focus();
|
||||
type.setWidthFull();
|
||||
type.setRequiredIndicatorVisible(true);
|
||||
type.setItemLabelGenerator(MgmtDistributionSetType::getName);
|
||||
name = Utils.textField(Constants.NAME, this::readyToCreate);
|
||||
version = Utils.textField(Constants.VERSION, this::readyToCreate);
|
||||
final TextField vendor = Utils.textField(Constants.VENDOR);
|
||||
description = new TextArea(Constants.DESCRIPTION);
|
||||
description.setWidthFull();
|
||||
description.setMinLength(2);
|
||||
requiredMigrationStep = new Checkbox("Required Migration Step");
|
||||
|
||||
create = Utils.tooltip(new Button("Create"), "Create (Enter)");
|
||||
create.setEnabled(false);
|
||||
addCreateClickListener();
|
||||
create.addClickShortcut(Key.ENTER);
|
||||
final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC);
|
||||
cancel.addClickListener(e -> close());
|
||||
create.addClickShortcut(Key.ESCAPE);
|
||||
create.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
getFooter().add(cancel);
|
||||
getFooter().add(create);
|
||||
|
||||
final VerticalLayout layout = new VerticalLayout();
|
||||
layout.setSizeFull();
|
||||
layout.setPadding(true);
|
||||
layout.setSpacing(false);
|
||||
layout.add(type, name, version, vendor, description, requiredMigrationStep);
|
||||
add(layout);
|
||||
open();
|
||||
}
|
||||
|
||||
private void readyToCreate(final Object v) {
|
||||
final boolean createEnabled = !type.isEmpty() && !name.isEmpty() && !version.isEmpty();
|
||||
if (create.isEnabled() != createEnabled) {
|
||||
create.setEnabled(createEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void addCreateClickListener() {
|
||||
create.addClickListener(e -> {
|
||||
close();
|
||||
final long distributionSetId = Optional.ofNullable(
|
||||
hawkbitClient.getDistributionSetRestApi()
|
||||
.createDistributionSets(
|
||||
List.of((MgmtDistributionSetRequestBodyPost) new MgmtDistributionSetRequestBodyPost()
|
||||
.setType(type.getValue().getKey())
|
||||
.setName(name.getValue())
|
||||
.setVersion(version.getValue())
|
||||
.setDescription(description.getValue())
|
||||
.setRequiredMigrationStep(requiredMigrationStep.getValue())))
|
||||
.getBody())
|
||||
.stream()
|
||||
.flatMap(Collection::stream)
|
||||
.findFirst()
|
||||
.orElseThrow()
|
||||
.getId();
|
||||
new AddSoftwareModulesDialog(distributionSetId, hawkbitClient).open();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "java:S1171", "java:S3599" })
|
||||
private static class AddSoftwareModulesDialog extends Utils.BaseDialog<Void> {
|
||||
|
||||
private final transient Set<MgmtSoftwareModule> softwareModules = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
private AddSoftwareModulesDialog(final long distributionSetId, final HawkbitMgmtClient hawkbitClient) {
|
||||
super("Add Software Modules");
|
||||
|
||||
final SelectionGrid<MgmtSoftwareModule, Long> softwareModulesGrid = selectSoftwareModuleGrid();
|
||||
softwareModulesGrid.setItems(query -> {
|
||||
query.getOffset(); // to keep vaadin contract
|
||||
return softwareModules.stream().limit(query.getLimit());
|
||||
});
|
||||
|
||||
final Component addRemoveControls = Utils.addRemoveControls(
|
||||
v -> new Utils.BaseDialog<Void>("Add Software Modules") {
|
||||
|
||||
{
|
||||
final SoftwareModuleView softwareModulesView = new SoftwareModuleView(false, hawkbitClient);
|
||||
add(softwareModulesView);
|
||||
final Button addBtn = new Button("Add");
|
||||
addBtn.addClickListener(e -> {
|
||||
softwareModules.addAll(softwareModulesView.getSelection());
|
||||
softwareModulesGrid.refreshGrid(false);
|
||||
close();
|
||||
});
|
||||
addBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
getFooter().add(addBtn);
|
||||
open();
|
||||
}
|
||||
}.result(),
|
||||
v -> {
|
||||
Utils.remove(softwareModulesGrid.getSelectedItems(), softwareModules, MgmtSoftwareModule::getId);
|
||||
softwareModulesGrid.refreshGrid(false);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
},
|
||||
softwareModulesGrid, true);
|
||||
final Button finishBtn = Utils.tooltip(new Button("Finish"), "Finish (Esc)");
|
||||
finishBtn.addClickListener(e -> {
|
||||
hawkbitClient.getDistributionSetRestApi().assignSoftwareModules(
|
||||
distributionSetId, softwareModules.stream().map(softwareModule -> {
|
||||
final MgmtSoftwareModuleAssignment assignment = new MgmtSoftwareModuleAssignment();
|
||||
assignment.setId(softwareModule.getId());
|
||||
return assignment;
|
||||
}).toList());
|
||||
close();
|
||||
});
|
||||
finishBtn.addClickShortcut(Key.ENTER);
|
||||
finishBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
getFooter().add(finishBtn);
|
||||
|
||||
final HorizontalLayout addRemove = new HorizontalLayout(addRemoveControls);
|
||||
addRemove.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
|
||||
addRemove.setWidthFull();
|
||||
|
||||
final VerticalLayout layout = new VerticalLayout();
|
||||
layout.setSizeFull();
|
||||
layout.setSpacing(false);
|
||||
layout.add(softwareModulesGrid, addRemove);
|
||||
add(layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.view;
|
||||
|
||||
import com.vaadin.flow.component.login.LoginI18n;
|
||||
import com.vaadin.flow.component.login.LoginOverlay;
|
||||
import com.vaadin.flow.router.BeforeEnterEvent;
|
||||
import com.vaadin.flow.router.BeforeEnterObserver;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.router.internal.RouteUtil;
|
||||
import com.vaadin.flow.server.VaadinService;
|
||||
import com.vaadin.flow.server.auth.AnonymousAllowed;
|
||||
import org.eclipse.hawkbit.ui.security.AuthenticatedUser;
|
||||
|
||||
@AnonymousAllowed
|
||||
@PageTitle("Login")
|
||||
@Route(value = "login")
|
||||
public class LoginView extends LoginOverlay implements BeforeEnterObserver {
|
||||
|
||||
private final transient AuthenticatedUser authenticatedUser;
|
||||
|
||||
public LoginView(final AuthenticatedUser authenticatedUser) {
|
||||
this.authenticatedUser = authenticatedUser;
|
||||
setAction(RouteUtil.getRoutePath(VaadinService.getCurrent().getContext(), getClass()));
|
||||
|
||||
final LoginI18n i18n = LoginI18n.createDefault();
|
||||
i18n.setHeader(new LoginI18n.Header());
|
||||
i18n.getHeader().setTitle("hawkBit");
|
||||
i18n.getHeader().setDescription("Login in hawkBit");
|
||||
i18n.setAdditionalInformation(null);
|
||||
setI18n(i18n);
|
||||
|
||||
setForgotPasswordButtonVisible(false);
|
||||
setOpened(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeEnter(final BeforeEnterEvent event) {
|
||||
if (authenticatedUser.getName().isPresent()) { // already logged in
|
||||
setOpened(false);
|
||||
event.forwardTo("");
|
||||
}
|
||||
|
||||
setError(event.getLocation().getQueryParameters().getParameters().containsKey("error"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.view;
|
||||
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.Key;
|
||||
import com.vaadin.flow.component.Text;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.datetimepicker.DateTimePicker;
|
||||
import com.vaadin.flow.component.dependency.Uses;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.select.Select;
|
||||
import com.vaadin.flow.component.textfield.NumberField;
|
||||
import com.vaadin.flow.component.textfield.TextArea;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.data.renderer.ComponentRenderer;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutCondition;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutErrorAction;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutResponseBody;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPost;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroupResponseBody;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQuery;
|
||||
import org.eclipse.hawkbit.ui.HawkbitMgmtClient;
|
||||
import org.eclipse.hawkbit.ui.MainLayout;
|
||||
import org.eclipse.hawkbit.ui.view.util.Filter;
|
||||
import org.eclipse.hawkbit.ui.view.util.SelectionGrid;
|
||||
import org.eclipse.hawkbit.ui.view.util.TableView;
|
||||
import org.eclipse.hawkbit.ui.view.util.Utils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
@PageTitle("Rollouts")
|
||||
@Route(value = "rollouts", layout = MainLayout.class)
|
||||
@RolesAllowed({ "ROLLOUT_READ" })
|
||||
@Uses(Icon.class)
|
||||
@SuppressWarnings({ "java:S1171", "java:S3599" })
|
||||
public class RolloutView extends TableView<MgmtRolloutResponseBody, Long> {
|
||||
|
||||
public RolloutView(final HawkbitMgmtClient hawkbitClient) {
|
||||
super(
|
||||
new RolloutFilter(),
|
||||
new SelectionGrid.EntityRepresentation<>(
|
||||
MgmtRolloutResponseBody.class, MgmtRolloutResponseBody::getId) {
|
||||
|
||||
private final RolloutDetails details = new RolloutDetails(hawkbitClient);
|
||||
|
||||
@Override
|
||||
protected void addColumns(final Grid<MgmtRolloutResponseBody> grid) {
|
||||
grid.addColumn(MgmtRolloutResponseBody::getId).setHeader(Constants.ID).setAutoWidth(true);
|
||||
grid.addColumn(MgmtRolloutResponseBody::getName).setHeader(Constants.NAME).setAutoWidth(true);
|
||||
grid.addColumn(MgmtRolloutResponseBody::getTotalGroups).setHeader(Constants.GROUP_COUNT).setAutoWidth(true);
|
||||
grid.addColumn(MgmtRolloutResponseBody::getTotalTargets).setHeader(Constants.TARGET_COUNT).setAutoWidth(true);
|
||||
grid.addColumn(MgmtRolloutResponseBody::getTotalTargetsPerStatus).setHeader(Constants.STATS).setAutoWidth(true);
|
||||
grid.addColumn(MgmtRolloutResponseBody::getStatus).setHeader(Constants.STATUS).setAutoWidth(true);
|
||||
|
||||
grid.addComponentColumn(rollout -> new Actions(rollout, grid, hawkbitClient)).setHeader(
|
||||
Constants.ACTIONS).setAutoWidth(true);
|
||||
|
||||
grid.setItemDetailsRenderer(new ComponentRenderer<>(
|
||||
() -> details, RolloutDetails::setItem));
|
||||
}
|
||||
},
|
||||
(query, rsqlFilter) -> Optional.ofNullable(
|
||||
hawkbitClient.getRolloutRestApi()
|
||||
.getRollouts(
|
||||
rsqlFilter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC, "full")
|
||||
.getBody()).stream().flatMap(page -> page.getContent().stream()),
|
||||
selectionGrid -> new CreateDialog(hawkbitClient).result(),
|
||||
selectionGrid -> {
|
||||
selectionGrid.getSelectedItems().forEach(
|
||||
rollout -> hawkbitClient.getRolloutRestApi().delete(rollout.getId()));
|
||||
selectionGrid.refreshGrid(false);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
});
|
||||
}
|
||||
|
||||
private static class Actions extends HorizontalLayout {
|
||||
|
||||
private final long rolloutId;
|
||||
private final Grid<MgmtRolloutResponseBody> grid;
|
||||
private final transient HawkbitMgmtClient hawkbitClient;
|
||||
|
||||
private Actions(final MgmtRolloutResponseBody rollout, final Grid<MgmtRolloutResponseBody> grid,
|
||||
final HawkbitMgmtClient hawkbitClient) {
|
||||
this.rolloutId = rollout.getId();
|
||||
this.grid = grid;
|
||||
this.hawkbitClient = hawkbitClient;
|
||||
init(rollout);
|
||||
}
|
||||
|
||||
private void init(final MgmtRolloutResponseBody rollout) {
|
||||
if ("READY".equalsIgnoreCase(rollout.getStatus())) {
|
||||
add(Utils.tooltip(new Button(VaadinIcon.START_COG.create()) {
|
||||
|
||||
{
|
||||
addClickListener(v -> {
|
||||
hawkbitClient.getRolloutRestApi().start(rollout.getId());
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
}, "Start"));
|
||||
} else if ("RUNNING".equalsIgnoreCase(rollout.getStatus())) {
|
||||
add(Utils.tooltip(new Button(VaadinIcon.PAUSE.create()) {
|
||||
|
||||
{
|
||||
addClickListener(v -> {
|
||||
hawkbitClient.getRolloutRestApi().pause(rollout.getId());
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
}, "Pause"));
|
||||
} else if ("PAUSED".equalsIgnoreCase(rollout.getStatus())) {
|
||||
add(Utils.tooltip(new Button(VaadinIcon.START_COG.create()) {
|
||||
|
||||
{
|
||||
addClickListener(v -> {
|
||||
hawkbitClient.getRolloutRestApi().resume(rollout.getId());
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
}, "Resume"));
|
||||
}
|
||||
add(Utils.tooltip(new Button(VaadinIcon.TRASH.create()) {
|
||||
|
||||
{
|
||||
addClickListener(v -> {
|
||||
hawkbitClient.getRolloutRestApi().delete(rollout.getId());
|
||||
grid.getDataProvider().refreshAll();
|
||||
});
|
||||
}
|
||||
}, "Cancel and Remove"));
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
removeAll();
|
||||
final MgmtRolloutResponseBody body = hawkbitClient.getRolloutRestApi().getRollout(rolloutId).getBody();
|
||||
if (body != null) {
|
||||
init(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class RolloutFilter implements Filter.Rsql {
|
||||
|
||||
private final TextField name = Utils.textField(Constants.NAME);
|
||||
|
||||
private RolloutFilter() {
|
||||
name.setPlaceholder("<name filter>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Component> components() {
|
||||
return List.of(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String filter() {
|
||||
return Filter.filter(Map.of("name", name.getOptionalValue()));
|
||||
}
|
||||
}
|
||||
|
||||
private static class RolloutDetails extends FormLayout {
|
||||
|
||||
private final transient HawkbitMgmtClient hawkbitClient;
|
||||
|
||||
private final TextArea description = new TextArea(Constants.DESCRIPTION);
|
||||
private final TextField createdBy = Utils.textField(Constants.CREATED_BY);
|
||||
private final TextField createdAt = Utils.textField(Constants.CREATED_AT);
|
||||
private final TextField lastModifiedBy = Utils.textField(Constants.LAST_MODIFIED_BY);
|
||||
private final TextField lastModifiedAt = Utils.textField(Constants.LAST_MODIFIED_AT);
|
||||
private final TextField targetFilter = Utils.textField(Constants.TARGET_FILTER);
|
||||
private final TextField distributionSet = Utils.textField(Constants.DISTRIBUTION_SET);
|
||||
private final TextField actonType = Utils.textField(Constants.ACTION_TYPE);
|
||||
private final TextField startAt = Utils.textField(Constants.START_AT);
|
||||
private final Checkbox dynamic = new Checkbox(Constants.DYNAMIC);
|
||||
private final SelectionGrid<MgmtRolloutGroupResponseBody, Long> groupGrid;
|
||||
|
||||
private RolloutDetails(final HawkbitMgmtClient hawkbitClient) {
|
||||
this.hawkbitClient = hawkbitClient;
|
||||
|
||||
description.setMinLength(2);
|
||||
groupGrid = createGroupGrid();
|
||||
Stream.of(
|
||||
description,
|
||||
createdBy, createdAt,
|
||||
lastModifiedBy, lastModifiedAt,
|
||||
targetFilter, distributionSet,
|
||||
actonType, startAt)
|
||||
.forEach(field -> {
|
||||
field.setReadOnly(true);
|
||||
add(field);
|
||||
});
|
||||
dynamic.setReadOnly(true);
|
||||
dynamic.setEnabled(false);
|
||||
add(dynamic);
|
||||
add(groupGrid);
|
||||
|
||||
setResponsiveSteps(new ResponsiveStep("0", 2));
|
||||
setColspan(description, 2);
|
||||
setColspan(groupGrid, 2);
|
||||
}
|
||||
|
||||
private void setItem(final MgmtRolloutResponseBody rollout) {
|
||||
description.setValue(rollout.getDescription());
|
||||
createdBy.setValue(rollout.getCreatedBy());
|
||||
createdAt.setValue(Utils.localDateTimeFromTs(rollout.getCreatedAt()));
|
||||
lastModifiedBy.setValue(rollout.getLastModifiedBy());
|
||||
lastModifiedAt.setValue(Utils.localDateTimeFromTs(rollout.getLastModifiedAt()));
|
||||
targetFilter.setValue(rollout.getTargetFilterQuery());
|
||||
final MgmtDistributionSet distributionSetMgmt = hawkbitClient.getDistributionSetRestApi()
|
||||
.getDistributionSet(rollout.getDistributionSetId()).getBody();
|
||||
distributionSet.setValue(distributionSetMgmt == null
|
||||
? NOT_AVAILABLE_NULL // should not be the case
|
||||
: distributionSetMgmt.getName() + ":" + distributionSetMgmt.getVersion());
|
||||
actonType.setValue(switch (rollout.getType()) {
|
||||
case SOFT -> Constants.SOFT;
|
||||
case FORCED -> Constants.FORCED;
|
||||
case DOWNLOAD_ONLY -> Constants.DOWNLOAD_ONLY;
|
||||
case TIMEFORCED -> "Scheduled at " + Utils.localDateTimeFromTs(rollout.getForcetime());
|
||||
});
|
||||
startAt.setValue(ObjectUtils.isEmpty(rollout.getStartAt()) ? "" : Utils.localDateTimeFromTs(rollout.getStartAt()));
|
||||
dynamic.setValue(rollout.isDynamic());
|
||||
|
||||
groupGrid.setItems(query -> Optional.ofNullable(
|
||||
hawkbitClient.getRolloutRestApi()
|
||||
.getRolloutGroups(
|
||||
rollout.getId(),
|
||||
null, query.getOffset(), query.getPageSize(),
|
||||
null, "full")
|
||||
.getBody())
|
||||
.stream().flatMap(body -> body.getContent().stream())
|
||||
.skip(query.getOffset())
|
||||
.limit(query.getPageSize()));
|
||||
groupGrid.setSelectionMode(Grid.SelectionMode.NONE);
|
||||
}
|
||||
|
||||
private static SelectionGrid<MgmtRolloutGroupResponseBody, Long> createGroupGrid() {
|
||||
return new SelectionGrid<>(
|
||||
new SelectionGrid.EntityRepresentation<>(MgmtRolloutGroupResponseBody.class, MgmtRolloutGroupResponseBody::getId) {
|
||||
|
||||
@Override
|
||||
protected void addColumns(final Grid<MgmtRolloutGroupResponseBody> grid) {
|
||||
grid.addColumn(MgmtRolloutGroupResponseBody::getId).setHeader(Constants.ID).setAutoWidth(true);
|
||||
grid.addColumn(MgmtRolloutGroupResponseBody::getName).setHeader(Constants.NAME).setAutoWidth(true);
|
||||
grid.addColumn(MgmtRolloutGroupResponseBody::getTotalTargets).setHeader(Constants.TARGET_COUNT).setAutoWidth(true);
|
||||
grid.addColumn(MgmtRolloutGroupResponseBody::getTotalTargetsPerStatus).setHeader(Constants.STATS).setAutoWidth(
|
||||
true);
|
||||
grid.addColumn(MgmtRolloutGroupResponseBody::getStatus).setHeader(Constants.STATUS).setAutoWidth(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static class CreateDialog extends Utils.BaseDialog<Void> {
|
||||
|
||||
private final TextField name;
|
||||
private final Select<MgmtDistributionSet> distributionSet;
|
||||
private final Select<MgmtTargetFilterQuery> targetFilter;
|
||||
private final TextArea description;
|
||||
private final Select<MgmtActionType> actionType;
|
||||
private final DateTimePicker forceTime = new DateTimePicker("Force Time");
|
||||
private final Select<StartType> startType;
|
||||
private final DateTimePicker startAt = new DateTimePicker(Constants.START_AT);
|
||||
private final NumberField groupNumber;
|
||||
private final NumberField triggerThreshold;
|
||||
private final NumberField errorThreshold;
|
||||
private final Checkbox dynamic = new Checkbox(Constants.DYNAMIC);
|
||||
private final Button create = new Button("Create");
|
||||
|
||||
private CreateDialog(final HawkbitMgmtClient hawkbitClient) {
|
||||
super("Create Rollout");
|
||||
|
||||
name = Utils.textField("Name", this::readyToCreate);
|
||||
name.focus();
|
||||
distributionSet = new Select<>(
|
||||
"Distribution Set",
|
||||
this::readyToCreate,
|
||||
Optional.ofNullable(
|
||||
hawkbitClient.getDistributionSetRestApi()
|
||||
.getDistributionSets(null, 0, 30, Constants.CREATED_AT_DESC)
|
||||
.getBody())
|
||||
.map(body -> body.getContent().toArray(new MgmtDistributionSet[0]))
|
||||
.orElseGet(() -> new MgmtDistributionSet[0]));
|
||||
distributionSet.setRequiredIndicatorVisible(true);
|
||||
distributionSet.setItemLabelGenerator(distributionSetO -> distributionSetO.getName() + ":" + distributionSetO.getVersion());
|
||||
distributionSet.setWidthFull();
|
||||
targetFilter = new Select<>(
|
||||
"Target Filter",
|
||||
this::readyToCreate,
|
||||
Optional.ofNullable(
|
||||
hawkbitClient.getTargetFilterQueryRestApi()
|
||||
.getFilters(null, 0, 30, Constants.NAME_ASC, null)
|
||||
.getBody())
|
||||
.map(body -> body.getContent().toArray(new MgmtTargetFilterQuery[0]))
|
||||
.orElseGet(() -> new MgmtTargetFilterQuery[0]));
|
||||
targetFilter.setRequiredIndicatorVisible(true);
|
||||
targetFilter.setItemLabelGenerator(MgmtTargetFilterQuery::getName);
|
||||
targetFilter.setWidthFull();
|
||||
description = new TextArea(Constants.DESCRIPTION);
|
||||
description.setMinLength(2);
|
||||
description.setWidthFull();
|
||||
|
||||
actionType = Utils.actionTypeControls(forceTime);
|
||||
|
||||
startType = new Select<>();
|
||||
startType.setValue(StartType.MANUAL);
|
||||
startType.setLabel(Constants.START_TYPE);
|
||||
startType.setItems(StartType.values());
|
||||
startType.setValue(StartType.MANUAL);
|
||||
final ComponentRenderer<Component, StartType> startTypeRenderer = new ComponentRenderer<>(startTypeO -> switch (startTypeO) {
|
||||
case MANUAL -> new Text(Constants.MANUAL);
|
||||
case AUTO -> new Text(Constants.AUTO);
|
||||
case SCHEDULED -> startAt;
|
||||
});
|
||||
startType.setRenderer(startTypeRenderer);
|
||||
startType.addValueChangeListener(e -> startType.setRenderer(startTypeRenderer));
|
||||
startType.setItemLabelGenerator(startTypeO -> switch (startTypeO) {
|
||||
case MANUAL -> Constants.MANUAL;
|
||||
case AUTO -> Constants.AUTO;
|
||||
case SCHEDULED -> "Scheduled" + (startAt.isEmpty() ? "" : " at " + startAt.getValue());
|
||||
});
|
||||
startType.setWidthFull();
|
||||
|
||||
final Div percentSuffix = new Div();
|
||||
percentSuffix.setText("%");
|
||||
groupNumber = Utils.numberField("Group number", this::readyToCreate);
|
||||
groupNumber.setMin(1);
|
||||
groupNumber.setValue(1.0);
|
||||
triggerThreshold = Utils.numberField("Trigger Threshold", this::readyToCreate);
|
||||
triggerThreshold.setMin(0);
|
||||
triggerThreshold.setMax(100);
|
||||
triggerThreshold.setValue(100.0);
|
||||
triggerThreshold.setSuffixComponent(percentSuffix);
|
||||
errorThreshold = Utils.numberField("Error Threshold", this::readyToCreate);
|
||||
errorThreshold.setMin(1);
|
||||
errorThreshold.setMax(100);
|
||||
errorThreshold.setValue(10.0);
|
||||
errorThreshold.setSuffixComponent(percentSuffix);
|
||||
|
||||
create.setEnabled(false);
|
||||
create.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
addCreateClickListener(hawkbitClient);
|
||||
final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC);
|
||||
cancel.addClickListener(e -> close());
|
||||
cancel.addClickShortcut(Key.ESCAPE);
|
||||
getFooter().add(cancel);
|
||||
getFooter().add(create);
|
||||
|
||||
final VerticalLayout layout = new VerticalLayout();
|
||||
layout.setSizeFull();
|
||||
layout.setSpacing(false);
|
||||
layout.add(
|
||||
name, distributionSet, targetFilter, description,
|
||||
actionType, startType,
|
||||
groupNumber, triggerThreshold, errorThreshold,
|
||||
dynamic);
|
||||
add(layout);
|
||||
open();
|
||||
}
|
||||
|
||||
private void readyToCreate(final Object v) {
|
||||
final boolean createEnabled = !name.isEmpty() && !distributionSet.isEmpty() && !targetFilter.isEmpty() && !groupNumber
|
||||
.isEmpty() && !triggerThreshold.isEmpty() && !errorThreshold.isEmpty();
|
||||
if (create.isEnabled() != createEnabled) {
|
||||
create.setEnabled(createEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void addCreateClickListener(final HawkbitMgmtClient hawkbitClient) {
|
||||
create.addClickListener(e -> {
|
||||
close();
|
||||
final MgmtRolloutRestRequestBodyPost request = new MgmtRolloutRestRequestBodyPost();
|
||||
request.setName(name.getValue());
|
||||
request.setDistributionSetId(distributionSet.getValue().getId());
|
||||
request.setTargetFilterQuery(targetFilter.getValue().getQuery());
|
||||
request.setDescription(description.getValue());
|
||||
|
||||
request.setType(actionType.getValue());
|
||||
if (actionType.getValue() == MgmtActionType.TIMEFORCED) {
|
||||
request.setForcetime(
|
||||
forceTime.isEmpty() ? System.currentTimeMillis() : forceTime.getValue().toEpochSecond(ZoneOffset.UTC) * 1000);
|
||||
}
|
||||
switch (startType.getValue()) {
|
||||
case AUTO -> request.setStartAt(System.currentTimeMillis());
|
||||
case SCHEDULED -> request.setStartAt(
|
||||
startAt.isEmpty() ? System.currentTimeMillis() : startAt.getValue().toEpochSecond(ZoneOffset.UTC) * 1000);
|
||||
case MANUAL -> {
|
||||
// do nothing, will be started manually
|
||||
}
|
||||
}
|
||||
|
||||
request.setAmountGroups(groupNumber.getValue().intValue());
|
||||
request.setSuccessCondition(
|
||||
new MgmtRolloutCondition(
|
||||
MgmtRolloutCondition.Condition.THRESHOLD,
|
||||
triggerThreshold.getValue().intValue() + ""));
|
||||
request.setErrorCondition(
|
||||
new MgmtRolloutCondition(
|
||||
MgmtRolloutCondition.Condition.THRESHOLD,
|
||||
errorThreshold.getValue().intValue() + ""));
|
||||
request.setErrorAction(
|
||||
new MgmtRolloutErrorAction(
|
||||
MgmtRolloutErrorAction.ErrorAction.PAUSE, ""));
|
||||
request.setDynamic(dynamic.getValue());
|
||||
hawkbitClient.getRolloutRestApi().create(request).getBody();
|
||||
});
|
||||
}
|
||||
|
||||
private enum StartType {
|
||||
MANUAL, AUTO, SCHEDULED
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.view;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.Key;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.checkbox.CheckboxGroup;
|
||||
import com.vaadin.flow.component.dependency.Uses;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.select.Select;
|
||||
import com.vaadin.flow.component.textfield.TextArea;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.component.upload.Upload;
|
||||
import com.vaadin.flow.data.renderer.ComponentRenderer;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.server.streams.UploadEvent;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.artifact.MgmtArtifact;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPost;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetType;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleAssignment;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleRequestBodyPost;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.softwaremoduletype.MgmtSoftwareModuleType;
|
||||
import org.eclipse.hawkbit.ui.HawkbitMgmtClient;
|
||||
import org.eclipse.hawkbit.ui.MainLayout;
|
||||
import org.eclipse.hawkbit.ui.view.util.Filter;
|
||||
import org.eclipse.hawkbit.ui.view.util.SelectionGrid;
|
||||
import org.eclipse.hawkbit.ui.view.util.TableView;
|
||||
import org.eclipse.hawkbit.ui.view.util.Utils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@PageTitle("Software Modules")
|
||||
@Route(value = "software_modules", layout = MainLayout.class)
|
||||
@RolesAllowed({ "SOFTWARE_MODULE_READ" })
|
||||
@Uses(Icon.class)
|
||||
@Slf4j
|
||||
public class SoftwareModuleView extends TableView<MgmtSoftwareModule, Long> {
|
||||
|
||||
@Autowired
|
||||
public SoftwareModuleView(final HawkbitMgmtClient hawkbitClient) {
|
||||
this(true, hawkbitClient);
|
||||
}
|
||||
|
||||
public SoftwareModuleView(final boolean isParent, final HawkbitMgmtClient hawkbitClient) {
|
||||
super(
|
||||
new SoftwareModuleFilter(hawkbitClient),
|
||||
new SelectionGrid.EntityRepresentation<>(MgmtSoftwareModule.class, MgmtSoftwareModule::getId) {
|
||||
|
||||
private final SoftwareModuleDetails details = new SoftwareModuleDetails(hawkbitClient);
|
||||
|
||||
@Override
|
||||
protected void addColumns(final Grid<MgmtSoftwareModule> grid) {
|
||||
grid.addColumn(MgmtSoftwareModule::getId).setHeader(Constants.ID).setAutoWidth(true);
|
||||
grid.addColumn(MgmtSoftwareModule::getName).setHeader(Constants.NAME).setAutoWidth(true);
|
||||
grid.addColumn(MgmtSoftwareModule::getVersion).setHeader(Constants.VERSION).setAutoWidth(true);
|
||||
grid.addColumn(MgmtSoftwareModule::getTypeName).setHeader(Constants.TYPE).setAutoWidth(true);
|
||||
grid.addColumn(MgmtSoftwareModule::getVendor).setHeader(Constants.VENDOR).setAutoWidth(true);
|
||||
|
||||
grid.setItemDetailsRenderer(new ComponentRenderer<>(() -> details, SoftwareModuleDetails::setItem));
|
||||
}
|
||||
},
|
||||
(query, rsqlFilter) -> Optional.ofNullable(
|
||||
hawkbitClient.getSoftwareModuleRestApi()
|
||||
.getSoftwareModules(rsqlFilter, query.getOffset(), query.getPageSize(), Constants.NAME_ASC)
|
||||
.getBody())
|
||||
.stream().map(PagedList::getContent).flatMap(List::stream),
|
||||
isParent ? v -> new CreateDialog(hawkbitClient).result() : null,
|
||||
isParent ? selectionGrid -> {
|
||||
selectionGrid.getSelectedItems().forEach(
|
||||
module -> hawkbitClient.getSoftwareModuleRestApi().deleteSoftwareModule(module.getId()));
|
||||
selectionGrid.refreshGrid(false);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
} : null);
|
||||
}
|
||||
|
||||
public Set<MgmtSoftwareModule> getSelection() {
|
||||
return selectionGrid.getSelectedItems();
|
||||
}
|
||||
|
||||
private static SelectionGrid<MgmtArtifact, Long> createArtifactGrid() {
|
||||
return new SelectionGrid<>(
|
||||
new SelectionGrid.EntityRepresentation<>(MgmtArtifact.class, MgmtArtifact::getId) {
|
||||
|
||||
@Override
|
||||
protected void addColumns(final Grid<MgmtArtifact> grid) {
|
||||
grid.addColumn(MgmtArtifact::getId).setHeader(Constants.ID).setAutoWidth(true);
|
||||
grid.addColumn(MgmtArtifact::getProvidedFilename).setHeader(Constants.NAME).setAutoWidth(true);
|
||||
grid.addColumn(MgmtArtifact::getSize).setHeader("Size").setAutoWidth(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class SoftwareModuleFilter implements Filter.Rsql {
|
||||
|
||||
private final TextField name = Utils.textField(Constants.NAME);
|
||||
private final CheckboxGroup<MgmtSoftwareModuleType> type = new CheckboxGroup<>(Constants.TYPE);
|
||||
|
||||
private SoftwareModuleFilter(final HawkbitMgmtClient hawkbitClient) {
|
||||
name.setPlaceholder("<name filter>");
|
||||
type.setItemLabelGenerator(MgmtSoftwareModuleType::getName);
|
||||
type.setItems(Optional.ofNullable(
|
||||
hawkbitClient.getSoftwareModuleTypeRestApi()
|
||||
.getTypes(null, 0, 20, Constants.NAME_ASC)
|
||||
.getBody())
|
||||
.map(PagedList::getContent)
|
||||
.orElseGet(Collections::emptyList));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Component> components() {
|
||||
return List.of(name, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String filter() {
|
||||
return Filter.filter(
|
||||
Map.of(
|
||||
"name", name.getOptionalValue(),
|
||||
"type", type.getSelectedItems().stream().map(MgmtSoftwareModuleType::getKey).toList()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private static class SoftwareModuleDetails extends FormLayout {
|
||||
|
||||
private final transient HawkbitMgmtClient hawkbitClient;
|
||||
|
||||
private final TextArea description = new TextArea(Constants.DESCRIPTION);
|
||||
private final TextField createdBy = Utils.textField(Constants.CREATED_BY);
|
||||
private final TextField createdAt = Utils.textField(Constants.CREATED_AT);
|
||||
private final TextField lastModifiedBy = Utils.textField(Constants.LAST_MODIFIED_BY);
|
||||
private final TextField lastModifiedAt = Utils.textField(Constants.LAST_MODIFIED_AT);
|
||||
private final SelectionGrid<MgmtArtifact, Long> artifactGrid;
|
||||
|
||||
private SoftwareModuleDetails(final HawkbitMgmtClient hawkbitClient) {
|
||||
this.hawkbitClient = hawkbitClient;
|
||||
|
||||
description.setMinLength(2);
|
||||
artifactGrid = createArtifactGrid();
|
||||
Stream.of(description, createdBy, createdAt, lastModifiedBy, lastModifiedAt).forEach(field -> {
|
||||
field.setReadOnly(true);
|
||||
add(field);
|
||||
});
|
||||
add(artifactGrid);
|
||||
|
||||
setResponsiveSteps(new ResponsiveStep("0", 2));
|
||||
setColspan(description, 2);
|
||||
setColspan(artifactGrid, 2);
|
||||
}
|
||||
|
||||
private void setItem(final MgmtSoftwareModule softwareModule) {
|
||||
description.setValue(softwareModule.getDescription());
|
||||
createdBy.setValue(softwareModule.getCreatedBy());
|
||||
createdAt.setValue(Utils.localDateTimeFromTs(softwareModule.getCreatedAt()));
|
||||
lastModifiedBy.setValue(softwareModule.getLastModifiedBy());
|
||||
lastModifiedAt.setValue(Utils.localDateTimeFromTs(softwareModule.getLastModifiedAt()));
|
||||
|
||||
artifactGrid.setItems(query -> Optional.ofNullable(
|
||||
hawkbitClient.getSoftwareModuleRestApi()
|
||||
.getArtifacts(softwareModule.getId(), null, null)
|
||||
.getBody())
|
||||
.stream()
|
||||
.flatMap(Collection::stream)
|
||||
.skip(query.getOffset())
|
||||
.limit(query.getPageSize()));
|
||||
artifactGrid.setSelectionMode(Grid.SelectionMode.NONE);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CreateDialog extends Utils.BaseDialog<Void> {
|
||||
|
||||
private final Select<MgmtSoftwareModuleType> type;
|
||||
private final TextField name;
|
||||
private final TextField version;
|
||||
private final TextField vendor;
|
||||
private final TextArea description;
|
||||
private final Checkbox enableArtifactEncryption;
|
||||
private final Checkbox createDistributionSet;
|
||||
private final Select<MgmtDistributionSetType> distType;
|
||||
private final Checkbox distRequiredMigrationStep;
|
||||
private final Button create;
|
||||
|
||||
private CreateDialog(final HawkbitMgmtClient hawkbitClient) {
|
||||
super("Create Software Module");
|
||||
|
||||
type = new Select<>(
|
||||
Constants.TYPE,
|
||||
this::readyToCreate,
|
||||
Optional.ofNullable(
|
||||
hawkbitClient.getSoftwareModuleTypeRestApi()
|
||||
.getTypes(null, 0, 30, Constants.NAME_ASC)
|
||||
.getBody())
|
||||
.map(body -> body.getContent().toArray(new MgmtSoftwareModuleType[0]))
|
||||
.orElseGet(() -> new MgmtSoftwareModuleType[0]));
|
||||
type.setWidthFull();
|
||||
type.setRequiredIndicatorVisible(true);
|
||||
type.setItemLabelGenerator(MgmtSoftwareModuleType::getName);
|
||||
type.focus();
|
||||
|
||||
name = Utils.textField(Constants.NAME, this::readyToCreate);
|
||||
version = Utils.textField(Constants.VERSION, this::readyToCreate);
|
||||
vendor = Utils.textField(Constants.VENDOR);
|
||||
description = new TextArea(Constants.DESCRIPTION);
|
||||
description.setWidthFull();
|
||||
description.setMinLength(2);
|
||||
enableArtifactEncryption = new Checkbox("Enable artifact encryption");
|
||||
|
||||
distType = new Select<>("Distribution Set Type", this::readyToCreate);
|
||||
distType.setWidthFull();
|
||||
distType.setRequiredIndicatorVisible(true);
|
||||
distType.setItemLabelGenerator(MgmtDistributionSetType::getName);
|
||||
distType.setVisible(false);
|
||||
|
||||
distRequiredMigrationStep = new Checkbox("Required Migration Step");
|
||||
distRequiredMigrationStep.setVisible(false);
|
||||
|
||||
createDistributionSet = new Checkbox("Create single software module distribution set");
|
||||
createDistributionSet.setHelperText("Create single software module distribution set with this software module");
|
||||
createDistributionSet.addValueChangeListener(e -> {
|
||||
if (Boolean.TRUE.equals(createDistributionSet.getValue()) && distType.isEmpty()) {
|
||||
distType.setItems(
|
||||
Optional.ofNullable(
|
||||
hawkbitClient.getDistributionSetTypeRestApi()
|
||||
.getDistributionSetTypes(null, 0, 30, Constants.NAME_ASC)
|
||||
.getBody())
|
||||
.map(body -> body.getContent().toArray(new MgmtDistributionSetType[0]))
|
||||
.orElseGet(() -> new MgmtDistributionSetType[0]));
|
||||
}
|
||||
distType.setVisible(createDistributionSet.getValue());
|
||||
distRequiredMigrationStep.setVisible(createDistributionSet.getValue());
|
||||
readyToCreate(e);
|
||||
});
|
||||
|
||||
create = Utils.tooltip(new Button("Create"), "Create (Enter)");
|
||||
create.setEnabled(false);
|
||||
addCreateClickListener(hawkbitClient);
|
||||
create.addClickShortcut(Key.ENTER);
|
||||
create.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC);
|
||||
cancel.addClickListener(e -> close());
|
||||
cancel.addClickShortcut(Key.ESCAPE);
|
||||
getFooter().add(cancel);
|
||||
getFooter().add(create);
|
||||
|
||||
final VerticalLayout layout = new VerticalLayout();
|
||||
layout.setSizeFull();
|
||||
layout.setSpacing(false);
|
||||
layout.add(
|
||||
type, name, version, vendor, description, enableArtifactEncryption,
|
||||
createDistributionSet, distType, distRequiredMigrationStep);
|
||||
add(layout);
|
||||
open();
|
||||
}
|
||||
|
||||
private void readyToCreate(final Object v) {
|
||||
final boolean createEnabled = !type.isEmpty() && !name.isEmpty() && !version.isEmpty() && (!createDistributionSet
|
||||
.getValue() || !distType.isEmpty());
|
||||
if (create.isEnabled() != createEnabled) {
|
||||
create.setEnabled(createEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void addCreateClickListener(final HawkbitMgmtClient hawkbitClient) {
|
||||
create.addClickListener(e -> {
|
||||
close();
|
||||
final long softwareModuleId = Optional.ofNullable(
|
||||
hawkbitClient.getSoftwareModuleRestApi().createSoftwareModules(
|
||||
List.of(new MgmtSoftwareModuleRequestBodyPost()
|
||||
.setType(type.getValue().getKey())
|
||||
.setName(name.getValue())
|
||||
.setVersion(version.getValue())
|
||||
.setVendor(vendor.getValue())
|
||||
.setDescription(description.getValue())
|
||||
.setEncrypted(enableArtifactEncryption.getValue())))
|
||||
.getBody())
|
||||
.stream().flatMap(Collection::stream)
|
||||
.findFirst()
|
||||
.orElseThrow()
|
||||
.getId();
|
||||
if (Boolean.TRUE.equals(createDistributionSet.getValue())) {
|
||||
final long distributionSetId = Optional.ofNullable(
|
||||
hawkbitClient.getDistributionSetRestApi()
|
||||
.createDistributionSets(
|
||||
List.of((MgmtDistributionSetRequestBodyPost) new MgmtDistributionSetRequestBodyPost()
|
||||
.setType(distType.getValue().getKey())
|
||||
.setName(name.getValue())
|
||||
.setVersion(version.getValue())
|
||||
.setDescription(description.getValue())
|
||||
.setRequiredMigrationStep(distRequiredMigrationStep.getValue())))
|
||||
.getBody())
|
||||
.stream()
|
||||
.flatMap(Collection::stream)
|
||||
.findFirst()
|
||||
.orElseThrow()
|
||||
.getId();
|
||||
hawkbitClient.getDistributionSetRestApi().assignSoftwareModules(
|
||||
distributionSetId, List.of((MgmtSoftwareModuleAssignment) new MgmtSoftwareModuleAssignment()
|
||||
.setId(softwareModuleId))).getBody();
|
||||
}
|
||||
new AddArtifactsDialog(softwareModuleId, hawkbitClient).open();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static class AddArtifactsDialog extends Utils.BaseDialog<Void> {
|
||||
|
||||
private final transient Set<MgmtArtifact> artifacts = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
private AddArtifactsDialog(
|
||||
final long softwareModuleId,
|
||||
final HawkbitMgmtClient hawkbitClient) {
|
||||
super("Add Artifacts");
|
||||
|
||||
final SelectionGrid<MgmtArtifact, Long> artifactGrid = createArtifactGrid();
|
||||
artifactGrid.setItems(query -> {
|
||||
query.getOffset(); // to keep vaadin contract
|
||||
return artifacts.stream().limit(query.getLimit());
|
||||
});
|
||||
artifactGrid.setSelectionMode(Grid.SelectionMode.NONE);
|
||||
|
||||
final Upload uploadBtn = new Upload(uploadEvent -> {
|
||||
final MgmtArtifact artifact = hawkbitClient.getSoftwareModuleRestApi()
|
||||
.uploadArtifact(
|
||||
softwareModuleId,
|
||||
new MultipartFileImpl(uploadEvent),
|
||||
uploadEvent.getFileName(), null, null, null)
|
||||
.getBody();
|
||||
artifacts.add(artifact);
|
||||
artifactGrid.refreshGrid(false);
|
||||
});
|
||||
uploadBtn.setMaxFiles(10);
|
||||
uploadBtn.setWidthFull();
|
||||
uploadBtn.setDropAllowed(true);
|
||||
|
||||
final Button finishBtn = Utils.tooltip(new Button("Finish"), "Finish (Enter)");
|
||||
finishBtn.addClickListener(e -> close());
|
||||
finishBtn.addClickShortcut(Key.ENTER);
|
||||
finishBtn.setHeightFull();
|
||||
finishBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
getFooter().add(finishBtn);
|
||||
|
||||
final VerticalLayout layout = new VerticalLayout(artifactGrid, uploadBtn);
|
||||
layout.setSizeFull();
|
||||
layout.setSpacing(false);
|
||||
add(layout);
|
||||
}
|
||||
|
||||
private static class MultipartFileImpl implements MultipartFile {
|
||||
|
||||
private final UploadEvent uploadEvent;
|
||||
|
||||
public MultipartFileImpl(final UploadEvent uploadEvent) {
|
||||
this.uploadEvent = uploadEvent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return uploadEvent.getFileName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOriginalFilename() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return uploadEvent.getContentType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return uploadEvent.getFileSize() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
return uploadEvent.getFileSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBytes() throws IOException {
|
||||
log.warn("Multipart file getBytes() is called. Whole input stream is loaded into the memory!");
|
||||
try (final InputStream is = getInputStream()) {
|
||||
return is.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return uploadEvent.getInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transferTo(final File dest) throws IllegalStateException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,963 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.view;
|
||||
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.vaadin.flow.data.provider.ListDataProvider;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
|
||||
import com.vaadin.flow.component.AttachEvent;
|
||||
import com.vaadin.flow.component.ClickEvent;
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.ComponentEventListener;
|
||||
import com.vaadin.flow.component.Key;
|
||||
import com.vaadin.flow.component.Unit;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.checkbox.CheckboxGroup;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.datetimepicker.DateTimePicker;
|
||||
import com.vaadin.flow.component.dependency.Uses;
|
||||
import com.vaadin.flow.component.formlayout.FormLayout;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.Input;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.select.Select;
|
||||
import com.vaadin.flow.component.tabs.TabSheet;
|
||||
import com.vaadin.flow.component.textfield.TextArea;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.data.renderer.ComponentRenderer;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.shared.Registration;
|
||||
import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.MgmtPollStatus;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionStatus;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentRequestBody;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.tag.MgmtTag;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.tag.MgmtTagRequestBodyPut;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetAttributes;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetRequestBody;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQuery;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQueryRequestBody;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.targettype.MgmtTargetType;
|
||||
import org.eclipse.hawkbit.ui.HawkbitMgmtClient;
|
||||
import org.eclipse.hawkbit.ui.MainLayout;
|
||||
import org.eclipse.hawkbit.ui.component.TargetActionsHistory;
|
||||
import org.eclipse.hawkbit.ui.view.util.Filter;
|
||||
import org.eclipse.hawkbit.ui.view.util.LinkedTextArea;
|
||||
import org.eclipse.hawkbit.ui.view.util.SelectionGrid;
|
||||
import org.eclipse.hawkbit.ui.view.util.TableView;
|
||||
import org.eclipse.hawkbit.ui.view.util.Utils;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
@PageTitle("Targets")
|
||||
@Route(value = "targets", layout = MainLayout.class)
|
||||
@RolesAllowed({ "TARGET_READ" })
|
||||
@Uses(Icon.class)
|
||||
public class TargetView extends TableView<TargetView.TargetWithDs, String> {
|
||||
|
||||
public static final String STATUS = "Status";
|
||||
public static final String UPDATE = "Sync";
|
||||
public static final String CONTROLLER_ID = "Controller Id";
|
||||
public static final String FILTER = "Filter";
|
||||
public static final String TAG = "Tag";
|
||||
|
||||
public static final String GREEN = "green";
|
||||
public static final String RED = "red";
|
||||
public static final String ORANGE = "orange";
|
||||
public static final String GRAY = "gray";
|
||||
public static final String BROWN = "brown";
|
||||
public static final String TEAL = "teal";
|
||||
public static final String PURPLE = "purple";
|
||||
public static final String CORAL = "coral";
|
||||
public static final String BLACK = "black";
|
||||
|
||||
public TargetView(final HawkbitMgmtClient hawkbitClient) {
|
||||
super(
|
||||
new RawFilter(hawkbitClient), new SimpleFilter(hawkbitClient),
|
||||
new SelectionGrid.EntityRepresentation<>(TargetWithDs.class, TargetWithDs::getControllerId) {
|
||||
|
||||
@Override
|
||||
protected void addColumns(final Grid<TargetWithDs> grid) {
|
||||
grid.addColumn(new ComponentRenderer<>(TargetStatusCell::new))
|
||||
.setHeader(STATUS)
|
||||
.setAutoWidth(true)
|
||||
.setFlexGrow(0).setKey("lastControllerRequestAt").setSortable(true);
|
||||
grid.addColumn(new ComponentRenderer<>(TargetUpdateStatusCell::new))
|
||||
.setHeader(UPDATE)
|
||||
.setAutoWidth(true)
|
||||
.setFlexGrow(0).setKey("updateStatus").setSortable(true);
|
||||
grid.addColumn(MgmtTarget::getControllerId).setHeader(CONTROLLER_ID).setAutoWidth(true).setKey("id").setSortable(true);
|
||||
grid.addColumn(Utils.localDateTimeRenderer(MgmtTarget::getLastModifiedAt)).setHeader(LAST_MODIFIED_AT).setAutoWidth(
|
||||
true).setKey("lastModifiedAt").setSortable(true);
|
||||
grid.addColumn(MgmtTarget::getName).setHeader(Constants.NAME).setAutoWidth(true).setKey("name").setSortable(true);
|
||||
grid.addColumn(MgmtTarget::getTargetTypeName).setHeader(Constants.TYPE).setAutoWidth(true).setKey("targetType")
|
||||
.setSortable(true);
|
||||
grid.addColumn(TargetWithDs::getDsName).setHeader(Constants.DISTRIBUTION_SET).setAutoWidth(true);
|
||||
grid.addColumn(TargetWithDs::getDsVersion).setHeader(Constants.VERSION).setAutoWidth(true).setKey("installedds")
|
||||
.setSortable(true);
|
||||
}
|
||||
},
|
||||
(query, filter) -> hawkbitClient.getTargetRestApi()
|
||||
.getTargets(filter, query.getOffset(), query.getPageSize(), Utils.getSortParam(query.getSortOrders(),
|
||||
"lastModifiedAt:desc"))
|
||||
.getBody()
|
||||
.getContent()
|
||||
.stream().map(m -> TargetWithDs.from(hawkbitClient, m)),
|
||||
source -> new RegisterDialog(hawkbitClient).result(),
|
||||
selectionGrid -> {
|
||||
selectionGrid.getSelectedItems()
|
||||
.forEach(toDelete -> hawkbitClient.getTargetRestApi().deleteTarget(toDelete.getControllerId()));
|
||||
return CompletableFuture.completedFuture(null);
|
||||
},
|
||||
target -> {
|
||||
final TargetDetailedView targetDetailedView = new TargetDetailedView(hawkbitClient);
|
||||
targetDetailedView.setItem(target);
|
||||
return targetDetailedView;
|
||||
}
|
||||
);
|
||||
|
||||
final Function<SelectionGrid<TargetWithDs, String>, CompletionStage<Void>> assignHandler = source -> new AssignDialog(
|
||||
hawkbitClient, source.getSelectedItems()).result();
|
||||
|
||||
final Button assignBtn = Utils.tooltip(new Button(VaadinIcon.LINK.create()), "Assign");
|
||||
assignBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
assignBtn.addClickListener(e -> assignHandler.apply(selectionGrid).thenAccept(v -> selectionGrid.refreshGrid(false)));
|
||||
controlsLayout.addComponentAtIndex(0, assignBtn);
|
||||
}
|
||||
|
||||
private static class SimpleFilter implements Filter.Rsql {
|
||||
|
||||
private final HawkbitMgmtClient hawkbitClient;
|
||||
|
||||
private final TextField textFilter;
|
||||
private final CheckboxGroup<MgmtTargetType> type;
|
||||
private final CheckboxGroup<MgmtTag> tag;
|
||||
|
||||
private SimpleFilter(final HawkbitMgmtClient hawkbitClient) {
|
||||
this.hawkbitClient = hawkbitClient;
|
||||
|
||||
textFilter = Utils.textField(FILTER);
|
||||
textFilter.setPlaceholder("<controller id/name filter>");
|
||||
type = new CheckboxGroup<>(Constants.TYPE);
|
||||
type.setItemLabelGenerator(MgmtTargetType::getName);
|
||||
tag = new CheckboxGroup<>(TAG);
|
||||
tag.setItemLabelGenerator(MgmtTag::getName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Component> components() {
|
||||
final List<Component> components = new LinkedList<>();
|
||||
components.add(textFilter);
|
||||
type.setItems(hawkbitClient.getTargetTypeRestApi().getTargetTypes(null, 0, 20, Constants.NAME_ASC).getBody().getContent());
|
||||
if (!((ListDataProvider) type.getDataProvider()).getItems().isEmpty()) {
|
||||
components.add(type);
|
||||
}
|
||||
tag.setItems(hawkbitClient.getTargetTagRestApi().getTargetTags(null, 0, 20, Constants.NAME_ASC).getBody().getContent());
|
||||
if (!((ListDataProvider) tag.getDataProvider()).getItems().isEmpty()) {
|
||||
components.add(tag);
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String filter() {
|
||||
return Filter.filter(
|
||||
Map.of(
|
||||
List.of("controllerid", "name"), textFilter.getOptionalValue().map(s -> "*" + s + "*"),
|
||||
"targettype.name", type.getSelectedItems().stream().map(MgmtTargetType::getName)
|
||||
.toList(),
|
||||
"tag", tag.getSelectedItems().stream().map(MgmtTag::getName).toList()));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "java:S1171", "java:S3599" })
|
||||
private static class RawFilter implements Filter.Rsql, Filter.RsqlRw {
|
||||
|
||||
private final TextField textFilter = new TextField("Raw Filter", "<raw filter>");
|
||||
private final VerticalLayout layout = new VerticalLayout();
|
||||
private final Select<MgmtTargetFilterQuery> savedFilters = new Select<>();
|
||||
|
||||
private RawFilter(final HawkbitMgmtClient hawkbitClient) {
|
||||
final Button createBtn = Utils.tooltip(new Button("Save", VaadinIcon.PLUS.create()), "Save");
|
||||
final Button updateBtn = Utils.tooltip(new Button(VaadinIcon.HARDDRIVE.create()), "Update");
|
||||
updateBtn.setEnabled(false);
|
||||
|
||||
savedFilters.setLabel("Saved Filters");
|
||||
savedFilters.addValueChangeListener(e -> {
|
||||
if (e.getValue() != null) {
|
||||
textFilter.setValue(e.getValue().getQuery());
|
||||
updateBtn.setEnabled(true);
|
||||
createBtn.setText("Save as");
|
||||
} else {
|
||||
textFilter.clear();
|
||||
updateBtn.setEnabled(false);
|
||||
createBtn.setText("Save");
|
||||
}
|
||||
});
|
||||
savedFilters.setEmptySelectionAllowed(true);
|
||||
savedFilters.setItems(listFilters(hawkbitClient));
|
||||
savedFilters.setItemLabelGenerator(query -> Optional.ofNullable(query).map(MgmtTargetFilterQuery::getName).orElse(
|
||||
"<select saved filter>"));
|
||||
savedFilters.setWidthFull();
|
||||
|
||||
textFilter.setWidthFull();
|
||||
createBtn.addClickListener(createBtnListener(hawkbitClient));
|
||||
updateBtn.addClickListener(updateBtnListener(hawkbitClient));
|
||||
|
||||
layout.setSpacing(false);
|
||||
layout.setPadding(false);
|
||||
final HorizontalLayout textSaveLayout = new HorizontalLayout(textFilter, createBtn, updateBtn);
|
||||
textSaveLayout.setAlignItems(FlexComponent.Alignment.BASELINE);
|
||||
textSaveLayout.setWidthFull();
|
||||
layout.add(savedFilters, textSaveLayout);
|
||||
layout.addClassNames(LumoUtility.Gap.SMALL);
|
||||
}
|
||||
|
||||
private static List<MgmtTargetFilterQuery> listFilters(HawkbitMgmtClient hawkbitClient) {
|
||||
return Optional
|
||||
.ofNullable(
|
||||
hawkbitClient.getTargetFilterQueryRestApi()
|
||||
.getFilters(null, 0, 30, null, null)
|
||||
.getBody()
|
||||
.getContent())
|
||||
.orElse(Collections.emptyList());
|
||||
}
|
||||
|
||||
private ComponentEventListener<ClickEvent<Button>> createBtnListener(HawkbitMgmtClient hawkbitClient) {
|
||||
return e -> new Utils.BaseDialog<Void>("Create New Filter") {
|
||||
|
||||
{
|
||||
final Button finishBtn = Utils.tooltip(new Button("Save"), "Save (Enter)");
|
||||
final TextField name = Utils.textField(Constants.NAME, e -> finishBtn.setEnabled(!e.getHasValue().isEmpty()));
|
||||
name.focus();
|
||||
finishBtn.addClickShortcut(Key.ENTER);
|
||||
finishBtn.setEnabled(false);
|
||||
finishBtn.addClickListener(e -> {
|
||||
final MgmtTargetFilterQueryRequestBody createRequest = new MgmtTargetFilterQueryRequestBody();
|
||||
createRequest.setName(name.getValue());
|
||||
createRequest.setQuery(textFilter.getValue());
|
||||
hawkbitClient.getTargetFilterQueryRestApi().createFilter(createRequest);
|
||||
savedFilters.setItems(listFilters(hawkbitClient));
|
||||
close();
|
||||
});
|
||||
getFooter().add(finishBtn);
|
||||
add(name);
|
||||
open();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private ComponentEventListener<ClickEvent<Button>> updateBtnListener(HawkbitMgmtClient hawkbitClient) {
|
||||
return e -> {
|
||||
final MgmtTargetFilterQuery selected = savedFilters.getValue();
|
||||
if (selected == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
new Utils.BaseDialog<Void>("Update Filter") {
|
||||
|
||||
{
|
||||
final Button finishBtn = Utils.tooltip(new Button("Update"), "Update (Enter)");
|
||||
finishBtn.setEnabled(false);
|
||||
|
||||
final TextField name = Utils.textField(Constants.NAME, e -> finishBtn.setEnabled(!e.getHasValue().isEmpty()));
|
||||
name.focus();
|
||||
name.setValue(selected.getName());
|
||||
|
||||
final TextArea filterValue = new TextArea("Filter Value");
|
||||
filterValue.setReadOnly(true);
|
||||
filterValue.setValue(textFilter.getValue());
|
||||
filterValue.setWidthFull();
|
||||
|
||||
finishBtn.addClickShortcut(Key.ENTER);
|
||||
finishBtn.addClickListener(e -> {
|
||||
final MgmtTargetFilterQueryRequestBody updateRequest = new MgmtTargetFilterQueryRequestBody();
|
||||
updateRequest.setName(name.getValue());
|
||||
updateRequest.setQuery(textFilter.getValue());
|
||||
hawkbitClient.getTargetFilterQueryRestApi().updateFilter(selected.getId(), updateRequest);
|
||||
savedFilters.setItems(listFilters(hawkbitClient));
|
||||
close();
|
||||
});
|
||||
getFooter().add(finishBtn);
|
||||
|
||||
add(name);
|
||||
add(filterValue);
|
||||
open();
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Component> components() {
|
||||
return List.of(layout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String filter() {
|
||||
return textFilter.getOptionalValue().orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFilter(String filter) {
|
||||
textFilter.setValue(filter);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class TargetDetailedView extends TabSheet {
|
||||
|
||||
private final TargetDetails targetDetails;
|
||||
private final TargetAssignedInstalled targetAssignedInstalled;
|
||||
private final TargetTags targetTags;
|
||||
private final TargetActionsHistoryLayout targetActionsHistoryLayout;
|
||||
|
||||
private TargetDetailedView(final HawkbitMgmtClient hawkbitClient) {
|
||||
targetDetails = new TargetDetails(hawkbitClient);
|
||||
targetAssignedInstalled = new TargetAssignedInstalled(hawkbitClient);
|
||||
targetTags = new TargetTags(hawkbitClient);
|
||||
targetActionsHistoryLayout = new TargetActionsHistoryLayout(hawkbitClient);
|
||||
setWidthFull();
|
||||
|
||||
add("Details", targetDetails);
|
||||
add("Assigned / Installed", targetAssignedInstalled);
|
||||
add("Tags", targetTags);
|
||||
add("Action History", targetActionsHistoryLayout);
|
||||
}
|
||||
|
||||
private void setItem(final MgmtTarget target) {
|
||||
this.targetDetails.setItem(target);
|
||||
this.targetAssignedInstalled.setItem(target);
|
||||
this.targetTags.setItem(target);
|
||||
this.targetActionsHistoryLayout.setItem(target);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TargetDetails extends FormLayout {
|
||||
|
||||
private final transient HawkbitMgmtClient hawkbitClient;
|
||||
private final TextArea description = new TextArea(Constants.DESCRIPTION);
|
||||
private final TextField createdBy = Utils.textField(Constants.CREATED_BY);
|
||||
private final TextField createdAt = Utils.textField(Constants.CREATED_AT);
|
||||
private final TextField lastModifiedBy = Utils.textField(Constants.LAST_MODIFIED_BY);
|
||||
private final TextField lastModifiedAt = Utils.textField(Constants.LAST_MODIFIED_AT);
|
||||
private final TextField securityToken = Utils.textField(Constants.SECURITY_TOKEN);
|
||||
private final TextField lastPoll = Utils.textField(Constants.LAST_POLL);
|
||||
private final TextField group = Utils.textField(Constants.GROUP);
|
||||
private final TextField targetAddress = Utils.textField(Constants.ADDRESS);
|
||||
private final TextArea targetAttributes = new TextArea(Constants.ATTRIBUTES);
|
||||
private transient MgmtTarget target;
|
||||
|
||||
private TargetDetails(final HawkbitMgmtClient hawkbitClient) {
|
||||
this.hawkbitClient = hawkbitClient;
|
||||
description.setMinLength(2);
|
||||
Stream.of(
|
||||
description,
|
||||
createdBy, createdAt,
|
||||
lastModifiedBy, lastModifiedAt,
|
||||
securityToken, lastPoll, group, targetAddress, targetAttributes
|
||||
)
|
||||
.forEach(field -> {
|
||||
field.setReadOnly(true);
|
||||
add(field);
|
||||
});
|
||||
|
||||
setResponsiveSteps(new ResponsiveStep("0", 2));
|
||||
setColspan(description, 2);
|
||||
}
|
||||
|
||||
private void setItem(final MgmtTarget target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttach(final AttachEvent attachEvent) {
|
||||
description.setValue(target.getDescription() == null ? "N/A" : target.getDescription());
|
||||
createdBy.setValue(target.getCreatedBy());
|
||||
createdAt.setValue(Utils.localDateTimeFromTs(target.getCreatedAt()));
|
||||
lastModifiedBy.setValue(target.getLastModifiedBy());
|
||||
lastModifiedAt.setValue(Utils.localDateTimeFromTs(target.getLastModifiedAt()));
|
||||
securityToken.setValue(Objects.requireNonNullElse(target.getSecurityToken(), ""));
|
||||
group.setValue(target.getGroup() != null ? target.getGroup() : "");
|
||||
targetAddress.setValue(target.getAddress() != null ? target.getAddress() : "");
|
||||
|
||||
final MgmtPollStatus pollStatus = target.getPollStatus();
|
||||
lastPoll.setValue(pollStatus == null ? NOT_AVAILABLE_NULL : Utils.localDateTimeFromTs(pollStatus.getLastRequestAt()));
|
||||
final ResponseEntity<MgmtTargetAttributes> response = hawkbitClient.getTargetRestApi().getAttributes(target.getControllerId());
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
targetAttributes.setValue(Objects.requireNonNullElse(response.getBody(), Collections.emptyMap()).entrySet().stream()
|
||||
.map(entry -> entry.getKey() + ": " + entry.getValue())
|
||||
.collect(Collectors.joining("\n")));
|
||||
} else {
|
||||
targetAttributes.setValue("Error occurred fetching attributes from server: " + response.getStatusCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class TargetAssignedInstalled extends FormLayout {
|
||||
|
||||
private final transient HawkbitMgmtClient hawkbitClient;
|
||||
private final LinkedTextArea assigned = new LinkedTextArea("Assigned Distribution Set", "/distribution_sets?");
|
||||
private final LinkedTextArea installed = new LinkedTextArea("Installed Distribution Set", "/distribution_sets?");
|
||||
private transient MgmtTarget target;
|
||||
|
||||
private TargetAssignedInstalled(HawkbitMgmtClient hawkbitClient) {
|
||||
this.hawkbitClient = hawkbitClient;
|
||||
assigned.setWidthFull();
|
||||
installed.setWidthFull();
|
||||
add(assigned, installed);
|
||||
setResponsiveSteps(new FormLayout.ResponsiveStep("0", 2));
|
||||
}
|
||||
|
||||
private void setItem(final MgmtTarget target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttach(AttachEvent attachEvent) {
|
||||
updateDistributionSetInfo(() -> hawkbitClient.getTargetRestApi().getInstalledDistributionSet(target.getControllerId()), installed);
|
||||
updateDistributionSetInfo(() -> hawkbitClient.getTargetRestApi().getAssignedDistributionSet(target.getControllerId()), assigned);
|
||||
}
|
||||
|
||||
private void updateDistributionSetInfo(Supplier<ResponseEntity<MgmtDistributionSet>> supplier, LinkedTextArea textArea) {
|
||||
Optional.ofNullable(supplier.get())
|
||||
.map(ResponseEntity<MgmtDistributionSet>::getBody)
|
||||
.ifPresentOrElse(value -> {
|
||||
final String description = """
|
||||
Name: %s
|
||||
Version: %s
|
||||
%s
|
||||
""".replace("\n", System.lineSeparator());
|
||||
textArea.setValueWithLink(description.formatted(
|
||||
value.getName(),
|
||||
value.getVersion(),
|
||||
value.getModules().stream().map(module -> module.getTypeName() + ": " + module.getVersion())
|
||||
.collect(Collectors.joining(System.lineSeparator()))
|
||||
), "q=id%3D%3D" + value.getId().toString());
|
||||
},
|
||||
() -> textArea.setValueWithLink("", null));
|
||||
}
|
||||
}
|
||||
|
||||
private static class TargetTags extends VerticalLayout {
|
||||
|
||||
private final transient HawkbitMgmtClient hawkbitClient;
|
||||
private final ComboBox<MgmtTag> tagSelector = new ComboBox<>(TAG);
|
||||
private final HorizontalLayout tagsArea = new HorizontalLayout();
|
||||
private Registration changeListener;
|
||||
private transient MgmtTarget target;
|
||||
|
||||
private TargetTags(HawkbitMgmtClient hawkbitClient) {
|
||||
this.hawkbitClient = hawkbitClient;
|
||||
setWidthFull();
|
||||
setPadding(false);
|
||||
setSpacing(true);
|
||||
setAlignItems(FlexComponent.Alignment.STRETCH);
|
||||
setJustifyContentMode(JustifyContentMode.CENTER);
|
||||
|
||||
final HorizontalLayout tagSelectorLayout = buildTagSelectionLayout(hawkbitClient);
|
||||
add(tagSelectorLayout);
|
||||
|
||||
tagsArea.setWrap(true);
|
||||
tagsArea.setWidthFull();
|
||||
add(tagsArea);
|
||||
}
|
||||
|
||||
private HorizontalLayout buildTagSelectionLayout(HawkbitMgmtClient hawkbitClient) {
|
||||
final Button createTagButton = new Button("Create Tag");
|
||||
createTagButton.addClickListener(event -> new CreateTagDialog(hawkbitClient, () -> tagSelector.setItems(fetchAvailableTags()))
|
||||
.result());
|
||||
|
||||
tagSelector.setWidthFull();
|
||||
tagSelector.setItemLabelGenerator(MgmtTag::getName);
|
||||
tagSelector.setClearButtonVisible(true);
|
||||
|
||||
final HorizontalLayout tagSelectorLayout = new HorizontalLayout(tagSelector, createTagButton);
|
||||
tagSelectorLayout.setWidthFull();
|
||||
tagSelectorLayout.setSpacing(true);
|
||||
tagSelectorLayout.setAlignItems(Alignment.END);
|
||||
tagSelectorLayout.setJustifyContentMode(JustifyContentMode.CENTER);
|
||||
return tagSelectorLayout;
|
||||
}
|
||||
|
||||
private void setItem(final MgmtTarget target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttach(AttachEvent attachEvent) {
|
||||
tagSelector.setItems(fetchAvailableTags());
|
||||
if (changeListener != null) {
|
||||
changeListener.remove();
|
||||
}
|
||||
changeListener = tagSelector.addValueChangeListener(event -> {
|
||||
if (event.getValue() != null) {
|
||||
hawkbitClient.getTargetTagRestApi().assignTarget(event.getValue().getId(), target.getControllerId());
|
||||
refreshTargetTagsList(target);
|
||||
tagSelector.clear();
|
||||
}
|
||||
});
|
||||
refreshTargetTagsList(target);
|
||||
}
|
||||
|
||||
private void refreshTargetTagsList(MgmtTarget target) {
|
||||
tagsArea.removeAll();
|
||||
|
||||
final List<MgmtTag> tagList = Optional.ofNullable(hawkbitClient.getTargetRestApi().getTags(target.getControllerId()).getBody())
|
||||
.orElse(Collections.emptyList());
|
||||
|
||||
for (MgmtTag tag : tagList) {
|
||||
tagsArea.add(buildTargetTagBadge(tag, target.getControllerId()));
|
||||
}
|
||||
}
|
||||
|
||||
private Span buildTargetTagBadge(MgmtTag tag, String controllerId) {
|
||||
Button clearButton = new Button(VaadinIcon.CLOSE_SMALL.create());
|
||||
clearButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST, ButtonVariant.LUMO_TERTIARY_INLINE);
|
||||
clearButton.getStyle().set("margin-inline-start", "var(--lumo-space-xs)");
|
||||
clearButton.getElement().setAttribute("aria-label", "Clear filter: " + tag.getName());
|
||||
clearButton.getElement().setAttribute("title", "Clear filter: " + tag.getName());
|
||||
|
||||
Span pill = new Span();
|
||||
pill.getElement().getThemeList().add("badge pill");
|
||||
pill.getStyle().set("background-color", tag.getColour());
|
||||
pill.getStyle().set("margin-inline-end", "var(--lumo-space-xs)");
|
||||
|
||||
Span badge = new Span(pill, new Span(tag.getName()), clearButton);
|
||||
badge.getElement().getThemeList().add("badge contrast pill");
|
||||
|
||||
clearButton.addClickListener(event -> {
|
||||
hawkbitClient.getTargetTagRestApi().unassignTarget(tag.getId(), controllerId);
|
||||
badge.getElement().removeFromParent();
|
||||
});
|
||||
|
||||
return badge;
|
||||
}
|
||||
|
||||
private List<MgmtTag> fetchAvailableTags() {
|
||||
List<MgmtTag> tags = new ArrayList<>();
|
||||
int fetched = 0;
|
||||
int offset = 0;
|
||||
do {
|
||||
List<MgmtTag> page = Optional.ofNullable(
|
||||
hawkbitClient.getTargetTagRestApi().getTargetTags(null, offset, 50, Constants.NAME_ASC).getBody())
|
||||
.map(PagedList::getContent)
|
||||
.orElse(Collections.emptyList());
|
||||
tags.addAll(page);
|
||||
fetched = page.size();
|
||||
offset += fetched;
|
||||
} while (fetched > 0);
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TargetActionsHistoryLayout extends VerticalLayout {
|
||||
|
||||
private final TargetActionsHistory targetActionsHistory;
|
||||
|
||||
public TargetActionsHistoryLayout(HawkbitMgmtClient hawkbitMgmtClient) {
|
||||
ActionStepsGrid actionStepsGrid = new ActionStepsGrid(hawkbitMgmtClient);
|
||||
targetActionsHistory = new TargetActionsHistory(hawkbitMgmtClient, actionStepsGrid);
|
||||
add(targetActionsHistory);
|
||||
add(actionStepsGrid);
|
||||
}
|
||||
|
||||
public void setItem(MgmtTarget target) {
|
||||
targetActionsHistory.setItem(target);
|
||||
}
|
||||
|
||||
public static class ActionStepsGrid extends Grid<ActionStepsGrid.ActionStepEntry> {
|
||||
|
||||
private final transient HawkbitMgmtClient hawkbitClient;
|
||||
private transient MgmtTarget target;
|
||||
private transient Long actionId;
|
||||
|
||||
private ActionStepsGrid(final HawkbitMgmtClient hawkbitClient) {
|
||||
|
||||
this.hawkbitClient = hawkbitClient;
|
||||
setWidthFull();
|
||||
addColumn(new ComponentRenderer<>(ActionStepEntry::getStatusIcon)).setHeader(STATUS).setAutoWidth(true)
|
||||
.setFlexGrow(0);
|
||||
addColumn(Utils.localDateTimeRenderer(ActionStepEntry::getLastModifiedAt)).setHeader("Time")
|
||||
.setAutoWidth(true).setFlexGrow(0).setComparator(ActionStepEntry::getLastModifiedAt);
|
||||
addColumn(new ComponentRenderer<>(ActionStepEntry::getMessage)).setHeader("Message").setAutoWidth(true).setFlexGrow(0);
|
||||
}
|
||||
|
||||
private List<ActionStepEntry> fetchActionSteps() {
|
||||
if (actionId == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return hawkbitClient.getTargetRestApi()
|
||||
.getActionStatusList(target.getControllerId(), actionId, 0, 30, null).getBody().getContent()
|
||||
.stream().map(ActionStepEntry::new)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttach(AttachEvent attachEvent) {
|
||||
setItems(fetchActionSteps());
|
||||
}
|
||||
|
||||
public void setActionId(Long id) {
|
||||
actionId = id;
|
||||
setItems(fetchActionSteps());
|
||||
}
|
||||
|
||||
public void setTarget(MgmtTarget target) {
|
||||
this.target = target;
|
||||
actionId = null;
|
||||
}
|
||||
|
||||
private static class ActionStepEntry extends Object {
|
||||
|
||||
final MgmtActionStatus status;
|
||||
|
||||
public ActionStepEntry(final MgmtActionStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Long getLastModifiedAt() {
|
||||
return status.getReportedAt();
|
||||
}
|
||||
|
||||
public Component getStatusIcon() {
|
||||
final HorizontalLayout layout = new HorizontalLayout();
|
||||
final Icon icon;
|
||||
|
||||
switch (status.getType()) {
|
||||
case FINISHED -> icon = Utils.iconColored(VaadinIcon.CHECK_CIRCLE, "Finished", GREEN);
|
||||
case ERROR -> icon = Utils.iconColored(VaadinIcon.CLOSE_CIRCLE, "Error", RED);
|
||||
case WARNING -> icon = Utils.iconColored(VaadinIcon.WARNING, "Warning", ORANGE);
|
||||
case RUNNING -> icon = Utils.iconColored(VaadinIcon.ADJUST, "Running", GREEN);
|
||||
case RETRIEVED -> icon = Utils.iconColored(VaadinIcon.CIRCLE_THIN, "Retrieved", GREEN);
|
||||
case CANCELED -> icon = Utils.iconColored(VaadinIcon.CLOSE_CIRCLE_O, "Canceled", GRAY);
|
||||
case CANCELING -> icon = Utils.iconColored(VaadinIcon.CLOSE_CIRCLE, "Cancelling", BROWN);
|
||||
case DOWNLOAD -> icon = Utils.iconColored(VaadinIcon.CLOUD_DOWNLOAD_O, "Download", TEAL);
|
||||
case DOWNLOADED -> icon = Utils.iconColored(VaadinIcon.CLOUD_DOWNLOAD, "Downloaded", PURPLE);
|
||||
case WAIT_FOR_CONFIRMATION -> icon = Utils.iconColored(VaadinIcon.QUESTION_CIRCLE, "Wait for confirmation", CORAL);
|
||||
default -> icon = Utils.iconColored(VaadinIcon.CIRCLE_THIN, status.getType().getName().toLowerCase(), BLACK);
|
||||
}
|
||||
|
||||
icon.addClassNames(LumoUtility.IconSize.SMALL);
|
||||
layout.add(icon);
|
||||
layout.setWidth(50, Unit.PIXELS);
|
||||
layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
|
||||
return layout;
|
||||
}
|
||||
|
||||
public VerticalLayout getMessage() {
|
||||
return new VerticalLayout(status.getMessages().stream().map(Span::new).toArray(Span[]::new));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class RegisterDialog extends Utils.BaseDialog<Void> {
|
||||
|
||||
private final Select<MgmtTargetType> type;
|
||||
private final TextField controllerId;
|
||||
private final TextField name;
|
||||
private final TextArea description;
|
||||
private final TextField group;
|
||||
|
||||
private RegisterDialog(final HawkbitMgmtClient hawkbitClient) {
|
||||
super("Register Target");
|
||||
|
||||
final Button register = Utils.tooltip(new Button("Register"), "Register (Enter)");
|
||||
type = new Select<>(
|
||||
"Type",
|
||||
e -> {},
|
||||
hawkbitClient.getTargetTypeRestApi()
|
||||
.getTargetTypes(null, 0, 30, Constants.NAME_ASC)
|
||||
.getBody()
|
||||
.getContent()
|
||||
.toArray(new MgmtTargetType[0])
|
||||
);
|
||||
type.setWidthFull();
|
||||
type.setEmptySelectionAllowed(true);
|
||||
type.setItemLabelGenerator(item -> item == null ? "" : item.getName());
|
||||
controllerId = Utils.textField(CONTROLLER_ID, e -> register.setEnabled(!e.getHasValue().isEmpty()));
|
||||
controllerId.focus();
|
||||
name = Utils.textField(Constants.NAME);
|
||||
name.setWidthFull();
|
||||
description = new TextArea(Constants.DESCRIPTION);
|
||||
description.setMinLength(2);
|
||||
description.setWidthFull();
|
||||
group = Utils.textField(Constants.GROUP);
|
||||
group.setWidthFull();
|
||||
|
||||
addCreateClickListener(register, hawkbitClient);
|
||||
register.setEnabled(false);
|
||||
register.addClickShortcut(Key.ENTER);
|
||||
register.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC);
|
||||
cancel.addClickListener(e -> close());
|
||||
cancel.addClickShortcut(Key.ESCAPE);
|
||||
getFooter().add(cancel);
|
||||
getFooter().add(register);
|
||||
|
||||
final VerticalLayout layout = new VerticalLayout();
|
||||
layout.setSizeFull();
|
||||
layout.setPadding(true);
|
||||
layout.setSpacing(false);
|
||||
layout.add(type, controllerId, name, description, group);
|
||||
add(layout);
|
||||
open();
|
||||
}
|
||||
|
||||
private void addCreateClickListener(final Button register, final HawkbitMgmtClient hawkbitClient) {
|
||||
register.addClickListener(e -> {
|
||||
final MgmtTargetRequestBody request = new MgmtTargetRequestBody()
|
||||
.setControllerId(controllerId.getValue())
|
||||
.setName(name.getValue())
|
||||
.setDescription(description.getValue())
|
||||
.setGroup(group.getValue());
|
||||
if (!ObjectUtils.isEmpty(type.getValue())) {
|
||||
request.setTargetType(type.getValue().getId());
|
||||
}
|
||||
hawkbitClient.getTargetRestApi().createTargets(
|
||||
List.of(request))
|
||||
.getBody()
|
||||
.stream()
|
||||
.findFirst()
|
||||
.orElseThrow()
|
||||
.getControllerId();
|
||||
close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static class AssignDialog extends Utils.BaseDialog<Void> {
|
||||
|
||||
private final Select<MgmtDistributionSet> distributionSet;
|
||||
private final Select<MgmtActionType> actionType;
|
||||
private final DateTimePicker forceTime = new DateTimePicker("Force Time");
|
||||
private final Button assign = new Button("Assign");
|
||||
|
||||
private AssignDialog(final HawkbitMgmtClient hawkbitClient, Set<TargetWithDs> selectedTargets) {
|
||||
super("Assign Distribution Set");
|
||||
|
||||
distributionSet = new Select<>(
|
||||
"Distribution Set",
|
||||
this::readyToAssign,
|
||||
Optional.ofNullable(
|
||||
hawkbitClient.getDistributionSetRestApi()
|
||||
.getDistributionSets(null, 0, 500, Constants.CREATED_AT_DESC)
|
||||
.getBody())
|
||||
.map(body -> body.getContent().toArray(new MgmtDistributionSet[0]))
|
||||
.orElseGet(() -> new MgmtDistributionSet[0])
|
||||
);
|
||||
distributionSet.setRequiredIndicatorVisible(true);
|
||||
distributionSet.setItemLabelGenerator(distributionSetO -> distributionSetO.getName() + ":" + distributionSetO.getVersion());
|
||||
distributionSet.setWidthFull();
|
||||
|
||||
actionType = Utils.actionTypeControls(forceTime);
|
||||
|
||||
assign.setEnabled(false);
|
||||
assign.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
addAssignClickListener(hawkbitClient, selectedTargets);
|
||||
final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC);
|
||||
cancel.addClickListener(e -> close());
|
||||
cancel.addClickShortcut(Key.ESCAPE);
|
||||
getFooter().add(cancel);
|
||||
getFooter().add(assign);
|
||||
|
||||
final VerticalLayout layout = new VerticalLayout();
|
||||
layout.setSizeFull();
|
||||
layout.setSpacing(false);
|
||||
layout.add(distributionSet, actionType);
|
||||
add(layout);
|
||||
open();
|
||||
}
|
||||
|
||||
private void readyToAssign(final Object v) {
|
||||
final boolean assignEnabled = !distributionSet.isEmpty();
|
||||
if (assign.isEnabled() != assignEnabled) {
|
||||
assign.setEnabled(assignEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void addAssignClickListener(final HawkbitMgmtClient hawkbitClient, final Set<TargetWithDs> selectedTargets) {
|
||||
assign.addClickListener(e -> {
|
||||
close();
|
||||
|
||||
final List<MgmtTargetAssignmentRequestBody> requests = new LinkedList<>();
|
||||
for (final MgmtTarget target : selectedTargets) {
|
||||
final MgmtTargetAssignmentRequestBody request = new MgmtTargetAssignmentRequestBody(target.getControllerId());
|
||||
|
||||
request.setType(actionType.getValue());
|
||||
if (actionType.getValue() == MgmtActionType.TIMEFORCED) {
|
||||
request.setForcetime(
|
||||
forceTime.isEmpty() ? System.currentTimeMillis() : forceTime.getValue().toEpochSecond(ZoneOffset.UTC) * 1000);
|
||||
}
|
||||
|
||||
requests.add(request);
|
||||
}
|
||||
|
||||
hawkbitClient.getDistributionSetRestApi().createAssignedTarget(distributionSet.getValue().getId(), requests, null).getBody();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static class CreateTagDialog extends Utils.BaseDialog<Void> {
|
||||
|
||||
private final TextField name;
|
||||
private final TextArea description = new TextArea(Constants.DESCRIPTION);
|
||||
|
||||
private CreateTagDialog(final HawkbitMgmtClient hawkbitClient, Runnable onSuccess) {
|
||||
super("Create Tag");
|
||||
|
||||
final FormLayout formLayout = new FormLayout();
|
||||
formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
|
||||
final Button create = Utils.tooltip(new Button("Create"), "Create (Enter)");
|
||||
final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC);
|
||||
|
||||
final Input colorInput = new Input();
|
||||
colorInput.setType("color");
|
||||
name = Utils.textField("Tag Name", e -> create.setEnabled(!e.getHasValue().isEmpty()));
|
||||
formLayout.add(name);
|
||||
formLayout.add(description);
|
||||
formLayout.addFormItem(colorInput, "Color Selection");
|
||||
add(formLayout);
|
||||
|
||||
create.setEnabled(false);
|
||||
create.addClickShortcut(Key.ENTER);
|
||||
create.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
create.addClickListener(e -> {
|
||||
hawkbitClient.getTargetTagRestApi().createTargetTags(List.of(
|
||||
new MgmtTagRequestBodyPut()
|
||||
.setName(name.getValue())
|
||||
.setDescription(description.getValue())
|
||||
.setColour(colorInput.getValue())));
|
||||
onSuccess.run();
|
||||
close();
|
||||
});
|
||||
|
||||
cancel.addClickListener(e -> close());
|
||||
cancel.addClickShortcut(Key.ESCAPE);
|
||||
|
||||
getFooter().add(cancel);
|
||||
getFooter().add(create);
|
||||
open();
|
||||
}
|
||||
}
|
||||
|
||||
private static class TargetStatusCell extends HorizontalLayout {
|
||||
|
||||
private TargetStatusCell(final MgmtTarget target) {
|
||||
final MgmtPollStatus pollStatus = target.getPollStatus();
|
||||
add(pollStatusIconMapper(pollStatus));
|
||||
setWidth(25, Unit.PIXELS);
|
||||
}
|
||||
|
||||
private Icon pollStatusIconMapper(MgmtPollStatus pollStatus) {
|
||||
final Icon pollIcon;
|
||||
if (pollStatus == null) {
|
||||
pollIcon = Utils.tooltip(VaadinIcon.QUESTION_CIRCLE.create(), "No Poll Status");
|
||||
} else if (pollStatus.isOverdue()) {
|
||||
pollIcon = Utils.tooltip(VaadinIcon.EXCLAMATION_CIRCLE.create(), "Overdue " + Utils.durationFromMillis(pollStatus
|
||||
.getLastRequestAt()));
|
||||
} else {
|
||||
pollIcon = Utils.tooltip(VaadinIcon.CLOCK.create(), "In Time " + Utils.durationFromMillis(pollStatus.getLastRequestAt()));
|
||||
}
|
||||
pollIcon.addClassNames(LumoUtility.IconSize.SMALL);
|
||||
return pollIcon;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TargetUpdateStatusCell extends HorizontalLayout {
|
||||
|
||||
private TargetUpdateStatusCell(final MgmtTarget target) {
|
||||
final String targetUpdateStatus = Optional.ofNullable(target.getUpdateStatus()).orElse("unknown");
|
||||
add(targetUpdateStatusMapper(targetUpdateStatus));
|
||||
setWidth(25, Unit.PIXELS);
|
||||
}
|
||||
|
||||
private Icon targetUpdateStatusMapper(final String targetUpdateStatus) {
|
||||
final VaadinIcon icon = switch (targetUpdateStatus) {
|
||||
case "error" -> VaadinIcon.EXCLAMATION_CIRCLE;
|
||||
case "in_sync" -> VaadinIcon.CHECK_CIRCLE;
|
||||
case "pending" -> VaadinIcon.ADJUST;
|
||||
case "registered" -> VaadinIcon.DOT_CIRCLE;
|
||||
default -> VaadinIcon.QUESTION_CIRCLE;
|
||||
};
|
||||
|
||||
final String color = switch (targetUpdateStatus) {
|
||||
case "error" -> RED;
|
||||
case "in_sync" -> GREEN;
|
||||
case "pending" -> ORANGE;
|
||||
case "registered" -> "lightblue";
|
||||
default -> "blue";
|
||||
};
|
||||
|
||||
final Icon statusIcon = Utils.tooltip(icon.create(), targetUpdateStatus.replace("_", " "));
|
||||
statusIcon.setColor(color);
|
||||
statusIcon.addClassNames(LumoUtility.IconSize.SMALL);
|
||||
return statusIcon;
|
||||
}
|
||||
}
|
||||
|
||||
// todo change /targets api to reduce api calls ?
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public static class TargetWithDs extends MgmtTarget {
|
||||
|
||||
TargetWithDs() {
|
||||
super();
|
||||
}
|
||||
|
||||
Optional<MgmtDistributionSet> ds;
|
||||
static ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public static TargetWithDs from(final HawkbitMgmtClient hawkbitClient, MgmtTarget target) {
|
||||
TargetWithDs targetWithDs = objectMapper.convertValue(target, TargetWithDs.class);
|
||||
|
||||
targetWithDs.ds = Optional.ofNullable(hawkbitClient.getTargetRestApi().getInstalledDistributionSet(targetWithDs
|
||||
.getControllerId())
|
||||
.getBody());
|
||||
return targetWithDs;
|
||||
}
|
||||
|
||||
public String getDsVersion() {
|
||||
return ds.map(MgmtDistributionSet::getVersion).orElse("");
|
||||
}
|
||||
|
||||
public String getDsName() {
|
||||
return ds.map(MgmtDistributionSet::getName).orElse("");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.view.util;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.HasValue;
|
||||
import com.vaadin.flow.component.Key;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
public class Filter extends Div {
|
||||
|
||||
private transient Rsql rsql;
|
||||
private final transient Rsql secondaryRsql;
|
||||
private final transient Rsql primaryRsql;
|
||||
private final transient Div filtersDiv;
|
||||
|
||||
public Filter(final Consumer<String> changeListener, final Rsql primaryRsql, final Rsql secondaryOptionalRsql) {
|
||||
rsql = primaryRsql;
|
||||
this.primaryRsql = primaryRsql;
|
||||
secondaryRsql = secondaryOptionalRsql;
|
||||
|
||||
final HorizontalLayout layout = new HorizontalLayout();
|
||||
|
||||
setWidthFull();
|
||||
addClassNames(LumoUtility.Padding.Horizontal.NONE, LumoUtility.Padding.Vertical.SMALL,
|
||||
LumoUtility.BoxSizing.BORDER);
|
||||
|
||||
filtersDiv = new Div();
|
||||
filtersDiv.setWidthFull();
|
||||
filtersDiv.add(primaryRsql.components());
|
||||
filtersDiv.addClassName(LumoUtility.Gap.SMALL);
|
||||
|
||||
final Button searchBtn = Utils.tooltip(new Button(VaadinIcon.SEARCH.create()), "Search (Enter)");
|
||||
searchBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
searchBtn.addClickListener(e -> changeListener.accept(rsql.filter()));
|
||||
searchBtn.addClickShortcut(Key.ENTER);
|
||||
final Button resetBtn = Utils.tooltip(new Button(VaadinIcon.REFRESH.create()), "Reset");
|
||||
resetBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
resetBtn.addClickListener(e -> {
|
||||
clear(layout.getChildren());
|
||||
changeListener.accept(primaryRsql.filter());
|
||||
});
|
||||
|
||||
final HorizontalLayout actions = new HorizontalLayout(searchBtn, resetBtn);
|
||||
actions.addClassNames(LumoUtility.Gap.SMALL);
|
||||
|
||||
layout.setPadding(true);
|
||||
layout.setAlignItems(FlexComponent.Alignment.BASELINE);
|
||||
layout.add(filtersDiv);
|
||||
layout.add(actions);
|
||||
|
||||
if (secondaryOptionalRsql != null) {
|
||||
final Button toggleBtn = Utils.tooltip(new Button(VaadinIcon.FLIP_V.create()), "Toggle Search");
|
||||
toggleBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||
toggleBtn.addClickListener(e -> {
|
||||
toggle();
|
||||
changeListener.accept(primaryRsql.filter());
|
||||
});
|
||||
layout.add(toggleBtn);
|
||||
}
|
||||
add(layout);
|
||||
|
||||
changeListener.accept(primaryRsql.filter());
|
||||
}
|
||||
|
||||
private void toggle() {
|
||||
// toggle
|
||||
filtersDiv.removeAll();
|
||||
synchronized (this) {
|
||||
rsql = rsql == primaryRsql ? secondaryRsql : primaryRsql;
|
||||
}
|
||||
filtersDiv.add(rsql.components());
|
||||
}
|
||||
|
||||
public void setFilter(String string, boolean allowToggle) {
|
||||
var otherFilter = rsql == primaryRsql ? secondaryRsql : primaryRsql;
|
||||
Stream<Filter.Rsql> rsqlFIlter;
|
||||
// logic to find the filter to use
|
||||
if (allowToggle) {
|
||||
rsqlFIlter = Stream.of(this.rsql);
|
||||
} else {
|
||||
rsqlFIlter = Stream.of(this.rsql, otherFilter);
|
||||
}
|
||||
rsqlFIlter.filter(RsqlRw.class::isInstance).findFirst().map(RsqlRw.class::cast).ifPresent(f -> {
|
||||
if (f == otherFilter) {
|
||||
toggle();
|
||||
}
|
||||
f.setFilter(string);
|
||||
});
|
||||
}
|
||||
|
||||
public static String filter(final Map<Object, Object> keyToValues) {
|
||||
final Map<Object, Object> normalized = new HashMap<>(keyToValues)
|
||||
.entrySet()
|
||||
.stream()
|
||||
.filter(e -> !ObjectUtils.isEmpty(e.getValue()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
if (normalized.isEmpty()) {
|
||||
return null;
|
||||
} else {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
normalized.forEach((k, v) -> {
|
||||
if (k instanceof Collection<?> keyList) {
|
||||
sb.append('(').append(
|
||||
keyList.stream().map(subKey -> filter((String) subKey, v))
|
||||
.collect(Collectors.joining(" or "))).append(")");
|
||||
} else if (k instanceof String key) {
|
||||
if (v instanceof Collection<?>) {
|
||||
sb.append('(').append(filter(key, v)).append(')');
|
||||
} else {
|
||||
sb.append(filter(key, v));
|
||||
}
|
||||
}
|
||||
sb.append(';');
|
||||
});
|
||||
return sb.substring(0, sb.length() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static String filter(final String key, final Object value) {
|
||||
if (value == null || (value instanceof Collection<?> coll && coll.isEmpty())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value instanceof Collection<?> coll) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
coll.stream().forEach(next -> sb.append(key).append("==").append(next).append(','));
|
||||
return sb.substring(0, sb.length() - 1);
|
||||
} else if (value instanceof Optional<?> opt) {
|
||||
if (opt.isEmpty()) {
|
||||
return null;
|
||||
} else {
|
||||
return key + "==" + opt.get();
|
||||
}
|
||||
} else {
|
||||
return key + "==" + value;
|
||||
}
|
||||
}
|
||||
|
||||
private static void clear(final Stream<Component> components) {
|
||||
if (components == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
components.forEach(component -> {
|
||||
if (component instanceof HasValue<?, ?> hasValue) {
|
||||
hasValue.clear();
|
||||
}
|
||||
clear(component.getChildren());
|
||||
});
|
||||
}
|
||||
|
||||
public interface Rsql {
|
||||
|
||||
List<Component> components();
|
||||
|
||||
String filter();
|
||||
}
|
||||
|
||||
public interface RsqlRw {
|
||||
|
||||
void setFilter(String filter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.view.util;
|
||||
|
||||
import com.vaadin.flow.component.card.Card;
|
||||
import com.vaadin.flow.component.card.CardVariant;
|
||||
import com.vaadin.flow.component.html.Anchor;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
|
||||
public class LinkedTextArea extends Div {
|
||||
|
||||
String queryPrefix;
|
||||
Card card;
|
||||
|
||||
public LinkedTextArea(String title, String queryPrefix) {
|
||||
super();
|
||||
card = new Card();
|
||||
card.setTitle(title);
|
||||
this.queryPrefix = queryPrefix;
|
||||
}
|
||||
|
||||
public void setValueWithLink(String value, String query) {
|
||||
var span = new Span(value);
|
||||
span.setWhiteSpace(WhiteSpace.PRE_WRAP);
|
||||
card.add(span);
|
||||
card.addThemeVariants(CardVariant.LUMO_ELEVATED);
|
||||
if (query != null) {
|
||||
var a = new Anchor(queryPrefix + query, card);
|
||||
a.addClassName("nocolor");
|
||||
add(a);
|
||||
} else {
|
||||
add(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.view.util;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.grid.GridVariant;
|
||||
import com.vaadin.flow.data.provider.Query;
|
||||
import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||
|
||||
// id type shall have proper equals and hashCode - i.e. eligible hash set element
|
||||
@SuppressWarnings("java:S119") // better readability
|
||||
public class SelectionGrid<T, ID> extends Grid<T> {
|
||||
|
||||
private volatile String rsqlFilter;
|
||||
|
||||
public SelectionGrid(
|
||||
final EntityRepresentation<T, ID> entityRepresentation) {
|
||||
this(entityRepresentation, null);
|
||||
}
|
||||
|
||||
public SelectionGrid(
|
||||
final EntityRepresentation<T, ID> entityRepresentation,
|
||||
final BiFunction<Query<T, Void>, String, Stream<T>> queryFn) {
|
||||
super(entityRepresentation.beanType, false);
|
||||
|
||||
addThemeVariants(GridVariant.LUMO_NO_BORDER);
|
||||
addClassNames(LumoUtility.Border.TOP, LumoUtility.BorderColor.CONTRAST_10);
|
||||
|
||||
setSelectionMode(Grid.SelectionMode.MULTI);
|
||||
entityRepresentation.addColumns(this);
|
||||
if (queryFn != null) {
|
||||
setItems(query -> {
|
||||
final Stream<T> fetch = queryFn.apply(query, rsqlFilter);
|
||||
final Set<T> selected = getSelectedItems();
|
||||
if (selected == null || selected.isEmpty()) {
|
||||
final List<T> fetchList = fetch.toList();
|
||||
if (fetchList.size() == 1) {
|
||||
this.setDetailsVisible(fetchList.get(0), true);
|
||||
}
|
||||
return fetchList.stream();
|
||||
} else {
|
||||
final Set<ID> selectedIds = new HashSet<>();
|
||||
selected.forEach(next -> selectedIds.add(entityRepresentation.idFn.apply(next)));
|
||||
// if matching keeps old entries instead of new the new ones in order to
|
||||
// select them in case refresh is made with keepSelection
|
||||
// this however means that if they are changed the old state will be shown!!!
|
||||
return Stream.concat(selected.stream(),
|
||||
fetch.filter(next -> !selectedIds.contains(entityRepresentation.idFn.apply(next))));
|
||||
}
|
||||
});
|
||||
} // else externally managed
|
||||
}
|
||||
|
||||
public void setRsqlFilter(final String rsqlFilter, boolean refreshGrid) {
|
||||
if (!Objects.equals(this.rsqlFilter, rsqlFilter)) {
|
||||
this.rsqlFilter = rsqlFilter;
|
||||
if (refreshGrid)
|
||||
refreshGrid(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshGrid(final boolean keepSelection) {
|
||||
if (keepSelection) {
|
||||
final Set<T> selected = getSelectedItems();
|
||||
getDataProvider().refreshAll();
|
||||
if (selected != null && !selected.isEmpty()) {
|
||||
selected.forEach(this::select);
|
||||
}
|
||||
} else {
|
||||
deselectAll();
|
||||
getDataProvider().refreshAll();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class EntityRepresentation<T, ID> {
|
||||
|
||||
private final Class<T> beanType;
|
||||
private final Function<T, ID> idFn;
|
||||
|
||||
protected EntityRepresentation(final Class<T> beanType, final Function<T, ID> idFn) {
|
||||
this.beanType = beanType;
|
||||
this.idFn = idFn;
|
||||
}
|
||||
|
||||
protected abstract void addColumns(final Grid<T> grid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.view.util;
|
||||
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.UI;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.splitlayout.SplitLayout;
|
||||
import com.vaadin.flow.component.splitlayout.SplitLayoutVariant;
|
||||
import com.vaadin.flow.data.provider.Query;
|
||||
import com.vaadin.flow.data.renderer.ComponentRenderer;
|
||||
import com.vaadin.flow.function.SerializableFunction;
|
||||
import com.vaadin.flow.router.BeforeEnterEvent;
|
||||
import com.vaadin.flow.router.BeforeEnterObserver;
|
||||
import com.vaadin.flow.router.NavigationTrigger;
|
||||
import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||
|
||||
import org.eclipse.hawkbit.ui.view.Constants;
|
||||
|
||||
@SuppressWarnings("java:S119") // better readability
|
||||
public class TableView<T, ID> extends Div implements Constants, BeforeEnterObserver {
|
||||
|
||||
private static final String COLOR = "color";
|
||||
private static final String VAR_LUMO_SECONDARY_TEXT_COLOR = "var(--lumo-secondary-text-color)";
|
||||
private static final String VAR_LUMO_PRIMARY_COLOR = "var(--lumo-primary-color)";
|
||||
private static final int DEFAULT_OPEN_POSITION_SIZE = 50;
|
||||
private static final String QUERY_PARAM_FILTER = "q";
|
||||
|
||||
protected SelectionGrid<T, ID> selectionGrid;
|
||||
private final Filter filter;
|
||||
private final VerticalLayout gridLayout;
|
||||
protected final HorizontalLayout controlsLayout;
|
||||
private final SplitLayout splitLayout;
|
||||
private final Div detailsPanel;
|
||||
private Button currentSelectionButton;
|
||||
|
||||
public TableView(
|
||||
final Filter.Rsql rsql,
|
||||
final SelectionGrid.EntityRepresentation<T, ID> entityRepresentation,
|
||||
final BiFunction<Query<T, Void>, String, Stream<T>> queryFn,
|
||||
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> addHandler,
|
||||
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> removeHandler) {
|
||||
this(rsql, null, entityRepresentation, queryFn, addHandler, removeHandler, null);
|
||||
}
|
||||
|
||||
public TableView(
|
||||
final Filter.Rsql rsql, final Filter.Rsql alternativeRsql,
|
||||
final SelectionGrid.EntityRepresentation<T, ID> entityRepresentation,
|
||||
final BiFunction<Query<T, Void>, String, Stream<T>> queryFn,
|
||||
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> addHandler,
|
||||
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> removeHandler,
|
||||
final Function<T, Component> detailsButtonHandler) {
|
||||
selectionGrid = new SelectionGrid<>(entityRepresentation, queryFn);
|
||||
selectionGrid.setSizeFull();
|
||||
setSizeFull();
|
||||
|
||||
splitLayout = new SplitLayout();
|
||||
splitLayout.setSizeFull();
|
||||
splitLayout.setOrientation(SplitLayout.Orientation.HORIZONTAL);
|
||||
splitLayout.addThemeVariants(SplitLayoutVariant.LUMO_SMALL);
|
||||
splitLayout.setSplitterPosition(100);
|
||||
splitLayout.addToPrimary(selectionGrid);
|
||||
detailsPanel = new Div();
|
||||
|
||||
if (detailsButtonHandler != null) {
|
||||
ComponentRenderer<Button, T> renderer = new ComponentRenderer<>(renderDetailsButton(detailsButtonHandler));
|
||||
selectionGrid.addColumn(renderer).setHeader("Details").setAutoWidth(true).setFlexGrow(0);
|
||||
}
|
||||
|
||||
filter = new Filter(
|
||||
rsqlFilter -> {
|
||||
closeDetailsPanel();
|
||||
if (rsqlFilter != null) {
|
||||
var queryParameters = UI.getCurrent().getActiveViewLocation()
|
||||
.getQueryParameters()
|
||||
.merging(QUERY_PARAM_FILTER, rsqlFilter);
|
||||
UI.getCurrent().navigate(this.getClass(), queryParameters);
|
||||
}
|
||||
selectionGrid.setRsqlFilter(rsqlFilter, true);
|
||||
}, rsql, alternativeRsql
|
||||
);
|
||||
gridLayout = new VerticalLayout(filter, splitLayout);
|
||||
gridLayout.setSizeFull();
|
||||
gridLayout.setPadding(false);
|
||||
gridLayout.setSpacing(false);
|
||||
if (addHandler != null || removeHandler != null) {
|
||||
controlsLayout = Utils.addRemoveControls(addHandler, removeHandler, selectionGrid, false);
|
||||
} else {
|
||||
controlsLayout = new HorizontalLayout();
|
||||
controlsLayout.setWidthFull();
|
||||
controlsLayout.addClassNames(
|
||||
LumoUtility.Padding.Horizontal.XLARGE, LumoUtility.Padding.Vertical.SMALL, LumoUtility.BoxSizing.BORDER);
|
||||
controlsLayout.setAlignItems(FlexComponent.Alignment.BASELINE);
|
||||
}
|
||||
gridLayout.add(controlsLayout);
|
||||
add(gridLayout);
|
||||
}
|
||||
|
||||
private void closeDetailsPanel() {
|
||||
detailsPanel.removeAll();
|
||||
if (splitLayout.getSecondaryComponent() != null) {
|
||||
splitLayout.remove(detailsPanel);
|
||||
}
|
||||
splitLayout.setSplitterPosition(100);
|
||||
currentSelectionButton = null;
|
||||
}
|
||||
|
||||
private SerializableFunction<T, Button> renderDetailsButton(final Function<T, Component> selectionHandler) {
|
||||
return selectedItem -> {
|
||||
final Button button = new Button(VaadinIcon.EYE.create());
|
||||
button.getStyle().set(COLOR, VAR_LUMO_SECONDARY_TEXT_COLOR);
|
||||
|
||||
button.addClickListener(event -> {
|
||||
final Icon eyeIcon = VaadinIcon.EYE.create();
|
||||
final Icon closeIcon = VaadinIcon.CLOSE_SMALL.create();
|
||||
|
||||
if (button == currentSelectionButton) {
|
||||
button.setIcon(eyeIcon);
|
||||
button.getStyle().set(COLOR, VAR_LUMO_SECONDARY_TEXT_COLOR);
|
||||
closeDetailsPanel();
|
||||
} else {
|
||||
button.setIcon(closeIcon);
|
||||
button.getStyle().set(COLOR, VAR_LUMO_PRIMARY_COLOR);
|
||||
if (currentSelectionButton == null) {
|
||||
splitLayout.addToSecondary(detailsPanel);
|
||||
} else {
|
||||
currentSelectionButton.setIcon(eyeIcon);
|
||||
currentSelectionButton.getStyle().set(COLOR, VAR_LUMO_SECONDARY_TEXT_COLOR);
|
||||
}
|
||||
detailsPanel.removeAll();
|
||||
splitLayout.setSplitterPosition(DEFAULT_OPEN_POSITION_SIZE);
|
||||
detailsPanel.add(selectionHandler.apply(selectedItem));
|
||||
currentSelectionButton = button;
|
||||
}
|
||||
});
|
||||
|
||||
button.setTooltipText("Show details");
|
||||
button.addThemeVariants(ButtonVariant.LUMO_SMALL);
|
||||
return button;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeEnter(BeforeEnterEvent event) {
|
||||
var params = event.getLocation().getQueryParameters();
|
||||
params.getSingleParameter(QUERY_PARAM_FILTER)
|
||||
.ifPresent(f -> {
|
||||
var newPage = event.getTrigger() == NavigationTrigger.UI_NAVIGATE;
|
||||
selectionGrid.setRsqlFilter(f, newPage);
|
||||
filter.setFilter(f, newPage);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.ui.view.util;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.ToLongFunction;
|
||||
|
||||
import com.vaadin.flow.component.icon.Icon;
|
||||
import com.vaadin.flow.component.icon.IconFactory;
|
||||
import com.vaadin.flow.data.provider.QuerySortOrder;
|
||||
import com.vaadin.flow.data.provider.SortDirection;
|
||||
import com.vaadin.flow.data.renderer.LocalDateTimeRenderer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
|
||||
import org.eclipse.hawkbit.ui.view.Constants;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.HasValue;
|
||||
import com.vaadin.flow.component.Text;
|
||||
import com.vaadin.flow.component.UI;
|
||||
import com.vaadin.flow.component.Unit;
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.confirmdialog.ConfirmDialog;
|
||||
import com.vaadin.flow.component.datetimepicker.DateTimePicker;
|
||||
import com.vaadin.flow.component.dialog.Dialog;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.select.Select;
|
||||
import com.vaadin.flow.component.shared.Tooltip;
|
||||
import com.vaadin.flow.component.textfield.NumberField;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import com.vaadin.flow.data.renderer.ComponentRenderer;
|
||||
import com.vaadin.flow.data.value.ValueChangeMode;
|
||||
import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||
|
||||
@Slf4j
|
||||
public class Utils {
|
||||
|
||||
private Utils() {
|
||||
// prevent initialization
|
||||
}
|
||||
|
||||
public static TextField textField(final String label) {
|
||||
return textField(label, null);
|
||||
}
|
||||
|
||||
public static TextField textField(final String label, final Consumer<HasValue.ValueChangeEvent<String>> changeListener) {
|
||||
final TextField textField = new TextField(label);
|
||||
textField.setWidthFull();
|
||||
if (changeListener != null) {
|
||||
textField.setRequired(true);
|
||||
textField.addValueChangeListener(changeListener::accept);
|
||||
textField.setValueChangeMode(ValueChangeMode.TIMEOUT);
|
||||
textField.setValueChangeTimeout(200);
|
||||
}
|
||||
return textField;
|
||||
}
|
||||
|
||||
public static NumberField numberField(final String label) {
|
||||
return numberField(label, null);
|
||||
}
|
||||
|
||||
public static NumberField numberField(final String label, final Consumer<HasValue.ValueChangeEvent<Double>> changeListener) {
|
||||
final NumberField numberField = new NumberField(label);
|
||||
numberField.setWidthFull();
|
||||
if (changeListener != null) {
|
||||
numberField.setRequired(true);
|
||||
numberField.addValueChangeListener(changeListener::accept);
|
||||
numberField.setValueChangeMode(ValueChangeMode.TIMEOUT);
|
||||
numberField.setValueChangeTimeout(200);
|
||||
}
|
||||
return numberField;
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S119") // better readability
|
||||
public static <T, ID> HorizontalLayout addRemoveControls(
|
||||
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> addHandler,
|
||||
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> removeHandler,
|
||||
final SelectionGrid<T, ID> selectionGrid, final boolean noPadding) {
|
||||
if (addHandler == null && removeHandler == null) {
|
||||
throw new IllegalArgumentException("At least one of add or remove handlers must not be null!");
|
||||
}
|
||||
|
||||
final HorizontalLayout layout = new HorizontalLayout();
|
||||
layout.setWidthFull();
|
||||
if (!noPadding) {
|
||||
layout.addClassNames(LumoUtility.Padding.Horizontal.XLARGE, LumoUtility.Padding.Vertical.SMALL, LumoUtility.BoxSizing.BORDER);
|
||||
}
|
||||
layout.setAlignItems(FlexComponent.Alignment.BASELINE);
|
||||
layout.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
|
||||
if (addHandler != null) {
|
||||
final Button addBtn = tooltip(new Button(VaadinIcon.PLUS.create()), "Add");
|
||||
addBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||
addBtn.addClickListener(e -> addHandler
|
||||
.apply(selectionGrid)
|
||||
.thenAccept(v -> selectionGrid.refreshGrid(true)));
|
||||
layout.add(addBtn);
|
||||
}
|
||||
if (removeHandler != null) {
|
||||
final ConfirmDialog dialog = promptForDeleteConfirmation(removeHandler, selectionGrid);
|
||||
final Button removeBtn = tooltip(new Button(VaadinIcon.MINUS.create()), "Remove");
|
||||
removeBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_CONTRAST);
|
||||
removeBtn.addClickListener(e -> dialog.open());
|
||||
|
||||
layout.add(removeBtn);
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
private static <T, ID> ConfirmDialog promptForDeleteConfirmation(Function<SelectionGrid<T, ID>, CompletionStage<Void>> removeHandler,
|
||||
SelectionGrid<T, ID> selectionGrid) {
|
||||
final ConfirmDialog dialog = new ConfirmDialog();
|
||||
dialog.setHeader("Confirm Deletion");
|
||||
dialog.setText("Are you sure you want to delete the selected items? This action cannot be undone.");
|
||||
|
||||
dialog.setCancelable(true);
|
||||
dialog.addCancelListener(event -> dialog.close());
|
||||
|
||||
dialog.setConfirmButtonTheme(ButtonVariant.LUMO_ERROR.getVariantName());
|
||||
dialog.setConfirmText("Delete");
|
||||
dialog.addConfirmListener(event -> {
|
||||
removeHandler
|
||||
.apply(selectionGrid)
|
||||
.thenAccept(v -> selectionGrid.refreshGrid(false));
|
||||
dialog.close();
|
||||
});
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public static <T> void remove(final Collection<T> remove, final Set<T> from, final Function<T, ?> idFn) {
|
||||
remove.forEach(toRemove -> {
|
||||
final Object id = idFn.apply(toRemove);
|
||||
for (final Iterator<T> i = from.iterator(); i.hasNext();) {
|
||||
if (idFn.apply(i.next()).equals(id)) {
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void errorNotification(final Throwable t) {
|
||||
if (UI.getCurrent() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Notification notification = new Notification();
|
||||
notification.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||
|
||||
final Div error = new Div(new Text("Error: " + t.getMessage()));
|
||||
|
||||
final Button closeButton = tooltip(new Button(VaadinIcon.CLOSE.create()), "Close");
|
||||
closeButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE);
|
||||
closeButton.addClickListener(event -> notification.close());
|
||||
|
||||
final HorizontalLayout layout = new HorizontalLayout(error, closeButton);
|
||||
layout.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||
|
||||
notification.add(layout);
|
||||
notification.open();
|
||||
}
|
||||
|
||||
public static <T extends Component> T tooltip(final T component, final String text) {
|
||||
Tooltip.forComponent(component)
|
||||
.withText(text)
|
||||
.withPosition(Tooltip.TooltipPosition.TOP_START);
|
||||
return component;
|
||||
}
|
||||
|
||||
public static Icon iconColored(final IconFactory component, final String text, final String color) {
|
||||
var icon = tooltip(component.create(), text);
|
||||
icon.setColor(color);
|
||||
return icon;
|
||||
}
|
||||
|
||||
public static Select<MgmtActionType> actionTypeControls(DateTimePicker forceTime) {
|
||||
|
||||
Select<MgmtActionType> actionType = new Select<>();
|
||||
actionType.setLabel(Constants.ACTION_TYPE);
|
||||
actionType.setItems(MgmtActionType.values());
|
||||
actionType.setValue(MgmtActionType.FORCED);
|
||||
final ComponentRenderer<Component, MgmtActionType> actionTypeRenderer = new ComponentRenderer<>(actionTypeO -> switch (actionTypeO) {
|
||||
case SOFT -> new Text(Constants.SOFT);
|
||||
case FORCED -> new Text(Constants.FORCED);
|
||||
case DOWNLOAD_ONLY -> new Text(Constants.DOWNLOAD_ONLY);
|
||||
case TIMEFORCED -> forceTime;
|
||||
});
|
||||
actionType.addValueChangeListener(e -> actionType.setRenderer(actionTypeRenderer));
|
||||
actionType.setItemLabelGenerator(startTypeO -> switch (startTypeO) {
|
||||
case SOFT -> Constants.SOFT;
|
||||
case FORCED -> Constants.FORCED;
|
||||
case DOWNLOAD_ONLY -> Constants.DOWNLOAD_ONLY;
|
||||
case TIMEFORCED -> "Time Forced at " + (forceTime.isEmpty() ? "" : " " + forceTime.getValue());
|
||||
});
|
||||
actionType.setWidthFull();
|
||||
return actionType;
|
||||
}
|
||||
|
||||
public static class BaseDialog<T> extends Dialog {
|
||||
|
||||
protected final transient CompletableFuture<T> result = new CompletableFuture<>();
|
||||
|
||||
protected BaseDialog(final String headerTitle) {
|
||||
setHeaderTitle(headerTitle);
|
||||
setMinWidth(640, Unit.PIXELS);
|
||||
|
||||
setModal(true);
|
||||
setDraggable(true);
|
||||
setResizable(true);
|
||||
|
||||
setCloseOnEsc(true);
|
||||
setCloseOnOutsideClick(true);
|
||||
|
||||
final Button closeBtn = tooltip(new Button(VaadinIcon.CLOSE.create()), "Close");
|
||||
closeBtn.addClickListener(e -> {
|
||||
result.complete(null);
|
||||
super.close();
|
||||
});
|
||||
getHeader().add(closeBtn);
|
||||
}
|
||||
|
||||
public CompletionStage<T> result() {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!result.isDone()) {
|
||||
result.complete(null);
|
||||
}
|
||||
super.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static ZoneId getZoneId() {
|
||||
CompletableFuture<ZoneId> zoneId = new CompletableFuture<>();
|
||||
UI.getCurrent().getPage().retrieveExtendedClientDetails(details -> zoneId.complete(ZoneId.of(details.getTimeZoneId())));
|
||||
try {
|
||||
return zoneId.get(1, TimeUnit.SECONDS);
|
||||
} catch (final InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (TimeoutException | ExecutionException ignored) {
|
||||
log.warn("failed to get zone");
|
||||
}
|
||||
return ZoneId.systemDefault();
|
||||
}
|
||||
|
||||
public static <G> LocalDateTimeRenderer<G> localDateTimeRenderer(ToLongFunction<G> f) {
|
||||
|
||||
return new LocalDateTimeRenderer<>((e) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(f.applyAsLong(e)), getZoneId()),
|
||||
() -> DateTimeFormatter.ofLocalizedDateTime(
|
||||
FormatStyle.SHORT,
|
||||
FormatStyle.MEDIUM).withLocale(UI.getCurrent().getLocale()));
|
||||
}
|
||||
|
||||
public static String localDateTimeFromTs(long timestamp) {
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), getZoneId()).format(DateTimeFormatter.ofLocalizedDateTime(
|
||||
FormatStyle.SHORT,
|
||||
FormatStyle.MEDIUM).withLocale(UI.getCurrent().getLocale()));
|
||||
}
|
||||
|
||||
public static String getSortParam(List<QuerySortOrder> querySortOrders) {
|
||||
return getSortParam(querySortOrders, null);
|
||||
}
|
||||
|
||||
public static String getSortParam(List<QuerySortOrder> querySortOrders, String defaultSort) {
|
||||
if (!querySortOrders.isEmpty()) {
|
||||
QuerySortOrder firstSort = querySortOrders.get(0);
|
||||
String order = firstSort.getDirection() == SortDirection.ASCENDING ? "asc" : "desc";
|
||||
return String.format("%s:%s", firstSort.getSorted(), order);
|
||||
}
|
||||
return defaultSort;
|
||||
}
|
||||
|
||||
public static String durationFromMillis(Long time) {
|
||||
var duration = Duration.between(Instant.ofEpochMilli(time), Instant.now());
|
||||
var day = duration.toDaysPart();
|
||||
if (day > 2) {
|
||||
return day + "d";
|
||||
}
|
||||
return duration.withNanos(0).toString()
|
||||
.substring(2)
|
||||
.replaceFirst("(^\\d+[HMS]\\d*M*)", "$1")
|
||||
.toLowerCase();
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
32
hawkbit-ui/src/main/resources/application.properties
Normal file
32
hawkbit-ui/src/main/resources/application.properties
Normal file
@@ -0,0 +1,32 @@
|
||||
#
|
||||
# Copyright (c) 2023 Contributors to the Eclipse Foundation
|
||||
#
|
||||
# This program and the accompanying materials are made
|
||||
# available under the terms of the Eclipse Public License 2.0
|
||||
# which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
#
|
||||
# SPDX-License-Identifier: EPL-2.0
|
||||
#
|
||||
|
||||
# Spring configuration
|
||||
server.port=${PORT:8088}
|
||||
|
||||
# Logging configuration
|
||||
logging.level.org.springframework.boot.actuate.audit.listener.AuditListener=WARN
|
||||
# logging pattern
|
||||
logging.pattern.console=%clr(%d{${logging.pattern.dateformat:yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${logging.pattern.level:%5p}) %clr(${PID:}){magenta} %clr(---){faint} %clr([${spring.application.name}] [%X{tenant}:%X{user}] [%15.15t]){faint} %clr(${logging.pattern.correlation:}){faint}%clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${logging.exception-conversion-word:%wEx}
|
||||
|
||||
### Vaadin start ###
|
||||
# build with mvn vaadin:build-frontend to enable / disable
|
||||
vaadin.productionMode=true
|
||||
vaadin.frontend.hotdeploy=false
|
||||
logging.level.org.atmosphere=warn
|
||||
spring.mustache.check-template-location=false
|
||||
# Launch the default browser when starting the application in development mode
|
||||
vaadin.launch-browser=true
|
||||
# To improve the performance during development.
|
||||
# For more information https://vaadin.com/docs/flow/spring/tutorial-spring-configuration.html#special-configuration-parameters
|
||||
vaadin.allowed-packages=com.vaadin,org.vaadin,dev.hilla,org.eclipse.hawkbit
|
||||
spring.application.name=hawkBit-UI
|
||||
server.servlet.session.persistent=false
|
||||
### Vaadin end ###
|
||||
18
hawkbit-ui/src/main/resources/banner.txt
Normal file
18
hawkbit-ui/src/main/resources/banner.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
______ _ _ _ _ ____ _ _
|
||||
| ____| | (_) | | | | | _ \(_) |
|
||||
| |__ ___| |_ _ __ ___ ___ | |__ __ ___ _| | _| |_) |_| |_
|
||||
| __| / __| | | '_ \/ __|/ _ \ | '_ \ / _` \ \ /\ / / |/ / _ <| | __|
|
||||
| |___| (__| | | |_) \__ \ __/ | | | | (_| |\ V V /| <| |_) | | |_
|
||||
|______\___|_|_| .__/|___/\___|_|_| |_|\__,_|_\_/\_/ |_|\_\____/|_|\__|
|
||||
/ ____(_)| | | | | | | |_ _|
|
||||
| (___ _ |_|_ ___ _ __ | | ___ | | | | | |
|
||||
\___ \| | '_ ` _ \| '_ \| |/ _ \ | | | | | |
|
||||
____) | | | | | | | |_) | | __/ | |__| |_| |_
|
||||
|_____/|_|_| |_| |_| .__/|_|\___| \____/|_____|
|
||||
| |
|
||||
|_|
|
||||
|
||||
Eclipse hawkBit UI ${application.formatted-version}
|
||||
using Spring Boot ${spring-boot.formatted-version}
|
||||
|
||||
Go to https://www.eclipse.org/hawkbit for more information.
|
||||
Reference in New Issue
Block a user