diff --git a/hawkbit-ui/src/main/frontend/themes/hawkbit/styles.css b/hawkbit-ui/src/main/frontend/themes/hawkbit/styles.css index 1441316880..875d71ea60 100644 --- a/hawkbit-ui/src/main/frontend/themes/hawkbit/styles.css +++ b/hawkbit-ui/src/main/frontend/themes/hawkbit/styles.css @@ -4,4 +4,14 @@ a.nocolor:link { a.nocolor:visited { color: inherit; +} + +vaadin-split-layout.no-splitter::part(splitter) { + display: none; +} + +vaadin-grid::part(selected-row) { + /* layer the tint over an opaque base so frozen (sticky) cells like Edit/Details stay opaque */ + background-image: linear-gradient(var(--lumo-primary-color-10pct), var(--lumo-primary-color-10pct)); + background-color: var(--lumo-base-color); } \ No newline at end of file diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/HawkbitMgmtClient.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/HawkbitMgmtClient.java index 5c986357a8..054ad00bac 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/HawkbitMgmtClient.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/HawkbitMgmtClient.java @@ -77,6 +77,10 @@ public boolean hasRolloutRead() { return hasRead(() -> rolloutRestApi.getRollout(-1L)); } + public boolean hasRolloutApprove() { + return hasRead(() -> rolloutRestApi.approve(-1L, null)); + } + public boolean hasDistributionSetRead() { return hasRead(() -> distributionSetRestApi.getDistributionSet(-1L)); } @@ -93,6 +97,10 @@ public boolean hasAutoAssignmentRead() { return hasRead(() -> autoAssignmentRestApi.getAutoAssignment(-1L)); } + public boolean hasAutoAssignmentApprove() { + return hasRead(() -> autoAssignmentRestApi.approve(-1L, null)); + } + private boolean hasRead(final Supplier> doCall) { try { final int statusCode = doCall.get().getStatusCode().value(); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/MainLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/MainLayout.java index 8faedfb33a..055f5fb610 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/MainLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/MainLayout.java @@ -107,13 +107,13 @@ private void addHeaderContent() { } private void addDrawerContent() { - final H1 appName = new H1("hawkBit UI"); + final H2 appName = new H2("HawkBit"); final HorizontalLayout layout = new HorizontalLayout(); layout.setPadding(true); - layout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); + layout.setJustifyContentMode(FlexComponent.JustifyContentMode.START); final Image icon = new Image("images/hawkbit.png", "hawkBit icon"); - icon.setMaxHeight(24, Unit.PIXELS); - icon.setMaxWidth(24, Unit.PIXELS); + icon.setMaxHeight(40, Unit.PIXELS); + icon.setMaxWidth(40, Unit.PIXELS); appName.addClassNames(LumoUtility.AlignItems.BASELINE, LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE); layout.add(icon, appName); final Header header = new Header(layout); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/security/GrantedAuthoritiesService.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/security/GrantedAuthoritiesService.java index 1943a67b34..3289a52e6a 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/security/GrantedAuthoritiesService.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/security/GrantedAuthoritiesService.java @@ -40,6 +40,9 @@ public List getGrantedAuthorities(Authentication authent if (hawkbitClient.hasRolloutRead()) { roles.add("ROLLOUT_READ"); } + if (hawkbitClient.hasRolloutApprove()) { + roles.add("ROLLOUT_APPROVE"); + } if (hawkbitClient.hasDistributionSetRead()) { roles.add("DISTRIBUTION_SET_READ"); } @@ -52,6 +55,9 @@ public List getGrantedAuthorities(Authentication authent if (hawkbitClient.hasAutoAssignmentRead()) { roles.add("AUTO_ASSIGNMENT_READ"); } + if (hawkbitClient.hasAutoAssignmentApprove()) { + roles.add("AUTO_ASSIGNMENT_APPROVE"); + } return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList(); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/AutoAssignmentView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/AutoAssignmentView.java index bef5d3308e..4d90835ff8 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/AutoAssignmentView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/AutoAssignmentView.java @@ -9,6 +9,8 @@ */ package org.eclipse.hawkbit.ui.view; +import static com.vaadin.flow.component.icon.VaadinIcon.CHECK; +import static com.vaadin.flow.component.icon.VaadinIcon.CLOSE; import static com.vaadin.flow.component.icon.VaadinIcon.PAUSE; import static com.vaadin.flow.component.icon.VaadinIcon.START_COG; import static com.vaadin.flow.component.icon.VaadinIcon.TRASH; @@ -28,22 +30,28 @@ import com.vaadin.flow.component.Key; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; import com.vaadin.flow.component.combobox.ComboBox; import com.vaadin.flow.component.datetimepicker.DateTimePicker; import com.vaadin.flow.component.dependency.Uses; import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.icon.Icon; 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.splitlayout.SplitLayout; +import com.vaadin.flow.component.tabs.TabSheet; +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.data.value.ValueChangeMode; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import org.eclipse.hawkbit.mgmt.json.model.autoassignment.MgmtAutoAssignmentResponseBody; import org.eclipse.hawkbit.mgmt.json.model.autoassignment.MgmtAutoAssignmentRestRequestBodyPost; +import org.eclipse.hawkbit.mgmt.json.model.autoassignment.MgmtAutoAssignmentRestRequestBodyPut; 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.targetfilter.MgmtTargetFilterQuery; @@ -53,6 +61,8 @@ import org.eclipse.hawkbit.ui.view.util.SelectionGrid; import org.eclipse.hawkbit.ui.view.util.TableView; import org.eclipse.hawkbit.ui.view.util.Utils; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.ObjectUtils; @PageTitle("Auto Assignments") @@ -67,10 +77,10 @@ public final class AutoAssignmentView extends TableView( MgmtAutoAssignmentResponseBody.class, MgmtAutoAssignmentResponseBody::getId) { - private final AutoAssignmentDetails details = new AutoAssignmentDetails(hawkbitClient); @Override protected void addColumns(final Grid grid) { grid.addColumn(MgmtAutoAssignmentResponseBody::getId).setHeader(Constants.ID).setAutoWidth(true); @@ -79,9 +89,6 @@ protected void addColumns(final Grid grid) { grid.addComponentColumn(autoAssignment -> new Actions(autoAssignment, grid, hawkbitClient)).setHeader(Constants.ACTIONS).setAutoWidth(true); - - grid.setItemDetailsRenderer(new ComponentRenderer<>( - () -> details, AutoAssignmentDetails::setItem)); } }, (query, rsqlFilter) -> Optional.ofNullable( @@ -98,8 +105,42 @@ protected void addColumns(final Grid grid) { autoAssignment -> hawkbitClient.getAutoAssignmentRestApi().delete(autoAssignment.getId())); selectionGrid.refreshGrid(false); return CompletableFuture.completedFuture(null); - }); - selectionGrid.getDataCommunicator().getKeyMapper().setIdentifierGetter(MgmtAutoAssignmentResponseBody::getId); + }, + autoAssignment -> { + final AutoAssignmentDetailedView detailedView = new AutoAssignmentDetailedView(hawkbitClient); + detailedView.setItem(autoAssignment); + return detailedView; + }, + SplitLayout.Orientation.VERTICAL, + autoAssignment -> new EditDialog(autoAssignment, hawkbitClient).result()); + } + + private static class AutoAssignmentDetailedView extends VerticalLayout { + + @Serial + private static final long serialVersionUID = 1L; + + private final Span autoAssignmentName; + private final AutoAssignmentDetails details; + + private AutoAssignmentDetailedView(final HawkbitMgmtClient hawkbitClient) { + autoAssignmentName = new Span(); + details = new AutoAssignmentDetails(hawkbitClient); + setWidthFull(); + setHeightFull(); + getStyle().set("overflow", "auto"); + + add(autoAssignmentName); + final TabSheet tabSheet = new TabSheet(); + tabSheet.setWidthFull(); + tabSheet.add("Details", details); + add(tabSheet); + } + + private void setItem(final MgmtAutoAssignmentResponseBody autoAssignment) { + autoAssignmentName.setText(autoAssignment.getName()); + details.setItem(autoAssignment); + } } private static class Actions extends HorizontalLayout { @@ -107,6 +148,8 @@ private static class Actions extends HorizontalLayout { @Serial private static final long serialVersionUID = 1L; + private static final String NO_APPROVE_PERMISSION = "Missing APPROVE_AUTO_ASSIGNMENT permission"; + private final long autoAssignmentId; private final Grid grid; private final transient HawkbitMgmtClient hawkbitClient; @@ -150,6 +193,18 @@ private void init(final MgmtAutoAssignmentResponseBody autoAssignment) { }); } }, "Resume")); + } else if("WAITING_FOR_APPROVAL".equalsIgnoreCase(autoAssignment.getStatus())) { + final boolean canApprove = hasApprovePermission(); + + final Button approve = new Button(CHECK.create()); + approve.setEnabled(canApprove); + approve.addClickListener(v -> new ApprovalDialog(autoAssignment, true, hawkbitClient, this::refresh)); + add(Utils.tooltip(approve, canApprove ? "Approve" : NO_APPROVE_PERMISSION)); + + final Button deny = new Button(CLOSE.create()); + deny.setEnabled(canApprove); + deny.addClickListener(v -> new ApprovalDialog(autoAssignment, false, hawkbitClient, this::refresh)); + add(Utils.tooltip(deny, canApprove ? "Deny" : NO_APPROVE_PERMISSION)); } add(Utils.tooltip(new Button(TRASH.create()) { @@ -173,6 +228,52 @@ private void refresh() { } } + private static boolean hasApprovePermission() { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication != null && authentication.getAuthorities().stream() + .anyMatch(authority -> "ROLE_AUTO_ASSIGNMENT_APPROVE".equals(authority.getAuthority())); + } + + } + + private static class ApprovalDialog extends Utils.BaseDialog { + + @Serial + private static final long serialVersionUID = 1L; + + private ApprovalDialog(final MgmtAutoAssignmentResponseBody autoAssignment, final boolean approve, + final HawkbitMgmtClient hawkbitClient, final Runnable onDone) { + super(approve ? "Approve Auto Assignment" : "Deny Auto Assignment"); + + final Span target = new Span((approve ? "Approve" : "Deny") + " auto assignment: " + autoAssignment.getName()); + final TextArea remark = new TextArea("Remark"); + remark.setWidthFull(); + + final Button confirm = new Button(approve ? "Approve" : "Deny"); + confirm.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + confirm.addClickListener(e -> { + close(); + final String remarkValue = ObjectUtils.isEmpty(remark.getValue()) ? null : remark.getValue(); + if(approve) { + hawkbitClient.getAutoAssignmentRestApi().approve(autoAssignment.getId(), remarkValue); + } else { + hawkbitClient.getAutoAssignmentRestApi().deny(autoAssignment.getId(), remarkValue); + } + onDone.run(); + }); + final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC); + cancel.addClickListener(e -> close()); + cancel.addClickShortcut(Key.ESCAPE); + getFooter().add(cancel); + getFooter().add(confirm); + + final VerticalLayout layout = new VerticalLayout(); + layout.setSizeFull(); + layout.setSpacing(false); + layout.add(target, remark); + add(layout); + open(); + } } private static class AutoAssignmentFilter implements Filter.Rsql { @@ -210,6 +311,10 @@ private static class AutoAssignmentDetails extends FormLayout { private final TextField distributionSet = Utils.textField(Constants.DISTRIBUTION_SET); private final TextField actonType = Utils.textField(Constants.ACTION_TYPE); private final TextField startAt = Utils.textField(Constants.START_AT); + private final TextField weight = Utils.textField(Constants.WEIGHT); + private final TextField approvalDecidedBy = Utils.textField(Constants.APPROVAL_DECIDED_BY); + private final TextField approvalRemark = Utils.textField(Constants.APPROVAL_REMARK); + private final Checkbox confirmationRequired = new Checkbox(Constants.CONFIRMATION_REQUIRED); private AutoAssignmentDetails(final HawkbitMgmtClient hawkbitClient) { @@ -221,17 +326,25 @@ private AutoAssignmentDetails(final HawkbitMgmtClient hawkbitClient) { createdBy, createdAt, lastModifiedBy, lastModifiedAt, targetFilter, distributionSet, - actonType, startAt) + actonType, startAt, weight, + approvalDecidedBy, approvalRemark) .forEach(field -> { field.setReadOnly(true); add(field); }); + confirmationRequired.setReadOnly(true); + confirmationRequired.setEnabled(false); + add(confirmationRequired); setResponsiveSteps(new ResponsiveStep("0", 2)); setColspan(description, 2); } private void setItem(final MgmtAutoAssignmentResponseBody autoAssignment) { description.setValue(Objects.requireNonNullElse(autoAssignment.getDescription(), "")); + weight.setValue(autoAssignment.getWeight() == null ? "" : String.valueOf(autoAssignment.getWeight())); + approvalDecidedBy.setValue(Objects.requireNonNullElse(autoAssignment.getApprovalDecidedBy(), "")); + approvalRemark.setValue(Objects.requireNonNullElse(autoAssignment.getApprovalRemark(), "")); + confirmationRequired.setValue(autoAssignment.isConfirmationRequired()); createdBy.setValue(autoAssignment.getCreatedBy()); createdAt.setValue(Utils.localDateTimeFromTs(autoAssignment.getCreatedAt())); @@ -253,6 +366,60 @@ private void setItem(final MgmtAutoAssignmentResponseBody autoAssignment) { } } + private static class EditDialog extends Utils.BaseDialog { + + @Serial + private static final long serialVersionUID = 1L; + + private final TextField name; + private final TextArea description; + private final Button save; + + private EditDialog(final MgmtAutoAssignmentResponseBody autoAssignment, final HawkbitMgmtClient hawkbitClient) { + super("Edit Auto Assignment"); + + name = Utils.textField(Constants.NAME, this::readyToSave); + name.setWidthFull(); + description = new TextArea(Constants.DESCRIPTION); + description.setWidthFull(); + description.setMinLength(2); + description.setValueChangeMode(ValueChangeMode.EAGER); + + save = Utils.tooltip(new Button("Save"), "Save (Enter)"); + name.setValue(Objects.requireNonNullElse(autoAssignment.getName(), "")); + description.setValue(Objects.requireNonNullElse(autoAssignment.getDescription(), "")); + save.addClickListener(e -> { + final MgmtAutoAssignmentRestRequestBodyPut body = new MgmtAutoAssignmentRestRequestBodyPut(); + body.setName(name.getValue()); + body.setDescription(description.getValue()); + hawkbitClient.getAutoAssignmentRestApi().update(autoAssignment.getId(), body); + close(); + }); + save.addClickShortcut(Key.ENTER); + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC); + cancel.addClickListener(e -> close()); + cancel.addClickShortcut(Key.ESCAPE); + getFooter().add(cancel); + getFooter().add(save); + + final VerticalLayout layout = new VerticalLayout(); + layout.setSizeFull(); + layout.setPadding(true); + layout.setSpacing(false); + layout.add(name, description); + add(layout); + open(); + } + + private void readyToSave(final Object v) { + final boolean saveEnabled = !name.isEmpty(); + if (save.isEnabled() != saveEnabled) { + save.setEnabled(saveEnabled); + } + } + } + private static class CreateDialog extends Utils.BaseDialog { @Serial @@ -264,6 +431,8 @@ private static class CreateDialog extends Utils.BaseDialog { private final ComboBox targetFilter; private final Select actionType; private final DateTimePicker startAt = new DateTimePicker(Constants.START_AT); + private final NumberField weight; + private final Checkbox confirmationRequired = new Checkbox("Confirmation Required"); private final Button create = new Button("Create"); private CreateDialog(final HawkbitMgmtClient hawkbitClient) { @@ -299,6 +468,11 @@ private CreateDialog(final HawkbitMgmtClient hawkbitClient) { new MgmtActionType[] { MgmtActionType.FORCED, MgmtActionType.SOFT, MgmtActionType.DOWNLOAD_ONLY }, MgmtActionType.FORCED, null); + weight = Utils.numberField("Weight"); + weight.setMin(0); + weight.setMax(1000); + weight.setWidthFull(); + create.setEnabled(false); create.addThemeVariants(ButtonVariant.LUMO_PRIMARY); addCreateClickListener(hawkbitClient); @@ -311,7 +485,7 @@ private CreateDialog(final HawkbitMgmtClient hawkbitClient) { final VerticalLayout layout = new VerticalLayout(); layout.setSizeFull(); layout.setSpacing(false); - layout.add(name, distributionSet, targetFilter, description, actionType, startAt); + layout.add(name, distributionSet, targetFilter, description, actionType, startAt, weight, confirmationRequired); add(layout); open(); } @@ -334,6 +508,10 @@ private void addCreateClickListener(final HawkbitMgmtClient hawkbitClient) { request.setActionType(actionType.getValue()); request.setStartAt(!startAt.isEmpty() ? startAt.getValue().toEpochSecond(ZoneOffset.UTC) * 1000 : null); + if (!weight.isEmpty()) { + request.setWeight(weight.getValue().intValue()); + } + request.setConfirmationRequired(confirmationRequired.getValue()); hawkbitClient.getAutoAssignmentRestApi().create(request); }); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/Constants.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/Constants.java index c389cc7f5b..4d8c0d1547 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/Constants.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/Constants.java @@ -30,14 +30,40 @@ public interface Constants { String LAST_POLL = "Last Poll"; String SECURITY_TOKEN = "Security Token"; String ATTRIBUTES = "Attributes"; + String COMPLETE = "Complete"; + String LOCKED = "Locked"; + String DELETED = "Deleted"; + String ENCRYPTED = "Encrypted"; + String VALID = "Valid"; + String REQUIRED_MIGRATION_STEP = "Required Migration Step"; + String WEIGHT = "Weight"; + + // target + String UPDATE_STATUS = "Update Status"; + String LAST_CONTROLLER_REQUEST_AT = "Last Controller Request At"; + String INSTALLED_AT = "Installed At"; + String IP_ADDRESS = "IP Address"; + String TARGET_TYPE = "Target Type"; + String REQUEST_ATTRIBUTES = "Request Attributes"; + String AUTO_CONFIRM_ACTIVE = "Auto Confirm Active"; + String NEXT_EXPECTED_POLL = "Next Expected Poll"; + String OVERDUE = "Overdue"; // rollout + String GROUPS = "Groups"; String GROUP_COUNT = "Group Count"; String TARGET_COUNT = "Target Count"; String STATS = "Stats"; String STATUS = "Status"; String ACTIONS = "Actions"; + String TOTAL_TARGETS = "Total Targets"; + String TOTAL_GROUPS = "Total Groups"; + String FORCE_TIME = "Force Time"; + String APPROVAL_REMARK = "Approval Remark"; + String APPROVAL_DECIDED_BY = "Approval Decided By"; + String CONFIRMATION_REQUIRED = "Confirmation Required"; + // create rollout String TARGET_FILTER = "Target Filter"; String DISTRIBUTION_SET = "Distribution Set"; diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/DistributionSetView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/DistributionSetView.java index 9e7870b8a7..e96124ff5f 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/DistributionSetView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/DistributionSetView.java @@ -31,23 +31,28 @@ 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.details.Details; import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.GridSortOrder; +import com.vaadin.flow.component.html.Span; 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.splitlayout.SplitLayout; +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.provider.SortDirection; -import com.vaadin.flow.data.renderer.ComponentRenderer; +import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import org.eclipse.hawkbit.mgmt.json.model.PagedList; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPost; +import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSetRequestBodyPut; import org.eclipse.hawkbit.mgmt.json.model.distributionsettype.MgmtDistributionSetType; import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule; import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleAssignment; @@ -74,8 +79,6 @@ public DistributionSetView(final HawkbitMgmtClient hawkbitClient) { new DistributionSetRawFilter(), new SelectionGrid.EntityRepresentation<>(MgmtDistributionSet.class, MgmtDistributionSet::getId) { - private final DistributionSetDetails details = new DistributionSetDetails(hawkbitClient); - @Override protected void addColumns(Grid grid) { var createdAtCol = grid.addColumn(Utils.localDateTimeRenderer(MgmtDistributionSet::getCreatedAt)).setHeader( @@ -87,9 +90,6 @@ protected void addColumns(Grid grid) { grid.addColumn(MgmtDistributionSet::getTypeName).setHeader(Constants.TYPE).setAutoWidth(true).setKey("typename") .setSortable(true); grid.sort(List.of(new GridSortOrder<>(createdAtCol, SortDirection.DESCENDING))); - - grid.setItemDetailsRenderer(new ComponentRenderer<>( - () -> details, DistributionSetDetails::setItem)); } }, (query, rsqlFilter) -> Optional.ofNullable( @@ -104,7 +104,42 @@ protected void addColumns(Grid grid) { distributionSet -> hawkbitClient.getDistributionSetRestApi() .deleteDistributionSet(distributionSet.getId())); return CompletableFuture.completedFuture(null); - }, null); + }, + distributionSet -> { + final DistributionSetDetailedView detailedView = new DistributionSetDetailedView(hawkbitClient); + detailedView.setItem(distributionSet); + return detailedView; + }, + SplitLayout.Orientation.VERTICAL, + distributionSet -> new EditDialog(distributionSet, hawkbitClient).result()); + } + + private static class DistributionSetDetailedView extends VerticalLayout { + + @Serial + private static final long serialVersionUID = 1L; + + private final Span distributionSetName; + private final DistributionSetDetails details; + + private DistributionSetDetailedView(final HawkbitMgmtClient hawkbitClient) { + distributionSetName = new Span(); + details = new DistributionSetDetails(hawkbitClient); + setWidthFull(); + setHeightFull(); + getStyle().set("overflow", "auto"); + + add(distributionSetName); + final TabSheet tabSheet = new TabSheet(); + tabSheet.setWidthFull(); + tabSheet.add("Details", details); + add(tabSheet); + } + + private void setItem(final MgmtDistributionSet distributionSet) { + distributionSetName.setText(distributionSet.getName() + ":" + distributionSet.getVersion()); + details.setItem(distributionSet); + } } private static SelectionGrid selectSoftwareModuleGrid() { @@ -194,39 +229,63 @@ private static class DistributionSetDetails extends FormLayout { private final transient HawkbitMgmtClient hawkbitClient; private final TextArea description = new TextArea("Description"); + private final TextField id = Utils.textField(Constants.ID); 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 Checkbox complete = new Checkbox(Constants.COMPLETE); + private final Checkbox valid = new Checkbox(Constants.VALID); + private final Checkbox locked = new Checkbox(Constants.LOCKED); + private final Checkbox requiredMigrationStep = new Checkbox(Constants.REQUIRED_MIGRATION_STEP); + private final Checkbox deleted = new Checkbox(Constants.DELETED); private final TextArea metadata = new TextArea("Metadata"); private final SelectionGrid softwareModulesGrid = selectSoftwareModuleGrid(); + private final Details softwareModulesSection; private DistributionSetDetails(final HawkbitMgmtClient hawkbitClient) { this.hawkbitClient = hawkbitClient; description.setMinLength(2); + softwareModulesGrid.setAllRowsVisible(true); + softwareModulesGrid.setWidthFull(); + softwareModulesSection = new Details("Software Modules", softwareModulesGrid); + softwareModulesSection.setOpened(false); + softwareModulesSection.setWidthFull(); Stream.of( description, - createdBy, createdAt, + id, + createdBy, lastModifiedBy, lastModifiedAt, metadata) .forEach(field -> { field.setReadOnly(true); add(field); }); - add(softwareModulesGrid); + Stream.of(complete, valid, locked, requiredMigrationStep, deleted) + .forEach(checkbox -> { + checkbox.setReadOnly(true); + checkbox.setEnabled(false); + add(checkbox); + }); + add(softwareModulesSection); setResponsiveSteps(new ResponsiveStep("0", 2)); setColspan(description, 2); - setColspan(softwareModulesGrid, 2); + setColspan(metadata, 2); + setColspan(softwareModulesSection, 2); } private void setItem(final MgmtDistributionSet distributionSet) { description.setValue(Objects.requireNonNullElse(distributionSet.getDescription(), "")); + id.setValue(distributionSet.getId() == null ? "" : String.valueOf(distributionSet.getId())); createdBy.setValue(distributionSet.getCreatedBy()); - createdAt.setValue(Utils.localDateTimeFromTs(distributionSet.getCreatedAt())); lastModifiedBy.setValue(distributionSet.getLastModifiedBy()); lastModifiedAt.setValue(Utils.localDateTimeFromTs(distributionSet.getLastModifiedAt())); + complete.setValue(Boolean.TRUE.equals(distributionSet.getComplete())); + valid.setValue(distributionSet.isValid()); + locked.setValue(distributionSet.isLocked()); + requiredMigrationStep.setValue(distributionSet.isRequiredMigrationStep()); + deleted.setValue(distributionSet.isDeleted()); metadata.setValue(Optional.ofNullable( hawkbitClient.getDistributionSetRestApi().getMetadata(distributionSet.getId()).getBody()) .map(body -> body.getContent().stream() @@ -255,6 +314,7 @@ private static class CreateDialog extends Utils.BaseDialog { private final TextField version; private final TextArea description; private final Checkbox requiredMigrationStep; + private final Checkbox locked; private final Button create; private CreateDialog(final HawkbitMgmtClient hawkbitClient) { @@ -280,7 +340,9 @@ private CreateDialog(final HawkbitMgmtClient hawkbitClient) { description = new TextArea(Constants.DESCRIPTION); description.setWidthFull(); description.setMinLength(2); + description.setValueChangeMode(ValueChangeMode.EAGER); requiredMigrationStep = new Checkbox("Required Migration Step"); + locked = new Checkbox("Locked"); create = Utils.tooltip(new Button("Create"), "Create (Enter)"); create.setEnabled(false); @@ -297,7 +359,7 @@ private CreateDialog(final HawkbitMgmtClient hawkbitClient) { layout.setSizeFull(); layout.setPadding(true); layout.setSpacing(false); - layout.add(type, name, version, vendor, description, requiredMigrationStep); + layout.add(type, name, version, vendor, description, requiredMigrationStep, locked); add(layout); open(); } @@ -320,7 +382,8 @@ private void addCreateClickListener() { .setName(name.getValue()) .setVersion(version.getValue()) .setDescription(description.getValue()) - .setRequiredMigrationStep(requiredMigrationStep.getValue()))) + .setRequiredMigrationStep(requiredMigrationStep.getValue()) + .setLocked(locked.getValue()))) .getBody()) .stream() .flatMap(Collection::stream) @@ -332,6 +395,73 @@ private void addCreateClickListener() { } } + private static class EditDialog extends Utils.BaseDialog { + + @Serial + private static final long serialVersionUID = 1L; + + private final TextField name; + private final TextField version; + private final TextArea description; + private final Checkbox requiredMigrationStep; + private final Button save; + + private EditDialog(final MgmtDistributionSet distributionSet, final HawkbitMgmtClient hawkbitClient) { + super("Edit Distribution Set"); + + final TextField type = Utils.textField(Constants.TYPE); + type.setValue(Objects.requireNonNullElse(distributionSet.getTypeName(), "")); + type.setReadOnly(true); + type.setWidthFull(); + + name = Utils.textField(Constants.NAME, this::readyToSave); + version = Utils.textField(Constants.VERSION, this::readyToSave); + description = new TextArea(Constants.DESCRIPTION); + description.setWidthFull(); + description.setMinLength(2); + description.setValueChangeMode(ValueChangeMode.EAGER); + requiredMigrationStep = new Checkbox("Required Migration Step"); + requiredMigrationStep.setValue(distributionSet.isRequiredMigrationStep()); + + save = Utils.tooltip(new Button("Save"), "Save (Enter)"); + name.setValue(Objects.requireNonNullElse(distributionSet.getName(), "")); + version.setValue(Objects.requireNonNullElse(distributionSet.getVersion(), "")); + description.setValue(Objects.requireNonNullElse(distributionSet.getDescription(), "")); + save.addClickListener(e -> { + hawkbitClient.getDistributionSetRestApi().updateDistributionSet( + distributionSet.getId(), + new MgmtDistributionSetRequestBodyPut() + .setName(name.getValue()) + .setVersion(version.getValue()) + .setDescription(description.getValue()) + .setRequiredMigrationStep(requiredMigrationStep.getValue())); + close(); + }); + save.addClickShortcut(Key.ENTER); + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC); + cancel.addClickListener(e -> close()); + cancel.addClickShortcut(Key.ESCAPE); + getFooter().add(cancel); + getFooter().add(save); + + final VerticalLayout layout = new VerticalLayout(); + layout.setSizeFull(); + layout.setPadding(true); + layout.setSpacing(false); + layout.add(type, name, version, description, requiredMigrationStep); + add(layout); + open(); + } + + private void readyToSave(final Object v) { + final boolean saveEnabled = !name.isEmpty() && !version.isEmpty(); + if (save.isEnabled() != saveEnabled) { + save.setEnabled(saveEnabled); + } + } + } + @SuppressWarnings({ "java:S1171", "java:S3599" }) private static class AddSoftwareModulesDialog extends Utils.BaseDialog { diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/RolloutView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/RolloutView.java index 59963683af..8ee38e7b05 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/RolloutView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/RolloutView.java @@ -29,26 +29,33 @@ import com.vaadin.flow.component.combobox.ComboBox; import com.vaadin.flow.component.datetimepicker.DateTimePicker; import com.vaadin.flow.component.dependency.Uses; +import com.vaadin.flow.component.details.Details; 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.html.Span; import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.select.Select; +import com.vaadin.flow.component.splitlayout.SplitLayout; +import com.vaadin.flow.component.tabs.TabSheet; 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.data.value.ValueChangeMode; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtDistributionSet; import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutCondition; +import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutSuccessAction; import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutErrorAction; import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutResponseBody; import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPost; +import org.eclipse.hawkbit.mgmt.json.model.rollout.MgmtRolloutRestRequestBodyPut; import org.eclipse.hawkbit.mgmt.json.model.rolloutgroup.MgmtRolloutGroupResponseBody; import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQuery; import org.eclipse.hawkbit.ui.HawkbitMgmtClient; @@ -57,6 +64,8 @@ import org.eclipse.hawkbit.ui.view.util.SelectionGrid; import org.eclipse.hawkbit.ui.view.util.TableView; import org.eclipse.hawkbit.ui.view.util.Utils; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.ObjectUtils; @PageTitle("Rollouts") @@ -72,11 +81,10 @@ public final class RolloutView extends TableView public RolloutView(final HawkbitMgmtClient hawkbitClient) { super( new RolloutFilter(), + null, new SelectionGrid.EntityRepresentation<>( MgmtRolloutResponseBody.class, MgmtRolloutResponseBody::getId) { - private final RolloutDetails details = new RolloutDetails(hawkbitClient); - @Override protected void addColumns(final Grid grid) { grid.addColumn(MgmtRolloutResponseBody::getId).setHeader(Constants.ID).setAutoWidth(true); @@ -88,9 +96,6 @@ protected void addColumns(final Grid grid) { grid.addComponentColumn(rollout -> new Actions(rollout, grid, hawkbitClient)).setHeader( Constants.ACTIONS).setAutoWidth(true); - - grid.setItemDetailsRenderer(new ComponentRenderer<>( - () -> details, RolloutDetails::setItem)); } }, (query, rsqlFilter) -> Optional.ofNullable( @@ -104,7 +109,14 @@ protected void addColumns(final Grid grid) { rollout -> hawkbitClient.getRolloutRestApi().delete(rollout.getId())); selectionGrid.refreshGrid(false); return CompletableFuture.completedFuture(null); - }); + }, + rollout -> { + final RolloutDetailedView detailedView = new RolloutDetailedView(hawkbitClient); + detailedView.setItem(rollout); + return detailedView; + }, + SplitLayout.Orientation.VERTICAL, + rollout -> new EditDialog(rollout, hawkbitClient).result()); selectionGrid.getDataCommunicator().getKeyMapper().setIdentifierGetter(MgmtRolloutResponseBody::getId); } @@ -113,6 +125,8 @@ private static class Actions extends HorizontalLayout { @Serial private static final long serialVersionUID = 1L; + private static final String NO_APPROVE_PERMISSION = "Missing APPROVE_ROLLOUT permission"; + private final long rolloutId; private final Grid grid; private final transient HawkbitMgmtClient hawkbitClient; @@ -165,6 +179,19 @@ private void init(final MgmtRolloutResponseBody rollout) { } }, "Resume")); } + if ("WAITING_FOR_APPROVAL".equalsIgnoreCase(rollout.getStatus())) { + final boolean canApprove = hasApprovePermission(); + + final Button approve = new Button(VaadinIcon.CHECK.create()); + approve.setEnabled(canApprove); + approve.addClickListener(v -> new ApprovalDialog(rollout, true, hawkbitClient, this::refresh)); + add(Utils.tooltip(approve, canApprove ? "Approve" : NO_APPROVE_PERMISSION)); + + final Button deny = new Button(VaadinIcon.CLOSE.create()); + deny.setEnabled(canApprove); + deny.addClickListener(v -> new ApprovalDialog(rollout, false, hawkbitClient, this::refresh)); + add(Utils.tooltip(deny, canApprove ? "Deny" : NO_APPROVE_PERMISSION)); + } if (ROLLOUT_STATUS_STOPPABLE.contains(rollout.getStatus())) { add(Utils.tooltip(new Button(VaadinIcon.STOP.create()) { @@ -203,6 +230,52 @@ private void refresh() { grid.getDataProvider().refreshItem(body); } } + + private static boolean hasApprovePermission() { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication != null && authentication.getAuthorities().stream() + .anyMatch(authority -> "ROLE_ROLLOUT_APPROVE".equals(authority.getAuthority())); + } + } + + private static class ApprovalDialog extends Utils.BaseDialog { + + @Serial + private static final long serialVersionUID = 1L; + + private ApprovalDialog(final MgmtRolloutResponseBody rollout, final boolean approve, + final HawkbitMgmtClient hawkbitClient, final Runnable onDone) { + super(approve ? "Approve Rollout" : "Deny Rollout"); + + final Span target = new Span((approve ? "Approve" : "Deny") + " rollout: " + rollout.getName()); + final TextArea remark = new TextArea("Remark"); + remark.setWidthFull(); + + final Button confirm = new Button(approve ? "Approve" : "Deny"); + confirm.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + confirm.addClickListener(e -> { + close(); + final String remarkValue = ObjectUtils.isEmpty(remark.getValue()) ? null : remark.getValue(); + if (approve) { + hawkbitClient.getRolloutRestApi().approve(rollout.getId(), remarkValue); + } else { + hawkbitClient.getRolloutRestApi().deny(rollout.getId(), remarkValue); + } + onDone.run(); + }); + final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC); + cancel.addClickListener(e -> close()); + cancel.addClickShortcut(Key.ESCAPE); + getFooter().add(cancel); + getFooter().add(confirm); + + final VerticalLayout layout = new VerticalLayout(); + layout.setSizeFull(); + layout.setSpacing(false); + layout.add(target, remark); + add(layout); + open(); + } } private static class RolloutFilter implements Filter.Rsql { @@ -224,6 +297,34 @@ public String filter() { } } + private static class RolloutDetailedView extends VerticalLayout { + + @Serial + private static final long serialVersionUID = 1L; + + private final Span rolloutName; + private final RolloutDetails details; + + private RolloutDetailedView(final HawkbitMgmtClient hawkbitClient) { + rolloutName = new Span(); + details = new RolloutDetails(hawkbitClient); + setWidthFull(); + setHeightFull(); + getStyle().set("overflow", "auto"); + + add(rolloutName); + final TabSheet tabSheet = new TabSheet(); + tabSheet.setWidthFull(); + tabSheet.add("Details", details); + add(tabSheet); + } + + private void setItem(final MgmtRolloutResponseBody rollout) { + rolloutName.setText(rollout.getName()); + details.setItem(rollout); + } + } + private static class RolloutDetails extends FormLayout { @Serial @@ -240,32 +341,48 @@ private static class RolloutDetails extends FormLayout { private final TextField distributionSet = Utils.textField(Constants.DISTRIBUTION_SET); private final TextField actonType = Utils.textField(Constants.ACTION_TYPE); private final TextField startAt = Utils.textField(Constants.START_AT); + private final TextField forceTime = Utils.textField(Constants.FORCE_TIME); + private final TextField weight = Utils.textField(Constants.WEIGHT); + private final TextField approvalDecidedBy = Utils.textField(Constants.APPROVAL_DECIDED_BY); + private final TextField approvalRemark = Utils.textField(Constants.APPROVAL_REMARK); private final Checkbox dynamic = new Checkbox(Constants.DYNAMIC); + private final Checkbox deleted = new Checkbox(Constants.DELETED); private final SelectionGrid groupGrid; + private final Details groupsSection; private RolloutDetails(final HawkbitMgmtClient hawkbitClient) { this.hawkbitClient = hawkbitClient; description.setMinLength(2); groupGrid = createGroupGrid(); + groupGrid.setAllRowsVisible(true); + groupGrid.setWidthFull(); + groupsSection = new Details(Constants.GROUPS, groupGrid); + groupsSection.setOpened(false); + groupsSection.setWidthFull(); Stream.of( description, createdBy, createdAt, lastModifiedBy, lastModifiedAt, targetFilter, distributionSet, - actonType, startAt) + actonType, startAt, forceTime, + weight, + approvalDecidedBy, approvalRemark) .forEach(field -> { field.setReadOnly(true); add(field); }); - dynamic.setReadOnly(true); - dynamic.setEnabled(false); - add(dynamic); - add(groupGrid); + Stream.of(dynamic, deleted) + .forEach(checkbox -> { + checkbox.setReadOnly(true); + checkbox.setEnabled(false); + add(checkbox); + }); + add(groupsSection); setResponsiveSteps(new ResponsiveStep("0", 2)); setColspan(description, 2); - setColspan(groupGrid, 2); + setColspan(groupsSection, 2); } private void setItem(final MgmtRolloutResponseBody rollout) { @@ -288,7 +405,12 @@ private void setItem(final MgmtRolloutResponseBody rollout) { case TIMEFORCED -> "Scheduled at " + Utils.localDateTimeFromTs(rollout.getForcetime()); }); startAt.setValue(ObjectUtils.isEmpty(rollout.getStartAt()) ? "" : Utils.localDateTimeFromTs(rollout.getStartAt())); + forceTime.setValue(ObjectUtils.isEmpty(rollout.getForcetime()) ? "" : Utils.localDateTimeFromTs(rollout.getForcetime())); + weight.setValue(rollout.getWeight() == null ? "" : String.valueOf(rollout.getWeight())); + approvalDecidedBy.setValue(Objects.requireNonNullElse(rollout.getApproveDecidedBy(), "")); + approvalRemark.setValue(Objects.requireNonNullElse(rollout.getApprovalRemark(), "")); dynamic.setValue(rollout.isDynamic()); + deleted.setValue(rollout.isDeleted()); groupGrid.setItems(query -> Optional.ofNullable( hawkbitClient.getRolloutRestApi() @@ -320,6 +442,60 @@ protected void addColumns(final Grid grid) { } } + private static class EditDialog extends Utils.BaseDialog { + + @Serial + private static final long serialVersionUID = 1L; + + private final TextField name; + private final TextArea description; + private final Button save; + + private EditDialog(final MgmtRolloutResponseBody rollout, final HawkbitMgmtClient hawkbitClient) { + super("Edit Rollout"); + + name = Utils.textField(Constants.NAME, this::readyToSave); + name.setWidthFull(); + description = new TextArea(Constants.DESCRIPTION); + description.setWidthFull(); + description.setMinLength(2); + description.setValueChangeMode(ValueChangeMode.EAGER); + + save = Utils.tooltip(new Button("Save"), "Save (Enter)"); + name.setValue(Objects.requireNonNullElse(rollout.getName(), "")); + description.setValue(Objects.requireNonNullElse(rollout.getDescription(), "")); + save.addClickListener(e -> { + final MgmtRolloutRestRequestBodyPut body = new MgmtRolloutRestRequestBodyPut(); + body.setName(name.getValue()); + body.setDescription(description.getValue()); + hawkbitClient.getRolloutRestApi().update(rollout.getId(), body); + close(); + }); + save.addClickShortcut(Key.ENTER); + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC); + cancel.addClickListener(e -> close()); + cancel.addClickShortcut(Key.ESCAPE); + getFooter().add(cancel); + getFooter().add(save); + + final VerticalLayout layout = new VerticalLayout(); + layout.setSizeFull(); + layout.setPadding(true); + layout.setSpacing(false); + layout.add(name, description); + add(layout); + open(); + } + + private void readyToSave(final Object v) { + final boolean saveEnabled = !name.isEmpty(); + if (save.isEnabled() != saveEnabled) { + save.setEnabled(saveEnabled); + } + } + } + private static class CreateDialog extends Utils.BaseDialog { @Serial @@ -336,6 +512,9 @@ private static class CreateDialog extends Utils.BaseDialog { private final NumberField groupNumber; private final NumberField triggerThreshold; private final NumberField errorThreshold; + private final NumberField weight; + private final Select successAction; + private final Checkbox confirmationRequired = new Checkbox("Confirmation Required"); private final Checkbox dynamic = new Checkbox(Constants.DYNAMIC); private final Button create = new Button("Create"); @@ -405,6 +584,17 @@ private CreateDialog(final HawkbitMgmtClient hawkbitClient) { errorThreshold.setValue(10.0); errorThreshold.setSuffixComponent(percentSuffix); + weight = Utils.numberField("Weight"); + weight.setMin(0); + weight.setMax(1000); + weight.setWidthFull(); + + successAction = new Select<>(); + successAction.setLabel("Success Action"); + successAction.setItems(MgmtRolloutSuccessAction.SuccessAction.values()); + successAction.setValue(MgmtRolloutSuccessAction.SuccessAction.NEXTGROUP); + successAction.setWidthFull(); + create.setEnabled(false); create.addThemeVariants(ButtonVariant.LUMO_PRIMARY); addCreateClickListener(hawkbitClient); @@ -421,7 +611,7 @@ private CreateDialog(final HawkbitMgmtClient hawkbitClient) { name, distributionSet, targetFilter, description, actionType, startType, groupNumber, triggerThreshold, errorThreshold, - dynamic); + weight, successAction, confirmationRequired, dynamic); add(layout); open(); } @@ -462,6 +652,8 @@ private void addCreateClickListener(final HawkbitMgmtClient hawkbitClient) { new MgmtRolloutCondition( MgmtRolloutCondition.Condition.THRESHOLD, triggerThreshold.getValue().intValue() + "")); + request.setSuccessAction( + new MgmtRolloutSuccessAction(successAction.getValue(), "")); request.setErrorCondition( new MgmtRolloutCondition( MgmtRolloutCondition.Condition.THRESHOLD, @@ -469,6 +661,10 @@ private void addCreateClickListener(final HawkbitMgmtClient hawkbitClient) { request.setErrorAction( new MgmtRolloutErrorAction( MgmtRolloutErrorAction.ErrorAction.PAUSE, "")); + if (!weight.isEmpty()) { + request.setWeight(weight.getValue().intValue()); + } + request.setConfirmationRequired(confirmationRequired.getValue()); request.setDynamic(dynamic.getValue()); hawkbitClient.getRolloutRestApi().create(request).getBody(); }); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/SoftwareModuleView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/SoftwareModuleView.java index 39f804b237..e8e994bf0e 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/SoftwareModuleView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/SoftwareModuleView.java @@ -33,15 +33,19 @@ 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.details.Details; import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.select.Select; +import com.vaadin.flow.component.splitlayout.SplitLayout; +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.component.upload.Upload; -import com.vaadin.flow.data.renderer.ComponentRenderer; +import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import com.vaadin.flow.server.streams.UploadEvent; @@ -53,6 +57,7 @@ import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModule; import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleAssignment; import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleRequestBodyPost; +import org.eclipse.hawkbit.mgmt.json.model.softwaremodule.MgmtSoftwareModuleRequestBodyPut; import org.eclipse.hawkbit.mgmt.json.model.softwaremoduletype.MgmtSoftwareModuleType; import org.eclipse.hawkbit.ui.HawkbitMgmtClient; import org.eclipse.hawkbit.ui.MainLayout; @@ -81,10 +86,9 @@ public SoftwareModuleView(final HawkbitMgmtClient hawkbitClient) { public SoftwareModuleView(final boolean isParent, final HawkbitMgmtClient hawkbitClient) { super( new SoftwareModuleFilter(hawkbitClient), + null, new SelectionGrid.EntityRepresentation<>(MgmtSoftwareModule.class, MgmtSoftwareModule::getId) { - private final SoftwareModuleDetails details = new SoftwareModuleDetails(hawkbitClient); - @Override protected void addColumns(final Grid grid) { grid.addColumn(MgmtSoftwareModule::getId).setHeader(Constants.ID).setAutoWidth(true); @@ -92,8 +96,6 @@ protected void addColumns(final Grid grid) { grid.addColumn(MgmtSoftwareModule::getVersion).setHeader(Constants.VERSION).setAutoWidth(true); grid.addColumn(MgmtSoftwareModule::getTypeName).setHeader(Constants.TYPE).setAutoWidth(true); grid.addColumn(MgmtSoftwareModule::getVendor).setHeader(Constants.VENDOR).setAutoWidth(true); - - grid.setItemDetailsRenderer(new ComponentRenderer<>(() -> details, SoftwareModuleDetails::setItem)); } }, (query, rsqlFilter) -> Optional.ofNullable( @@ -108,7 +110,42 @@ protected void addColumns(final Grid grid) { module -> hawkbitClient.getSoftwareModuleRestApi().deleteSoftwareModule(module.getId())); selectionGrid.refreshGrid(false); return CompletableFuture.completedFuture(null); - } : null); + } : null, + softwareModule -> { + final SoftwareModuleDetailedView detailedView = new SoftwareModuleDetailedView(hawkbitClient); + detailedView.setItem(softwareModule); + return detailedView; + }, + SplitLayout.Orientation.VERTICAL, + isParent ? softwareModule -> new EditDialog(softwareModule, hawkbitClient).result() : null); + } + + private static class SoftwareModuleDetailedView extends VerticalLayout { + + @Serial + private static final long serialVersionUID = 1L; + + private final Span softwareModuleName; + private final SoftwareModuleDetails details; + + private SoftwareModuleDetailedView(final HawkbitMgmtClient hawkbitClient) { + softwareModuleName = new Span(); + details = new SoftwareModuleDetails(hawkbitClient); + setWidthFull(); + setHeightFull(); + getStyle().set("overflow", "auto"); + + add(softwareModuleName); + final TabSheet tabSheet = new TabSheet(); + tabSheet.setWidthFull(); + tabSheet.add("Details", details); + add(tabSheet); + } + + private void setItem(final MgmtSoftwareModule softwareModule) { + softwareModuleName.setText(softwareModule.getName() + ":" + softwareModule.getVersion()); + details.setItem(softwareModule); + } } public Set getSelection() { @@ -171,22 +208,37 @@ private static class SoftwareModuleDetails extends FormLayout { private final TextField createdAt = Utils.textField(Constants.CREATED_AT); private final TextField lastModifiedBy = Utils.textField(Constants.LAST_MODIFIED_BY); private final TextField lastModifiedAt = Utils.textField(Constants.LAST_MODIFIED_AT); + private final Checkbox complete = new Checkbox(Constants.COMPLETE); + private final Checkbox encrypted = new Checkbox(Constants.ENCRYPTED); + private final Checkbox locked = new Checkbox(Constants.LOCKED); + private final Checkbox deleted = new Checkbox(Constants.DELETED); private final SelectionGrid artifactGrid; + private final Details artifactsSection; private SoftwareModuleDetails(final HawkbitMgmtClient hawkbitClient) { this.hawkbitClient = hawkbitClient; description.setMinLength(2); artifactGrid = createArtifactGrid(); + artifactGrid.setAllRowsVisible(true); + artifactGrid.setWidthFull(); + artifactsSection = new Details("Artifacts", artifactGrid); + artifactsSection.setOpened(false); + artifactsSection.setWidthFull(); Stream.of(description, createdBy, createdAt, lastModifiedBy, lastModifiedAt).forEach(field -> { field.setReadOnly(true); add(field); }); - add(artifactGrid); + Stream.of(complete, encrypted, locked, deleted).forEach(checkbox -> { + checkbox.setReadOnly(true); + checkbox.setEnabled(false); + add(checkbox); + }); + add(artifactsSection); setResponsiveSteps(new ResponsiveStep("0", 2)); setColspan(description, 2); - setColspan(artifactGrid, 2); + setColspan(artifactsSection, 2); } private void setItem(final MgmtSoftwareModule softwareModule) { @@ -195,6 +247,10 @@ private void setItem(final MgmtSoftwareModule softwareModule) { createdAt.setValue(Utils.localDateTimeFromTs(softwareModule.getCreatedAt())); lastModifiedBy.setValue(softwareModule.getLastModifiedBy()); lastModifiedAt.setValue(Utils.localDateTimeFromTs(softwareModule.getLastModifiedAt())); + complete.setValue(Boolean.TRUE.equals(softwareModule.getComplete())); + encrypted.setValue(softwareModule.isEncrypted()); + locked.setValue(softwareModule.isLocked()); + deleted.setValue(softwareModule.isDeleted()); artifactGrid.setItems(query -> Optional.ofNullable( hawkbitClient.getSoftwareModuleRestApi() @@ -244,9 +300,11 @@ private CreateDialog(final HawkbitMgmtClient hawkbitClient) { name = Utils.textField(Constants.NAME, this::readyToCreate); version = Utils.textField(Constants.VERSION, this::readyToCreate); vendor = Utils.textField(Constants.VENDOR); + vendor.setValueChangeMode(ValueChangeMode.EAGER); description = new TextArea(Constants.DESCRIPTION); description.setWidthFull(); description.setMinLength(2); + description.setValueChangeMode(ValueChangeMode.EAGER); enableArtifactEncryption = new Checkbox("Enable artifact encryption"); distType = new Select<>("Distribution Set Type", this::readyToCreate); @@ -259,7 +317,6 @@ private CreateDialog(final HawkbitMgmtClient hawkbitClient) { distRequiredMigrationStep.setVisible(false); createDistributionSet = new Checkbox("Create single software module distribution set"); - createDistributionSet.setHelperText("Create single software module distribution set with this software module"); createDistributionSet.addValueChangeListener(e -> { if (Boolean.TRUE.equals(createDistributionSet.getValue()) && distType.isEmpty()) { distType.setItems( @@ -346,6 +403,71 @@ private void addCreateClickListener(final HawkbitMgmtClient hawkbitClient) { } } + private static class EditDialog extends Utils.BaseDialog { + + @Serial + private static final long serialVersionUID = 1L; + + private final TextField vendor; + private final TextArea description; + private final Button save; + + private EditDialog(final MgmtSoftwareModule softwareModule, final HawkbitMgmtClient hawkbitClient) { + super("Edit Software Module"); + + final TextField name = Utils.textField(Constants.NAME); + name.setValue(Objects.requireNonNullElse(softwareModule.getName(), "")); + name.setReadOnly(true); + name.setWidthFull(); + final TextField version = Utils.textField(Constants.VERSION); + version.setValue(Objects.requireNonNullElse(softwareModule.getVersion(), "")); + version.setReadOnly(true); + version.setWidthFull(); + final TextField type = Utils.textField(Constants.TYPE); + type.setValue(Objects.requireNonNullElse(softwareModule.getTypeName(), "")); + type.setReadOnly(true); + type.setWidthFull(); + + vendor = Utils.textField(Constants.VENDOR); + vendor.setValue(Objects.requireNonNullElse(softwareModule.getVendor(), "")); + vendor.setWidthFull(); + vendor.setValueChangeMode(ValueChangeMode.EAGER); + description = new TextArea(Constants.DESCRIPTION); + description.setValue(Objects.requireNonNullElse(softwareModule.getDescription(), "")); + description.setWidthFull(); + description.setMinLength(2); + description.setValueChangeMode(ValueChangeMode.EAGER); + + save = Utils.tooltip(new Button("Save"), "Save (Enter)"); + addSaveClickListener(softwareModule.getId(), hawkbitClient); + save.addClickShortcut(Key.ENTER); + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC); + cancel.addClickListener(e -> close()); + cancel.addClickShortcut(Key.ESCAPE); + getFooter().add(cancel); + getFooter().add(save); + + final VerticalLayout layout = new VerticalLayout(); + layout.setSizeFull(); + layout.setSpacing(false); + layout.add(name, version, type, vendor, description); + add(layout); + open(); + } + + private void addSaveClickListener(final Long softwareModuleId, final HawkbitMgmtClient hawkbitClient) { + save.addClickListener(e -> { + hawkbitClient.getSoftwareModuleRestApi().updateSoftwareModule( + softwareModuleId, + new MgmtSoftwareModuleRequestBodyPut() + .setVendor(vendor.getValue()) + .setDescription(description.getValue())); + close(); + }); + } + } + private static class AddArtifactsDialog extends Utils.BaseDialog { @Serial diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/TargetFilterQueryView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/TargetFilterQueryView.java index 4984c50aab..20073b893c 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/TargetFilterQueryView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/TargetFilterQueryView.java @@ -20,8 +20,10 @@ import jakarta.annotation.security.RolesAllowed; import com.vaadin.flow.component.Component; +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.confirmdialog.ConfirmDialog; import com.vaadin.flow.component.dependency.Uses; import com.vaadin.flow.component.formlayout.FormLayout; @@ -32,15 +34,18 @@ import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.splitlayout.SplitLayout; 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.data.value.ValueChangeMode; import com.vaadin.flow.dom.Style; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import org.eclipse.hawkbit.mgmt.json.model.PagedList; import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQuery; +import org.eclipse.hawkbit.mgmt.json.model.targetfilter.MgmtTargetFilterQueryRequestBody; import org.eclipse.hawkbit.ui.HawkbitMgmtClient; import org.eclipse.hawkbit.ui.MainLayout; import org.eclipse.hawkbit.ui.view.util.Filter; @@ -99,7 +104,9 @@ protected void addColumns(final Grid grid) { final TargetFilterQueryDetailedView detailedView = new TargetFilterQueryDetailedView(); detailedView.setItem(filterQuery); return detailedView; - } + }, + SplitLayout.Orientation.VERTICAL, + filterQuery -> new EditDialog(filterQuery, hawkbitClient).result() ); } @@ -177,6 +184,8 @@ private TargetFilterQueryDetailedView() { filterName = new Span(); details = new TargetFilterQueryDetails(); setWidthFull(); + setHeightFull(); + getStyle().set("overflow", "auto"); add(filterName); final TabSheet tabSheet = new TabSheet(); @@ -193,19 +202,17 @@ private void setItem(final TargetFilterQueryGridItem filterQuery) { private static class TargetFilterQueryDetails extends FormLayout { - private final TextField name = Utils.textField(Constants.NAME); private final TextArea query = new TextArea("Query"); private final TextField createdBy = Utils.textField(Constants.CREATED_BY); private final TextField createdAt = Utils.textField(Constants.CREATED_AT); private final TextField lastModifiedBy = Utils.textField(Constants.LAST_MODIFIED_BY); - private final TextField lastModifiedAt = Utils.textField(Constants.LAST_MODIFIED_AT); private TargetFilterQueryDetails() { query.setMinLength(2); Stream.of( - name, query, + query, createdBy, createdAt, - lastModifiedBy, lastModifiedAt) + lastModifiedBy) .forEach(field -> { field.setReadOnly(true); add(field); @@ -216,12 +223,66 @@ private TargetFilterQueryDetails() { } private void setItem(final TargetFilterQueryGridItem filterQuery) { - name.setValue(filterQuery.getName() != null ? filterQuery.getName() : ""); query.setValue(filterQuery.getQuery() != null ? filterQuery.getQuery() : ""); createdBy.setValue(filterQuery.getCreatedBy() != null ? filterQuery.getCreatedBy() : ""); createdAt.setValue(Utils.localDateTimeFromTs(filterQuery.getCreatedAt())); lastModifiedBy.setValue(filterQuery.getLastModifiedBy() != null ? filterQuery.getLastModifiedBy() : ""); - lastModifiedAt.setValue(Utils.localDateTimeFromTs(filterQuery.getLastModifiedAt())); + } + } + + private static class EditDialog extends Utils.BaseDialog { + + @Serial + private static final long serialVersionUID = 1L; + + private final TextField name; + private final TextArea query; + private final Button save; + + private EditDialog(final TargetFilterQueryGridItem filterQuery, final HawkbitMgmtClient hawkbitClient) { + super("Edit Target Filter Query"); + + name = Utils.textField(Constants.NAME, this::readyToSave); + name.setWidthFull(); + query = new TextArea("Query"); + query.setWidthFull(); + query.setMinLength(2); + query.setValueChangeMode(ValueChangeMode.EAGER); + query.addValueChangeListener(this::readyToSave); + + save = Utils.tooltip(new Button("Save"), "Save (Enter)"); + name.setValue(Objects.requireNonNullElse(filterQuery.getName(), "")); + query.setValue(Objects.requireNonNullElse(filterQuery.getQuery(), "")); + save.addClickListener(e -> { + hawkbitClient.getTargetFilterQueryRestApi().updateFilter( + filterQuery.getId(), + new MgmtTargetFilterQueryRequestBody() + .setName(name.getValue()) + .setQuery(query.getValue())); + close(); + }); + save.addClickShortcut(Key.ENTER); + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC); + cancel.addClickListener(e -> close()); + cancel.addClickShortcut(Key.ESCAPE); + getFooter().add(cancel); + getFooter().add(save); + + final VerticalLayout layout = new VerticalLayout(); + layout.setSizeFull(); + layout.setPadding(true); + layout.setSpacing(false); + layout.add(name, query); + add(layout); + open(); + } + + private void readyToSave(final Object v) { + final boolean saveEnabled = !name.isEmpty() && !query.isEmpty(); + if (save.isEnabled() != saveEnabled) { + save.setEnabled(saveEnabled); + } } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/TargetView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/TargetView.java index 2e6324b03e..d3f0d34872 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/TargetView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/TargetView.java @@ -36,6 +36,7 @@ import com.vaadin.flow.component.Unit; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; import com.vaadin.flow.component.checkbox.CheckboxGroup; import com.vaadin.flow.component.combobox.ComboBox; import com.vaadin.flow.component.datetimepicker.DateTimePicker; @@ -50,11 +51,13 @@ 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.splitlayout.SplitLayout; 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.provider.ListDataProvider; import com.vaadin.flow.data.renderer.ComponentRenderer; +import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import com.vaadin.flow.shared.Registration; @@ -154,7 +157,9 @@ protected void addColumns(final Grid grid) { final TargetDetailedView targetDetailedView = new TargetDetailedView(hawkbitClient); targetDetailedView.setItem(target); return targetDetailedView; - } + }, + SplitLayout.Orientation.VERTICAL, + target -> new EditDialog(target, hawkbitClient).result() ); final Function, CompletionStage> assignHandler = source -> new AssignDialog( @@ -363,6 +368,7 @@ private TargetDetailedView(final HawkbitMgmtClient hawkbitClient) { targetMetadata = new TargetMetadata(hawkbitClient); targetActionsHistoryLayout = new TargetActionsHistoryLayout(hawkbitClient); setSizeFull(); + getStyle().set("overflow", "auto"); add(targetId); tabSheet.add("Details", targetDetails); @@ -393,11 +399,17 @@ private static class TargetDetails extends FormLayout { private final TextField createdBy = Utils.textField(Constants.CREATED_BY); private final TextField createdAt = Utils.textField(Constants.CREATED_AT); private final TextField lastModifiedBy = Utils.textField(Constants.LAST_MODIFIED_BY); - private final TextField lastModifiedAt = Utils.textField(Constants.LAST_MODIFIED_AT); private final TextField securityToken = Utils.textField(Constants.SECURITY_TOKEN); + private final TextField ipAddress = Utils.textField(Constants.IP_ADDRESS); + private final TextField lastControllerRequestAt = Utils.textField(Constants.LAST_CONTROLLER_REQUEST_AT); + private final TextField installedAt = Utils.textField(Constants.INSTALLED_AT); private final TextField lastPoll = Utils.textField(Constants.LAST_POLL); + private final TextField nextExpectedPoll = Utils.textField(Constants.NEXT_EXPECTED_POLL); private final TextField group = Utils.textField(Constants.GROUP); private final TextField targetAddress = Utils.textField(Constants.ADDRESS); + private final Checkbox requestAttributes = new Checkbox(Constants.REQUEST_ATTRIBUTES); + private final Checkbox autoConfirmActive = new Checkbox(Constants.AUTO_CONFIRM_ACTIVE); + private final Checkbox overdue = new Checkbox(Constants.OVERDUE); private final TextArea targetAttributes = new TextArea(Constants.ATTRIBUTES); private transient MgmtTarget target; @@ -407,16 +419,27 @@ private TargetDetails(final HawkbitMgmtClient hawkbitClient) { Stream.of( description, createdBy, createdAt, - lastModifiedBy, lastModifiedAt, - securityToken, lastPoll, group, targetAddress, targetAttributes + lastModifiedBy, + securityToken, ipAddress, + lastControllerRequestAt, installedAt, lastPoll, nextExpectedPoll, + group, targetAddress ) .forEach(field -> { field.setReadOnly(true); add(field); }); + Stream.of(requestAttributes, autoConfirmActive, overdue) + .forEach(checkbox -> { + checkbox.setReadOnly(true); + checkbox.setEnabled(false); + add(checkbox); + }); + targetAttributes.setReadOnly(true); + add(targetAttributes); setResponsiveSteps(new ResponsiveStep("0", 2)); setColspan(description, 2); + setColspan(targetAttributes, 2); } private void setItem(final MgmtTarget target) { @@ -429,13 +452,22 @@ protected void onAttach(final AttachEvent attachEvent) { createdBy.setValue(target.getCreatedBy()); createdAt.setValue(Utils.localDateTimeFromTs(target.getCreatedAt())); lastModifiedBy.setValue(target.getLastModifiedBy()); - lastModifiedAt.setValue(Utils.localDateTimeFromTs(target.getLastModifiedAt())); securityToken.setValue(Objects.requireNonNullElse(target.getSecurityToken(), "")); + ipAddress.setValue(Objects.requireNonNullElse(target.getIpAddress(), "")); + lastControllerRequestAt.setValue( + target.getLastControllerRequestAt() == null ? "" : Utils.localDateTimeFromTs(target.getLastControllerRequestAt())); + installedAt.setValue(target.getInstalledAt() == null ? "" : Utils.localDateTimeFromTs(target.getInstalledAt())); group.setValue(target.getGroup() != null ? target.getGroup() : ""); targetAddress.setValue(target.getAddress() != null ? target.getAddress() : ""); + requestAttributes.setValue(target.isRequestAttributes()); + autoConfirmActive.setValue(Boolean.TRUE.equals(target.getAutoConfirmActive())); final MgmtPollStatus pollStatus = target.getPollStatus(); lastPoll.setValue(pollStatus == null ? NOT_AVAILABLE_NULL : Utils.localDateTimeFromTs(pollStatus.getLastRequestAt())); + nextExpectedPoll.setValue(pollStatus == null || pollStatus.getNextExpectedRequestAt() == null + ? NOT_AVAILABLE_NULL + : Utils.localDateTimeFromTs(pollStatus.getNextExpectedRequestAt())); + overdue.setValue(pollStatus != null && pollStatus.isOverdue()); final ResponseEntity response = hawkbitClient.getTargetRestApi().getAttributes(target.getControllerId()); if (response.getStatusCode().is2xxSuccessful()) { targetAttributes.setValue(Objects.requireNonNullElse(response.getBody(), Collections.emptyMap()).entrySet().stream() @@ -809,6 +841,9 @@ private static class RegisterDialog extends Utils.BaseDialog { private final TextField name; private final TextArea description; private final TextField group; + private final TextField address; + private final TextField securityToken; + private final Checkbox requestAttributes; private RegisterDialog(final HawkbitMgmtClient hawkbitClient) { super("Register Target"); @@ -833,8 +868,14 @@ private RegisterDialog(final HawkbitMgmtClient hawkbitClient) { description = new TextArea(Constants.DESCRIPTION); description.setMinLength(2); description.setWidthFull(); + description.setValueChangeMode(ValueChangeMode.EAGER); group = Utils.textField(Constants.GROUP); group.setWidthFull(); + address = Utils.textField(Constants.ADDRESS); + address.setWidthFull(); + securityToken = Utils.textField(Constants.SECURITY_TOKEN); + securityToken.setWidthFull(); + requestAttributes = new Checkbox("Request Attributes"); addCreateClickListener(register, hawkbitClient); register.setEnabled(false); @@ -850,7 +891,7 @@ private RegisterDialog(final HawkbitMgmtClient hawkbitClient) { layout.setSizeFull(); layout.setPadding(true); layout.setSpacing(false); - layout.add(type, controllerId, name, description, group); + layout.add(type, controllerId, name, description, group, address, securityToken, requestAttributes); add(layout); open(); } @@ -861,10 +902,17 @@ private void addCreateClickListener(final Button register, final HawkbitMgmtClie .setControllerId(controllerId.getValue()) .setName(name.getValue()) .setDescription(description.getValue()) - .setGroup(group.getValue()); + .setGroup(group.getValue()) + .setRequestAttributes(requestAttributes.getValue()); if (!ObjectUtils.isEmpty(type.getValue())) { request.setTargetType(type.getValue().getId()); } + if (!ObjectUtils.isEmpty(address.getValue())) { + request.setAddress(address.getValue()); + } + if (!ObjectUtils.isEmpty(securityToken.getValue())) { + request.setSecurityToken(securityToken.getValue()); + } hawkbitClient.getTargetRestApi().createTargets( List.of(request)) .getBody() @@ -877,6 +925,98 @@ private void addCreateClickListener(final Button register, final HawkbitMgmtClie } } + private static class EditDialog extends Utils.BaseDialog { + + @Serial + private static final long serialVersionUID = 1L; + + private final Select type; + private final TextField name; + private final TextArea description; + private final TextField group; + private final TextField address; + private final TextField securityToken; + private final Checkbox requestAttributes; + + private EditDialog(final MgmtTarget target, final HawkbitMgmtClient hawkbitClient) { + super("Edit Target"); + + final TextField controllerId = Utils.textField(CONTROLLER_ID); + controllerId.setValue(target.getControllerId()); + controllerId.setReadOnly(true); + controllerId.setWidthFull(); + + final MgmtTargetType[] types = hawkbitClient.getTargetTypeRestApi() + .getTargetTypes(null, 0, 30, Constants.NAME_ASC) + .getBody() + .getContent() + .toArray(new MgmtTargetType[0]); + type = new Select<>("Type", e -> {}, types); + type.setWidthFull(); + type.setEmptySelectionAllowed(true); + type.setItemLabelGenerator(item -> item == null ? "" : item.getName()); + if (target.getTargetType() != null) { + Stream.of(types) + .filter(t -> target.getTargetType().equals(t.getId())) + .findFirst() + .ifPresent(type::setValue); + } + + name = Utils.textField(Constants.NAME); + name.setValue(Objects.requireNonNullElse(target.getName(), "")); + name.setWidthFull(); + description = new TextArea(Constants.DESCRIPTION); + description.setValue(Objects.requireNonNullElse(target.getDescription(), "")); + description.setMinLength(2); + description.setWidthFull(); + description.setValueChangeMode(ValueChangeMode.EAGER); + group = Utils.textField(Constants.GROUP); + group.setValue(Objects.requireNonNullElse(target.getGroup(), "")); + group.setWidthFull(); + address = Utils.textField(Constants.ADDRESS); + address.setValue(Objects.requireNonNullElse(target.getAddress(), "")); + address.setWidthFull(); + securityToken = Utils.textField(Constants.SECURITY_TOKEN); + securityToken.setValue(Objects.requireNonNullElse(target.getSecurityToken(), "")); + securityToken.setWidthFull(); + requestAttributes = new Checkbox("Request Attributes"); + requestAttributes.setValue(target.isRequestAttributes()); + + final Button save = Utils.tooltip(new Button("Save"), "Save (Enter)"); + save.addClickListener(e -> { + final MgmtTargetRequestBody request = new MgmtTargetRequestBody() + .setName(name.getValue()) + .setDescription(description.getValue()) + .setGroup(group.getValue()) + .setRequestAttributes(requestAttributes.getValue()); + request.setTargetType(ObjectUtils.isEmpty(type.getValue()) ? null : type.getValue().getId()); + if (!ObjectUtils.isEmpty(address.getValue())) { + request.setAddress(address.getValue()); + } + if (!ObjectUtils.isEmpty(securityToken.getValue())) { + request.setSecurityToken(securityToken.getValue()); + } + hawkbitClient.getTargetRestApi().updateTarget(target.getControllerId(), request); + close(); + }); + save.addClickShortcut(Key.ENTER); + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + final Button cancel = Utils.tooltip(new Button(CANCEL), CANCEL_ESC); + cancel.addClickListener(e -> close()); + cancel.addClickShortcut(Key.ESCAPE); + getFooter().add(cancel); + getFooter().add(save); + + final VerticalLayout layout = new VerticalLayout(); + layout.setSizeFull(); + layout.setPadding(true); + layout.setSpacing(false); + layout.add(controllerId, type, name, description, group, address, securityToken, requestAttributes); + add(layout); + open(); + } + } + private static class AssignDialog extends Utils.BaseDialog { @Serial @@ -969,6 +1109,7 @@ private CreateTagDialog(final HawkbitMgmtClient hawkbitClient, Runnable onSucces final Input colorInput = new Input(); colorInput.setType("color"); name = Utils.textField("Tag Name", e -> create.setEnabled(!e.getHasValue().isEmpty())); + description.setValueChangeMode(ValueChangeMode.EAGER); formLayout.add(name); formLayout.add(description); formLayout.addFormItem(colorInput, "Color Selection"); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/util/SelectionGrid.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/util/SelectionGrid.java index 64296eb2c4..eed8b7104b 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/util/SelectionGrid.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/util/SelectionGrid.java @@ -30,7 +30,12 @@ public final class SelectionGrid extends Grid { @Serial private static final long serialVersionUID = 1L; + private static final String HIGHLIGHTED_ROW_PART = "selected-row"; + + private final transient Function idFn; + private volatile String rsqlFilter; + private transient T highlightedItem; public SelectionGrid( final EntityRepresentation entityRepresentation) { @@ -42,9 +47,15 @@ public SelectionGrid( final BiFunction, String, Stream> queryFn) { super(entityRepresentation.beanType, false); + this.idFn = entityRepresentation.idFn; + addThemeVariants(GridVariant.LUMO_NO_BORDER); addClassNames(LumoUtility.Border.TOP, LumoUtility.BorderColor.CONTRAST_10); + getDataCommunicator().getKeyMapper().setIdentifierGetter(idFn::apply); + setPartNameGenerator(item -> highlightedItem != null + && Objects.equals(idFn.apply(item), idFn.apply(highlightedItem)) ? HIGHLIGHTED_ROW_PART : null); + setSelectionMode(Grid.SelectionMode.MULTI); entityRepresentation.addColumns(this); if (queryFn != null) { @@ -60,14 +71,11 @@ public SelectionGrid( } else { final Set selectedIds = new HashSet<>(); selected.forEach(next -> selectedIds.add(entityRepresentation.idFn.apply(next))); - // if matching keeps old entries instead of new the new ones in order to - // select them in case refresh is made with keepSelection - // this however means that if they are changed the old state will be shown!!! return Stream.concat(selected.stream(), fetch.filter(next -> !selectedIds.contains(entityRepresentation.idFn.apply(next)))); } }); - } // else externally managed + } } public void setRsqlFilter(final String rsqlFilter, boolean refreshGrid) { @@ -91,6 +99,35 @@ public void refreshGrid(final boolean keepSelection) { } } + /** + * Highlights the given item's row (e.g. the row whose details panel is open) and clears the + * previous highlight. Only the affected rows are re-rendered. Passing {@code null} clears it. + */ + public void setHighlightedItem(final T item) { + final T previous = highlightedItem; + if (Objects.equals(idOf(previous), idOf(item))) { + return; + } + highlightedItem = item; + if (previous != null) { + getDataProvider().refreshItem(previous); + } + if (item != null) { + getDataProvider().refreshItem(item); + } + } + + /** + * Whether the given item is the currently highlighted row (i.e. the row whose details panel is open). + */ + public boolean isHighlighted(final T item) { + return highlightedItem != null && Objects.equals(idOf(item), idOf(highlightedItem)); + } + + private ID idOf(final T item) { + return item == null ? null : idFn.apply(item); + } + public abstract static class EntityRepresentation { private final Class beanType; diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/util/TableView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/util/TableView.java index 6cf67291e3..fd33f8b66e 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/util/TableView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/view/util/TableView.java @@ -20,7 +20,6 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Div; -import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; @@ -46,6 +45,7 @@ public class TableView extends Div implements Constants, BeforeEnterObser private static final String VAR_LUMO_PRIMARY_COLOR = "var(--lumo-primary-color)"; private static final int DEFAULT_OPEN_POSITION_SIZE = 50; private static final String QUERY_PARAM_FILTER = "q"; + private static final String NO_SPLITTER = "no-splitter"; protected SelectionGrid selectionGrid; private final Filter filter; @@ -53,7 +53,6 @@ public class TableView extends Div implements Constants, BeforeEnterObser protected final HorizontalLayout controlsLayout; private final SplitLayout splitLayout; private final Div detailsPanel; - private Button currentSelectionButton; public TableView( final Filter.Rsql rsql, @@ -71,19 +70,50 @@ public TableView( final Function, CompletionStage> addHandler, final Function, CompletionStage> removeHandler, final Function detailsButtonHandler) { + this(rsql, alternativeRsql, entityRepresentation, queryFn, addHandler, removeHandler, detailsButtonHandler, + SplitLayout.Orientation.HORIZONTAL); + } + + public TableView( + final Filter.Rsql rsql, final Filter.Rsql alternativeRsql, + final SelectionGrid.EntityRepresentation entityRepresentation, + final BiFunction, String, Stream> queryFn, + final Function, CompletionStage> addHandler, + final Function, CompletionStage> removeHandler, + final Function detailsButtonHandler, + final SplitLayout.Orientation detailsOrientation) { + this(rsql, alternativeRsql, entityRepresentation, queryFn, addHandler, removeHandler, detailsButtonHandler, + detailsOrientation, null); + } + + public TableView( + final Filter.Rsql rsql, final Filter.Rsql alternativeRsql, + final SelectionGrid.EntityRepresentation entityRepresentation, + final BiFunction, String, Stream> queryFn, + final Function, CompletionStage> addHandler, + final Function, CompletionStage> removeHandler, + final Function detailsButtonHandler, + final SplitLayout.Orientation detailsOrientation, + final Function> editHandler) { selectionGrid = new SelectionGrid<>(entityRepresentation, queryFn); selectionGrid.setSizeFull(); setSizeFull(); splitLayout = new SplitLayout(); splitLayout.setSizeFull(); - splitLayout.setOrientation(SplitLayout.Orientation.HORIZONTAL); + splitLayout.setOrientation(detailsOrientation); splitLayout.setSplitterPosition(100); + splitLayout.addClassName(NO_SPLITTER); splitLayout.addToPrimary(selectionGrid); detailsPanel = new Div(); detailsPanel.setSizeFull(); detailsPanel.getStyle().set("display", "flex").set("flex-direction", "column").set("min-height", "0"); + if (editHandler != null) { + ComponentRenderer renderer = new ComponentRenderer<>(renderEditButton(editHandler)); + selectionGrid.addColumn(renderer).setHeader("Edit").setAutoWidth(true).setFlexGrow(0).setFrozenToEnd(true); + } + if (detailsButtonHandler != null) { ComponentRenderer renderer = new ComponentRenderer<>(renderDetailsButton(detailsButtonHandler)); selectionGrid.addColumn(renderer).setHeader("Details").setAutoWidth(true).setFlexGrow(0).setFrozenToEnd(true); @@ -124,35 +154,35 @@ private void closeDetailsPanel() { splitLayout.remove(detailsPanel); } splitLayout.setSplitterPosition(100); - currentSelectionButton = null; + splitLayout.addClassName(NO_SPLITTER); + selectionGrid.setHighlightedItem(null); + } + + private void openDetailsPanel(final T selectedItem, final Function selectionHandler) { + if (splitLayout.getSecondaryComponent() == null) { + splitLayout.addToSecondary(detailsPanel); + } + detailsPanel.removeAll(); + splitLayout.removeClassName(NO_SPLITTER); + splitLayout.setSplitterPosition(DEFAULT_OPEN_POSITION_SIZE); + detailsPanel.add(selectionHandler.apply(selectedItem)); + // updates the highlight and re-renders the affected rows, so their details buttons + // reflect the open/closed state (see renderDetailsButton) + selectionGrid.setHighlightedItem(selectedItem); } private SerializableFunction renderDetailsButton(final Function selectionHandler) { return selectedItem -> { - final Button button = new Button(VaadinIcon.EYE.create()); - button.getStyle().set(COLOR, VAR_LUMO_SECONDARY_TEXT_COLOR); + // derive the icon from the open/closed state so it stays correct across row re-renders + final boolean open = selectionGrid.isHighlighted(selectedItem); + final Button button = new Button((open ? VaadinIcon.CLOSE_SMALL : VaadinIcon.EYE).create()); + button.getStyle().set(COLOR, open ? VAR_LUMO_PRIMARY_COLOR : VAR_LUMO_SECONDARY_TEXT_COLOR); button.addClickListener(event -> { - final Icon eyeIcon = VaadinIcon.EYE.create(); - final Icon closeIcon = VaadinIcon.CLOSE_SMALL.create(); - - if (button == currentSelectionButton) { - button.setIcon(eyeIcon); - button.getStyle().set(COLOR, VAR_LUMO_SECONDARY_TEXT_COLOR); + if (selectionGrid.isHighlighted(selectedItem)) { closeDetailsPanel(); } else { - button.setIcon(closeIcon); - button.getStyle().set(COLOR, VAR_LUMO_PRIMARY_COLOR); - if (currentSelectionButton == null) { - splitLayout.addToSecondary(detailsPanel); - } else { - currentSelectionButton.setIcon(eyeIcon); - currentSelectionButton.getStyle().set(COLOR, VAR_LUMO_SECONDARY_TEXT_COLOR); - } - detailsPanel.removeAll(); - splitLayout.setSplitterPosition(DEFAULT_OPEN_POSITION_SIZE); - detailsPanel.add(selectionHandler.apply(selectedItem)); - currentSelectionButton = button; + openDetailsPanel(selectedItem, selectionHandler); } }); @@ -162,6 +192,17 @@ private SerializableFunction renderDetailsButton(final Function renderEditButton(final Function> editHandler) { + return item -> { + final Button button = new Button(VaadinIcon.EDIT.create()); + button.getStyle().set(COLOR, VAR_LUMO_SECONDARY_TEXT_COLOR); + button.addClickListener(event -> editHandler.apply(item).thenAccept(v -> selectionGrid.refreshGrid(false))); + button.setTooltipText("Edit"); + button.addThemeVariants(ButtonVariant.LUMO_SMALL); + return button; + }; + } + @Override public void beforeEnter(BeforeEnterEvent event) { var params = event.getLocation().getQueryParameters();