Move Simple UI at root level (#2005)

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

View File

@@ -0,0 +1,98 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple;
import java.util.function.Supplier;
import feign.FeignException;
import lombok.Getter;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetTagRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetTypeRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRolloutRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftwareModuleRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftwareModuleTypeRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetFilterQueryRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetTagRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetTypeRestApi;
import org.eclipse.hawkbit.mgmt.rest.api.MgmtTenantManagementRestApi;
import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.Tenant;
import org.springframework.http.ResponseEntity;
@Getter
public class HawkbitMgmtClient {
private final Tenant tenant;
private final HawkbitClient hawkbitClient;
private final MgmtSoftwareModuleRestApi softwareModuleRestApi;
private final MgmtSoftwareModuleTypeRestApi softwareModuleTypeRestApi;
private final MgmtDistributionSetRestApi distributionSetRestApi;
private final MgmtDistributionSetTypeRestApi distributionSetTypeRestApi;
private final MgmtDistributionSetTagRestApi distributionSetTagRestApi;
private final MgmtTargetRestApi targetRestApi;
private final MgmtTargetTypeRestApi targetTypeRestApi;
private final MgmtTargetTagRestApi targetTagRestApi;
private final MgmtTargetFilterQueryRestApi targetFilterQueryRestApi;
private final MgmtRolloutRestApi rolloutRestApi;
private final MgmtTenantManagementRestApi tenantManagementRestApi;
HawkbitMgmtClient(final Tenant tenant, final HawkbitClient hawkbitClient) {
this.tenant = tenant;
this.hawkbitClient = hawkbitClient;
softwareModuleRestApi = service(MgmtSoftwareModuleRestApi.class);
softwareModuleTypeRestApi = service(MgmtSoftwareModuleTypeRestApi.class);
distributionSetRestApi = service(MgmtDistributionSetRestApi.class);
distributionSetTypeRestApi = service(MgmtDistributionSetTypeRestApi.class);
distributionSetTagRestApi = service(MgmtDistributionSetTagRestApi.class);
targetRestApi = service(MgmtTargetRestApi.class);
targetTypeRestApi = service(MgmtTargetTypeRestApi.class);
targetTagRestApi = service(MgmtTargetTagRestApi.class);
targetFilterQueryRestApi = service(MgmtTargetFilterQueryRestApi.class);
rolloutRestApi = service(MgmtRolloutRestApi.class);
tenantManagementRestApi = service(MgmtTenantManagementRestApi.class);
}
boolean hasSoftwareModulesRead() {
return hasRead(() -> softwareModuleRestApi.getSoftwareModule(-1L));
}
boolean hasRolloutRead() {
return hasRead(() -> rolloutRestApi.getRollout(-1L));
}
boolean hasDistributionSetRead() {
return hasRead(() -> distributionSetRestApi.getDistributionSet(-1L));
}
boolean hasTargetRead() {
return hasRead(() -> targetRestApi.getTarget("_#ETE$ER"));
}
boolean hasConfigRead() {
return hasRead(() -> tenantManagementRestApi.getTenantConfigurationValue("_#ETE$ER"));
}
private boolean hasRead(final Supplier<ResponseEntity<?>> doCall) {
try {
final int statusCode = doCall.get().getStatusCode().value();
return statusCode != 401 && statusCode != 403;
} catch (final FeignException e) {
return !(e instanceof FeignException.Unauthorized) && !(e instanceof FeignException.Forbidden);
}
}
private <T> T service(final Class<T> serviceType) {
return hawkbitClient.mgmtService(serviceType, tenant);
}
}

View File

@@ -0,0 +1,154 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple;
import java.util.Optional;
import com.vaadin.flow.component.Unit;
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.applayout.DrawerToggle;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.contextmenu.MenuItem;
import com.vaadin.flow.component.html.Anchor;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Footer;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Header;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.menubar.MenuBar;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.Scroller;
import com.vaadin.flow.component.sidenav.SideNav;
import com.vaadin.flow.component.sidenav.SideNavItem;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.server.auth.AccessAnnotationChecker;
import com.vaadin.flow.theme.lumo.LumoUtility;
import org.eclipse.hawkbit.ui.simple.security.AuthenticatedUser;
import org.eclipse.hawkbit.ui.simple.view.AboutView;
import org.eclipse.hawkbit.ui.simple.view.ConfigView;
import org.eclipse.hawkbit.ui.simple.view.DistributionSetView;
import org.eclipse.hawkbit.ui.simple.view.RolloutView;
import org.eclipse.hawkbit.ui.simple.view.SoftwareModuleView;
import org.eclipse.hawkbit.ui.simple.view.TargetView;
/**
* The main view is a top-level placeholder for other views.
*/
public class MainLayout extends AppLayout {
private final transient AuthenticatedUser authenticatedUser;
private final AccessAnnotationChecker accessChecker;
private H2 viewTitle;
public MainLayout(final AuthenticatedUser authenticatedUser, final AccessAnnotationChecker accessChecker) {
this.authenticatedUser = authenticatedUser;
this.accessChecker = accessChecker;
setPrimarySection(Section.DRAWER);
addDrawerContent();
setDrawerOpened(true);
addHeaderContent();
}
@Override
protected void afterNavigation() {
super.afterNavigation();
viewTitle.setText(
Optional.ofNullable(getContent().getClass().getAnnotation(PageTitle.class))
.map(PageTitle::value)
.orElse(""));
}
private void addHeaderContent() {
final DrawerToggle toggle = new DrawerToggle();
toggle.setAriaLabel("Menu toggle");
viewTitle = new H2();
viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE);
addToNavbar(false, toggle, viewTitle);
}
private void addDrawerContent() {
final H1 appName = new H1("hawkBit UI (Experimental!)");
final HorizontalLayout layout = new HorizontalLayout();
layout.setPadding(true);
layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
final Image icon = new Image("images/header_icon.png", "hawkBit icon");
icon.setMaxHeight(24, Unit.PIXELS);
icon.setMaxWidth(24, Unit.PIXELS);
appName.addClassNames(LumoUtility.AlignItems.BASELINE, LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE);
layout.add(icon, appName);
final Header header = new Header(layout);
final Scroller scroller = new Scroller(createNavigation());
addToDrawer(header, scroller, createFooter());
}
private SideNav createNavigation() {
final SideNav nav = new SideNav();
if (accessChecker.hasAccess(TargetView.class)) {
nav.addItem(new SideNavItem("Targets", TargetView.class, VaadinIcon.FILTER.create()));
}
if (accessChecker.hasAccess(RolloutView.class)) {
nav.addItem(new SideNavItem("Rollouts", RolloutView.class, VaadinIcon.COGS.create()));
}
if (accessChecker.hasAccess(DistributionSetView.class)) {
nav.addItem(new SideNavItem("Distribution Sets", DistributionSetView.class, VaadinIcon.FILE_TREE.create()));
}
if (accessChecker.hasAccess(SoftwareModuleView.class)) {
nav.addItem(new SideNavItem("Software Modules", SoftwareModuleView.class, VaadinIcon.FILE.create()));
}
if (accessChecker.hasAccess(ConfigView.class)) {
nav.addItem(new SideNavItem("Config", ConfigView.class, VaadinIcon.COG.create()));
}
if (accessChecker.hasAccess(AboutView.class)) {
nav.addItem(new SideNavItem("About", AboutView.class, VaadinIcon.INFO_CIRCLE.create()));
}
return nav;
}
private Footer createFooter() {
final Footer layout = new Footer();
final Optional<String> maybeUser = authenticatedUser.getName();
if (maybeUser.isPresent()) {
final String user = maybeUser.get();
final Avatar avatar = new Avatar(user);
final MenuBar userMenu = new MenuBar();
userMenu.setThemeName("tertiary-inline contrast");
final MenuItem userName = userMenu.addItem("");
final Div div = new Div();
div.add(avatar);
div.add(user);
div.add(new Icon("lumo", "dropdown"));
div.getElement().getStyle().set("display", "flex");
div.getElement().getStyle().set("align-items", "center");
div.getElement().getStyle().set("gap", "var(--lumo-space-s)");
userName.add(div);
userName.getSubMenu().addItem("Sign out", e -> authenticatedUser.logout());
layout.add(userMenu);
} else {
final Anchor loginLink = new Anchor("login", "Sign in");
layout.add(loginLink);
}
return layout;
}
}

View File

@@ -0,0 +1,131 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple;
import static feign.Util.ISO_8859_1;
import java.util.Base64;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import com.vaadin.flow.component.page.AppShellConfigurator;
import com.vaadin.flow.server.PWA;
import com.vaadin.flow.theme.Theme;
import com.vaadin.flow.theme.lumo.Lumo;
import feign.Client;
import feign.Contract;
import feign.RequestInterceptor;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import org.eclipse.hawkbit.sdk.HawkbitClient;
import org.eclipse.hawkbit.sdk.HawkbitServer;
import org.eclipse.hawkbit.sdk.Tenant;
import org.eclipse.hawkbit.ui.simple.view.util.Utils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
@Theme(themeClass = Lumo.class)
@PWA(name = "hawkBit UI", shortName = "hawkBit UI")
@SpringBootApplication
@Import(FeignClientsConfiguration.class)
public class SimpleUIApp implements AppShellConfigurator {
private static final RequestInterceptor AUTHORIZATION = requestTemplate -> {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
requestTemplate.header("Authorization", "Basic " + Base64.getEncoder().encodeToString(
(Objects.requireNonNull(authentication.getPrincipal(), "User is null!") + ":" + Objects.requireNonNull(
authentication.getCredentials(), "Password is not available!")).getBytes(ISO_8859_1)));
};
private static final ErrorDecoder DEFAULT_ERROR_DECODER = new ErrorDecoder.Default();
private static final ErrorDecoder ERROR_DECODER = (methodKey, response) -> {
final Exception e = DEFAULT_ERROR_DECODER.decode(methodKey, response);
Utils.errorNotification(e);
return e;
};
public static void main(String[] args) {
SpringApplication.run(SimpleUIApp.class, args);
}
@Bean
HawkbitClient hawkbitClient(
final HawkbitServer hawkBitServer,
final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
return new HawkbitClient(
hawkBitServer, client, encoder, decoder, contract,
ERROR_DECODER,
(tenant, controller) ->
controller == null ?
AUTHORIZATION : HawkbitClient.DEFAULT_REQUEST_INTERCEPTOR_FN.apply(tenant, controller));
}
@Bean
HawkbitMgmtClient hawkbitMgmtClient(final Tenant tenant, final HawkbitClient hawkbitClient) {
return new HawkbitMgmtClient(tenant, hawkbitClient);
}
// accepts all user / pass, just delegating them to the feign client
@Bean
AuthenticationManager authenticationManager(final HawkbitMgmtClient hawkbitClient) {
return authentication -> {
final String username = authentication.getName();
final String password = authentication.getCredentials().toString();
final List<String> roles = new LinkedList<>();
roles.add("ANONYMOUS");
final SecurityContext unauthorizedContext = SecurityContextHolder.createEmptyContext();
unauthorizedContext.setAuthentication(
new UsernamePasswordAuthenticationToken(username, password));
final SecurityContext currentContext = SecurityContextHolder.getContext();
try {
SecurityContextHolder.setContext(unauthorizedContext);
if (hawkbitClient.hasSoftwareModulesRead()) {
roles.add("SOFTWARE_MODULE_READ");
}
if (hawkbitClient.hasRolloutRead()) {
roles.add("ROLLOUT_READ");
}
if (hawkbitClient.hasDistributionSetRead()) {
roles.add("DISTRIBUTION_SET_READ");
}
if (hawkbitClient.hasTargetRead()) {
roles.add("TARGET_READ");
}
if (hawkbitClient.hasConfigRead()) {
roles.add("CONFIG_READ");
}
} finally {
SecurityContextHolder.setContext(currentContext);
}
return new UsernamePasswordAuthenticationToken(
username, password,
roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList()) {
@Override
public void eraseCredentials() {
// don't erase credentials because they will be used
// to authenticate to the hawkBit update server / mgmt server
}
};
};
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple.security;
import java.util.Optional;
import com.vaadin.flow.spring.security.AuthenticationContext;
import org.springframework.stereotype.Component;
@Component
public class AuthenticatedUser {
private final AuthenticationContext authenticationContext;
public AuthenticatedUser(final AuthenticationContext authenticationContext) {
this.authenticationContext = authenticationContext;
}
public Optional<String> getName() {
return authenticationContext.getPrincipalName();
}
public void logout() {
authenticationContext.logout();
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple.security;
import com.vaadin.flow.spring.security.VaadinWebSecurity;
import org.eclipse.hawkbit.ui.simple.view.LoginView;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends VaadinWebSecurity {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(final HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
authorize -> authorize.requestMatchers(new AntPathRequestMatcher("/images/*.png")).permitAll());
super.configure(http);
setLoginView(http, LoginView.class);
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple.view;
import jakarta.annotation.security.RolesAllowed;
import com.vaadin.flow.component.html.H2;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.RouteAlias;
import com.vaadin.flow.theme.lumo.LumoUtility.Margin;
import org.eclipse.hawkbit.ui.simple.MainLayout;
@PageTitle("About")
@Route(value = "about", layout = MainLayout.class)
@RouteAlias(value = "", layout = MainLayout.class)
@RolesAllowed({ "ANONYMOUS" })
public class AboutView extends VerticalLayout {
public AboutView() {
setSpacing(false);
final Image img = new Image("images/about_image.png", "hawkBit");
img.setWidth("200px");
add(img);
final H2 header = new H2("Eclipse hawkBit UI");
header.addClassNames(Margin.Top.XLARGE, Margin.Bottom.MEDIUM);
setSizeFull();
setJustifyContentMode(JustifyContentMode.CENTER);
setDefaultHorizontalComponentAlignment(Alignment.CENTER);
getStyle().set("text-align", "center");
}
}

View File

@@ -0,0 +1,89 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple.view;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import jakarta.annotation.security.RolesAllowed;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.NumberField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.mgmt.json.model.system.MgmtSystemTenantConfigurationValueRequest;
import org.eclipse.hawkbit.ui.simple.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.simple.MainLayout;
@PageTitle("Config")
@Route(value = "config", layout = MainLayout.class)
@RolesAllowed({ "CONFIG_READ" })
@Slf4j
public class ConfigView extends VerticalLayout {
private static final String WIDTH = "width";
private static final String PX_300 = "300px";
private final Map<String, MgmtSystemTenantConfigurationValueRequest> configValue = new HashMap<>();
public ConfigView(final HawkbitMgmtClient hawkbitClient) {
setSpacing(false);
final Button saveButton = new Button("Save");
Optional.ofNullable(
hawkbitClient.getTenantManagementRestApi().getTenantConfiguration().getBody()).ifPresent(config ->
config.forEach((k, v) -> {
if (v.getValue() instanceof String strValue) {
final TextField tf = new TextField(k, strValue, event -> {
final MgmtSystemTenantConfigurationValueRequest vre = new MgmtSystemTenantConfigurationValueRequest();
vre.setValue(event.getValue());
configValue.put(k, vre);
});
tf.getElement().getStyle().set(WIDTH, PX_300);
add(tf);
} else if (v.getValue() instanceof Boolean boolValue) {
add(new Checkbox(k, boolValue, event -> {
final MgmtSystemTenantConfigurationValueRequest vre = new MgmtSystemTenantConfigurationValueRequest();
vre.setValue(event.getValue());
configValue.put(k, vre);
}));
} else if (v.getValue() instanceof Long longValue) {
final NumberField nf = new NumberField(k, (double) longValue, event -> {
final MgmtSystemTenantConfigurationValueRequest vre = new MgmtSystemTenantConfigurationValueRequest();
vre.setValue(event.getValue());
configValue.put(k, vre);
});
nf.getElement().getStyle().set(WIDTH, PX_300);
add(nf);
} else if (v.getValue() instanceof Integer intValue) {
final NumberField nf = new NumberField(k, (double) intValue, event -> {
MgmtSystemTenantConfigurationValueRequest vre = new MgmtSystemTenantConfigurationValueRequest();
vre.setValue(event.getValue());
configValue.put(k, vre);
});
nf.getElement().getStyle().set(WIDTH, PX_300);
add(nf);
} else {
log.debug("Unexpected value type: {} -> {} (class: {})",
k, v.getValue(), v.getValue() == null ? "null" : v.getValue().getClass());
}
}));
saveButton.addClickListener(click -> configValue.forEach(
(key, value) -> hawkbitClient.getTenantManagementRestApi().updateTenantConfigurationValue(key, value)));
saveButton.addClickShortcut(Key.ENTER);
add(saveButton);
}
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple.view;
public interface Constants {
// properties
String ID = "Id";
String NAME = "Name";
String DESCRIPTION = "Description";
String VERSION = "Version";
String VENDOR = "Vendor";
String TYPE = "Type";
String CREATED_BY = "Created by";
String CREATED_AT = "Created at";
String LAST_MODIFIED_BY = "Last modified by";
String LAST_MODIFIED_AT = "Last modified at";
String SECURITY_TOKEN = "Security Token";
// rollout
String GROUP_COUNT = "Group Count";
String TARGET_COUNT = "Target Count";
String STATS = "Stats";
String STATUS = "Status";
String ACTIONS = "Actions";
// create rollout
String TARGET_FILTER = "Target Filter";
String DISTRIBUTION_SET = "Distribution Set";
String ACTION_TYPE = "Action Type";
String START_AT = "Start At";
String SOFT = "Soft";
String FORCED = "Forced";
String DOWNLOAD_ONLY = "Download Only";
String START_TYPE = "Start Type";
String MANUAL = "Manual";
String AUTO = "Auto";
String DYNAMIC = "Dynamic";
String NAME_ASC = "name:asc";
}

View File

@@ -0,0 +1,341 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple.view;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import jakarta.annotation.security.RolesAllowed;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.checkbox.CheckboxGroup;
import com.vaadin.flow.component.dependency.Uses;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPost;
import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetType;
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule;
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleAssigment;
import org.eclipse.hawkbit.mgmt.json.model.tag.MgmtTag;
import org.eclipse.hawkbit.ui.simple.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.simple.MainLayout;
import org.eclipse.hawkbit.ui.simple.view.util.Filter;
import org.eclipse.hawkbit.ui.simple.view.util.SelectionGrid;
import org.eclipse.hawkbit.ui.simple.view.util.TableView;
import org.eclipse.hawkbit.ui.simple.view.util.Utils;
@PageTitle("Distribution Sets")
@Route(value = "distribution_sets", layout = MainLayout.class)
@RolesAllowed({ "DISTRIBUTION_SET_READ" })
@Uses(Icon.class)
public class DistributionSetView extends TableView<MgmtDistributionSet, Long> {
public DistributionSetView(final HawkbitMgmtClient hawkbitClient) {
super(
new DistributionSetFilter(hawkbitClient),
new SelectionGrid.EntityRepresentation<>(MgmtDistributionSet.class, MgmtDistributionSet::getDsId) {
private final DistributionSetDetails details = new DistributionSetDetails(hawkbitClient);
@Override
protected void addColumns(Grid<MgmtDistributionSet> grid) {
grid.addColumn(MgmtDistributionSet::getDsId).setHeader(Constants.ID).setAutoWidth(true);
grid.addColumn(MgmtDistributionSet::getName).setHeader(Constants.NAME).setAutoWidth(true);
grid.addColumn(MgmtDistributionSet::getVersion).setHeader(Constants.VERSION).setAutoWidth(true);
grid.addColumn(MgmtDistributionSet::getTypeName).setHeader(Constants.TYPE).setAutoWidth(true);
grid.setItemDetailsRenderer(new ComponentRenderer<>(
() -> details, DistributionSetDetails::setItem));
}
},
(query, rsqlFilter) -> Optional.ofNullable(
hawkbitClient.getDistributionSetRestApi()
.getDistributionSets(query.getOffset(), query.getPageSize(), Constants.NAME_ASC, rsqlFilter)
.getBody())
.stream().flatMap(body -> body.getContent().stream()),
e -> new CreateDialog(hawkbitClient).result(),
selectionGrid -> {
selectionGrid.getSelectedItems().forEach(
distributionSet -> hawkbitClient.getDistributionSetRestApi()
.deleteDistributionSet(distributionSet.getDsId()));
return CompletableFuture.completedFuture(null);
});
}
private static SelectionGrid<MgmtSoftwareModule, Long> selectSoftwareModuleGrid() {
return new SelectionGrid<>(
new SelectionGrid.EntityRepresentation<>(
MgmtSoftwareModule.class, MgmtSoftwareModule::getModuleId) {
@Override
protected void addColumns(Grid<MgmtSoftwareModule> grid) {
grid.addColumn(MgmtSoftwareModule::getModuleId).setHeader(Constants.ID).setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getName).setHeader(Constants.NAME).setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getVersion).setHeader(Constants.VERSION).setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getTypeName).setHeader(Constants.TYPE).setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getVendor).setHeader(Constants.VENDOR).setAutoWidth(true);
}
});
}
private static class DistributionSetFilter implements Filter.Rsql {
private final TextField name = Utils.textField("Name");
private final CheckboxGroup<MgmtDistributionSetType> type = new CheckboxGroup<>("Type");
private final CheckboxGroup<MgmtTag> tag = new CheckboxGroup<>("Tag");
private DistributionSetFilter(final HawkbitMgmtClient hawkbitClient) {
name.setPlaceholder("<name filter>");
type.setItemLabelGenerator(MgmtDistributionSetType::getName);
type.setItems(Optional.ofNullable(
hawkbitClient.getDistributionSetTypeRestApi()
.getDistributionSetTypes(0, 20, Constants.NAME_ASC, null)
.getBody())
.map(PagedList::getContent)
.orElseGet(Collections::emptyList));
tag.setItemLabelGenerator(MgmtTag::getName);
tag.setItems(Optional.ofNullable(
hawkbitClient.getDistributionSetTagRestApi()
.getDistributionSetTags(0, 20, Constants.NAME_ASC, null)
.getBody())
.map(PagedList::getContent)
.orElseGet(Collections::emptyList));
}
@Override
public List<Component> components() {
return List.of(name, type);
}
@Override
public String filter() {
return Filter.filter(
Map.of(
"name", name.getOptionalValue(),
"type", type.getSelectedItems().stream().map(MgmtDistributionSetType::getKey)
.toList(),
"tag", tag.getSelectedItems()));
}
}
private static class DistributionSetDetails extends FormLayout {
private final transient HawkbitMgmtClient hawkbitClient;
private final TextArea description = new TextArea("Description");
private final TextField createdBy = Utils.textField("Created by");
private final TextField createdAt = Utils.textField("Created at");
private final TextField lastModifiedBy = Utils.textField("Last modified by");
private final TextField lastModifiedAt = Utils.textField("Last modified at");
private final SelectionGrid<MgmtSoftwareModule, Long> softwareModulesGrid = selectSoftwareModuleGrid();
private DistributionSetDetails(final HawkbitMgmtClient hawkbitClient) {
this.hawkbitClient = hawkbitClient;
description.setMinLength(2);
Stream.of(
description,
createdBy, createdAt,
lastModifiedBy, lastModifiedAt)
.forEach(field -> {
field.setReadOnly(true);
add(field);
});
add(softwareModulesGrid);
setResponsiveSteps(new ResponsiveStep("0", 2));
setColspan(description, 2);
setColspan(softwareModulesGrid, 2);
}
private void setItem(final MgmtDistributionSet distributionSet) {
description.setValue(distributionSet.getDescription());
createdBy.setValue(distributionSet.getCreatedBy());
createdAt.setValue(new Date(distributionSet.getCreatedAt()).toString());
lastModifiedBy.setValue(distributionSet.getLastModifiedBy());
lastModifiedAt.setValue(new Date(distributionSet.getLastModifiedAt()).toString());
softwareModulesGrid.setItems(query -> Optional.ofNullable(
hawkbitClient.getDistributionSetRestApi()
.getAssignedSoftwareModules(
distributionSet.getDsId(),
query.getOffset(), query.getLimit(), Constants.NAME_ASC)
.getBody()).stream().flatMap(body -> body.getContent().stream()));
softwareModulesGrid.setSelectionMode(Grid.SelectionMode.NONE);
}
}
private static class CreateDialog extends Utils.BaseDialog<Void> {
private final transient HawkbitMgmtClient hawkbitClient;
private final Select<MgmtDistributionSetType> type;
private final TextField name;
private final TextField version;
private final TextArea description;
private final Checkbox requiredMigrationStep;
private final Button create;
private CreateDialog(final HawkbitMgmtClient hawkbitClient) {
super("Create Distribution Set");
this.hawkbitClient = hawkbitClient;
type = new Select<>(
"Type",
this::readyToCreate,
Optional.ofNullable(
hawkbitClient.getDistributionSetTypeRestApi()
.getDistributionSetTypes(0, 30, Constants.NAME_ASC, null)
.getBody())
.map(body -> body.getContent().toArray(new MgmtDistributionSetType[0]))
.orElseGet(() -> new MgmtDistributionSetType[0]));
type.focus();
type.setWidthFull();
type.setRequiredIndicatorVisible(true);
type.setItemLabelGenerator(MgmtDistributionSetType::getName);
name = Utils.textField(Constants.NAME, this::readyToCreate);
version = Utils.textField(Constants.VERSION, this::readyToCreate);
final TextField vendor = Utils.textField(Constants.VENDOR);
description = new TextArea(Constants.DESCRIPTION);
description.setWidthFull();
description.setMinLength(2);
requiredMigrationStep = new Checkbox("Required Migration Step");
create = Utils.tooltip(new Button("Create"), "Create (Enter)");
create.setEnabled(false);
addCreateClickListener();
create.addClickShortcut(Key.ENTER);
final Button cancel = Utils.tooltip(new Button("Cancel"), "Cancel (Esc)");
cancel.addClickListener(e -> close());
create.addClickShortcut(Key.ESCAPE);
final HorizontalLayout actions = new HorizontalLayout(create, cancel);
actions.setSizeFull();
actions.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
final VerticalLayout layout = new VerticalLayout();
layout.setSizeFull();
layout.setPadding(true);
layout.setSpacing(false);
layout.add(type, name, version, vendor, description, requiredMigrationStep, actions);
add(layout);
open();
}
private void readyToCreate(final Object v) {
final boolean createEnabled = !type.isEmpty() && !name.isEmpty() && !version.isEmpty();
if (create.isEnabled() != createEnabled) {
create.setEnabled(createEnabled);
}
}
private void addCreateClickListener() {
create.addClickListener(e -> {
close();
final long distributionSetId = Optional.ofNullable(
hawkbitClient.getDistributionSetRestApi()
.createDistributionSets(
List.of((MgmtDistributionSetRequestBodyPost) new MgmtDistributionSetRequestBodyPost()
.setType(type.getValue().getKey())
.setName(name.getValue())
.setVersion(version.getValue())
.setDescription(description.getValue())
.setRequiredMigrationStep(requiredMigrationStep.getValue())))
.getBody())
.stream()
.flatMap(Collection::stream)
.findFirst()
.orElseThrow()
.getDsId();
new AddSoftwareModulesDialog(distributionSetId, hawkbitClient).open();
});
}
}
private static class AddSoftwareModulesDialog extends Utils.BaseDialog<Void> {
private final transient Set<MgmtSoftwareModule> softwareModules = Collections.synchronizedSet(new HashSet<>());
private AddSoftwareModulesDialog(final long distributionSetId, final HawkbitMgmtClient hawkbitClient) {
super("Add Software Modules");
final SelectionGrid<MgmtSoftwareModule, Long> softwareModulesGrid = selectSoftwareModuleGrid();
softwareModulesGrid.setItems(query -> {
query.getOffset(); // to keep vaadin contract
return softwareModules.stream().limit(query.getLimit());
});
final Component addRemoveControls = Utils.addRemoveControls(
v -> new Utils.BaseDialog<Void>("Add Software Modules") {{
final SoftwareModuleView softwareModulesView = new SoftwareModuleView(false, hawkbitClient);
add(softwareModulesView);
final Button addBtn = new Button("Add");
addBtn.addClickListener(e -> {
softwareModules.addAll(softwareModulesView.getSelection());
softwareModulesGrid.refreshGrid(false);
close();
});
add(addBtn);
open();
}}.result(),
v -> {
Utils.remove(softwareModulesGrid.getSelectedItems(), softwareModules, MgmtSoftwareModule::getModuleId);
softwareModulesGrid.refreshGrid(false);
return CompletableFuture.completedFuture(null);
},
softwareModulesGrid, true);
final Button finishBtn = Utils.tooltip(new Button("Finish"), "Finish (Esc)");
finishBtn.addClickListener(e -> {
hawkbitClient.getDistributionSetRestApi().assignSoftwareModules(
distributionSetId, softwareModules.stream().map(softwareModule -> {
final MgmtSoftwareModuleAssigment assignment = new MgmtSoftwareModuleAssigment();
assignment.setId(softwareModule.getModuleId());
return assignment;
}).toList());
close();
});
finishBtn.addClickShortcut(Key.ENTER);
final HorizontalLayout finish = new HorizontalLayout(finishBtn);
finish.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
finish.setWidthFull();
final HorizontalLayout addRemove = new HorizontalLayout(addRemoveControls);
addRemove.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
addRemove.setWidthFull();
final VerticalLayout layout = new VerticalLayout();
layout.setSizeFull();
layout.setSpacing(false);
layout.add(softwareModulesGrid, addRemove, finish);
add(layout);
}
}
}

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple.view;
import com.vaadin.flow.component.login.LoginI18n;
import com.vaadin.flow.component.login.LoginOverlay;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.internal.RouteUtil;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import org.eclipse.hawkbit.ui.simple.security.AuthenticatedUser;
@AnonymousAllowed
@PageTitle("Login")
@Route(value = "login")
public class LoginView extends LoginOverlay implements BeforeEnterObserver {
private final transient AuthenticatedUser authenticatedUser;
public LoginView(final AuthenticatedUser authenticatedUser) {
this.authenticatedUser = authenticatedUser;
setAction(RouteUtil.getRoutePath(VaadinService.getCurrent().getContext(), getClass()));
final LoginI18n i18n = LoginI18n.createDefault();
i18n.setHeader(new LoginI18n.Header());
i18n.getHeader().setTitle("hawkBit");
i18n.getHeader().setDescription("Login in hawkBit");
i18n.setAdditionalInformation(null);
setI18n(i18n);
setForgotPasswordButtonVisible(false);
setOpened(true);
}
@Override
public void beforeEnter(final BeforeEnterEvent event) {
if (authenticatedUser.getName().isPresent()) { // already logged in
setOpened(false);
event.forwardTo("");
}
setError(event.getLocation().getQueryParameters().getParameters().containsKey("error"));
}
}

View File

@@ -0,0 +1,459 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple.view;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import jakarta.annotation.security.RolesAllowed;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.Text;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.datetimepicker.DateTimePicker;
import com.vaadin.flow.component.dependency.Uses;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.component.textfield.NumberField;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutCondition;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutErrorAction;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutResponseBody;
import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPost;
import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroupResponseBody;
import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQuery;
import org.eclipse.hawkbit.ui.simple.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.simple.MainLayout;
import org.eclipse.hawkbit.ui.simple.view.util.Filter;
import org.eclipse.hawkbit.ui.simple.view.util.SelectionGrid;
import org.eclipse.hawkbit.ui.simple.view.util.TableView;
import org.eclipse.hawkbit.ui.simple.view.util.Utils;
import org.springframework.util.ObjectUtils;
@PageTitle("Rollouts")
@Route(value = "rollouts", layout = MainLayout.class)
@RolesAllowed({ "ROLLOUT_READ" })
@Uses(Icon.class)
public class RolloutView extends TableView<MgmtRolloutResponseBody, Long> {
public RolloutView(final HawkbitMgmtClient hawkbitClient) {
super(
new RolloutFilter(),
new SelectionGrid.EntityRepresentation<>(
MgmtRolloutResponseBody.class, MgmtRolloutResponseBody::getRolloutId) {
private final RolloutDetails details = new RolloutDetails(hawkbitClient);
@Override
protected void addColumns(final Grid<MgmtRolloutResponseBody> grid) {
grid.addColumn(MgmtRolloutResponseBody::getRolloutId).setHeader(Constants.ID).setAutoWidth(true);
grid.addColumn(MgmtRolloutResponseBody::getName).setHeader(Constants.NAME).setAutoWidth(true);
grid.addColumn(MgmtRolloutResponseBody::getTotalGroups).setHeader(Constants.GROUP_COUNT).setAutoWidth(true);
grid.addColumn(MgmtRolloutResponseBody::getTotalTargets).setHeader(Constants.TARGET_COUNT).setAutoWidth(true);
grid.addColumn(MgmtRolloutResponseBody::getTotalTargetsPerStatus).setHeader(Constants.STATS).setAutoWidth(true);
grid.addColumn(MgmtRolloutResponseBody::getStatus).setHeader(Constants.STATUS).setAutoWidth(true);
grid.addComponentColumn(rollout -> new Actions(rollout, grid, hawkbitClient)).setHeader(
Constants.ACTIONS).setAutoWidth(true);
grid.setItemDetailsRenderer(new ComponentRenderer<>(
() -> details, RolloutDetails::setItem));
}
},
(query, rsqlFilter) -> Optional.ofNullable(
hawkbitClient.getRolloutRestApi()
.getRollouts(
query.getOffset(), query.getPageSize(), Constants.NAME_ASC, rsqlFilter, "full")
.getBody()).stream().flatMap(page -> page.getContent().stream()),
selectionGrid -> new CreateDialog(hawkbitClient).result(),
selectionGrid -> {
selectionGrid.getSelectedItems().forEach(
rollout -> hawkbitClient.getRolloutRestApi().delete(rollout.getRolloutId()));
selectionGrid.refreshGrid(false);
return CompletableFuture.completedFuture(null);
});
}
private static SelectionGrid<MgmtRolloutGroupResponseBody, Long> createGroupGrid() {
return new SelectionGrid<>(
new SelectionGrid.EntityRepresentation<>(MgmtRolloutGroupResponseBody.class, MgmtRolloutGroupResponseBody::getRolloutGroupId) {
@Override
protected void addColumns(final Grid<MgmtRolloutGroupResponseBody> grid) {
grid.addColumn(MgmtRolloutGroupResponseBody::getRolloutGroupId).setHeader(Constants.ID).setAutoWidth(true);
grid.addColumn(MgmtRolloutGroupResponseBody::getName).setHeader(Constants.NAME).setAutoWidth(true);
grid.addColumn(MgmtRolloutGroupResponseBody::getTotalTargets).setHeader(Constants.TARGET_COUNT).setAutoWidth(true);
grid.addColumn(MgmtRolloutGroupResponseBody::getTotalTargetsPerStatus).setHeader(Constants.STATS).setAutoWidth(true);
grid.addColumn(MgmtRolloutGroupResponseBody::getStatus).setHeader(Constants.STATUS).setAutoWidth(true);
}
});
}
private static class Actions extends HorizontalLayout {
private final long rolloutId;
private final Grid<MgmtRolloutResponseBody> grid;
private final transient HawkbitMgmtClient hawkbitClient;
private Actions(final MgmtRolloutResponseBody rollout, final Grid<MgmtRolloutResponseBody> grid,
final HawkbitMgmtClient hawkbitClient) {
this.rolloutId = rollout.getRolloutId();
this.grid = grid;
this.hawkbitClient = hawkbitClient;
init(rollout);
}
private void init(final MgmtRolloutResponseBody rollout) {
if ("READY".equalsIgnoreCase(rollout.getStatus())) {
add(Utils.tooltip(new Button(VaadinIcon.START_COG.create()) {{
addClickListener(v -> {
hawkbitClient.getRolloutRestApi().start(rollout.getRolloutId());
refresh();
});
}}, "Start"));
} else if ("RUNNING".equalsIgnoreCase(rollout.getStatus())) {
add(Utils.tooltip(new Button(VaadinIcon.PAUSE.create()) {{
addClickListener(v -> {
hawkbitClient.getRolloutRestApi().pause(rollout.getRolloutId());
refresh();
});
}}, "Pause"));
} else if ("PAUSED".equalsIgnoreCase(rollout.getStatus())) {
add(Utils.tooltip(new Button(VaadinIcon.START_COG.create()) {{
addClickListener(v -> {
hawkbitClient.getRolloutRestApi().resume(rollout.getRolloutId());
refresh();
});
}}, "Resume"));
}
add(Utils.tooltip(new Button(VaadinIcon.TRASH.create()) {{
addClickListener(v -> {
hawkbitClient.getRolloutRestApi().delete(rollout.getRolloutId());
grid.getDataProvider().refreshAll();
});
}}, "Cancel and Remove"));
}
private void refresh() {
removeAll();
final MgmtRolloutResponseBody body = hawkbitClient.getRolloutRestApi().getRollout(rolloutId).getBody();
if (body != null) {
init(body);
}
}
}
private static class RolloutFilter implements Filter.Rsql {
private final TextField name = Utils.textField(Constants.NAME);
private RolloutFilter() {
name.setPlaceholder("<name filter>");
}
@Override
public List<Component> components() {
return List.of(name);
}
@Override
public String filter() {
return Filter.filter(Map.of("name", name.getOptionalValue()));
}
}
private static class RolloutDetails extends FormLayout {
private final transient HawkbitMgmtClient hawkbitClient;
private final TextArea description = new TextArea(Constants.DESCRIPTION);
private final TextField createdBy = Utils.textField(Constants.CREATED_BY);
private final TextField createdAt = Utils.textField(Constants.CREATED_AT);
private final TextField lastModifiedBy = Utils.textField(Constants.LAST_MODIFIED_BY);
private final TextField lastModifiedAt = Utils.textField(Constants.LAST_MODIFIED_AT);
private final TextField targetFilter = Utils.textField(Constants.TARGET_FILTER);
private final TextField distributionSet = Utils.textField(Constants.DISTRIBUTION_SET);
private final TextField actonType = Utils.textField(Constants.ACTION_TYPE);
private final TextField startAt = Utils.textField(Constants.START_AT);
private final Checkbox dynamic = new Checkbox(Constants.DYNAMIC);
private final SelectionGrid<MgmtRolloutGroupResponseBody, Long> groupGrid;
private RolloutDetails(final HawkbitMgmtClient hawkbitClient) {
this.hawkbitClient = hawkbitClient;
description.setMinLength(2);
groupGrid = createGroupGrid();
Stream.of(
description,
createdBy, createdAt,
lastModifiedBy, lastModifiedAt,
targetFilter, distributionSet,
actonType, startAt)
.forEach(field -> {
field.setReadOnly(true);
add(field);
});
dynamic.setReadOnly(true);
dynamic.setEnabled(false);
add(dynamic);
add(groupGrid);
setResponsiveSteps(new ResponsiveStep("0", 2));
setColspan(description, 2);
setColspan(groupGrid, 2);
}
private void setItem(final MgmtRolloutResponseBody rollout) {
description.setValue(rollout.getDescription());
createdBy.setValue(rollout.getCreatedBy());
createdAt.setValue(new Date(rollout.getCreatedAt()).toString());
lastModifiedBy.setValue(rollout.getLastModifiedBy());
lastModifiedAt.setValue(new Date(rollout.getLastModifiedAt()).toString());
targetFilter.setValue(rollout.getTargetFilterQuery());
final MgmtDistributionSet distributionSetMgmt = hawkbitClient.getDistributionSetRestApi()
.getDistributionSet(rollout.getDistributionSetId()).getBody();
if (distributionSetMgmt == null) { // should not be here
distributionSet.setValue("n/a (null)");
} else {
distributionSet.setValue(distributionSetMgmt.getName() + ":" + distributionSetMgmt.getVersion());
}
actonType.setValue(switch (rollout.getType()) {
case SOFT -> Constants.SOFT;
case FORCED -> Constants.FORCED;
case DOWNLOAD_ONLY -> Constants.DOWNLOAD_ONLY;
case TIMEFORCED -> "Scheduled at " + new Date(rollout.getForcetime());
});
startAt.setValue(ObjectUtils.isEmpty(rollout.getStartAt()) ? "" : new Date(rollout.getStartAt()).toString());
dynamic.setValue(rollout.isDynamic());
groupGrid.setItems(query -> Optional.ofNullable(
hawkbitClient.getRolloutRestApi()
.getRolloutGroups(
rollout.getRolloutId(),
query.getOffset(), query.getPageSize(),
null, null, "full")
.getBody())
.stream().flatMap(body -> body.getContent().stream())
.skip(query.getOffset())
.limit(query.getPageSize()));
groupGrid.setSelectionMode(Grid.SelectionMode.NONE);
}
}
private static class CreateDialog extends Utils.BaseDialog<Void> {
private final TextField name;
private final Select<MgmtDistributionSet> distributionSet;
private final Select<MgmtTargetFilterQuery> targetFilter;
private final TextArea description;
private final Select<MgmtActionType> actionType;
private final DateTimePicker forceTime = new DateTimePicker("Force Time");
private final Select<StartType> startType;
private final DateTimePicker startAt = new DateTimePicker(Constants.START_AT);
private final NumberField groupNumber;
private final NumberField triggerThreshold;
private final NumberField errorThreshold;
private final Checkbox dynamic = new Checkbox(Constants.DYNAMIC);
private final Button create = new Button("Create");
private CreateDialog(final HawkbitMgmtClient hawkbitClient) {
super("Create Rollout");
name = Utils.textField("Name", this::readyToCreate);
name.focus();
distributionSet = new Select<>(
"Distribution Set",
this::readyToCreate,
Optional.ofNullable(
hawkbitClient.getDistributionSetRestApi()
.getDistributionSets(0, 30, Constants.NAME_ASC, null)
.getBody())
.map(body -> body.getContent().toArray(new MgmtDistributionSet[0]))
.orElseGet(() -> new MgmtDistributionSet[0]));
distributionSet.setRequiredIndicatorVisible(true);
distributionSet.setItemLabelGenerator(distributionSetO ->
distributionSetO.getName() + ":" + distributionSetO.getVersion());
distributionSet.setWidthFull();
targetFilter = new Select<>(
"Target Filter",
this::readyToCreate,
Optional.ofNullable(
hawkbitClient.getTargetFilterQueryRestApi()
.getFilters(0, 30, Constants.NAME_ASC, null, null)
.getBody())
.map(body -> body.getContent().toArray(new MgmtTargetFilterQuery[0]))
.orElseGet(() -> new MgmtTargetFilterQuery[0]));
targetFilter.setRequiredIndicatorVisible(true);
targetFilter.setItemLabelGenerator(MgmtTargetFilterQuery::getName);
targetFilter.setWidthFull();
description = new TextArea(Constants.DESCRIPTION);
description.setMinLength(2);
description.setWidthFull();
actionType = new Select<>();
actionType.setLabel(Constants.ACTION_TYPE);
actionType.setItems(MgmtActionType.values());
actionType.setValue(MgmtActionType.FORCED);
final ComponentRenderer<Component, MgmtActionType> actionTypeRenderer = new ComponentRenderer<>(actionTypeO ->
switch (actionTypeO) {
case SOFT -> new Text(Constants.SOFT);
case FORCED -> new Text(Constants.FORCED);
case DOWNLOAD_ONLY -> new Text(Constants.DOWNLOAD_ONLY);
case TIMEFORCED -> forceTime;
});
actionType.addValueChangeListener(e -> actionType.setRenderer(actionTypeRenderer));
actionType.setItemLabelGenerator(startTypeO ->
switch (startTypeO) {
case SOFT -> Constants.SOFT;
case FORCED -> Constants.FORCED;
case DOWNLOAD_ONLY -> Constants.DOWNLOAD_ONLY;
case TIMEFORCED -> "Time Forced at " + (forceTime.isEmpty() ? "" : " " + forceTime.getValue());
});
actionType.setWidthFull();
startType = new Select<>();
startType.setValue(StartType.MANUAL);
startType.setLabel(Constants.START_TYPE);
startType.setItems(StartType.values());
startType.setValue(StartType.MANUAL);
final ComponentRenderer<Component, StartType> startTypeRenderer = new ComponentRenderer<>(startTypeO ->
switch (startTypeO) {
case MANUAL -> new Text(Constants.MANUAL);
case AUTO -> new Text(Constants.AUTO);
case SCHEDULED -> startAt;
});
startType.setRenderer(startTypeRenderer);
startType.addValueChangeListener(e -> startType.setRenderer(startTypeRenderer));
startType.setItemLabelGenerator(startTypeO ->
switch (startTypeO) {
case MANUAL -> Constants.MANUAL;
case AUTO -> Constants.AUTO;
case SCHEDULED -> "Scheduled" + (startAt.isEmpty() ? "" : " at " + startAt.getValue());
});
startType.setWidthFull();
final Div percentSuffix = new Div();
percentSuffix.setText("%");
groupNumber = Utils.numberField("Group number", this::readyToCreate);
groupNumber.setMin(1);
groupNumber.setValue(1.0);
triggerThreshold = Utils.numberField("Trigger Threshold", this::readyToCreate);
triggerThreshold.setMin(0);
triggerThreshold.setMax(100);
triggerThreshold.setValue(100.0);
triggerThreshold.setSuffixComponent(percentSuffix);
errorThreshold = Utils.numberField("Error Threshold", this::readyToCreate);
errorThreshold.setMin(1);
errorThreshold.setMax(100);
errorThreshold.setValue(10.0);
errorThreshold.setSuffixComponent(percentSuffix);
create.setEnabled(false);
addCreateClickListener(hawkbitClient);
final Button cancel = Utils.tooltip(new Button("Cancel"), "Cancel (Esc)");
cancel.addClickListener(e -> close());
cancel.addClickShortcut(Key.ESCAPE);
final HorizontalLayout actions = new HorizontalLayout(create, cancel);
actions.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
actions.setSizeFull();
final VerticalLayout layout = new VerticalLayout();
layout.setSizeFull();
layout.setSpacing(false);
layout.add(
name, distributionSet, targetFilter, description,
actionType, startType,
groupNumber, triggerThreshold, errorThreshold,
dynamic,
actions);
add(layout);
open();
}
private void readyToCreate(final Object v) {
final boolean createEnabled = !name.isEmpty() &&
!distributionSet.isEmpty() &&
!targetFilter.isEmpty() &&
!groupNumber.isEmpty() &&
!triggerThreshold.isEmpty() &&
!errorThreshold.isEmpty();
if (create.isEnabled() != createEnabled) {
create.setEnabled(createEnabled);
}
}
private void addCreateClickListener(final HawkbitMgmtClient hawkbitClient) {
create.addClickListener(e -> {
close();
final MgmtRolloutRestRequestBodyPost request = new MgmtRolloutRestRequestBodyPost();
request.setName(name.getValue());
request.setDistributionSetId(distributionSet.getValue().getDsId());
request.setTargetFilterQuery(targetFilter.getValue().getQuery());
request.setDescription(description.getValue());
request.setType(actionType.getValue());
if (actionType.getValue() == MgmtActionType.TIMEFORCED) {
request.setForcetime(
forceTime.isEmpty() ?
System.currentTimeMillis() :
forceTime.getValue().toEpochSecond(ZoneOffset.UTC) * 1000);
}
switch (startType.getValue()) {
case AUTO -> request.setStartAt(System.currentTimeMillis());
case SCHEDULED -> request.setStartAt(
startAt.isEmpty() ?
System.currentTimeMillis() :
startAt.getValue().toEpochSecond(ZoneOffset.UTC) * 1000);
case MANUAL -> {
// do nothing, will be started manually
}
}
request.setAmountGroups(groupNumber.getValue().intValue());
request.setSuccessCondition(
new MgmtRolloutCondition(
MgmtRolloutCondition.Condition.THRESHOLD,
triggerThreshold.getValue().intValue() + ""));
request.setErrorCondition(
new MgmtRolloutCondition(
MgmtRolloutCondition.Condition.THRESHOLD,
errorThreshold.getValue().intValue() + ""));
request.setErrorAction(
new MgmtRolloutErrorAction(
MgmtRolloutErrorAction.ErrorAction.PAUSE, ""));
request.setDynamic(dynamic.getValue());
hawkbitClient.getRolloutRestApi().create(request).getBody();
});
}
private enum StartType {
MANUAL, AUTO, SCHEDULED
}
}
}

View File

@@ -0,0 +1,386 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple.view;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import jakarta.annotation.security.RolesAllowed;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.checkbox.CheckboxGroup;
import com.vaadin.flow.component.dependency.Uses;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.upload.Upload;
import com.vaadin.flow.component.upload.receivers.FileBuffer;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import org.eclipse.hawkbit.mgmt.json.model.PagedList;
import org.eclipse.hawkbit.mgmt.json.model.artifact.MgmtArtifact;
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule;
import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleRequestBodyPost;
import org.eclipse.hawkbit.mgmt.json.model.softwaremoduletype.MgmtSoftwareModuleType;
import org.eclipse.hawkbit.ui.simple.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.simple.MainLayout;
import org.eclipse.hawkbit.ui.simple.view.util.Filter;
import org.eclipse.hawkbit.ui.simple.view.util.SelectionGrid;
import org.eclipse.hawkbit.ui.simple.view.util.TableView;
import org.eclipse.hawkbit.ui.simple.view.util.Utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.multipart.MultipartFile;
@PageTitle("Software Modules")
@Route(value = "software_modules", layout = MainLayout.class)
@RolesAllowed({ "SOFTWARE_MODULE_READ" })
@Uses(Icon.class)
public class SoftwareModuleView extends TableView<MgmtSoftwareModule, Long> {
@Autowired
public SoftwareModuleView(final HawkbitMgmtClient hawkbitClient) {
this(true, hawkbitClient);
}
public SoftwareModuleView(final boolean isParent, final HawkbitMgmtClient hawkbitClient) {
super(
new SoftwareModuleFilter(hawkbitClient),
new SelectionGrid.EntityRepresentation<>(MgmtSoftwareModule.class, MgmtSoftwareModule::getModuleId) {
private final SoftwareModuleDetails details = new SoftwareModuleDetails(hawkbitClient);
@Override
protected void addColumns(final Grid<MgmtSoftwareModule> grid) {
grid.addColumn(MgmtSoftwareModule::getModuleId).setHeader(Constants.ID).setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getName).setHeader(Constants.NAME).setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getVersion).setHeader(Constants.VERSION).setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getTypeName).setHeader(Constants.TYPE).setAutoWidth(true);
grid.addColumn(MgmtSoftwareModule::getVendor).setHeader(Constants.VENDOR).setAutoWidth(true);
grid.setItemDetailsRenderer(new ComponentRenderer<>(
() -> details, SoftwareModuleDetails::setItem));
}
},
(query, rsqlFilter) -> Optional.ofNullable(
hawkbitClient.getSoftwareModuleRestApi()
.getSoftwareModules(
query.getOffset(), query.getPageSize(), Constants.NAME_ASC, rsqlFilter)
.getBody())
.stream().flatMap(body -> body.getContent().stream()),
isParent ? v -> new CreateDialog(hawkbitClient).result() : null,
isParent ? selectionGrid -> {
selectionGrid.getSelectedItems().forEach(
module -> hawkbitClient.getSoftwareModuleRestApi().deleteSoftwareModule(module.getModuleId()));
selectionGrid.refreshGrid(false);
return CompletableFuture.completedFuture(null);
} : null);
}
public Set<MgmtSoftwareModule> getSelection() {
return selectionGrid.getSelectedItems();
}
private static SelectionGrid<MgmtArtifact, Long> createArtifactGrid() {
return new SelectionGrid<>(
new SelectionGrid.EntityRepresentation<>(MgmtArtifact.class, MgmtArtifact::getArtifactId) {
@Override
protected void addColumns(final Grid<MgmtArtifact> grid) {
grid.addColumn(MgmtArtifact::getArtifactId).setHeader(Constants.ID).setAutoWidth(true);
grid.addColumn(MgmtArtifact::getProvidedFilename).setHeader(Constants.NAME).setAutoWidth(true);
grid.addColumn(MgmtArtifact::getSize).setHeader("Size").setAutoWidth(true);
}
});
}
private static class SoftwareModuleFilter implements Filter.Rsql {
private final TextField name = Utils.textField(Constants.NAME);
private final CheckboxGroup<MgmtSoftwareModuleType> type = new CheckboxGroup<>(Constants.TYPE);
private SoftwareModuleFilter(final HawkbitMgmtClient hawkbitClient) {
name.setPlaceholder("<name filter>");
type.setItemLabelGenerator(MgmtSoftwareModuleType::getName);
type.setItems(Optional.ofNullable(
hawkbitClient.getSoftwareModuleTypeRestApi()
.getTypes(0, 20, Constants.NAME_ASC, null)
.getBody()).map(PagedList::getContent)
.orElseGet(Collections::emptyList));
}
@Override
public List<Component> components() {
return List.of(name, type);
}
@Override
public String filter() {
return Filter.filter(
Map.of(
"name", name.getOptionalValue(),
"type", type.getSelectedItems().stream().map(MgmtSoftwareModuleType::getKey)
.toList()
));
}
}
private static class SoftwareModuleDetails extends FormLayout {
private final transient HawkbitMgmtClient hawkbitClient;
private final TextArea description = new TextArea(Constants.DESCRIPTION);
private final TextField createdBy = Utils.textField(Constants.CREATED_BY);
private final TextField createdAt = Utils.textField(Constants.CREATED_AT);
private final TextField lastModifiedBy = Utils.textField(Constants.LAST_MODIFIED_BY);
private final TextField lastModifiedAt = Utils.textField(Constants.LAST_MODIFIED_AT);
private final SelectionGrid<MgmtArtifact, Long> artifactGrid;
private SoftwareModuleDetails(final HawkbitMgmtClient hawkbitClient) {
this.hawkbitClient = hawkbitClient;
description.setMinLength(2);
artifactGrid = createArtifactGrid();
Stream.of(
description,
createdBy, createdAt,
lastModifiedBy, lastModifiedAt)
.forEach(field -> {
field.setReadOnly(true);
add(field);
});
add(artifactGrid);
setResponsiveSteps(new ResponsiveStep("0", 2));
setColspan(description, 2);
setColspan(artifactGrid, 2);
}
private void setItem(final MgmtSoftwareModule softwareModule) {
description.setValue(softwareModule.getDescription());
createdBy.setValue(softwareModule.getCreatedBy());
createdAt.setValue(new Date(softwareModule.getCreatedAt()).toString());
lastModifiedBy.setValue(softwareModule.getLastModifiedBy());
lastModifiedAt.setValue(new Date(softwareModule.getLastModifiedAt()).toString());
artifactGrid.setItems(query -> Optional.ofNullable(
hawkbitClient.getSoftwareModuleRestApi()
.getArtifacts(
softwareModule.getModuleId(), null, null)
.getBody())
.stream().flatMap(Collection::stream)
.skip(query.getOffset())
.limit(query.getPageSize()));
artifactGrid.setSelectionMode(Grid.SelectionMode.NONE);
}
}
private static class CreateDialog extends Utils.BaseDialog<Void> {
private final Select<MgmtSoftwareModuleType> type;
private final TextField name;
private final TextField version;
private final TextField vendor;
private final TextArea description;
private final Checkbox enableArtifactEncryption;
private final Button create;
private CreateDialog(final HawkbitMgmtClient hawkbitClient) {
super("Create Software Module");
type = new Select<>(
Constants.TYPE,
this::readyToCreate,
Optional.ofNullable(
hawkbitClient.getSoftwareModuleTypeRestApi()
.getTypes(0, 30, Constants.NAME_ASC, null)
.getBody()).map(body -> body.getContent().toArray(new MgmtSoftwareModuleType[0]))
.orElseGet(() -> new MgmtSoftwareModuleType[0]));
type.setWidthFull();
type.setRequiredIndicatorVisible(true);
type.setItemLabelGenerator(MgmtSoftwareModuleType::getName);
type.focus();
name = Utils.textField(Constants.NAME, this::readyToCreate);
version = Utils.textField(Constants.VERSION, this::readyToCreate);
vendor = Utils.textField(Constants.VENDOR);
description = new TextArea(Constants.DESCRIPTION);
description.setWidthFull();
description.setMinLength(2);
enableArtifactEncryption = new Checkbox("Enable artifact encryption");
create = Utils.tooltip(new Button("Create"), "Create (Enter)");
create.setEnabled(false);
addCreateClickListener(hawkbitClient);
create.addClickShortcut(Key.ENTER);
final Button cancel = Utils.tooltip(new Button("Cancel"), "Cancel (Esc)");
cancel.addClickListener(e -> close());
cancel.addClickShortcut(Key.ESCAPE);
final HorizontalLayout actions = new HorizontalLayout(create, cancel);
actions.setSizeFull();
actions.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
final VerticalLayout layout = new VerticalLayout();
layout.setSizeFull();
layout.setSpacing(false);
layout.add(type, name, version, vendor, description, enableArtifactEncryption, actions);
add(layout);
open();
}
private void readyToCreate(final Object v) {
final boolean createEnabled = !type.isEmpty() && !name.isEmpty() && !version.isEmpty();
if (create.isEnabled() != createEnabled) {
create.setEnabled(createEnabled);
}
}
private void addCreateClickListener(final HawkbitMgmtClient hawkbitClient) {
create.addClickListener(e -> {
close();
final long softwareModuleId = Optional.ofNullable(
hawkbitClient.getSoftwareModuleRestApi().createSoftwareModules(
List.of(new MgmtSoftwareModuleRequestBodyPost()
.setType(type.getValue().getKey())
.setName(name.getValue())
.setVersion(version.getValue())
.setVendor(vendor.getValue())
.setDescription(description.getValue())
.setEncrypted(enableArtifactEncryption.getValue())))
.getBody())
.stream().flatMap(Collection::stream)
.findFirst()
.orElseThrow()
.getModuleId();
new AddArtifactsDialog(softwareModuleId, hawkbitClient).open();
});
}
}
private static class AddArtifactsDialog extends Utils.BaseDialog<Void> {
private final transient Set<MgmtArtifact> artifacts = Collections.synchronizedSet(new HashSet<>());
private AddArtifactsDialog(
final long softwareModuleId,
final HawkbitMgmtClient hawkbitClient) {
super("Add Artifacts");
final SelectionGrid<MgmtArtifact, Long> artifactGrid = createArtifactGrid();
artifactGrid.setItems(query -> {
query.getOffset(); // to keep vaadin contract
return artifacts.stream().limit(query.getLimit());
});
artifactGrid.setSelectionMode(Grid.SelectionMode.NONE);
final FileBuffer fileBuffer = new FileBuffer();
final Upload uploadBtn = new Upload(fileBuffer);
uploadBtn.setMaxFiles(10);
uploadBtn.setWidthFull();
uploadBtn.setDropAllowed(true);
uploadBtn.addSucceededListener(e -> {
final MgmtArtifact artifact = hawkbitClient.getSoftwareModuleRestApi()
.uploadArtifact(softwareModuleId,
new MultipartFileImpl(fileBuffer, e.getContentLength(), e.getMIMEType()), fileBuffer.getFileName(), null, null,
null).getBody();
artifacts.add(artifact);
artifactGrid.refreshGrid(false);
});
final Button finishBtn = Utils.tooltip(new Button("Finish"), "Finish (Enter)");
finishBtn.addClickListener(e -> close());
finishBtn.addClickShortcut(Key.ENTER);
finishBtn.setHeightFull();
final HorizontalLayout finish = new HorizontalLayout(finishBtn);
finish.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
finish.setWidthFull();
final VerticalLayout layout = new VerticalLayout(artifactGrid, uploadBtn, finish);
layout.setSizeFull();
layout.setSpacing(false);
add(layout);
}
private static class MultipartFileImpl implements MultipartFile {
private final FileBuffer fileBuffer;
private final String mimeType;
private final long contentLength;
public MultipartFileImpl(final FileBuffer fileBuffer, final long contentLength, final String mimeType) {
this.fileBuffer = fileBuffer;
this.contentLength = contentLength;
this.mimeType = mimeType;
}
@Override
public String getName() {
return fileBuffer.getFileName();
}
@Override
public String getOriginalFilename() {
return getName();
}
@Override
public String getContentType() {
return mimeType;
}
@Override
public boolean isEmpty() {
return contentLength == 0;
}
@Override
public long getSize() {
return contentLength;
}
@Override
public byte[] getBytes() throws IOException {
try (final InputStream is = getInputStream()) {
return is.readAllBytes();
}
}
@Override
public InputStream getInputStream() {
return fileBuffer.getInputStream();
}
@Override
public void transferTo(final File dest) throws IllegalStateException {
throw new UnsupportedOperationException();
}
}
}
}

View File

@@ -0,0 +1,315 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple.view;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import jakarta.annotation.security.RolesAllowed;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.checkbox.CheckboxGroup;
import com.vaadin.flow.component.dependency.Uses;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import org.eclipse.hawkbit.mgmt.json.model.tag.MgmtTag;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetRequestBody;
import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQuery;
import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQueryRequestBody;
import org.eclipse.hawkbit.mgmt.json.model.targettype.MgmtTargetType;
import org.eclipse.hawkbit.ui.simple.HawkbitMgmtClient;
import org.eclipse.hawkbit.ui.simple.MainLayout;
import org.eclipse.hawkbit.ui.simple.view.util.Filter;
import org.eclipse.hawkbit.ui.simple.view.util.SelectionGrid;
import org.eclipse.hawkbit.ui.simple.view.util.TableView;
import org.eclipse.hawkbit.ui.simple.view.util.Utils;
import org.springframework.util.ObjectUtils;
@PageTitle("Targets")
@Route(value = "targets", layout = MainLayout.class)
@RolesAllowed({ "TARGET_READ" })
@Uses(Icon.class)
public class TargetView extends TableView<MgmtTarget, String> {
public static final String CONTROLLER_ID = "Controller Id";
public static final String TAG = "Tag";
public TargetView(final HawkbitMgmtClient hawkbitClient) {
super(
new RawFilter(hawkbitClient), new SimpleFilter(hawkbitClient),
new SelectionGrid.EntityRepresentation<>(MgmtTarget.class, MgmtTarget::getControllerId) {
@Override
protected void addColumns(final Grid<MgmtTarget> grid) {
grid.addColumn(MgmtTarget::getControllerId).setHeader(CONTROLLER_ID).setAutoWidth(true);
grid.addColumn(MgmtTarget::getName).setHeader(Constants.NAME).setAutoWidth(true);
grid.addColumn(MgmtTarget::getTargetTypeName).setHeader(Constants.TYPE).setAutoWidth(true);
grid.setItemDetailsRenderer(new ComponentRenderer<>(
TargetDetails::new, TargetDetails::setItem));
}
},
(query, filter) -> hawkbitClient.getTargetRestApi()
.getTargets(
query.getOffset(), query.getPageSize(), Constants.NAME_ASC,
filter)
.getBody()
.getContent()
.stream(),
source -> new RegisterDialog(hawkbitClient).result(),
selectionGrid -> {
selectionGrid.getSelectedItems().forEach(toDelete ->
hawkbitClient.getTargetRestApi().deleteTarget(toDelete.getControllerId()));
return CompletableFuture.completedFuture(null);
});
}
private static class SimpleFilter implements Filter.Rsql {
private final HawkbitMgmtClient hawkbitClient;
private final TextField controllerId;
private final CheckboxGroup<MgmtTargetType> type;
private final CheckboxGroup<MgmtTag> tag;
private SimpleFilter(final HawkbitMgmtClient hawkbitClient) {
this.hawkbitClient = hawkbitClient;
controllerId = Utils.textField(CONTROLLER_ID);
controllerId.setPlaceholder("<controller id filter>");
type = new CheckboxGroup<>(Constants.TYPE);
type.setItemLabelGenerator(MgmtTargetType::getName);
tag = new CheckboxGroup<>(TAG);
tag.setItemLabelGenerator(MgmtTag::getName);
}
@Override
public List<Component> components() {
final List<Component> components = new LinkedList<>();
components.add(controllerId);
type.setItems(hawkbitClient.getTargetTypeRestApi().getTargetTypes(0, 20, Constants.NAME_ASC, null).getBody().getContent());
if (!type.getValue().isEmpty()) {
components.add(type);
}
tag.setItems(hawkbitClient.getTargetTagRestApi().getTargetTags(0, 20, Constants.NAME_ASC, null).getBody().getContent());
if (!tag.isEmpty()) {
components.add(tag);
}
return components;
}
@Override
public String filter() {
return Filter.filter(
Map.of(
"controllerid", controllerId.getOptionalValue(),
"targettype.name", type.getSelectedItems().stream().map(MgmtTargetType::getName)
.toList(),
"tag", tag.getSelectedItems()));
}
}
private static class RawFilter implements Filter.Rsql {
private final TextField textFilter = new TextField("Raw Filter");
private final VerticalLayout layout = new VerticalLayout();
private RawFilter(final HawkbitMgmtClient hawkbitClient) {
textFilter.setPlaceholder("<raw filter>");
final Select<MgmtTargetFilterQuery> savedFilters = new Select<>(
"Saved Filters",
e -> {
if (e.getValue() != null) {
textFilter.setValue(e.getValue().getQuery());
}
});
savedFilters.setEmptySelectionAllowed(true);
savedFilters.setItems(
Optional.ofNullable(
hawkbitClient.getTargetFilterQueryRestApi()
.getFilters(0, 30, null, null, null)
.getBody().getContent())
.orElse(Collections.emptyList()));
savedFilters.setItemLabelGenerator(
query -> Optional.ofNullable(query).map(MgmtTargetFilterQuery::getName).orElse("<select saved filter>"));
savedFilters.setWidthFull();
textFilter.setWidthFull();
final Button saveBtn = Utils.tooltip(new Button(VaadinIcon.ARCHIVE.create()), "Save (Enter)");
saveBtn.addClickListener(e ->
new Utils.BaseDialog<Void>("Save Filter") {{
setHeight("40%");
final Button finishBtn = Utils.tooltip(new Button("Save"), "Save (Enter)");
final TextField name = Utils.textField(
Constants.NAME,
e -> finishBtn.setEnabled(!e.getHasValue().isEmpty()));
name.focus();
finishBtn.addClickShortcut(Key.ENTER);
finishBtn.setEnabled(false);
finishBtn.addClickListener(e -> {
final MgmtTargetFilterQueryRequestBody createRequest = new MgmtTargetFilterQueryRequestBody();
createRequest.setName(name.getValue());
createRequest.setQuery(textFilter.getValue());
hawkbitClient.getTargetFilterQueryRestApi().createFilter(createRequest);
savedFilters.setItems(
hawkbitClient.getTargetFilterQueryRestApi()
.getFilters(0, 30, null, null, null).getBody().getContent());
close();
});
add(name, finishBtn);
open();
}});
saveBtn.addClickShortcut(Key.ENTER);
layout.setSpacing(false);
final HorizontalLayout textSaveLayout = new HorizontalLayout(textFilter, saveBtn);
textSaveLayout.setAlignItems(FlexComponent.Alignment.BASELINE);
textSaveLayout.setWidthFull();
layout.add(savedFilters, textSaveLayout);
}
@Override
public List<Component> components() {
return List.of(layout);
}
@Override
public String filter() {
return textFilter.getOptionalValue().orElse(null);
}
}
private static class TargetDetails extends FormLayout {
private final TextArea description = new TextArea(Constants.DESCRIPTION);
private final TextField createdBy = Utils.textField(Constants.CREATED_BY);
private final TextField createdAt = Utils.textField(Constants.CREATED_AT);
private final TextField lastModifiedBy = Utils.textField(Constants.LAST_MODIFIED_BY);
private final TextField lastModifiedAt = Utils.textField(Constants.LAST_MODIFIED_AT);
private final TextField securityToken = Utils.textField(Constants.SECURITY_TOKEN);
private TargetDetails() {
description.setMinLength(2);
Stream.of(
description,
createdBy, createdAt,
lastModifiedBy, lastModifiedAt,
securityToken)
.forEach(field -> {
field.setReadOnly(true);
add(field);
});
setResponsiveSteps(new ResponsiveStep("0", 2));
setColspan(description, 2);
}
private void setItem(final MgmtTarget target) {
description.setValue(target.getDescription() == null ? "N/A" : target.getDescription());
createdBy.setValue(target.getCreatedBy());
createdAt.setValue(new Date(target.getCreatedAt()).toString());
lastModifiedBy.setValue(target.getLastModifiedBy());
lastModifiedAt.setValue(new Date(target.getLastModifiedAt()).toString());
securityToken.setValue(target.getSecurityToken());
}
}
private static class RegisterDialog extends Utils.BaseDialog<Void> {
private final Select<MgmtTargetType> type;
private final TextField controllerId;
private final TextField name;
private final TextArea description;
private RegisterDialog(final HawkbitMgmtClient hawkbitClient) {
super("Register Target");
final Button register = Utils.tooltip(new Button("Register"), "Register (Enter)");
type = new Select<>(
"Type",
e -> {},
hawkbitClient.getTargetTypeRestApi()
.getTargetTypes(0, 30, Constants.NAME_ASC, null)
.getBody()
.getContent()
.toArray(new MgmtTargetType[0]));
type.setWidthFull();
type.setEmptySelectionAllowed(true);
type.setItemLabelGenerator(item -> item == null ? "" : item.getName());
controllerId = Utils.textField(CONTROLLER_ID,
e -> register.setEnabled(!e.getHasValue().isEmpty()));
controllerId.focus();
name = Utils.textField(Constants.NAME);
name.setWidthFull();
description = new TextArea(Constants.DESCRIPTION);
description.setMinLength(2);
description.setWidthFull();
addCreateClickListener(register, hawkbitClient);
register.setEnabled(false);
register.addClickShortcut(Key.ENTER);
final Button cancel = Utils.tooltip(new Button("Cancel"), "Cancel (Esc)");
cancel.addClickListener(e -> close());
register.addClickShortcut(Key.ESCAPE);
final HorizontalLayout actions = new HorizontalLayout(register, cancel);
actions.setSizeFull();
actions.setPadding(true);
actions.setSpacing(true);
final VerticalLayout layout = new VerticalLayout();
layout.setSizeFull();
layout.setPadding(true);
layout.setSpacing(false);
layout.add(type, controllerId, name, description, actions);
add(layout);
open();
}
private void addCreateClickListener(final Button register, final HawkbitMgmtClient hawkbitClient) {
register.addClickListener(e -> {
final MgmtTargetRequestBody request = new MgmtTargetRequestBody()
.setControllerId(controllerId.getValue())
.setName(name.getValue())
.setDescription(description.getValue());
if (!ObjectUtils.isEmpty(type.getValue())) {
request.setTargetType(type.getValue().getTypeId());
}
hawkbitClient.getTargetRestApi().createTargets(
List.of(request))
.getBody()
.stream()
.findFirst()
.orElseThrow()
.getControllerId();
close();
});
}
}
}

View File

@@ -0,0 +1,160 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple.view.util;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasValue;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.theme.lumo.LumoUtility;
public class Filter extends Div {
private transient Rsql rsql;
public Filter(final Consumer<String> changeListener, final Rsql primaryRsql, final Rsql secondaryOptionalRsql) {
rsql = primaryRsql;
final HorizontalLayout layout = new HorizontalLayout();
setWidthFull();
addClassNames(LumoUtility.Padding.Horizontal.NONE, LumoUtility.Padding.Vertical.SMALL,
LumoUtility.BoxSizing.BORDER);
final Div filtersDiv = new Div();
filtersDiv.setWidthFull();
filtersDiv.add(primaryRsql.components());
filtersDiv.addClassName(LumoUtility.Gap.SMALL);
final Button searchBtn = Utils.tooltip(new Button(VaadinIcon.REFRESH.create()), "Search");
searchBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
searchBtn.addClickListener(e -> changeListener.accept(rsql.filter()));
final Button resetBtn = Utils.tooltip(new Button(VaadinIcon.ERASER.create()), "Reset");
resetBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
resetBtn.addClickListener(e -> {
clear(layout.getChildren());
changeListener.accept(primaryRsql.filter());
});
final Div actionDiv = new Div();
actionDiv.add(searchBtn, resetBtn);
actionDiv.addClassNames(LumoUtility.Gap.SMALL);
layout.setPadding(true);
layout.setAlignItems(FlexComponent.Alignment.BASELINE);
layout.add(filtersDiv);
layout.add(actionDiv);
if (secondaryOptionalRsql != null) {
final Button toggleBtn = Utils.tooltip(new Button(VaadinIcon.FLIP_V.create()), "Toggle Search");
toggleBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
toggleBtn.addClickListener(e -> {
filtersDiv.removeAll();
synchronized (this) { // toggle
rsql = rsql == primaryRsql ? secondaryOptionalRsql : primaryRsql;
}
filtersDiv.add(rsql.components());
changeListener.accept(primaryRsql.filter());
});
layout.add(toggleBtn);
}
add(layout);
changeListener.accept(primaryRsql.filter());
}
public static String filter(final Map<String, Object> keyToValues) {
final Map<String, Object> normalized =
new HashMap<>(keyToValues)
.entrySet()
.stream()
.filter(e -> {
if (e.getValue() instanceof Optional<?> opt) {
return opt.isPresent();
} else {
return e.getValue() != null;
}
})
.filter(e -> !(e.getValue() instanceof Collection<?> coll && coll.isEmpty()))
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
if (normalized.isEmpty()) {
return null;
} else if (normalized.size() == 1) {
return normalized.entrySet().stream()
.findFirst().map(e -> filter(e.getKey(), e.getValue())).orElse(null); // never return null!
} else {
final StringBuilder sb = new StringBuilder();
normalized.forEach((k, v) -> {
if (v instanceof Collection<?>) {
sb.append('(').append(filter(k, v)).append(')');
} else if (v instanceof Optional<?> opt) {
sb.append(filter(k, opt.get()));
} else {
sb.append(filter(k, v));
}
sb.append(';');
});
return sb.substring(0, sb.length() - 1);
}
}
private static String filter(final String key, final Object value) {
if (value == null || (value instanceof Collection<?> coll && coll.isEmpty())) {
return null;
}
if (value instanceof Collection<?> coll) {
final StringBuilder sb = new StringBuilder();
coll.stream().forEach(next -> sb.append(key).append("==").append(next).append(','));
return sb.substring(0, sb.length() - 1);
} else if (value instanceof Optional<?> opt) {
if (opt.isEmpty()) {
return null;
} else {
return key + "==" + opt.get();
}
} else {
return key + "==" + value;
}
}
private static void clear(final Stream<Component> components) {
if (components == null) {
return;
}
components.forEach(component -> {
if (component instanceof HasValue<?, ?> hasValue) {
hasValue.clear();
}
clear(component.getChildren());
});
}
public interface Rsql {
List<Component> components();
String filter();
}
}

View File

@@ -0,0 +1,95 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple.view.util;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Stream;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.grid.GridVariant;
import com.vaadin.flow.data.provider.Query;
import com.vaadin.flow.theme.lumo.LumoUtility;
// id type shall have proper equals and hashCode - i.e. eligible hash set element
public class SelectionGrid<T, ID> extends Grid<T> {
private volatile String rsqlFilter;
public SelectionGrid(
final EntityRepresentation<T, ID> entityRepresentation) {
this(entityRepresentation, null);
}
public SelectionGrid(
final EntityRepresentation<T, ID> entityRepresentation,
final BiFunction<Query<T, Void>, String, Stream<T>> queryFn) {
super(entityRepresentation.beanType, false);
addThemeVariants(GridVariant.LUMO_NO_BORDER);
addClassNames(LumoUtility.Border.TOP, LumoUtility.BorderColor.CONTRAST_10);
setSelectionMode(Grid.SelectionMode.MULTI);
entityRepresentation.addColumns(this);
if (queryFn != null) {
setItems(query -> {
final Stream<T> fetch = queryFn.apply(query, rsqlFilter);
final Set<T> selected = getSelectedItems();
if (selected == null || selected.isEmpty()) {
return fetch;
} else {
final Set<ID> selectedIds = new HashSet<>();
selected.forEach(next -> selectedIds.add(entityRepresentation.idFn.apply(next)));
// if matching keeps old entries instead of new the new ones in order to
// select them in case refresh is made with keepSelection
// this however means that if they are changed the old state will be shown!!!
return Stream.concat(selected.stream(),
fetch.filter(next -> !selectedIds.contains(entityRepresentation.idFn.apply(next))));
}
});
} // else externally managed
}
public void setRsqlFilter(final String rsqlFilter) {
if (!Objects.equals(this.rsqlFilter, rsqlFilter)) {
this.rsqlFilter = rsqlFilter;
refreshGrid(true);
}
}
public void refreshGrid(final boolean keepSelection) {
if (keepSelection) {
final Set<T> selected = getSelectedItems();
getDataProvider().refreshAll();
if (selected != null && !selected.isEmpty()) {
selected.forEach(this::select);
}
} else {
deselectAll();
getDataProvider().refreshAll();
}
}
public static abstract class EntityRepresentation<T, ID> {
private final Class<T> beanType;
private final Function<T, ID> idFn;
protected EntityRepresentation(final Class<T> beanType, final Function<T, ID> idFn) {
this.beanType = beanType;
this.idFn = idFn;
}
protected abstract void addColumns(final Grid<T> grid);
}
}

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple.view.util;
import java.util.concurrent.CompletionStage;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Stream;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.data.provider.Query;
import org.eclipse.hawkbit.ui.simple.view.Constants;
public class TableView<T, ID> extends Div implements Constants {
protected SelectionGrid<T, ID> selectionGrid;
private final Filter filter;
public TableView(
final Filter.Rsql rsql,
final SelectionGrid.EntityRepresentation<T, ID> entityRepresentation,
final BiFunction<Query<T, Void>, String, Stream<T>> queryFn) {
this(rsql, null, entityRepresentation, queryFn, null, null);
}
public TableView(
final Filter.Rsql rsql, final Filter.Rsql alternativeRsql,
final SelectionGrid.EntityRepresentation<T, ID> entityRepresentation,
final BiFunction<Query<T, Void>, String, Stream<T>> queryFn) {
this(rsql, alternativeRsql, entityRepresentation, queryFn, null, null);
}
public TableView(
final Filter.Rsql rsql,
final SelectionGrid.EntityRepresentation<T, ID> entityRepresentation,
final BiFunction<Query<T, Void>, String, Stream<T>> queryFn,
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> addHandler,
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> removeHandler) {
this(rsql, null, entityRepresentation, queryFn, addHandler, removeHandler);
}
public TableView(
final Filter.Rsql rsql, final Filter.Rsql alternativeRsql,
final SelectionGrid.EntityRepresentation<T, ID> entityRepresentation,
final BiFunction<Query<T, Void>, String, Stream<T>> queryFn,
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> addHandler,
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> removeHandler) {
selectionGrid = new SelectionGrid<>(entityRepresentation, queryFn);
filter = new Filter(selectionGrid::setRsqlFilter, rsql, alternativeRsql);
setSizeFull();
final VerticalLayout layout = new VerticalLayout(filter, selectionGrid);
layout.setSizeFull();
layout.setPadding(false);
layout.setSpacing(false);
if (addHandler != null || removeHandler != null) {
layout.add(Utils.addRemoveControls(addHandler, removeHandler, selectionGrid, false));
}
add(layout);
}
}

View File

@@ -0,0 +1,185 @@
/**
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.ui.simple.view.util;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
import java.util.function.Function;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasValue;
import com.vaadin.flow.component.Text;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.shared.Tooltip;
import com.vaadin.flow.component.textfield.NumberField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.theme.lumo.LumoUtility;
public class Utils {
private Utils() {
// prevent initialization
}
public static TextField textField(final String label) {
return textField(label, null);
}
public static TextField textField(final String label, final Consumer<HasValue.ValueChangeEvent<String>> changeListener) {
final TextField textField = new TextField(label);
textField.setWidthFull();
if (changeListener != null) {
textField.setRequired(true);
textField.addValueChangeListener(changeListener::accept);
textField.setValueChangeMode(ValueChangeMode.TIMEOUT);
textField.setValueChangeTimeout(200);
}
return textField;
}
public static NumberField numberField(final String label) {
return numberField(label, null);
}
public static NumberField numberField(final String label, final Consumer<HasValue.ValueChangeEvent<Double>> changeListener) {
final NumberField numberField = new NumberField(label);
numberField.setWidthFull();
if (changeListener != null) {
numberField.setRequired(true);
numberField.addValueChangeListener(changeListener::accept);
numberField.setValueChangeMode(ValueChangeMode.TIMEOUT);
numberField.setValueChangeTimeout(200);
}
return numberField;
}
public static <T, ID> HorizontalLayout addRemoveControls(
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> addHandler,
final Function<SelectionGrid<T, ID>, CompletionStage<Void>> removeHandler,
final SelectionGrid<T, ID> selectionGrid, final boolean noPadding) {
if (addHandler == null && removeHandler == null) {
throw new IllegalArgumentException("At least one of add or remove handlers must not be null!");
}
final HorizontalLayout layout = new HorizontalLayout();
layout.setWidthFull();
if (!noPadding) {
layout.addClassNames(LumoUtility.Padding.Horizontal.XLARGE, LumoUtility.Padding.Vertical.SMALL, LumoUtility.BoxSizing.BORDER);
}
layout.setAlignItems(FlexComponent.Alignment.BASELINE);
layout.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
if (addHandler != null) {
final Button addBtn = tooltip(new Button(VaadinIcon.PLUS.create()), "Add");
addBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
addBtn.addClickListener(e -> addHandler
.apply(selectionGrid)
.thenAccept(v -> selectionGrid.refreshGrid(true)));
layout.add(addBtn);
}
if (removeHandler != null) {
final Button removeBtn = tooltip(new Button(VaadinIcon.MINUS.create()), "Remove");
removeBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_CONTRAST);
removeBtn.addClickListener(e -> removeHandler
.apply(selectionGrid)
.thenAccept(v -> selectionGrid.refreshGrid(false)));
layout.add(removeBtn);
}
return layout;
}
public static <T> void remove(final Collection<T> remove, final Set<T> from, final Function<T, ?> idFn) {
remove.forEach(toRemove -> {
final Object id = idFn.apply(toRemove);
for (final Iterator<T> i = from.iterator(); i.hasNext(); ) {
if (idFn.apply(i.next()).equals(id)) {
i.remove();
}
}
});
}
public static void errorNotification(final Throwable t) {
if (UI.getCurrent() == null) {
return;
}
final Notification notification = new Notification();
notification.addThemeVariants(NotificationVariant.LUMO_ERROR);
final Div error = new Div(new Text("Error: " + t.getMessage()));
final Button closeButton = tooltip(new Button(VaadinIcon.CLOSE.create()), "Close");
closeButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE);
closeButton.addClickListener(event -> notification.close());
final HorizontalLayout layout = new HorizontalLayout(error, closeButton);
layout.setAlignItems(FlexComponent.Alignment.CENTER);
notification.add(layout);
notification.open();
}
public static <T extends Component> T tooltip(final T component, final String text) {
Tooltip.forComponent(component)
.withText(text)
.withPosition(Tooltip.TooltipPosition.TOP_START);
return component;
}
public static class BaseDialog<T> extends Dialog {
protected final transient CompletableFuture<T> result = new CompletableFuture<>();
protected BaseDialog(final String headerTitle) {
setHeaderTitle(headerTitle);
setHeightFull();
setModal(true);
setDraggable(true);
setResizable(true);
setCloseOnEsc(true);
setCloseOnOutsideClick(true);
final Button closeBtn = tooltip(new Button(VaadinIcon.CLOSE.create()), "Close");
closeBtn.addClickListener(e -> {
result.complete(null);
super.close();
});
getHeader().add(closeBtn);
}
public CompletionStage<T> result() {
return result;
}
@Override
public void close() {
if (!result.isDone()) {
result.complete(null);
}
super.close();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,29 @@
#
# Copyright (c) 2023 Contributors to the Eclipse Foundation
#
# This program and the accompanying materials are made
# available under the terms of the Eclipse Public License 2.0
# which is available at https://www.eclipse.org/legal/epl-2.0/
#
# SPDX-License-Identifier: EPL-2.0
#
# Spring configuration
server.port=${PORT:8088}
# Logging configuration
logging.level.org.springframework.boot.actuate.audit.listener.AuditListener=WARN
# logging pattern
logging.pattern.console=%clr(%d{${logging.pattern.dateformat:yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${logging.pattern.level:%5p}) %clr(${PID:}){magenta} %clr(---){faint} %clr([${spring.application.name}] [%X{tenant}:%X{user}] [%15.15t]){faint} %clr(${logging.pattern.correlation:}){faint}%clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${logging.exception-conversion-word:%wEx}
### Vaadin start ###`
# build with mvn vaadin:build-frontend to enable / disable
vaadin.frontend.hotdeploy=false
logging.level.org.atmosphere=warn
spring.mustache.check-template-location=false
# Launch the default browser when starting the application in development mode
vaadin.launch-browser=true
# To improve the performance during development.
# For more information https://vaadin.com/docs/flow/spring/tutorial-spring-configuration.html#special-configuration-parameters
vaadin.whitelisted-packages=com.vaadin,org.vaadin,dev.hilla,org.eclipse.hawkbit
### Vaadin end ###

View File

@@ -0,0 +1,18 @@
______ _ _ _ _ ____ _ _
| ____| | (_) | | | | | _ \(_) |
| |__ ___| |_ _ __ ___ ___ | |__ __ ___ _| | _| |_) |_| |_
| __| / __| | | '_ \/ __|/ _ \ | '_ \ / _` \ \ /\ / / |/ / _ <| | __|
| |___| (__| | | |_) \__ \ __/ | | | | (_| |\ V V /| <| |_) | | |_
|______\___|_|_| .__/|___/\___|_|_| |_|\__,_|_\_/\_/ |_|\_\____/|_|\__|
/ ____(_)| | | | | | | |_ _|
| (___ _ |_|_ ___ _ __ | | ___ | | | | | |
\___ \| | '_ ` _ \| '_ \| |/ _ \ | | | | | |
____) | | | | | | | |_) | | __/ | |__| |_| |_
|_____/|_|_| |_| |_| .__/|_|\___| \____/|_____|
| |
|_|
Eclipse hawkBit UI ${application.formatted-version}
using Spring Boot ${spring-boot.formatted-version}
Go to https://www.eclipse.org/hawkbit for more information.