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("