diff --git a/hawkbit-runtime/hawkbit-simple-ui/.gitignore b/hawkbit-runtime/hawkbit-simple-ui/.gitignore
new file mode 100644
index 0000000000..10d4fb7ac2
--- /dev/null
+++ b/hawkbit-runtime/hawkbit-simple-ui/.gitignore
@@ -0,0 +1 @@
+frontend
\ No newline at end of file
diff --git a/hawkbit-runtime/hawkbit-simple-ui/pom.xml b/hawkbit-runtime/hawkbit-simple-ui/pom.xml
new file mode 100644
index 0000000000..69a8bc9359
--- /dev/null
+++ b/hawkbit-runtime/hawkbit-simple-ui/pom.xml
@@ -0,0 +1,156 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.1.5
+
+
+ com.eclipse.hawkbit
+ hawkbit-simple-ui
+
+ hawkBit :: Runtime :: Simple UI
+ 0.4.0-SNAPSHOT
+ jar
+
+
+ 17
+ 24.2.2
+ 4.0.4
+
+
+
+
+ Vaadin Repo
+ https://maven.vaadin.com/vaadin-addons
+
+ false
+
+
+
+
+
+
+
+ com.vaadin
+ vaadin-bom
+ ${vaadin.version}
+ pom
+ import
+
+
+
+
+
+
+ org.eclipse.hawkbit
+ hawkbit-mgmt-api
+ 0.4.0-SNAPSHOT
+
+
+
+ com.vaadin
+ vaadin-core
+
+
+ com.vaadin
+ vaadin-dev
+
+
+
+
+ com.vaadin
+ vaadin-spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-openfeign
+ ${spring-cloud-starter-openfeign.version}
+
+
+ io.github.openfeign
+ feign-hc5
+ 13.0
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+ spring-boot:run
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ com.vaadin
+ vaadin-maven-plugin
+ ${vaadin.version}
+
+
+
+ build-frontend
+
+ compile
+
+
+
+
+
+
+ com.mycila
+ license-maven-plugin
+ 2.11
+
+ licenses/LICENSE_HEADER_TEMPLATE.txt
+
+ **/banner.txt
+ **/README.md
+ **/frontend/**
+
+
+ JAVADOC_STYLE
+
+
+
+
+
+
+
+
+
+ EPL-2.0
+ https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
+ Eclipse Public License - Version 2.0
+
+
+
\ No newline at end of file
diff --git a/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/HawkbitClient.java b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/HawkbitClient.java
new file mode 100644
index 0000000000..325cebbc9d
--- /dev/null
+++ b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/HawkbitClient.java
@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package com.eclipse.hawkbit.ui;
+
+import com.eclipse.hawkbit.ui.view.util.Utils;
+import com.vaadin.flow.component.UI;
+import feign.Client;
+import feign.Contract;
+import feign.Feign;
+import feign.FeignException;
+import feign.RequestInterceptor;
+import feign.Response;
+import feign.codec.Decoder;
+import feign.codec.Encoder;
+import feign.codec.ErrorDecoder;
+import lombok.Getter;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetTagRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetTypeRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtRolloutRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftwareModuleRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtSoftwareModuleTypeRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetFilterQueryRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetTagRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetTypeRestApi;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.util.Base64;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+import static feign.Util.ISO_8859_1;
+
+@Getter
+public class HawkbitClient {
+
+ private static final RequestInterceptor AUTHORIZATION = requestTemplate -> {
+ final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ requestTemplate.header("Authorization", "Basic " + Base64.getEncoder().encodeToString(
+ (Objects.requireNonNull(authentication.getPrincipal(), "User is null!") + ":" + Objects.requireNonNull(
+ authentication.getCredentials(), "Password is not available!")).getBytes(ISO_8859_1)));
+ };
+
+ private final String hawkbitUrl;
+ private final MgmtSoftwareModuleRestApi softwareModuleRestApi;
+ private final MgmtSoftwareModuleTypeRestApi softwareModuleTypeRestApi;
+ private final MgmtDistributionSetRestApi distributionSetRestApi;
+ private final MgmtDistributionSetTypeRestApi distributionSetTypeRestApi;
+ private final MgmtDistributionSetTagRestApi distributionSetTagRestApi;
+ private final MgmtTargetRestApi targetRestApi;
+ private final MgmtTargetTypeRestApi targetTypeRestApi;
+ private final MgmtTargetTagRestApi targetTagRestApi;
+ private final MgmtTargetFilterQueryRestApi targetFilterQueryRestApi;
+ private final MgmtRolloutRestApi rolloutRestApi;
+
+ HawkbitClient(final String hawkbitUrl,
+ final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
+ this.hawkbitUrl = hawkbitUrl;
+
+ softwareModuleRestApi = service(MgmtSoftwareModuleRestApi .class, client, encoder, decoder, contract);
+ softwareModuleTypeRestApi = service(MgmtSoftwareModuleTypeRestApi.class, client, encoder, decoder, contract);
+ distributionSetRestApi = service(MgmtDistributionSetRestApi.class, client, encoder, decoder, contract);
+ distributionSetTypeRestApi = service(MgmtDistributionSetTypeRestApi.class, client, encoder, decoder, contract);
+ distributionSetTagRestApi = service(MgmtDistributionSetTagRestApi.class, client, encoder, decoder, contract);
+ targetRestApi = service(MgmtTargetRestApi.class, client, encoder, decoder, contract);
+ targetTypeRestApi = service(MgmtTargetTypeRestApi.class, client, encoder, decoder, contract);
+ targetTagRestApi = service(MgmtTargetTagRestApi.class, client, encoder, decoder, contract);
+ targetFilterQueryRestApi = service(MgmtTargetFilterQueryRestApi.class, client, encoder, decoder, contract);
+ rolloutRestApi = service(MgmtRolloutRestApi.class, client, encoder, decoder, contract);
+ }
+
+ boolean hasSoftwareModulesRead() {
+ return hasRead(() -> softwareModuleRestApi.getSoftwareModule(-1L));
+ }
+
+ boolean hasRolloutRead() {
+ return hasRead(() -> rolloutRestApi.getRollout(-1L));
+ }
+
+ boolean hasDistributionSetRead() {
+ return hasRead(() -> distributionSetRestApi.getDistributionSet(-1L));
+ }
+
+ boolean hasTargetRead() {
+ return hasRead(() -> targetRestApi.getTarget("_#ETE$ER"));
+ }
+
+ private boolean hasRead(final Supplier> doCall) {
+ try {
+ final int statusCode = doCall.get().getStatusCode().value();
+ return statusCode != 401 && statusCode != 403;
+ } catch (final FeignException e) {
+ return !(e instanceof FeignException.Unauthorized) && !(e instanceof FeignException.Forbidden);
+ }
+ }
+
+ private static final ErrorDecoder DEFAULT_ERROR_DECODER = new ErrorDecoder.Default();
+ private T service(final Class serviceType,
+ final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
+ return Feign.builder().client(client)
+ .encoder(encoder)
+ .decoder(decoder)
+ .errorDecoder((methodKey, response) -> {
+ final Exception e = DEFAULT_ERROR_DECODER.decode(methodKey, response);
+ Utils.errorNotification(e);
+ return e;
+ })
+ .contract(contract)
+ .requestInterceptor(AUTHORIZATION)
+ .target(serviceType, hawkbitUrl);
+ }
+}
diff --git a/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/MainLayout.java b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/MainLayout.java
new file mode 100644
index 0000000000..787a9e395d
--- /dev/null
+++ b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/MainLayout.java
@@ -0,0 +1,150 @@
+/**
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package com.eclipse.hawkbit.ui;
+
+import com.eclipse.hawkbit.ui.view.RolloutView;
+import com.eclipse.hawkbit.ui.view.TargetView;
+import com.eclipse.hawkbit.ui.security.AuthenticatedUser;
+import com.eclipse.hawkbit.ui.view.AboutView;
+import com.eclipse.hawkbit.ui.view.DistributionSetView;
+import com.eclipse.hawkbit.ui.view.SoftwareModuleView;
+import com.vaadin.flow.component.Unit;
+import com.vaadin.flow.component.applayout.AppLayout;
+import com.vaadin.flow.component.applayout.DrawerToggle;
+import com.vaadin.flow.component.avatar.Avatar;
+import com.vaadin.flow.component.contextmenu.MenuItem;
+import com.vaadin.flow.component.html.Anchor;
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.html.Footer;
+import com.vaadin.flow.component.html.H1;
+import com.vaadin.flow.component.html.H2;
+import com.vaadin.flow.component.html.Header;
+import com.vaadin.flow.component.html.Image;
+import com.vaadin.flow.component.icon.Icon;
+import com.vaadin.flow.component.icon.VaadinIcon;
+import com.vaadin.flow.component.menubar.MenuBar;
+import com.vaadin.flow.component.orderedlayout.FlexComponent;
+import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
+import com.vaadin.flow.component.orderedlayout.Scroller;
+import com.vaadin.flow.component.sidenav.SideNav;
+import com.vaadin.flow.component.sidenav.SideNavItem;
+import com.vaadin.flow.router.PageTitle;
+import com.vaadin.flow.server.auth.AccessAnnotationChecker;
+import com.vaadin.flow.theme.lumo.LumoUtility;
+import java.util.Optional;
+
+/**
+ * The main view is a top-level placeholder for other views.
+ */
+public class MainLayout extends AppLayout {
+
+ private H2 viewTitle;
+
+ private final AuthenticatedUser authenticatedUser;
+ private final AccessAnnotationChecker accessChecker;
+
+ public MainLayout(final AuthenticatedUser authenticatedUser, final AccessAnnotationChecker accessChecker) {
+ this.authenticatedUser = authenticatedUser;
+ this.accessChecker = accessChecker;
+
+ setPrimarySection(Section.DRAWER);
+ addDrawerContent();
+ setDrawerOpened(true);
+ addHeaderContent();
+ }
+
+ private void addHeaderContent() {
+ final DrawerToggle toggle = new DrawerToggle();
+ toggle.setAriaLabel("Menu toggle");
+
+ viewTitle = new H2();
+ viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE);
+
+ addToNavbar(false, toggle, viewTitle);
+ }
+
+ private void addDrawerContent() {
+ final H1 appName = new H1("hawkBit UI (Experimental!)");
+ final HorizontalLayout layout = new HorizontalLayout();
+ layout.setPadding(true);
+ layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
+ final Image icon = new Image("images/header_icon.png", "hawkBit icon");
+ icon.setMaxHeight(24, Unit.PIXELS);
+ icon.setMaxWidth(24, Unit.PIXELS);
+ appName.addClassNames(LumoUtility.AlignItems.BASELINE, LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE);
+ layout.add(icon, appName);
+ final Header header = new Header(layout);
+
+ final Scroller scroller = new Scroller(createNavigation());
+
+ addToDrawer(header, scroller, createFooter());
+ }
+
+ private SideNav createNavigation() {
+ final SideNav nav = new SideNav();
+ if (accessChecker.hasAccess(TargetView.class)) {
+ nav.addItem(new SideNavItem("Targets", TargetView.class, VaadinIcon.FILTER.create()));
+ }
+ if (accessChecker.hasAccess(RolloutView.class)) {
+ nav.addItem(new SideNavItem("Rollouts", RolloutView.class, VaadinIcon.COGS.create()));
+ }
+ if (accessChecker.hasAccess(DistributionSetView.class)) {
+ nav.addItem(new SideNavItem("Distribution Sets", DistributionSetView.class, VaadinIcon.FILE_TREE.create()));
+ }
+ if (accessChecker.hasAccess(SoftwareModuleView.class)) {
+ nav.addItem(new SideNavItem("Software Modules", SoftwareModuleView.class, VaadinIcon.FILE.create()));
+ }
+ if (accessChecker.hasAccess(AboutView.class)) {
+ nav.addItem(new SideNavItem("About", AboutView.class, VaadinIcon.INFO_CIRCLE.create()));
+ }
+ return nav;
+ }
+
+ private Footer createFooter() {
+ final Footer layout = new Footer();
+
+ final Optional maybeUser = authenticatedUser.getName();
+ if (maybeUser.isPresent()) {
+ final String user = maybeUser.get();
+
+ final Avatar avatar = new Avatar(user);
+
+ final MenuBar userMenu = new MenuBar();
+ userMenu.setThemeName("tertiary-inline contrast");
+
+ final MenuItem userName = userMenu.addItem("");
+ final Div div = new Div();
+ div.add(avatar);
+ div.add(user);
+ div.add(new Icon("lumo", "dropdown"));
+ div.getElement().getStyle().set("display", "flex");
+ div.getElement().getStyle().set("align-items", "center");
+ div.getElement().getStyle().set("gap", "var(--lumo-space-s)");
+ userName.add(div);
+ userName.getSubMenu().addItem("Sign out", e -> authenticatedUser.logout());
+
+ layout.add(userMenu);
+ } else {
+ final Anchor loginLink = new Anchor("login", "Sign in");
+ layout.add(loginLink);
+ }
+
+ return layout;
+ }
+
+ @Override
+ protected void afterNavigation() {
+ super.afterNavigation();
+ viewTitle.setText(
+ Optional.ofNullable(getContent().getClass().getAnnotation(PageTitle.class))
+ .map(PageTitle::value)
+ .orElse(""));
+ }
+}
diff --git a/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/SimpleUIApp.java b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/SimpleUIApp.java
new file mode 100644
index 0000000000..3ca8d66f6d
--- /dev/null
+++ b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/SimpleUIApp.java
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package com.eclipse.hawkbit.ui;
+
+import com.vaadin.flow.component.page.AppShellConfigurator;
+import com.vaadin.flow.server.PWA;
+import com.vaadin.flow.theme.Theme;
+import com.vaadin.flow.theme.lumo.Lumo;
+import feign.Client;
+import feign.Contract;
+import feign.codec.Decoder;
+import feign.codec.Encoder;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.FeignClientsConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Import;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.util.LinkedList;
+import java.util.List;
+
+@Theme(themeClass = Lumo.class)
+@PWA(name="hawkBit UI", shortName="hawkBit UI")
+@SpringBootApplication
+@Import(FeignClientsConfiguration.class)
+public class SimpleUIApp implements AppShellConfigurator {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SimpleUIApp.class, args);
+ }
+
+ @Bean
+ HawkbitClient hawkbitClient(
+ @Value("${hawkbit.url:http://localhost:8080}")
+ final String hawkbitUrl,
+ final Client client, final Encoder encoder, final Decoder decoder, final Contract contract) {
+ return new HawkbitClient(hawkbitUrl, client, encoder, decoder, contract);
+ }
+
+ // accepts all user / pass, just delegating them to the feign client
+ @Bean
+ AuthenticationManager authenticationManager(final HawkbitClient hawkbitClient) {
+ return authentication-> {
+ final String username = authentication.getName();
+ final String password = authentication.getCredentials().toString();
+
+ final List roles = new LinkedList<>();
+ roles.add("ANONYMOUS");
+ final SecurityContext unauthorizedContext = SecurityContextHolder.createEmptyContext();
+ unauthorizedContext.setAuthentication(
+ new UsernamePasswordAuthenticationToken(username, password));
+ final SecurityContext currentContext = SecurityContextHolder.getContext();
+ try {
+ SecurityContextHolder.setContext(unauthorizedContext);
+ if (hawkbitClient.hasSoftwareModulesRead()) {
+ roles.add("SOFTWARE_MODULE_READ");
+ }
+ if (hawkbitClient.hasRolloutRead()) {
+ roles.add("ROLLOUT_READ");
+ }
+ if (hawkbitClient.hasDistributionSetRead()) {
+ roles.add("DISTRIBUTION_SET_READ");
+ }
+ if (hawkbitClient.hasTargetRead()) {
+ roles.add("TARGET_READ");
+ }
+ } finally {
+ SecurityContextHolder.setContext(currentContext);
+ }
+ return new UsernamePasswordAuthenticationToken(
+ username, password,
+ roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList()) {
+ public void eraseCredentials() {}
+ };
+ };
+ }
+}
diff --git a/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/security/AuthenticatedUser.java b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/security/AuthenticatedUser.java
new file mode 100644
index 0000000000..6faa1bb6e0
--- /dev/null
+++ b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/security/AuthenticatedUser.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package com.eclipse.hawkbit.ui.security;
+
+import com.vaadin.flow.spring.security.AuthenticationContext;
+import org.springframework.stereotype.Component;
+
+import java.util.Optional;
+
+@Component
+public class AuthenticatedUser {
+ private final AuthenticationContext authenticationContext;
+
+ public AuthenticatedUser(final AuthenticationContext authenticationContext) {
+ this.authenticationContext = authenticationContext;
+ }
+
+ public Optional getName() {
+ return authenticationContext.getPrincipalName();
+ }
+
+ public void logout() {
+ authenticationContext.logout();
+ }
+}
diff --git a/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/security/SecurityConfiguration.java b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/security/SecurityConfiguration.java
new file mode 100644
index 0000000000..07cc876b73
--- /dev/null
+++ b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/security/SecurityConfiguration.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package com.eclipse.hawkbit.ui.security;
+
+import com.eclipse.hawkbit.ui.view.LoginView;
+import com.vaadin.flow.spring.security.VaadinWebSecurity;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+
+@EnableWebSecurity
+@Configuration
+public class SecurityConfiguration extends VaadinWebSecurity {
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Override
+ protected void configure(final HttpSecurity http) throws Exception {
+ http.authorizeHttpRequests(
+ authorize -> authorize.requestMatchers(new AntPathRequestMatcher("/images/*.png")).permitAll());
+
+ super.configure(http);
+ setLoginView(http, LoginView.class);
+ }
+}
diff --git a/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/AboutView.java b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/AboutView.java
new file mode 100644
index 0000000000..ef585e64ad
--- /dev/null
+++ b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/AboutView.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package com.eclipse.hawkbit.ui.view;
+
+import com.eclipse.hawkbit.ui.MainLayout;
+import com.vaadin.flow.component.html.H2;
+import com.vaadin.flow.component.html.Image;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.flow.router.PageTitle;
+import com.vaadin.flow.router.Route;
+import com.vaadin.flow.router.RouteAlias;
+import com.vaadin.flow.theme.lumo.LumoUtility.Margin;
+import jakarta.annotation.security.RolesAllowed;
+
+@PageTitle("About")
+@Route(value = "about", layout = MainLayout.class)
+@RouteAlias(value = "", layout = MainLayout.class)
+@RolesAllowed({"ANONYMOUS"})
+public class AboutView extends VerticalLayout {
+
+ public AboutView() {
+ setSpacing(false);
+
+ final Image img = new Image("images/about_image.png", "hawkBit");
+ img.setWidth("200px");
+ add(img);
+
+ final H2 header = new H2("Eclipse hawkBit UI");
+ header.addClassNames(Margin.Top.XLARGE, Margin.Bottom.MEDIUM);
+
+ setSizeFull();
+ setJustifyContentMode(JustifyContentMode.CENTER);
+ setDefaultHorizontalComponentAlignment(Alignment.CENTER);
+ getStyle().set("text-align", "center");
+ }
+}
diff --git a/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/DistributionSetView.java b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/DistributionSetView.java
new file mode 100644
index 0000000000..321f40fe26
--- /dev/null
+++ b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/DistributionSetView.java
@@ -0,0 +1,333 @@
+/**
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package com.eclipse.hawkbit.ui.view;
+
+import com.eclipse.hawkbit.ui.HawkbitClient;
+import com.eclipse.hawkbit.ui.view.util.Filter;
+import com.eclipse.hawkbit.ui.MainLayout;
+import com.eclipse.hawkbit.ui.view.util.SelectionGrid;
+import com.eclipse.hawkbit.ui.view.util.TableView;
+import com.eclipse.hawkbit.ui.view.util.Utils;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.Key;
+import com.vaadin.flow.component.button.Button;
+import com.vaadin.flow.component.checkbox.Checkbox;
+import com.vaadin.flow.component.checkbox.CheckboxGroup;
+import com.vaadin.flow.component.dependency.Uses;
+import com.vaadin.flow.component.formlayout.FormLayout;
+import com.vaadin.flow.component.grid.Grid;
+import com.vaadin.flow.component.icon.Icon;
+import com.vaadin.flow.component.orderedlayout.FlexComponent;
+import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.flow.component.select.Select;
+import com.vaadin.flow.component.textfield.TextArea;
+import com.vaadin.flow.component.textfield.TextField;
+import com.vaadin.flow.data.renderer.ComponentRenderer;
+import com.vaadin.flow.router.PageTitle;
+import com.vaadin.flow.router.Route;
+import jakarta.annotation.security.RolesAllowed;
+import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
+import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPost;
+import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetType;
+import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule;
+import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleAssigment;
+import org.eclipse.hawkbit.mgmt.json.model.tag.MgmtTag;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Stream;
+
+@PageTitle("Distribution Sets")
+@Route(value = "distribution_sets", layout = MainLayout.class)
+@RolesAllowed({"DISTRIBUTION_SET_READ"})
+@Uses(Icon.class)
+public class DistributionSetView extends TableView {
+
+ public DistributionSetView(final HawkbitClient hawkbitClient) {
+ super(
+ new DistributionSetFilter(hawkbitClient),
+ new SelectionGrid.EntityRepresentation<>(MgmtDistributionSet.class, MgmtDistributionSet::getDsId) {
+
+ private final DistributionSetDetails details = new DistributionSetDetails(hawkbitClient);
+
+ @Override
+ protected void addColumns(Grid grid) {
+ grid.addColumn(MgmtDistributionSet::getDsId).setHeader("Id").setAutoWidth(true);
+ grid.addColumn(MgmtDistributionSet::getName).setHeader("Name").setAutoWidth(true);
+ grid.addColumn(MgmtDistributionSet::getVersion).setHeader("Version").setAutoWidth(true);
+ grid.addColumn(MgmtDistributionSet::getTypeName).setHeader("Type").setAutoWidth(true);
+
+ grid.setItemDetailsRenderer(new ComponentRenderer<>(
+ () -> details, DistributionSetDetails::setItem));
+ }
+ },
+ (query, rsqlFilter) -> hawkbitClient.getDistributionSetRestApi()
+ .getDistributionSets(
+ query.getOffset(), query.getPageSize(), "name:asc", rsqlFilter)
+ .getBody()
+ .getContent()
+ .stream(),
+ e -> new CreateDialog(hawkbitClient).result(),
+ selectionGrid -> {
+ selectionGrid.getSelectedItems().forEach(
+ distributionSet -> hawkbitClient.getDistributionSetRestApi()
+ .deleteDistributionSet(distributionSet.getDsId()));
+ return CompletableFuture.completedFuture(null);
+ });
+ }
+
+ private static SelectionGrid selectSoftwareModuleGrid() {
+ return new SelectionGrid<>(
+ new SelectionGrid.EntityRepresentation<>(
+ MgmtSoftwareModule.class, MgmtSoftwareModule::getModuleId) {
+ @Override
+ protected void addColumns(Grid grid) {
+ grid.addColumn(MgmtSoftwareModule::getModuleId).setHeader("Id").setAutoWidth(true);
+ grid.addColumn(MgmtSoftwareModule::getVersion).setHeader("Version").setAutoWidth(true);
+ grid.addColumn(MgmtSoftwareModule::getTypeName).setHeader("Name").setAutoWidth(true);
+ grid.addColumn(MgmtSoftwareModule::getVendor).setHeader("Vendor").setAutoWidth(true);
+ }
+ });
+ }
+
+ private static class DistributionSetFilter implements Filter.Rsql {
+
+ private final TextField name = Utils.textField("Name");
+ private final CheckboxGroup type = new CheckboxGroup<>("Type");
+ private final CheckboxGroup tag = new CheckboxGroup<>("Tag");
+
+ private DistributionSetFilter(final HawkbitClient hawkbitClient) {
+ name.setPlaceholder("");
+ type.setItemLabelGenerator(MgmtDistributionSetType::getName);
+ type.setItems(
+ hawkbitClient.getDistributionSetTypeRestApi()
+ .getDistributionSetTypes(0, 20, "name:asc", null)
+ .getBody()
+ .getContent());
+ tag.setItemLabelGenerator(MgmtTag::getName);
+ tag.setItems(
+ hawkbitClient.getDistributionSetTagRestApi()
+ .getDistributionSetTags(0, 20, "name:asc", null)
+ .getBody()
+ .getContent());
+ }
+
+ @Override
+ public List components() {
+ return List.of(name, type);
+ }
+
+ @Override
+ public String filter() {
+ return Filter.filter(
+ Map.of(
+ "name", name.getOptionalValue(),
+ "type", type.getSelectedItems().stream().map(MgmtDistributionSetType::getKey)
+ .toList(),
+ "tag", tag.getSelectedItems()));
+ }
+ }
+
+ private static class DistributionSetDetails extends FormLayout {
+
+ private final HawkbitClient hawkbitClient;
+
+ private final TextArea description = new TextArea("Description");
+ private final TextField createdBy = Utils.textField("Created by");
+ private final TextField createdAt = Utils.textField("Created at");
+ private final TextField lastModifiedBy = Utils.textField("Last modified by");
+ private final TextField lastModifiedAt = Utils.textField("Last modified at");
+ private final SelectionGrid softwareModulesGrid = selectSoftwareModuleGrid();
+
+ private DistributionSetDetails(final HawkbitClient hawkbitClient) {
+ this.hawkbitClient = hawkbitClient;
+
+ description.setMinLength(2);
+ Stream.of(
+ description,
+ createdBy, createdAt,
+ lastModifiedBy, lastModifiedAt)
+ .forEach(field -> {
+ field.setReadOnly(true);
+ add(field);
+ });
+ add(softwareModulesGrid);
+
+ setResponsiveSteps(new ResponsiveStep("0", 2));
+ setColspan(description, 2);
+ setColspan(softwareModulesGrid, 2);
+ }
+
+ private void setItem(final MgmtDistributionSet distributionSet) {
+ description.setValue(distributionSet.getDescription());
+ createdBy.setValue(distributionSet.getCreatedBy());
+ createdAt.setValue(new Date(distributionSet.getCreatedAt()).toString());
+ lastModifiedBy.setValue(distributionSet.getLastModifiedBy());
+ lastModifiedAt.setValue(new Date(distributionSet.getLastModifiedAt()).toString());
+
+ softwareModulesGrid.setItems(query ->
+ hawkbitClient.getDistributionSetRestApi()
+ .getAssignedSoftwareModules(
+ distributionSet.getDsId(),
+ query.getOffset(), query.getLimit(), "name:asc")
+ .getBody()
+ .getContent()
+ .stream());
+ softwareModulesGrid.setSelectionMode(Grid.SelectionMode.NONE);
+ }
+ }
+
+ private static class CreateDialog extends Utils.BaseDialog {
+
+ private final HawkbitClient hawkbitClient;
+
+ private final Select type;
+ private final TextField name;
+ private final TextField version;
+ private final TextArea description;
+ private final Checkbox requiredMigrationStep;
+ private final Button create;
+
+ private CreateDialog(final HawkbitClient hawkbitClient) {
+ super("Create Distribution Set");
+ this.hawkbitClient = hawkbitClient;
+
+ type = new Select<>(
+ "Type",
+ this::readyToCreate,
+ hawkbitClient.getDistributionSetTypeRestApi()
+ .getDistributionSetTypes(0, 30, "name:asc", null)
+ .getBody()
+ .getContent()
+ .toArray(new MgmtDistributionSetType[0]));
+ type.focus();
+ type.setWidthFull();
+ type.setRequiredIndicatorVisible(true);
+ type.setItemLabelGenerator(MgmtDistributionSetType::getName);
+ name = Utils.textField("Name", this::readyToCreate);
+ version = Utils.textField("Version", this::readyToCreate);
+ final TextField vendor = Utils.textField("Vendor");
+ description = new TextArea("Description");
+ description.setWidthFull();
+ description.setMinLength(2);
+ requiredMigrationStep = new Checkbox("Reguired Migration Step");
+
+ create = Utils.tooltip(new Button("Create"), "Create (Enter)");
+ create.setEnabled(false);
+ addCreateClickListener();
+ create.addClickShortcut(Key.ENTER);
+ final Button cancel = Utils.tooltip(new Button("Cancel"), "Cancel (Esc)");
+ cancel.addClickListener(e -> close());
+ create.addClickShortcut(Key.ESCAPE);
+ final HorizontalLayout actions = new HorizontalLayout(create, cancel);
+ actions.setSizeFull();
+ actions.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
+
+ final VerticalLayout layout = new VerticalLayout();
+ layout.setSizeFull();
+ layout.setPadding(true);
+ layout.setSpacing(false);
+ layout.add(type, name, version, vendor, description, requiredMigrationStep, actions);
+ add(layout);
+ open();
+ }
+
+ private void readyToCreate(final Object v) {
+ final boolean createEnabled = !type.isEmpty() && !name.isEmpty() && !version.isEmpty();
+ if (create.isEnabled() != createEnabled) {
+ create.setEnabled(createEnabled);
+ }
+ }
+
+ private void addCreateClickListener() {
+ create.addClickListener(e -> {
+ close();
+ final long distributionSetId = hawkbitClient.getDistributionSetRestApi()
+ .createDistributionSets(
+ List.of((MgmtDistributionSetRequestBodyPost)new MgmtDistributionSetRequestBodyPost()
+ .setType(type.getValue().getKey())
+ .setName(name.getValue())
+ .setVersion(version.getValue())
+ .setDescription(description.getValue())
+ .setRequiredMigrationStep(requiredMigrationStep.getValue())))
+ .getBody()
+ .stream()
+ .findFirst()
+ .orElseThrow()
+ .getDsId();
+ new AddSoftwareModulesDialog(distributionSetId, hawkbitClient).open();
+ });
+ }
+ }
+
+ private static class AddSoftwareModulesDialog extends Utils.BaseDialog {
+
+ private final Set softwareModules = Collections.synchronizedSet(new HashSet<>());
+
+ private AddSoftwareModulesDialog(final long distributionSetId, final HawkbitClient hawkbitClient) {
+ super("Add Software Modules");
+
+ final SelectionGrid 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("Add Software Modules") {{
+ final SoftwareModuleView softwareModulesView = new SoftwareModuleView(false, hawkbitClient);
+ add(softwareModulesView);
+ final Button addBtn = new Button("Add");
+ addBtn.addClickListener(e -> {
+ softwareModules.addAll(softwareModulesView.getSelection());
+ softwareModulesGrid.refreshGrid(false);
+ close();
+ });
+ add(addBtn);
+ open();
+ }}.result(),
+ v -> {
+ Utils.remove(softwareModulesGrid.getSelectedItems(), softwareModules, MgmtSoftwareModule::getModuleId);
+ softwareModulesGrid.refreshGrid(false);
+ return CompletableFuture.completedFuture(null);
+ },
+ softwareModulesGrid, true).get();
+ final Button finishBtn = Utils.tooltip(new Button("Finish"), "Finish (Esc)");
+ finishBtn.addClickListener(e -> {
+ hawkbitClient.getDistributionSetRestApi().assignSoftwareModules(
+ distributionSetId, softwareModules.stream().map(softwareModule -> {
+ final MgmtSoftwareModuleAssigment assignment = new MgmtSoftwareModuleAssigment();
+ assignment.setId(softwareModule.getModuleId());
+ return assignment;
+ }).toList());
+ close();
+ });
+ finishBtn.addClickShortcut(Key.ENTER);
+ final HorizontalLayout finish = new HorizontalLayout(finishBtn);
+ finish.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
+ finish.setWidthFull();
+ final HorizontalLayout addRemove = new HorizontalLayout(addRemoveControls);
+ addRemove.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
+ addRemove.setWidthFull();
+
+ final VerticalLayout layout = new VerticalLayout();
+ layout.setSizeFull();
+ layout.setSpacing(false);
+ layout.add(softwareModulesGrid, addRemove, finish);
+ add(layout);
+ }
+ }
+}
diff --git a/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/LoginView.java b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/LoginView.java
new file mode 100644
index 0000000000..d261d0bb1a
--- /dev/null
+++ b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/LoginView.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package com.eclipse.hawkbit.ui.view;
+
+import com.eclipse.hawkbit.ui.security.AuthenticatedUser;
+import com.vaadin.flow.component.login.LoginI18n;
+import com.vaadin.flow.component.login.LoginOverlay;
+import com.vaadin.flow.router.BeforeEnterEvent;
+import com.vaadin.flow.router.BeforeEnterObserver;
+import com.vaadin.flow.router.PageTitle;
+import com.vaadin.flow.router.Route;
+import com.vaadin.flow.router.internal.RouteUtil;
+import com.vaadin.flow.server.VaadinService;
+import com.vaadin.flow.server.auth.AnonymousAllowed;
+
+@AnonymousAllowed
+@PageTitle("Login")
+@Route(value = "login")
+public class LoginView extends LoginOverlay implements BeforeEnterObserver {
+
+ private final AuthenticatedUser authenticatedUser;
+
+ public LoginView(final AuthenticatedUser authenticatedUser) {
+ this.authenticatedUser = authenticatedUser;
+ setAction(RouteUtil.getRoutePath(VaadinService.getCurrent().getContext(), getClass()));
+
+ final LoginI18n i18n = LoginI18n.createDefault();
+ i18n.setHeader(new LoginI18n.Header());
+ i18n.getHeader().setTitle("hawkBit");
+ i18n.getHeader().setDescription("Login in hawkBit");
+ i18n.setAdditionalInformation(null);
+ setI18n(i18n);
+
+ setForgotPasswordButtonVisible(false);
+ setOpened(true);
+ }
+
+ @Override
+ public void beforeEnter(final BeforeEnterEvent event) {
+ if (authenticatedUser.getName().isPresent()) { // already logged in
+ setOpened(false);
+ event.forwardTo("");
+ }
+
+ setError(event.getLocation().getQueryParameters().getParameters().containsKey("error"));
+ }
+}
diff --git a/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/RolloutView.java b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/RolloutView.java
new file mode 100644
index 0000000000..f5a941ab53
--- /dev/null
+++ b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/RolloutView.java
@@ -0,0 +1,427 @@
+/**
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package com.eclipse.hawkbit.ui.view;
+
+import com.eclipse.hawkbit.ui.HawkbitClient;
+import com.eclipse.hawkbit.ui.view.util.Filter;
+import com.eclipse.hawkbit.ui.MainLayout;
+import com.eclipse.hawkbit.ui.view.util.SelectionGrid;
+import com.eclipse.hawkbit.ui.view.util.TableView;
+import com.eclipse.hawkbit.ui.view.util.Utils;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.Key;
+import com.vaadin.flow.component.Text;
+import com.vaadin.flow.component.button.Button;
+import com.vaadin.flow.component.datetimepicker.DateTimePicker;
+import com.vaadin.flow.component.dependency.Uses;
+import com.vaadin.flow.component.formlayout.FormLayout;
+import com.vaadin.flow.component.grid.Grid;
+import com.vaadin.flow.component.html.Div;
+import com.vaadin.flow.component.icon.Icon;
+import com.vaadin.flow.component.icon.VaadinIcon;
+import com.vaadin.flow.component.orderedlayout.FlexComponent;
+import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.flow.component.select.Select;
+import com.vaadin.flow.component.textfield.NumberField;
+import com.vaadin.flow.component.textfield.TextArea;
+import com.vaadin.flow.component.textfield.TextField;
+import com.vaadin.flow.data.renderer.ComponentRenderer;
+import com.vaadin.flow.router.PageTitle;
+import com.vaadin.flow.router.Route;
+import jakarta.annotation.security.RolesAllowed;
+import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
+import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet;
+import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutCondition;
+import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutErrorAction;
+import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutResponseBody;
+import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBody;
+import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroupResponseBody;
+import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQuery;
+import org.springframework.util.ObjectUtils;
+
+import java.time.ZoneOffset;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Stream;
+
+@PageTitle("Rollouts")
+@Route(value = "rollouts", layout = MainLayout.class)
+@RolesAllowed({"ROLLOUT_READ"})
+@Uses(Icon.class)
+public class RolloutView extends TableView {
+
+ public RolloutView(final HawkbitClient hawkbitClient) {
+ super(
+ new RolloutFilter(hawkbitClient),
+ new SelectionGrid.EntityRepresentation<>(
+ MgmtRolloutResponseBody.class, MgmtRolloutResponseBody::getRolloutId) {
+
+ private final RolloutDetails details = new RolloutDetails(hawkbitClient);
+ @Override
+ protected void addColumns(final Grid grid) {
+ grid.addColumn(MgmtRolloutResponseBody::getRolloutId).setHeader("Id").setAutoWidth(true);
+ grid.addColumn(MgmtRolloutResponseBody::getName).setHeader("Name").setAutoWidth(true);
+ grid.addColumn(MgmtRolloutResponseBody::getTotalGroups).setHeader("Group Count").setAutoWidth(true);
+ grid.addColumn(MgmtRolloutResponseBody::getTotalTargets).setHeader("Target Count").setAutoWidth(true);
+ grid.addColumn(MgmtRolloutResponseBody::getTotalTargetsPerStatus).setHeader("Stats").setAutoWidth(true);
+ grid.addColumn(MgmtRolloutResponseBody::getStatus).setHeader("Status").setAutoWidth(true);
+
+ grid.addComponentColumn(rollout -> new Actions(rollout, grid, hawkbitClient)).setHeader("Actions").setAutoWidth(true);
+
+ grid.setItemDetailsRenderer(new ComponentRenderer<>(
+ () -> details, RolloutDetails::setItem));
+ }
+ },
+ (query, rsqlFilter) -> hawkbitClient.getRolloutRestApi()
+ .getRollouts(
+ query.getOffset(), query.getPageSize(), "name:asc", rsqlFilter, null)
+ .getBody()
+ .getContent()
+ .stream(),
+ selectionGrid -> new CreateDialog(hawkbitClient).result(),
+ selectionGrid -> {
+ selectionGrid.getSelectedItems().forEach(
+ rollout -> hawkbitClient.getRolloutRestApi().delete(rollout.getRolloutId()));
+ selectionGrid.refreshGrid(false);
+ return CompletableFuture.completedFuture(null);
+ });
+ }
+
+ private static SelectionGrid createGroupGrid() {
+ return new SelectionGrid<>(
+ new SelectionGrid.EntityRepresentation<>(MgmtRolloutGroupResponseBody.class, MgmtRolloutGroupResponseBody::getRolloutGroupId) {
+ @Override
+ protected void addColumns(final Grid grid) {
+ grid.addColumn(MgmtRolloutGroupResponseBody::getRolloutGroupId).setHeader("Id").setAutoWidth(true);
+ grid.addColumn(MgmtRolloutGroupResponseBody::getName).setHeader("Name").setAutoWidth(true);
+ grid.addColumn(MgmtRolloutGroupResponseBody::getTotalTargets).setHeader("Target Count").setAutoWidth(true);
+ grid.addColumn(MgmtRolloutGroupResponseBody::getTotalTargetsPerStatus).setHeader("Stats").setAutoWidth(true);
+ grid.addColumn(MgmtRolloutGroupResponseBody::getStatus).setHeader("Status").setAutoWidth(true);
+ }
+ });
+ }
+
+ private static class Actions extends HorizontalLayout {
+
+ private final long rolloutId;
+ private final Grid grid;
+ private final HawkbitClient hawkbitClient;
+
+ private Actions(final MgmtRolloutResponseBody rollout, final Grid grid, final HawkbitClient hawkbitClient) {
+ this.rolloutId = rollout.getRolloutId();
+ this.grid = grid;
+ this.hawkbitClient = hawkbitClient;
+ init(rollout);
+ }
+
+ private void init(final MgmtRolloutResponseBody rollout) {
+ if ("READY".equalsIgnoreCase(rollout.getStatus())) {
+ add(Utils.tooltip(new Button(VaadinIcon.START_COG.create()) {{
+ addClickListener(v -> {
+ hawkbitClient.getRolloutRestApi().start(rollout.getRolloutId());
+ refresh();
+ });
+ }}, "Start"));
+ } else if ("RUNNING".equalsIgnoreCase(rollout.getStatus())) {
+ add(Utils.tooltip(new Button(VaadinIcon.PAUSE.create()) {{
+ addClickListener(v -> {
+ hawkbitClient.getRolloutRestApi().pause(rollout.getRolloutId());
+ refresh();
+ });
+ }}, "Pause"));
+ } else if ("PAUSED".equalsIgnoreCase(rollout.getStatus())) {
+ add(Utils.tooltip(new Button(VaadinIcon.START_COG.create()) {{
+ addClickListener(v -> {
+ hawkbitClient.getRolloutRestApi().resume(rollout.getRolloutId());
+ refresh();
+ });
+ }}, "Resume"));
+ }
+ add(Utils.tooltip(new Button(VaadinIcon.TRASH.create()) {{
+ addClickListener(v -> {
+ hawkbitClient.getRolloutRestApi().delete(rollout.getRolloutId());
+ grid.getDataProvider().refreshAll();
+ });
+ }}, "Cancel and Remove"));
+ }
+
+ private void refresh() {
+ removeAll();
+ init(hawkbitClient.getRolloutRestApi().getRollout(rolloutId).getBody());
+ }
+ }
+
+ private static class RolloutFilter implements Filter.Rsql {
+
+ private final TextField name = Utils.textField("Name");
+
+ private RolloutFilter(final HawkbitClient hawkbitClient) {
+ name.setPlaceholder("");
+ }
+
+ @Override
+ public List components() {
+ return List.of(name);
+ }
+
+ @Override
+ public String filter() {
+ return Filter.filter(Map.of("name", name.getOptionalValue()));
+ }
+ }
+
+ private static class RolloutDetails extends FormLayout {
+
+ private final HawkbitClient hawkbitClient;
+
+ private final TextArea description = new TextArea("Description");
+ private final TextField createdBy = Utils.textField("Created by");
+ private final TextField createdAt = Utils.textField("Created at");
+ private final TextField lastModifiedBy = Utils.textField("Last modified by");
+ private final TextField lastModifiedAt = Utils.textField("Last modified at");
+ private final TextField targetFilter = Utils.textField("Target Filter");
+ private final TextField distributionSet = Utils.textField("Distribution Set");
+ private final TextField actonType = Utils.textField("Action Type");
+ private final TextField startAt = Utils.textField("Start At");
+ private final SelectionGrid groupGrid;
+
+ private RolloutDetails(final HawkbitClient hawkbitClient) {
+ this.hawkbitClient = hawkbitClient;
+
+ description.setMinLength(2);
+ groupGrid = createGroupGrid();
+ Stream.of(
+ description,
+ createdBy, createdAt,
+ lastModifiedBy, lastModifiedAt,
+ targetFilter, distributionSet,
+ actonType, startAt)
+ .forEach(field -> {
+ field.setReadOnly(true);
+ add(field);
+ });
+ add(groupGrid);
+
+ setResponsiveSteps(new ResponsiveStep("0", 2));
+ setColspan(description, 2);
+ setColspan(groupGrid, 2);
+ }
+
+ private void setItem(final MgmtRolloutResponseBody rollout) {
+ description.setValue(rollout.getDescription());
+ createdBy.setValue(rollout.getCreatedBy());
+ createdAt.setValue(new Date(rollout.getCreatedAt()).toString());
+ lastModifiedBy.setValue(rollout.getLastModifiedBy());
+ lastModifiedAt.setValue(new Date(rollout.getLastModifiedAt()).toString());
+ targetFilter.setValue(rollout.getTargetFilterQuery());
+ final MgmtDistributionSet distributionSetMgmt = hawkbitClient.getDistributionSetRestApi()
+ .getDistributionSet(rollout.getDistributionSetId()).getBody();
+ distributionSet.setValue(distributionSetMgmt.getName() + ":" + distributionSetMgmt.getVersion());
+ actonType.setValue(switch (rollout.getType()) {
+ case SOFT -> "Soft";
+ case FORCED -> "Forced";
+ case DOWNLOAD_ONLY -> "Download Only";
+ case TIMEFORCED -> "Scheduled at " + new Date(rollout.getForcetime());
+ });
+ startAt.setValue(ObjectUtils.isEmpty(rollout.getStartAt()) ? "" : new Date(rollout.getStartAt()).toString());
+
+ groupGrid.setItems(query ->
+ hawkbitClient.getRolloutRestApi()
+ .getRolloutGroups(
+ rollout.getRolloutId(),
+ query.getOffset(), query.getPageSize(),
+ null, null, null)
+ .getBody().getContent().stream()
+ .skip(query.getOffset())
+ .limit(query.getPageSize()));
+ groupGrid.setSelectionMode(Grid.SelectionMode.NONE);
+ }
+ }
+
+ private static class CreateDialog extends Utils.BaseDialog {
+
+ private enum StartType {
+ MANUAL, AUTO, SCHEDULED
+ }
+
+ private final TextField name;
+ private final Select distributionSet;
+ private final Select targetFilter;
+ private final TextArea description;
+ private final Select actionType;
+ private final DateTimePicker forceTime = new DateTimePicker("Force Time");
+ private final Select startType;
+ private final DateTimePicker startAt = new DateTimePicker("Start At");
+ private final NumberField groupNumber;
+ private final NumberField triggerThreshold;
+ private final NumberField errorThreshold;
+
+ private final Button create = new Button("Create");
+
+ private CreateDialog(final HawkbitClient hawkbitClient) {
+ super("Create Rollout");
+
+ name = Utils.textField("Name", this::readyToCreate);
+ name.focus();
+ distributionSet = new Select<>(
+ "Distribution Set",
+ this::readyToCreate,
+ hawkbitClient.getDistributionSetRestApi()
+ .getDistributionSets(0, 30, "name:asc", null)
+ .getBody()
+ .getContent()
+ .toArray(new MgmtDistributionSet[0]));
+ distributionSet.setRequiredIndicatorVisible(true);
+ distributionSet.setItemLabelGenerator(distributionSet ->
+ distributionSet.getName() + ":" + distributionSet.getVersion());
+ distributionSet.setWidthFull();
+ targetFilter = new Select<>(
+ "Target Filter",
+ this::readyToCreate,
+ hawkbitClient.getTargetFilterQueryRestApi()
+ .getFilters(0, 30, "name:asc", null, null)
+ .getBody()
+ .getContent()
+ .toArray(new MgmtTargetFilterQuery[0]));
+ targetFilter.setRequiredIndicatorVisible(true);
+ targetFilter.setItemLabelGenerator(MgmtTargetFilterQuery::getName);
+ targetFilter.setWidthFull();
+ description = new TextArea("Description");
+ description.setMinLength(2);
+ description.setWidthFull();
+
+ actionType = new Select<>();
+ actionType.setLabel("Action Type");
+ actionType.setItems(MgmtActionType.values());
+ actionType.setValue(MgmtActionType.FORCED);
+ final ComponentRenderer actionTypeRenderer = new ComponentRenderer<>(actionType ->
+ switch (actionType) {
+ case SOFT -> new Text("Soft");
+ case FORCED -> new Text("Forced");
+ case DOWNLOAD_ONLY -> new Text("Download Only");
+ case TIMEFORCED -> forceTime;
+ });
+ actionType.addValueChangeListener(e -> actionType.setRenderer(actionTypeRenderer));
+ actionType.setItemLabelGenerator(startType ->
+ switch (startType) {
+ case SOFT -> "Soft";
+ case FORCED -> "Forced";
+ case DOWNLOAD_ONLY -> "Download Only";
+ case TIMEFORCED -> "Time Forced at " + (forceTime.isEmpty() ? "" : " " + forceTime.getValue());
+ });
+ actionType.setWidthFull();
+ startType = new Select<>();
+ startType.setValue(StartType.MANUAL);
+ startType.setLabel("Start Type");
+ startType.setItems(StartType.values());
+ startType.setValue(StartType.MANUAL);
+ final ComponentRenderer startTypeRenderer = new ComponentRenderer<>(startType ->
+ switch (startType) {
+ case MANUAL -> new Text("Manual");
+ case AUTO -> new Text("Auto");
+ case SCHEDULED -> startAt;
+ });
+ startType.setRenderer(startTypeRenderer);
+ startType.addValueChangeListener(e -> startType.setRenderer(startTypeRenderer));
+ startType.setItemLabelGenerator(startType ->
+ switch (startType) {
+ case MANUAL -> "Manual";
+ case AUTO -> "Auto";
+ case SCHEDULED -> "Scheduled" + (startAt.isEmpty() ? "" : " at " + startAt.getValue());
+ });
+ startType.setWidthFull();
+
+ final Div percentSuffix = new Div();
+ percentSuffix.setText("%");
+ groupNumber = Utils.numberField("Group number", this::readyToCreate);
+ groupNumber.setMin(1);
+ groupNumber.setValue(1.0);
+ triggerThreshold = Utils.numberField("Trigger Threshold", this::readyToCreate);
+ triggerThreshold.setMin(0);
+ triggerThreshold.setMax(100);
+ triggerThreshold.setValue(100.0);
+ triggerThreshold.setSuffixComponent(percentSuffix);
+ errorThreshold = Utils.numberField("Error Threshold", this::readyToCreate);
+ errorThreshold.setMin(1);
+ errorThreshold.setMax(100);
+ errorThreshold.setValue(10.0);
+ errorThreshold.setSuffixComponent(percentSuffix);
+
+ create.setEnabled(false);
+ addCreateClickListener(hawkbitClient);
+ final Button cancel = Utils.tooltip(new Button("Cancel"), "Cancel (Esc)");
+ cancel.addClickListener(e -> close());
+ cancel.addClickShortcut(Key.ESCAPE);
+ final HorizontalLayout actions = new HorizontalLayout(create, cancel);
+ actions.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
+ actions.setSizeFull();
+
+ final VerticalLayout layout = new VerticalLayout();
+ layout.setSizeFull();
+ layout.setSpacing(false);
+ layout.add(
+ name, distributionSet, targetFilter, description,
+ actionType, startType,
+ groupNumber, triggerThreshold, errorThreshold,
+ actions);
+ add(layout);
+ open();
+ }
+
+ private void readyToCreate(final Object v) {
+ final boolean createEnabled = !name.isEmpty() &&
+ !distributionSet.isEmpty() &&
+ !targetFilter.isEmpty() &&
+ !groupNumber.isEmpty() &&
+ !triggerThreshold.isEmpty() &&
+ !errorThreshold.isEmpty();
+ if (create.isEnabled() != createEnabled) {
+ create.setEnabled(createEnabled);
+ }
+ }
+
+ private void addCreateClickListener(final HawkbitClient hawkbitClient) {
+ create.addClickListener(e -> {
+ close();
+ final MgmtRolloutRestRequestBody request = new MgmtRolloutRestRequestBody();
+ request.setName(name.getValue());
+ request.setDistributionSetId(distributionSet.getValue().getDsId());
+ request.setTargetFilterQuery(targetFilter.getValue().getName());
+ request.setDescription(description.getValue());
+
+ request.setType(actionType.getValue());
+ if (actionType.getValue() == MgmtActionType.FORCED) {
+ request.setForcetime(forceTime.getValue().toEpochSecond(ZoneOffset.UTC) * 1000);
+ }
+ switch (startType.getValue()) {
+ case AUTO -> request.setStartAt(System.currentTimeMillis());
+ case SCHEDULED -> request.setStartAt(startAt.getValue().toEpochSecond(ZoneOffset.UTC) * 1000);
+ } // else - manual, do not start
+
+ request.setAmountGroups(groupNumber.getValue().intValue());
+ request.setSuccessCondition(
+ new MgmtRolloutCondition(
+ MgmtRolloutCondition.Condition.THRESHOLD,
+ triggerThreshold.getValue().intValue() + "%"));
+ request.setErrorCondition(
+ new MgmtRolloutCondition(
+ MgmtRolloutCondition.Condition.THRESHOLD,
+ errorThreshold.getValue().intValue() + "%"));
+ request.setErrorAction(
+ new MgmtRolloutErrorAction(
+ MgmtRolloutErrorAction.ErrorAction.PAUSE, ""));
+ hawkbitClient.getRolloutRestApi().create(request).getBody();
+ });
+ }
+ }
+}
diff --git a/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/SoftwareModuleView.java b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/SoftwareModuleView.java
new file mode 100644
index 0000000000..a4fce787d6
--- /dev/null
+++ b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/SoftwareModuleView.java
@@ -0,0 +1,378 @@
+/**
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package com.eclipse.hawkbit.ui.view;
+
+import com.eclipse.hawkbit.ui.HawkbitClient;
+import com.eclipse.hawkbit.ui.view.util.Filter;
+import com.eclipse.hawkbit.ui.view.util.SelectionGrid;
+import com.eclipse.hawkbit.ui.MainLayout;
+import com.eclipse.hawkbit.ui.view.util.TableView;
+import com.eclipse.hawkbit.ui.view.util.Utils;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.Key;
+import com.vaadin.flow.component.button.Button;
+import com.vaadin.flow.component.checkbox.Checkbox;
+import com.vaadin.flow.component.checkbox.CheckboxGroup;
+import com.vaadin.flow.component.dependency.Uses;
+import com.vaadin.flow.component.formlayout.FormLayout;
+import com.vaadin.flow.component.grid.Grid;
+import com.vaadin.flow.component.icon.Icon;
+import com.vaadin.flow.component.orderedlayout.FlexComponent;
+import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.flow.component.select.Select;
+import com.vaadin.flow.component.textfield.TextArea;
+import com.vaadin.flow.component.textfield.TextField;
+import com.vaadin.flow.component.upload.Upload;
+import com.vaadin.flow.component.upload.receivers.FileBuffer;
+import com.vaadin.flow.data.renderer.ComponentRenderer;
+import com.vaadin.flow.router.PageTitle;
+import com.vaadin.flow.router.Route;
+import jakarta.annotation.security.RolesAllowed;
+
+import org.eclipse.hawkbit.mgmt.json.model.artifact.MgmtArtifact;
+import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule;
+import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleRequestBodyPost;
+import org.eclipse.hawkbit.mgmt.json.model.softwaremoduletype.MgmtSoftwareModuleType;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Stream;
+
+@PageTitle("Software Modules")
+@Route(value = "software_modules", layout = MainLayout.class)
+@RolesAllowed({"SOFTWARE_MODULE_READ"})
+@Uses(Icon.class)
+public class SoftwareModuleView extends TableView {
+
+ @Autowired
+ public SoftwareModuleView(final HawkbitClient hawkbitClient) {
+ this(true, hawkbitClient);
+ }
+
+ public SoftwareModuleView(final boolean isParent, final HawkbitClient hawkbitClient) {
+ super(
+ new SoftwareModuleFilter(hawkbitClient),
+ new SelectionGrid.EntityRepresentation<>(MgmtSoftwareModule.class, MgmtSoftwareModule::getModuleId) {
+
+ private final SoftwareModuleDetails details = new SoftwareModuleDetails(hawkbitClient);
+ @Override
+ protected void addColumns(final Grid grid) {
+ grid.addColumn(MgmtSoftwareModule::getModuleId).setHeader("Id").setAutoWidth(true);
+ grid.addColumn(MgmtSoftwareModule::getName).setHeader("Name").setAutoWidth(true);
+ grid.addColumn(MgmtSoftwareModule::getVersion).setHeader("Version").setAutoWidth(true);
+ grid.addColumn(MgmtSoftwareModule::getTypeName).setHeader("Type").setAutoWidth(true);
+ grid.addColumn(MgmtSoftwareModule::getVendor).setHeader("Vendor").setAutoWidth(true);
+
+ grid.setItemDetailsRenderer(new ComponentRenderer<>(
+ () -> details, SoftwareModuleDetails::setItem));
+
+ }
+ },
+ (query, rsqlFilter) -> hawkbitClient.getSoftwareModuleRestApi()
+ .getSoftwareModules(
+ query.getOffset(), query.getPageSize(), "name:asc", rsqlFilter)
+ .getBody()
+ .getContent()
+ .stream(),
+ isParent ? v -> new CreateDialog(hawkbitClient).result() : null,
+ isParent ? selectionGrid -> {
+ selectionGrid.getSelectedItems().forEach(
+ module -> hawkbitClient.getSoftwareModuleRestApi().deleteSoftwareModule(module.getModuleId()));
+ selectionGrid.refreshGrid(false);
+ return CompletableFuture.completedFuture(null);
+ } : null);
+ }
+
+ public Set getSelection() {
+ return selectionGrid.getSelectedItems();
+ }
+
+ private static SelectionGrid createArtifactGrid() {
+ return new SelectionGrid<>(
+ new SelectionGrid.EntityRepresentation<>(MgmtArtifact.class, MgmtArtifact::getArtifactId) {
+ @Override
+ protected void addColumns(final Grid grid) {
+ grid.addColumn(MgmtArtifact::getArtifactId).setHeader("Id").setAutoWidth(true);
+ grid.addColumn(MgmtArtifact::getProvidedFilename).setHeader("Name").setAutoWidth(true);
+ grid.addColumn(MgmtArtifact::getSize).setHeader("Size").setAutoWidth(true);
+ grid.addColumn(MgmtArtifact::getHashes).setHeader("Hashes").setAutoWidth(true);
+ }
+ });
+ }
+
+ private static class SoftwareModuleFilter implements Filter.Rsql {
+
+ private final TextField name = Utils.textField("Name");
+ private final CheckboxGroup type = new CheckboxGroup<>("Type");
+
+ private SoftwareModuleFilter(final HawkbitClient hawkbitClient) {
+ name.setPlaceholder("");
+ type.setItemLabelGenerator(MgmtSoftwareModuleType::getName);
+ type.setItems(
+ hawkbitClient.getSoftwareModuleTypeRestApi()
+ .getTypes(0, 20, "name:asc", null)
+ .getBody()
+ .getContent());
+ }
+
+ @Override
+ public List components() {
+ return List.of(name, type);
+ }
+
+ @Override
+ public String filter() {
+ return Filter.filter(
+ Map.of(
+ "name", name.getOptionalValue(),
+ "type", type.getSelectedItems().stream().map(MgmtSoftwareModuleType::getKey)
+ .toList()
+ ));
+ }
+ }
+
+ private static class SoftwareModuleDetails extends FormLayout {
+
+ private final HawkbitClient hawkbitClient;
+
+ private final TextArea description = new TextArea("Description");
+ private final TextField createdBy = Utils.textField("Created by");
+ private final TextField createdAt = Utils.textField("Created at");
+ private final TextField lastModifiedBy = Utils.textField("Last modified by");
+ private final TextField lastModifiedAt = Utils.textField("Last modified at");
+ private final SelectionGrid artifactGrid;
+
+ private SoftwareModuleDetails(final HawkbitClient hawkbitClient) {
+ this.hawkbitClient = hawkbitClient;
+
+ description.setMinLength(2);
+ artifactGrid = createArtifactGrid();
+ Stream.of(
+ description,
+ createdBy, createdAt,
+ lastModifiedBy, lastModifiedAt)
+ .forEach(field -> {
+ field.setReadOnly(true);
+ add(field);
+ });
+ add(artifactGrid);
+
+ setResponsiveSteps(new ResponsiveStep("0", 2));
+ setColspan(description, 2);
+ setColspan(artifactGrid, 2);
+ }
+
+ private void setItem(final MgmtSoftwareModule softwareModule) {
+ description.setValue(softwareModule.getDescription());
+ createdBy.setValue(softwareModule.getCreatedBy());
+ createdAt.setValue(new Date(softwareModule.getCreatedAt()).toString());
+ lastModifiedBy.setValue(softwareModule.getLastModifiedBy());
+ lastModifiedAt.setValue(new Date(softwareModule.getLastModifiedAt()).toString());
+
+ artifactGrid.setItems(query ->
+ hawkbitClient.getSoftwareModuleRestApi()
+ .getArtifacts(
+ softwareModule.getModuleId(), null, null)
+ .getBody().stream()
+ .skip(query.getOffset())
+ .limit(query.getPageSize()));
+ artifactGrid.setSelectionMode(Grid.SelectionMode.NONE);
+ }
+ }
+
+ private static class CreateDialog extends Utils.BaseDialog {
+
+ private final Select type;
+ private final TextField name;
+ private final TextField version;
+ private final TextField vendor;
+ private final TextArea description;
+ private final Checkbox enableArtifactEncryption;
+ private final Button create;
+
+ private CreateDialog(final HawkbitClient hawkbitClient) {
+ super("Create Software Module");
+
+ type = new Select<>(
+ "Type",
+ this::readyToCreate,
+ hawkbitClient.getSoftwareModuleTypeRestApi()
+ .getTypes(0, 30, "name:asc", null)
+ .getBody()
+ .getContent()
+ .toArray(new MgmtSoftwareModuleType[0]));
+ type.setWidthFull();
+ type.setRequiredIndicatorVisible(true);
+ type.setItemLabelGenerator(MgmtSoftwareModuleType::getName);
+ type.focus();
+ name = Utils.textField("Name", this::readyToCreate);
+ version = Utils.textField("Version", this::readyToCreate);
+ vendor = Utils.textField("Vendor");
+ description = new TextArea("Description");
+ description.setWidthFull();
+ description.setMinLength(2);
+ enableArtifactEncryption = new Checkbox("Enable artifact encryption");
+
+ create = Utils.tooltip(new Button("Create"), "Create (Enter)");
+ create.setEnabled(false);
+ addCreateClickListener(hawkbitClient);
+ create.addClickShortcut(Key.ENTER);
+ final Button cancel = Utils.tooltip(new Button("Cancel"), "Cancel (Esc)");
+ cancel.addClickListener(e -> close());
+ cancel.addClickShortcut(Key.ESCAPE);
+ final HorizontalLayout actions = new HorizontalLayout(create, cancel);
+ actions.setSizeFull();
+ actions.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
+
+ final VerticalLayout layout = new VerticalLayout();
+ layout.setSizeFull();
+ layout.setSpacing(false);
+ layout.add(type, name, version, vendor, description, enableArtifactEncryption, actions);
+ add(layout);
+ open();
+ }
+
+ private void readyToCreate(final Object v) {
+ final boolean createEnabled = !type.isEmpty() && !name.isEmpty() && !version.isEmpty();
+ if (create.isEnabled() != createEnabled) {
+ create.setEnabled(createEnabled);
+ }
+ }
+
+ private void addCreateClickListener(final HawkbitClient hawkbitClient) {
+ create.addClickListener(e -> {
+ close();
+ final long softwareModuleId = hawkbitClient.getSoftwareModuleRestApi().createSoftwareModules(
+ List.of(new MgmtSoftwareModuleRequestBodyPost()
+ .setType(type.getValue().getKey())
+ .setName(name.getValue())
+ .setVersion(version.getValue())
+ .setVendor(vendor.getValue())
+ .setDescription(description.getValue())
+ .setEncrypted(enableArtifactEncryption.getValue())))
+ .getBody()
+ .stream()
+ .findFirst()
+ .orElseThrow()
+ .getModuleId();
+ new AddArtifactsDialog(softwareModuleId, hawkbitClient).open();
+ });
+ }
+ }
+
+ private static class AddArtifactsDialog extends Utils.BaseDialog {
+
+ private final Set artifacts = Collections.synchronizedSet(new HashSet<>());
+
+ private AddArtifactsDialog(
+ final long softwareModuleId,
+ final HawkbitClient hawkbitClient) {
+ super("Add Artifacts");
+
+ final SelectionGrid artifactGrid = createArtifactGrid();
+ artifactGrid.setItems(query -> {
+ query.getOffset(); // to keep vaadin contract
+ return artifacts.stream().limit(query.getLimit());
+ });
+ artifactGrid.setSelectionMode(Grid.SelectionMode.NONE);
+
+ final FileBuffer fileBuffer = new FileBuffer();
+ final Upload uploadBtn = new Upload(fileBuffer);
+ uploadBtn.setMaxFiles(10);
+ uploadBtn.setWidthFull();
+ uploadBtn.setDropAllowed(true);
+ uploadBtn.addSucceededListener(e -> {
+ final MgmtArtifact artifact = hawkbitClient.getSoftwareModuleRestApi()
+ .uploadArtifact(softwareModuleId,
+ new MultipartFileImpl(fileBuffer, e.getContentLength(), e.getMIMEType()), fileBuffer.getFileName(), null, null,
+ null).getBody();
+ artifacts.add(artifact);
+ artifactGrid.refreshGrid(false);
+ });
+
+ final Button finishBtn = Utils.tooltip(new Button("Finish"), "Finish (Enter)");
+ finishBtn.addClickListener(e -> close());
+ finishBtn.addClickShortcut(Key.ENTER);
+ finishBtn.setHeightFull();
+ final HorizontalLayout finish = new HorizontalLayout(finishBtn);
+ finish.setJustifyContentMode(FlexComponent.JustifyContentMode.END);
+ finish.setWidthFull();
+
+ final VerticalLayout layout = new VerticalLayout(artifactGrid, uploadBtn, finish);
+ layout.setSizeFull();
+ layout.setSpacing(false);
+ add(layout);
+ }
+
+ private static class MultipartFileImpl implements MultipartFile {
+
+ private final FileBuffer fileBuffer;
+ private final String mimeType;
+ private final long contentLength;
+
+ public MultipartFileImpl(final FileBuffer fileBuffer, final long contentLength, final String mimeType) {
+ this.fileBuffer = fileBuffer;
+ this. contentLength = contentLength;
+ this.mimeType = mimeType;
+ }
+
+ @Override
+ public String getName() {
+ return fileBuffer.getFileName();
+ }
+
+ @Override
+ public String getOriginalFilename() {
+ return getName();
+ }
+
+ @Override
+ public String getContentType() {
+ return mimeType;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return contentLength == 0;
+ }
+
+ @Override
+ public long getSize() {
+ return contentLength;
+ }
+
+ @Override
+ public byte[] getBytes() throws IOException {
+ return getInputStream().readAllBytes();
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return fileBuffer.getInputStream();
+ }
+
+ @Override
+ public void transferTo(final File dest) throws IllegalStateException {
+ throw new UnsupportedOperationException();
+ }
+ }
+ }
+}
diff --git a/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/TargetView.java b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/TargetView.java
new file mode 100644
index 0000000000..6e86c044c7
--- /dev/null
+++ b/hawkbit-runtime/hawkbit-simple-ui/src/main/java/com/eclipse/hawkbit/ui/view/TargetView.java
@@ -0,0 +1,329 @@
+/**
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package com.eclipse.hawkbit.ui.view;
+
+import com.eclipse.hawkbit.ui.HawkbitClient;
+import com.eclipse.hawkbit.ui.view.util.Filter;
+import com.eclipse.hawkbit.ui.MainLayout;
+import com.eclipse.hawkbit.ui.view.util.SelectionGrid;
+import com.eclipse.hawkbit.ui.view.util.TableView;
+import com.eclipse.hawkbit.ui.view.util.Utils;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.Key;
+import com.vaadin.flow.component.button.Button;
+import com.vaadin.flow.component.checkbox.CheckboxGroup;
+import com.vaadin.flow.component.dependency.Uses;
+import com.vaadin.flow.component.formlayout.FormLayout;
+import com.vaadin.flow.component.grid.Grid;
+import com.vaadin.flow.component.icon.Icon;
+import com.vaadin.flow.component.icon.VaadinIcon;
+import com.vaadin.flow.component.orderedlayout.FlexComponent;
+import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
+import com.vaadin.flow.component.orderedlayout.VerticalLayout;
+import com.vaadin.flow.component.select.Select;
+import com.vaadin.flow.component.textfield.TextArea;
+import com.vaadin.flow.component.textfield.TextField;
+import com.vaadin.flow.data.renderer.ComponentRenderer;
+import com.vaadin.flow.router.PageTitle;
+import com.vaadin.flow.router.Route;
+import jakarta.annotation.security.RolesAllowed;
+import org.eclipse.hawkbit.mgmt.json.model.tag.MgmtTag;
+import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget;
+import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetRequestBody;
+import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQuery;
+import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQueryRequestBody;
+import org.eclipse.hawkbit.mgmt.json.model.targettype.MgmtTargetType;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetFilterQueryRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetTagRestApi;
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetTypeRestApi;
+import org.springframework.util.ObjectUtils;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Stream;
+
+@PageTitle("Targets")
+@Route(value = "targets", layout = MainLayout.class)
+@RolesAllowed({"TARGET_READ"})
+@Uses(Icon.class)
+public class TargetView extends TableView {
+
+ private final HawkbitClient hawkbitClient;
+
+ private final MgmtTargetRestApi targetRestApi;
+ private final MgmtTargetTypeRestApi targetTypeRestApi;
+ private final MgmtTargetTagRestApi targetTagRestApi;
+ private final MgmtTargetFilterQueryRestApi targetFilterQueryRestApi;
+
+ public TargetView(final HawkbitClient hawkbitClient) {
+ super(
+ new RawFilter(hawkbitClient), new SimpleFilter(hawkbitClient),
+ new SelectionGrid.EntityRepresentation<>(MgmtTarget.class, MgmtTarget::getControllerId) {
+
+ @Override
+ protected void addColumns(final Grid grid) {
+ grid.addColumn(MgmtTarget::getControllerId).setHeader("Controller Id").setAutoWidth(true);
+ grid.addColumn(MgmtTarget::getName).setHeader("Name").setAutoWidth(true);
+ grid.addColumn(MgmtTarget::getTargetTypeName).setHeader("Type").setAutoWidth(true);
+
+ grid.setItemDetailsRenderer(new ComponentRenderer<>(
+ TargetDetails::new, TargetDetails::setItem));
+ }
+ },
+ (query, filter) -> hawkbitClient.getTargetRestApi()
+ .getTargets(
+ query.getOffset(), query.getPageSize(), "name:asc",
+ filter)
+ .getBody()
+ .getContent()
+ .stream(),
+ source -> new RegisterDialog(hawkbitClient).result(),
+ selectionGrid -> {
+ selectionGrid.getSelectedItems().forEach(toDelete ->
+ hawkbitClient.getTargetRestApi().deleteTarget(toDelete.getControllerId()));
+ return CompletableFuture.completedFuture(null);
+ });
+ this.hawkbitClient = hawkbitClient;
+ this.targetRestApi = hawkbitClient.getTargetRestApi();
+ this.targetTypeRestApi = hawkbitClient.getTargetTypeRestApi();
+ this.targetTagRestApi = hawkbitClient.getTargetTagRestApi();
+ this.targetFilterQueryRestApi = hawkbitClient.getTargetFilterQueryRestApi();
+ }
+
+ private static class SimpleFilter implements Filter.Rsql {
+
+ private final HawkbitClient hawkbitClient;
+
+ private final TextField controllerId;
+ private final CheckboxGroup type;
+ private final CheckboxGroup tag;
+
+ private SimpleFilter(final HawkbitClient hawkbitClient) {
+ this.hawkbitClient = hawkbitClient;
+
+ controllerId = Utils.textField("Controller Id");
+ controllerId.setPlaceholder("");
+ type = new CheckboxGroup<>("Type");
+ type.setItemLabelGenerator(MgmtTargetType::getName);
+ tag = new CheckboxGroup<>("Tag");
+ tag.setItemLabelGenerator(MgmtTag::getName);
+ }
+
+ @Override
+ public List components() {
+ final List components = new LinkedList<>();
+ components.add(controllerId);
+ type.setItems(hawkbitClient.getTargetTypeRestApi().getTargetTypes(0, 20, "name:asc", null).getBody().getContent());
+ if (!type.getValue().isEmpty()) {
+ components.add(type);
+ }
+ tag.setItems(hawkbitClient.getTargetTagRestApi().getTargetTags(0, 20, "name:asc", null).getBody().getContent());
+ if (!tag.isEmpty()) {
+ components.add(tag);
+ }
+ return components;
+ }
+
+ @Override
+ public String filter() {
+ return Filter.filter(
+ Map.of(
+ "controllerid", controllerId.getOptionalValue(),
+ "targettype.name", type.getSelectedItems().stream().map(MgmtTargetType::getName)
+ .toList(),
+ "tag", tag.getSelectedItems()));
+ }
+ }
+
+ private static class RawFilter implements Filter.Rsql {
+
+ private final HawkbitClient hawkbitClient;
+
+ private final TextField textFilter = new TextField("Raw Filter");
+ private final VerticalLayout layout = new VerticalLayout();
+
+ private RawFilter(final HawkbitClient hawkbitClient) {
+ this.hawkbitClient = hawkbitClient;
+
+ textFilter.setPlaceholder("");
+ final Select 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("