diff --git a/hawkbit-simple-ui/.gitignore b/hawkbit-simple-ui/.gitignore index 59e48e1c2..50c593a1c 100644 --- a/hawkbit-simple-ui/.gitignore +++ b/hawkbit-simple-ui/.gitignore @@ -1,5 +1,6 @@ bundles -frontend +src/main/frontend/* +!src/main/frontend/themes/ node_modules package.json package-lock.json diff --git a/hawkbit-simple-ui/src/main/frontend/themes/hawkbit/styles.css b/hawkbit-simple-ui/src/main/frontend/themes/hawkbit/styles.css new file mode 100644 index 000000000..fdae9c548 --- /dev/null +++ b/hawkbit-simple-ui/src/main/frontend/themes/hawkbit/styles.css @@ -0,0 +1 @@ +/* Import your application global css files here or add the styles directly to this file */ \ No newline at end of file diff --git a/hawkbit-simple-ui/src/main/frontend/themes/hawkbit/theme.json b/hawkbit-simple-ui/src/main/frontend/themes/hawkbit/theme.json new file mode 100644 index 000000000..d8c6397e4 --- /dev/null +++ b/hawkbit-simple-ui/src/main/frontend/themes/hawkbit/theme.json @@ -0,0 +1,3 @@ +{ + "lumoImports": ["badge"] +} \ No newline at end of file diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/HawkbitMgmtClient.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/HawkbitMgmtClient.java index e122bd00d..7fe0e2e1c 100644 --- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/HawkbitMgmtClient.java +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/HawkbitMgmtClient.java @@ -13,6 +13,7 @@ import java.util.function.Supplier; import feign.FeignException; import lombok.Getter; +import org.eclipse.hawkbit.mgmt.rest.api.MgmtActionRestApi; import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetRestApi; import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetTagRestApi; import org.eclipse.hawkbit.mgmt.rest.api.MgmtDistributionSetTypeRestApi; @@ -45,6 +46,7 @@ public class HawkbitMgmtClient { private final MgmtTargetFilterQueryRestApi targetFilterQueryRestApi; private final MgmtRolloutRestApi rolloutRestApi; private final MgmtTenantManagementRestApi tenantManagementRestApi; + private final MgmtActionRestApi actionRestApi; HawkbitMgmtClient(final Tenant tenant, final HawkbitClient hawkbitClient) { this.tenant = tenant; @@ -61,6 +63,7 @@ public class HawkbitMgmtClient { targetFilterQueryRestApi = service(MgmtTargetFilterQueryRestApi.class); rolloutRestApi = service(MgmtRolloutRestApi.class); tenantManagementRestApi = service(MgmtTenantManagementRestApi.class); + actionRestApi = service(MgmtActionRestApi.class); } boolean hasSoftwareModulesRead() { diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/SimpleUIApp.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/SimpleUIApp.java index 7abb6092f..92abe852c 100644 --- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/SimpleUIApp.java +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/SimpleUIApp.java @@ -51,7 +51,7 @@ import java.util.function.Function; import static feign.Util.ISO_8859_1; import static java.util.Collections.emptyList; -@Theme(themeClass = Lumo.class) +@Theme("hawkbit") @PWA(name = "hawkBit UI", shortName = "hawkBit UI") @SpringBootApplication @Import(FeignClientsConfiguration.class) diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/OAuth2TokenManager.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/OAuth2TokenManager.java index e9c2924ad..d4d00f221 100644 --- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/OAuth2TokenManager.java +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/security/OAuth2TokenManager.java @@ -9,7 +9,7 @@ */ package org.eclipse.hawkbit.ui.simple.security; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; @@ -28,7 +28,7 @@ import org.springframework.stereotype.Component; import java.util.Optional; @Component -@ConditionalOnBean(name = "hawkbitOAuth2ClientCustomizer") +@ConditionalOnProperty(prefix = "hawkbit.server.security.oauth2.client", name = "enabled") public class OAuth2TokenManager { private final OAuth2AuthorizedClientService clientService; diff --git a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/TargetView.java b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/TargetView.java index 98d4f5523..7da51fc9c 100644 --- a/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/TargetView.java +++ b/hawkbit-simple-ui/src/main/java/org/eclipse/hawkbit/ui/simple/view/TargetView.java @@ -9,6 +9,9 @@ */ package org.eclipse.hawkbit.ui.simple.view; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.LinkedList; @@ -17,43 +20,55 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.time.ZoneOffset; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; import java.util.stream.Stream; import jakarta.annotation.security.RolesAllowed; +import com.vaadin.flow.component.AttachEvent; +import com.vaadin.flow.component.ClickEvent; import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.ComponentEventListener; import com.vaadin.flow.component.Key; import com.vaadin.flow.component.Unit; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.ClickEvent; -import com.vaadin.flow.component.ComponentEventListener; import com.vaadin.flow.component.checkbox.CheckboxGroup; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.confirmdialog.ConfirmDialog; import com.vaadin.flow.component.datetimepicker.DateTimePicker; import com.vaadin.flow.component.dependency.Uses; import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.html.Input; +import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.select.Select; +import com.vaadin.flow.component.tabs.TabSheet; import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.data.renderer.ComponentRenderer; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; +import com.vaadin.flow.shared.Registration; import com.vaadin.flow.theme.lumo.LumoUtility; import org.eclipse.hawkbit.mgmt.json.model.MgmtPollStatus; +import org.eclipse.hawkbit.mgmt.json.model.PagedList; +import org.eclipse.hawkbit.mgmt.json.model.action.MgmtAction; +import org.eclipse.hawkbit.mgmt.json.model.action.MgmtActionRequestBodyPut; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtTargetAssignmentRequestBody; import org.eclipse.hawkbit.mgmt.json.model.tag.MgmtTag; +import org.eclipse.hawkbit.mgmt.json.model.tag.MgmtTagRequestBodyPut; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTarget; import org.eclipse.hawkbit.mgmt.json.model.target.MgmtTargetRequestBody; import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQuery; @@ -65,8 +80,9 @@ import org.eclipse.hawkbit.ui.simple.view.util.Filter; import org.eclipse.hawkbit.ui.simple.view.util.SelectionGrid; import org.eclipse.hawkbit.ui.simple.view.util.TableView; import org.eclipse.hawkbit.ui.simple.view.util.Utils; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; import org.springframework.util.ObjectUtils; -import java.util.stream.Collectors; @PageTitle("Targets") @Route(value = "targets", layout = MainLayout.class) @@ -92,13 +108,14 @@ public class TargetView extends TableView { grid.addColumn(MgmtTarget::getTargetTypeName).setHeader(Constants.TYPE).setAutoWidth(true); grid.setItemDetailsRenderer(new ComponentRenderer<>( - () -> new TargetDetails(hawkbitClient), TargetDetails::setItem)); + () -> new TargetDetailedView(hawkbitClient), TargetDetailedView::setItem)); } }, (query, filter) -> hawkbitClient.getTargetRestApi() .getTargets( query.getOffset(), query.getPageSize(), Constants.NAME_ASC, - filter) + filter + ) .getBody() .getContent() .stream(), @@ -107,7 +124,8 @@ public class TargetView extends TableView { selectionGrid.getSelectedItems().forEach(toDelete -> hawkbitClient.getTargetRestApi().deleteTarget(toDelete.getControllerId())); return CompletableFuture.completedFuture(null); - }); + } + ); Function, CompletionStage> assignHandler = source -> new AssignDialog( hawkbitClient, source.getSelectedItems()).result(); @@ -161,7 +179,8 @@ public class TargetView extends TableView { "controllerid", controllerId.getOptionalValue(), "targettype.name", type.getSelectedItems().stream().map(MgmtTargetType::getName) .toList(), - "tag", tag.getSelectedItems())); + "tag", tag.getSelectedItems() + )); } } @@ -298,6 +317,34 @@ public class TargetView extends TableView { } } + private static class TargetDetailedView extends TabSheet { + + private final TargetDetails targetDetails; + private final TargetAssignedInstalled targetAssignedInstalled; + private final TargetTags targetTags; + private final TargetActions targetActions; + + private TargetDetailedView(HawkbitMgmtClient hawkbitClient) { + targetDetails = new TargetDetails(hawkbitClient); + targetAssignedInstalled = new TargetAssignedInstalled(hawkbitClient); + targetTags = new TargetTags(hawkbitClient); + targetActions = new TargetActions(hawkbitClient); + setWidthFull(); + + add("Details", targetDetails); + add("Assigned / Installed", targetAssignedInstalled); + add("Tags", targetTags); + add("Action History", targetActions); + } + + private void setItem(final MgmtTarget target) { + this.targetDetails.setItem(target); + this.targetAssignedInstalled.setItem(target); + this.targetTags.setItem(target); + this.targetActions.setItem(target); + } + } + private static class TargetDetails extends FormLayout { private final transient HawkbitMgmtClient hawkbitClient; @@ -308,6 +355,7 @@ public class TargetView extends TableView { private final TextField lastModifiedAt = Utils.textField(Constants.LAST_MODIFIED_AT); private final TextField securityToken = Utils.textField(Constants.SECURITY_TOKEN); private final TextArea targetAttributes = new TextArea(Constants.ATTRIBUTES); + private MgmtTarget target; private TargetDetails(HawkbitMgmtClient hawkbitClient) { this.hawkbitClient = hawkbitClient; @@ -316,7 +364,8 @@ public class TargetView extends TableView { description, createdBy, createdAt, lastModifiedBy, lastModifiedAt, - securityToken, targetAttributes) + securityToken, targetAttributes + ) .forEach(field -> { field.setReadOnly(true); add(field); @@ -327,6 +376,11 @@ public class TargetView extends TableView { } private void setItem(final MgmtTarget target) { + this.target = target; + } + + @Override + protected void onAttach(AttachEvent attachEvent) { description.setValue(target.getDescription() == null ? "N/A" : target.getDescription()); createdBy.setValue(target.getCreatedBy()); createdAt.setValue(new Date(target.getCreatedAt()).toString()); @@ -335,14 +389,384 @@ public class TargetView extends TableView { securityToken.setValue(target.getSecurityToken()); var response = hawkbitClient.getTargetRestApi().getAttributes(target.getControllerId()); if (response.getStatusCode().is2xxSuccessful()) { - targetAttributes.setValue(Objects.requireNonNullElse(response.getBody(), Collections.emptyMap()).entrySet().stream().map(entry -> entry.getKey() + ": " + - entry.getValue()).collect(Collectors.joining("\n"))); + targetAttributes.setValue(Objects.requireNonNullElse(response.getBody(), Collections.emptyMap()).entrySet().stream() + .map(entry -> entry.getKey() + ": " + + entry.getValue()).collect(Collectors.joining("\n"))); } else { targetAttributes.setValue("Error occurred fetching attributes from server: " + response.getStatusCode()); } } } + private static class TargetAssignedInstalled extends FormLayout { + + private final transient HawkbitMgmtClient hawkbitClient; + private final TextArea assigned = new TextArea("Assigned Distribution Set"); + private final TextArea installed = new TextArea("Installed Distribution Set"); + private MgmtTarget target; + + private TargetAssignedInstalled(HawkbitMgmtClient hawkbitClient) { + this.hawkbitClient = hawkbitClient; + assigned.setReadOnly(true); + installed.setReadOnly(true); + assigned.setWidthFull(); + installed.setWidthFull(); + add(assigned, installed); + setResponsiveSteps(new FormLayout.ResponsiveStep("0", 2)); + } + + private void setItem(final MgmtTarget target) { + this.target = target; + } + + @Override + protected void onAttach(AttachEvent attachEvent) { + updateDistributionSetInfo( + () -> hawkbitClient.getTargetRestApi().getInstalledDistributionSet(target.getControllerId()), + installed + ); + updateDistributionSetInfo( + () -> hawkbitClient.getTargetRestApi().getAssignedDistributionSet(target.getControllerId()), + assigned + ); + } + + private void updateDistributionSetInfo(Supplier> supplier, TextArea textArea) { + Optional.ofNullable(supplier.get()) + .map(ResponseEntity::getBody) + .ifPresent(value -> { + final String description = """ + Name: %s + Version: %s + %s + """.replaceAll("\n", System.lineSeparator()); + textArea.setValue(description.formatted( + value.getName(), + value.getVersion(), + value.getModules().stream().map(module -> module.getTypeName() + ": " + module.getVersion()) + .collect(Collectors.joining(System.lineSeparator())) + )); + }); + } + } + + private static class TargetTags extends VerticalLayout { + + private final transient HawkbitMgmtClient hawkbitClient; + private final ComboBox tagSelector = new ComboBox<>(TAG); + private final HorizontalLayout tagsArea = new HorizontalLayout(); + private Registration changeListener; + private MgmtTarget target; + + private TargetTags(HawkbitMgmtClient hawkbitClient) { + this.hawkbitClient = hawkbitClient; + setWidthFull(); + setPadding(false); + setSpacing(true); + setAlignItems(FlexComponent.Alignment.STRETCH); + setJustifyContentMode(JustifyContentMode.CENTER); + + final HorizontalLayout tagSelectorLayout = buildTagSelectionLayout(hawkbitClient); + add(tagSelectorLayout); + + tagsArea.setWrap(true); + tagsArea.setWidthFull(); + add(tagsArea); + } + + private HorizontalLayout buildTagSelectionLayout(HawkbitMgmtClient hawkbitClient) { + final Button createTagButton = new Button("Create Tag"); + createTagButton.addClickListener(event -> { + new CreateTagDialog(hawkbitClient, () -> tagSelector.setItems(fetchAvailableTags())).result(); + }); + + tagSelector.setWidthFull(); + tagSelector.setItemLabelGenerator(MgmtTag::getName); + tagSelector.setClearButtonVisible(true); + tagSelector.setItems(fetchAvailableTags()); + + final HorizontalLayout tagSelectorLayout = new HorizontalLayout(tagSelector, createTagButton); + tagSelectorLayout.setWidthFull(); + tagSelectorLayout.setSpacing(true); + tagSelectorLayout.setAlignItems(Alignment.END); + tagSelectorLayout.setJustifyContentMode(JustifyContentMode.CENTER); + return tagSelectorLayout; + } + + private void setItem(final MgmtTarget target) { + this.target = target; + } + + @Override + protected void onAttach(AttachEvent attachEvent) { + if (changeListener != null) { + changeListener.remove(); + } + changeListener = tagSelector.addValueChangeListener(event -> { + if (event.getValue() != null) { + hawkbitClient.getTargetTagRestApi().assignTarget(event.getValue().getId(), target.getControllerId()); + refreshTargetTagsList(target); + tagSelector.clear(); + } + }); + refreshTargetTagsList(target); + } + + private void refreshTargetTagsList(MgmtTarget target) { + tagsArea.removeAll(); + + final List tagList = Optional.ofNullable(hawkbitClient.getTargetRestApi().getTags(target.getControllerId()).getBody()) + .orElse(Collections.emptyList()); + + for (MgmtTag tag : tagList) { + tagsArea.add(buildTargetTagBadge(tag, target.getControllerId())); + } + } + + private Span buildTargetTagBadge(MgmtTag tag, String controllerId) { + Button clearButton = new Button(VaadinIcon.CLOSE_SMALL.create()); + clearButton.addThemeVariants(ButtonVariant.LUMO_CONTRAST, ButtonVariant.LUMO_TERTIARY_INLINE); + clearButton.getStyle().set("margin-inline-start", "var(--lumo-space-xs)"); + clearButton.getElement().setAttribute("aria-label", "Clear filter: " + tag.getName()); + clearButton.getElement().setAttribute("title", "Clear filter: " + tag.getName()); + + Span pill = new Span(); + pill.getElement().getThemeList().add("badge pill"); + pill.getStyle().set("background-color", tag.getColour()); + pill.getStyle().set("margin-inline-end", "var(--lumo-space-xs)"); + + Span badge = new Span(pill, new Span(tag.getName()), clearButton); + badge.getElement().getThemeList().add("badge contrast pill"); + + clearButton.addClickListener(event -> { + hawkbitClient.getTargetTagRestApi().unassignTarget(tag.getId(), controllerId); + badge.getElement().removeFromParent(); + }); + + return badge; + } + + private List fetchAvailableTags() { + List tags = new ArrayList<>(); + int fetched = 0; + int offset = 0; + do { + List page = Optional.ofNullable( + hawkbitClient.getTargetTagRestApi().getTargetTags(offset, 50, Constants.NAME_ASC, null).getBody()) + .map(PagedList::getContent) + .orElse(Collections.emptyList()); + tags.addAll(page); + fetched = page.size(); + offset += fetched; + } while (fetched > 0); + return tags; + } + } + + private static class TargetActions extends Grid { + + private final transient HawkbitMgmtClient hawkbitClient; + private MgmtTarget target; + + private TargetActions(HawkbitMgmtClient hawkbitClient) { + this.hawkbitClient = hawkbitClient; + setWidthFull(); + addColumn(new ComponentRenderer(ActionStatusEntry::getStatusIcon)).setHeader("Status") + .setAutoWidth(true) + .setFlexGrow(0); + addColumn(ActionStatusEntry::getDistributionSetName).setHeader("Distribution Set").setAutoWidth(true); + addColumn(ActionStatusEntry::getLastModifiedAt).setHeader("Last Modified").setAutoWidth(true) + .setFlexGrow(0).setComparator(ActionStatusEntry::getLastModifiedAt); + addColumn(new ComponentRenderer(ActionStatusEntry::getForceTypeIcon)).setHeader("Type") + .setAutoWidth(true) + .setFlexGrow(0); + addColumn(new ComponentRenderer(ActionStatusEntry::getActionsLayout)).setHeader("Actions") + .setAutoWidth(true) + .setFlexGrow(0); + addColumn(new ComponentRenderer(ActionStatusEntry::getForceQuitLayout)).setHeader("Force Quit") + .setAutoWidth(true) + .setFlexGrow(0); + } + + private void setItem(final MgmtTarget target) { + this.target = target; + } + + private List fetchActions() { + return this.hawkbitClient.getTargetRestApi().getActionHistory(target.getControllerId(), 0, 30, null, null) + .getBody() + .getContent() + .stream() + .map(action -> new ActionStatusEntry(action, () -> setItems(fetchActions()))) + .filter(value -> value.action != null) + .collect(Collectors.toList()); + } + + @Override + protected void onAttach(AttachEvent attachEvent) { + setItems(fetchActions()); + } + + private class ActionStatusEntry { + + MgmtAction action; + MgmtDistributionSet distributionSet; + Runnable onUpdate; + + public ActionStatusEntry(MgmtAction mgmtAction, Runnable onUpdate) { + this.action = hawkbitClient.getActionRestApi().getAction(mgmtAction.getId()).getBody(); + this.onUpdate = onUpdate; + if (action == null) { + LoggerFactory.getLogger(ActionStatusEntry.class).error("Unable to fetch the action with id : {}", mgmtAction.getId()); + return; + } + this.action.getLink("distributionset").ifPresent(link -> { + try { + Long dsId = Long.parseLong(link.getHref().substring(link.getHref().lastIndexOf("/") + 1)); + this.distributionSet = hawkbitClient.getDistributionSetRestApi().getDistributionSet(dsId).getBody(); + } catch (NumberFormatException e) { + LoggerFactory.getLogger(ActionStatusEntry.class).error("Error parsing distribution set ID", e); + } + }); + } + + private Boolean isActive() { + return action.getStatus().equals(MgmtAction.ACTION_PENDING); + } + + private Boolean isCancelingOrCanceled() { + return action.getType().equals(MgmtAction.ACTION_CANCEL); + } + + public Component getStatusIcon() { + HorizontalLayout layout = new HorizontalLayout(); + Icon icon; + if (isActive()) { + if (isCancelingOrCanceled()) { + icon = Utils.tooltip(VaadinIcon.ADJUST.create(), "Pending Cancellation"); + icon.setColor("red"); + } else { + icon = Utils.tooltip(VaadinIcon.ADJUST.create(), "Pending Update"); + icon.setColor("orange"); + } + } else if (action.getType().equals(MgmtAction.ACTION_UPDATE)) { + icon = Utils.tooltip(VaadinIcon.CHECK_CIRCLE.create(), "Updated"); + icon.setColor("green"); + } else { + icon = Utils.tooltip(VaadinIcon.CLOSE_CIRCLE.create(), "Canceled"); + icon.setColor("red"); + } + + icon.addClassNames(LumoUtility.IconSize.SMALL); + layout.add(icon); + layout.setWidth(50, Unit.PIXELS); + layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); + return layout; + } + + public String getDistributionSetName() { + return distributionSet != null ? distributionSet.getName() : "Distribution Set not found"; + } + + public Instant getLastModifiedAt() { + return Instant.ofEpochMilli(action.getLastModifiedAt()); + } + + public Icon getForceTypeIcon() { + Icon icon = switch (action.getForceType()) { + case FORCED -> VaadinIcon.BOLT.create(); + case TIMEFORCED -> VaadinIcon.USER_CLOCK.create(); + case SOFT -> VaadinIcon.USER_CHECK.create(); + case DOWNLOAD_ONLY -> VaadinIcon.DOWNLOAD.create(); + }; + return Utils.tooltip(icon, action.getForceType().getName()); + } + + public HorizontalLayout getActionsLayout() { + HorizontalLayout actionsLayout = new HorizontalLayout(); + actionsLayout.setSpacing(true); + + Button cancelButton = Utils.tooltip(new Button(VaadinIcon.CLOSE.create()), "Cancel Action"); + if (isActive() && !isCancelingOrCanceled()) { + cancelButton.addClickListener(e -> { + String message = "Are you sure you want to cancel the action ?"; + promptForConfirmAction( + message, onUpdate, () -> { + hawkbitClient.getTargetRestApi().cancelAction(target.getControllerId(), action.getId(), false); + } + ).open(); + }); + } else { + cancelButton.setEnabled(false); + } + + Button forceButton = Utils.tooltip(new Button(VaadinIcon.BOLT.create()), "Force Action"); + if (isActive() && !isCancelingOrCanceled() && action.getForceType() != MgmtActionType.FORCED) { + forceButton.addClickListener(e -> { + String message = "Are you sure you want to force the action ?"; + promptForConfirmAction( + message, onUpdate, () -> { + MgmtActionRequestBodyPut setForced = new MgmtActionRequestBodyPut(); + setForced.setForceType(MgmtActionType.FORCED); + hawkbitClient.getTargetRestApi().updateAction(target.getControllerId(), action.getId(), setForced); + } + ).open(); + }); + } else { + forceButton.setEnabled(false); + } + + actionsLayout.add(cancelButton, forceButton); + return actionsLayout; + } + + public HorizontalLayout getForceQuitLayout() { + HorizontalLayout forceQuitLayout = new HorizontalLayout(); + forceQuitLayout.setSpacing(true); + forceQuitLayout.setPadding(true); + forceQuitLayout.setWidthFull(); + forceQuitLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); + + Button forceQuitButton = Utils.tooltip(new Button(VaadinIcon.CLOSE.create()), "Force Cancel"); + forceQuitButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY_INLINE); + + if (isActive() && isCancelingOrCanceled()) { + forceQuitButton.addClickListener(e -> { + String message = "Are you sure you want to force cancel the action ?"; + promptForConfirmAction( + message, onUpdate, () -> { + hawkbitClient.getTargetRestApi().cancelAction(target.getControllerId(), action.getId(), true); + } + ).open(); + }); + } else { + forceQuitButton.setEnabled(false); + } + + forceQuitLayout.add(forceQuitButton); + return forceQuitLayout; + } + + private static ConfirmDialog promptForConfirmAction(String message, Runnable refreshActions, Runnable actionConsumer) { + final ConfirmDialog dialog = new ConfirmDialog(); + dialog.setHeader("Confirm Action"); + dialog.setText(message); + + dialog.setCancelable(true); + dialog.addCancelListener(event -> dialog.close()); + + dialog.setConfirmButtonTheme(ButtonVariant.LUMO_ERROR.getVariantName()); + dialog.setConfirmText("Confirm"); + dialog.addConfirmListener(event -> { + actionConsumer.run(); + refreshActions.run(); + dialog.close(); + }); + return dialog; + } + } + } + private static class RegisterDialog extends Utils.BaseDialog { private final Select type; @@ -362,12 +786,15 @@ public class TargetView extends TableView { .getTargetTypes(0, 30, Constants.NAME_ASC, null) .getBody() .getContent() - .toArray(new MgmtTargetType[0])); + .toArray(new MgmtTargetType[0]) + ); type.setWidthFull(); type.setEmptySelectionAllowed(true); type.setItemLabelGenerator(item -> item == null ? "" : item.getName()); - controllerId = Utils.textField(CONTROLLER_ID, - e -> register.setEnabled(!e.getHasValue().isEmpty())); + controllerId = Utils.textField( + CONTROLLER_ID, + e -> register.setEnabled(!e.getHasValue().isEmpty()) + ); controllerId.focus(); name = Utils.textField(Constants.NAME); name.setWidthFull(); @@ -433,7 +860,8 @@ public class TargetView extends TableView { .getDistributionSets(0, 30, Constants.NAME_ASC, null) .getBody()) .map(body -> body.getContent().toArray(new MgmtDistributionSet[0])) - .orElseGet(() -> new MgmtDistributionSet[0])); + .orElseGet(() -> new MgmtDistributionSet[0]) + ); distributionSet.setRequiredIndicatorVisible(true); distributionSet.setItemLabelGenerator(distributionSetO -> distributionSetO.getName() + ":" + distributionSetO.getVersion()); @@ -490,7 +918,52 @@ public class TargetView extends TableView { } } + private static class CreateTagDialog extends Utils.BaseDialog { + + private final TextField name; + private final TextArea description = new TextArea(Constants.DESCRIPTION); + + private CreateTagDialog(final HawkbitMgmtClient hawkbitClient, Runnable onSuccess) { + super("Create Tag"); + + FormLayout formLayout = new FormLayout(); + formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1)); + final Button create = Utils.tooltip(new Button("Create"), "Create (Enter)"); + final Button cancel = Utils.tooltip(new Button("Cancel"), "Cancel (Esc)"); + + Input colorInput = new Input(); + colorInput.setType("color"); + name = Utils.textField("Tag Name", e -> create.setEnabled(!e.getHasValue().isEmpty())); + formLayout.add(name); + formLayout.add(description); + formLayout.addFormItem(colorInput, "Color Selection"); + add(formLayout); + + create.setEnabled(false); + create.addClickShortcut(Key.ENTER); + create.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + create.addClickListener(e -> { + hawkbitClient.getTargetTagRestApi().createTargetTags(List.of( + new MgmtTagRequestBodyPut() + .setName(name.getValue()) + .setDescription(description.getValue()) + .setColour(colorInput.getValue()) + )); + onSuccess.run(); + close(); + }); + + cancel.addClickListener(e -> close()); + cancel.addClickShortcut(Key.ESCAPE); + + getFooter().add(cancel); + getFooter().add(create); + open(); + } + } + private static class TargetStatusCell extends HorizontalLayout { + private TargetStatusCell(MgmtTarget target) { MgmtPollStatus pollStatus = target.getPollStatus(); String targetUpdateStatus = Optional.ofNullable(target.getUpdateStatus()).orElse("unknown");