You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2022/10/21 13:58:14 UTC

[brooklyn-ui] 05/24: expand actions, move to top-right

This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-ui.git

commit 0526f027e1c1682e56f71ca7cb360f141408ff28
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Wed Oct 5 15:51:33 2022 +0100

    expand actions, move to top-right
    
    supports cancel, most of what we need for replaying also.
    tweaks to the nested dropdown so that it works as we want.
---
 .../components/providers/activity-api.provider.js  |  8 +-
 .../inspect/activities/detail/detail.controller.js | 31 +++++++-
 .../main/inspect/activities/detail/detail.less     | 30 ++++++--
 .../inspect/activities/detail/detail.template.html | 31 ++++++--
 .../inspect/activities/detail/dropdown-nested.js   | 90 ++++++++++++++++++----
 5 files changed, 159 insertions(+), 31 deletions(-)

diff --git a/ui-modules/app-inspector/app/components/providers/activity-api.provider.js b/ui-modules/app-inspector/app/components/providers/activity-api.provider.js
index cdb36630..27deb5cc 100644
--- a/ui-modules/app-inspector/app/components/providers/activity-api.provider.js
+++ b/ui-modules/app-inspector/app/components/providers/activity-api.provider.js
@@ -39,7 +39,8 @@ function ActivityApi($http) {
         activity: getActivity,
         activityChildren: getActivityChildren,
         activityDescendants: getActivityDescendants,
-        activityStream: getActivityStream
+        activityStream: getActivityStream,
+        cancelActivity: cancelActivity,
     };
 
     function getActivities() {
@@ -64,4 +65,9 @@ function ActivityApi($http) {
             }
         }});
     }
+
+    function cancelActivity(activityId) {
+        return $http.post('/v1/activities/' + activityId + '/cancel');
+    }
+
 }
\ No newline at end of file
diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.controller.js b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.controller.js
index 7ef65aba..9a9e1e0c 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.controller.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.controller.js
@@ -25,7 +25,7 @@ export const detailState = {
     url: '/:activityId',
     template: template,
     controller: ['$scope', '$state', '$stateParams', '$log', '$uibModal', '$timeout', '$sanitize', '$sce', 'activityApi', 'entityApi', 'brUtilsGeneral', DetailController],
-    controllerAs: 'vm'
+    controllerAs: 'vm',
 }
 function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeout, $sanitize, $sce, activityApi, entityApi, Utils) {
     $scope.$emit(HIDE_INTERSTITIAL_SPINNER_EVENT);
@@ -71,10 +71,27 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
                 vm.model.workflow.applicationId = workflowTag.applicationId;
                 vm.model.workflow.entityId = workflowTag.entityId;
 
+                vm.actions.workflowReplays = [];
+                if (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc<=0) {
+                    // can't replay if active (same logic as 'cancel')
+                } else {
+                    [
+                        // TODO get from server
+                        // [ 'step 3 (continuing)', null ],
+                        // [ 'step 3 (replay point)', [2] ],
+                        // [ 'start (replay point)', [0] ],
+                    ].forEach(r => vm.actions.workflowReplays.push(r));
+                    vm.actions.workflowReplays.forEach(r => {
+                        r.push( () => console.log("TODO - replay from "+r[0], r[1]) );
+                    })
+                }
+                if (!vm.actions.workflowReplays.length) delete vm.actions['workflowReplays'];
+
                 observers.push(wResponse.subscribe((wResponse2)=> {
                     // change the workflow object so widgets get refreshed
                     vm.model.workflow = { ...vm.model.workflow, data: wResponse2.data };
                 }));
+
             }).catch(error => {
                 if (optimistic) {
                     vm.model.workflow.loading = null;
@@ -89,7 +106,9 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
         activityApi.activity(activityId).then((response)=> {
             vm.model.activity = response.data;
 
-            vm.actions = {};
+            vm.actions = vm.actions || {};
+            delete vm.actions['effector'];
+            delete vm.actions['invokeAgain'];
             if ((vm.model.activity.tags || []).find(t => t=="EFFECTOR")) {
                 const effectorName = (vm.model.activity.tags.find(t => t.effectorName) || {}).effectorName;
                 const effectorParams = (vm.model.activity.tags.find(t => t.effectorParams) || {}).effectorParams;
@@ -101,6 +120,11 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
                 }
             }
 
+            delete vm.actions['cancel'];
+            if (!vm.model.activity.endTimeUtc || vm.model.activity.endTimeUtc<=0) {
+                vm.actions.cancel = { doAction: () => { activityApi.cancelActivity(activityId); } };
+            }
+
             if ((vm.model.activity.tags || []).find(t => t=="WORKFLOW")) {
                 const workflowTag = findWorkflowTag(vm.model.activity);
                 if (workflowTag) {
@@ -241,7 +265,8 @@ function DetailController($scope, $state, $stateParams, $log, $uibModal, $timeou
     }
 
     vm.isNullish = _.isNil;
-
+    vm.isEmpty = x => vm.isNullish(x) || (x.length==0) || (typeof x === 'object' && !Object.keys(x).length);
+    vm.isNonEmpty = x => !vm.isEmpty(x);
 }
 
 function findWorkflowTag(task) {
diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.less b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.less
index 2dc74ee4..63068c45 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.less
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.less
@@ -21,6 +21,7 @@
     .activity-header {
         -webkit-font-smoothing: antialiased;
         margin-bottom: 40px;
+
         status {
             width: 50px;
             height: 50px;
@@ -50,7 +51,7 @@
             color: @gray-light;
         }
     }
-    
+
     table.summary {
         tr td:first-child {
             width: 25%;
@@ -69,7 +70,7 @@
     .summary-body {
         margin-bottom: 24px;
     }
-    
+
     .summary-item {
         margin-bottom: 35px;
 
@@ -77,20 +78,24 @@
         @redColor: @brand-danger;
         @greenColor: #58BA58;
         @redColor: #BA5858;
+
         .summary-item-value {
             &.status-completed {
                 color: @greenColor;
                 font-weight: bold;
                 -webkit-font-smoothing: antialiased;
             }
+
             &.status-failed {
                 color: @redColor;
                 font-weight: bold;
                 -webkit-font-smoothing: antialiased;
             }
+
             &.status-in-progress {
 
             }
+
             &.status-unknown {
 
             }
@@ -137,6 +142,7 @@
                 > div {
                     opacity: 1.0;
                     position: absolute;
+
                     &.fade.ng-hide {
                         opacity: 0;
                     }
@@ -157,20 +163,23 @@
             }
         }
     }
+
     .monospace, .result-parent {
         .monospace();
     }
+
     .result-parent.big-result {
-       border: 1px solid @gray-lighter;
-      .result-body {
-        padding: 4px;
-      }
+        border: 1px solid @gray-lighter;
+        .result-body {
+            padding: 4px;
+        }
     }
+
     .result-body {
         max-height: 56pt;
         overflow: scroll;
     }
-    
+
     .collapsing {
         // internal class used by bootstrap when opening/closing:
         // activity viewers are probably power users - don't want to
@@ -205,10 +214,17 @@
                     margin-bottom: -3px;
                 }
             }
+
             .check {
                 display: none;
             }
         }
     }
+}
 
+.dropdown-at-root {
+    width: auto;
+    &.dropdown-submenu-left, .dropdown-submenu-left {
+        margin-right: 36px;
+    }
 }
diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.template.html b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.template.html
index 09e45831..254435f4 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.template.html
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/detail.template.html
@@ -38,12 +38,38 @@
 
         <div>
             <div class="activity-detail">
+
                 <div class="alert alert-info" ng-if="vm.model.activity.blockingTask">
                     <strong>Blocked on:</strong>
                     <span ng-if="vm.model.activity.blockingDetails">{{vm.model.activity.blockingDetails}}:</span>
                     <code><a ui-sref="main.inspect.activities.detail({entityId: vm.model.activity.blockingTask.metadata.entityId, activityId: vm.model.activity.blockingTask.metadata.id})">{{vm.model.activity.blockingTask.metadata.taskName}}</a></code> for <strong><a ui-sref="main.inspect.summary({entityId: vm.model.activity.blockingTask.metadata.entityId})">{{vm.model.activity.blockingTask.metadata.entityDisplayName}} entity</a></strong>
                 </div>
 
+                <div style="float:right;" ng-if="vm.isNonEmpty(vm.actions)" class="btn-group dropdown-nested dropdown-menu-right" uib-dropdown-nested dropdown-append-to-body="true">
+                    <br-button uib-dropdown-toggle-nested type="btn-primary">
+                        Actions <span class="caret"></span>
+                    </br-button>
+                    <ul uib-dropdown-menu-nested class="dropdown-at-root dropdown-menu-right">
+
+                        <li><a href="" ng-if="vm.actions.cancel" ng-click="vm.actions.cancel.doAction()">Cancel</a></li>
+
+                        <li ng-if="vm.actions.workflowReplays" uib-dropdown-nested dropdown-append-to-body="true">
+
+                            <a href="" uib-dropdown-toggle-nested>Replay workflow <span class="caret"></span></a>
+                            <ul class="dropdown-submenu-left dropdown-at-root dropdown-menu-right" uib-dropdown-menu-nested>
+                                <li ng-repeat="replay in vm.actions.workflowReplays track by $index" id="replay {{ replay[0] }}">
+                                    <a class="dropdown-item" href="" ng-click="replay[2]()">From {{ replay[0] }}</a>
+                                </li>
+                            </ul>
+
+                        </li>
+
+                        <li><a href="" ng-if="vm.actions.invokeAgain" ng-click="vm.actions.invokeAgain.doAction()">Reinvoke effector</a></li>
+                        <li><a href="" ng-if="vm.actions.effector" ui-sref="main.inspect.effectors({search: vm.actions.effector.effectorName})">Open in effector tab</a></li>
+
+                    </ul>
+                </div>
+
                 <div class="activity-header" ng-if="vm.model.activity.id">
                     <div class="activity-title">{{vm.model.activity.displayName}}</div>
                     <div class="activity-entity">{{vm.model.activity.entityDisplayName}}</div>
@@ -126,11 +152,6 @@
                         </div>
                     </div>
 
-                    <div class="activity-actions" ng-if="vm.actions">
-                        <br-button ng-if="vm.actions.invokeAgain" on-click="vm.actions.invokeAgain.doAction()">Execute again</br-button>
-                        <br-button ng-if="vm.actions.effector" ui-sref="main.inspect.effectors({search: vm.actions.effector.effectorName})">Open effector tab</br-button>
-                    </div>
-
                     <br-collapsible class="activity-streams"
                                     ng-if="vm.isNonEmpty(vm.model.activity.streams)"
                                     state="vm.model.accordion.streamsOpen">
diff --git a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/dropdown-nested.js b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/dropdown-nested.js
index 6d983bb9..00680a61 100644
--- a/ui-modules/app-inspector/app/views/main/inspect/activities/detail/dropdown-nested.js
+++ b/ui-modules/app-inspector/app/views/main/inspect/activities/detail/dropdown-nested.js
@@ -1,3 +1,5 @@
+import {drop} from "lodash/array";
+
 const MODULE_NAME = 'ui.bootstrap.dropdown.nested';
 
 export default MODULE_NAME;
@@ -11,6 +13,7 @@ angular.module(MODULE_NAME, ['ui.bootstrap.multiMap', 'ui.bootstrap.position'])
 
     .service('uibDropdownServiceNested', ['$document', '$rootScope', '$$multiMap', function($document, $rootScope, $$multiMap) {
         var openScope = null;
+        var oldOpenScopes = [];
         var openedContainers = $$multiMap.createNew();
 
         this.isOnlyOpen = function(dropdownScope, appendTo) {
@@ -37,7 +40,8 @@ angular.module(MODULE_NAME, ['ui.bootstrap.multiMap', 'ui.bootstrap.position'])
             }
 
             if (openScope && openScope !== dropdownScope) {
-                openScope.isOpen = false;
+                // just remember it
+                oldOpenScopes.push(openScope);
             }
 
             openScope = dropdownScope;
@@ -65,9 +69,15 @@ angular.module(MODULE_NAME, ['ui.bootstrap.multiMap', 'ui.bootstrap.position'])
 
         this.close = function(dropdownScope, element, appendTo) {
             if (openScope === dropdownScope) {
+                openScope = null;
+            }
+            const indexOfOpen = oldOpenScopes.indexOf(dropdownScope);
+            if (indexOfOpen>=0) {
+                oldOpenScopes.splice(indexOfOpen, 1);
+            }
+            if (openScope==null && oldOpenScopes.length) {
                 $document.off('click', closeDropdown);
                 $document.off('keydown', this.keybindFilter);
-                openScope = null;
             }
 
             if (!appendTo) {
@@ -92,28 +102,78 @@ angular.module(MODULE_NAME, ['ui.bootstrap.multiMap', 'ui.bootstrap.position'])
         var closeDropdown = function(evt) {
             // This method may still be called during the same mouse event that
             // unbound this event handler. So check openScope before proceeding.
-            if (!openScope || !openScope.isOpen) { return; }
+            let scopesToApply = [];
+
+            function containsNested(container, target) {
+                if (!container) return false;
+                if (container==target) return true;
+                if (container[0] && container[0].contains && container[0].contains(target)) return true;
+                if (container.contains && container.contains(target)) return true;
+
+                let kids = angular.element(container).children();
+                if (kids && kids.length) {
+                    for (let i=0; i<kids.length; i++) {
+                        let found = containsNested(kids[i], target);
+                        if (found) return true;
+                    }
+                }
+                return false;
+            }
 
-            if (evt && openScope.getAutoClose() === 'disabled') { return; }
+            function isAnyTrigger(element) {
+                return element.hasClass('dropdown-toggle');
+            }
 
-            if (evt && evt.which === 3) { return; }
+            function closeIfApplicable(scope) {
+                if (evt && scope.getAutoClose() === 'disabled') {
+                    return;
+                }
 
-            var toggleElement = openScope.getToggleElement();
-            if (evt && toggleElement && toggleElement[0].contains(evt.target)) {
-                return;
+                if (evt && evt.which === 3) {
+                    return;
+                }
+
+                if (evt &&
+                    isAnyTrigger(angular.element(evt.target))) {
+                    return;
+                }
+                // could do "is contained in any trigger"; but doesn't seem needed yet
+
+                var toggleElement = scope.getToggleElement();
+                if (evt && toggleElement && containsNested(toggleElement, evt.target)) {
+                    return;
+                }
+
+                var dropdownElement = scope.getDropdownElement();
+                if (evt &&
+                    scope.getAutoClose() === 'outsideClick' &&
+                    dropdownElement && containsNested(dropdownElement, evt.target)) {
+                    return;
+                }
+                scope.isOpen = false;
+                scopesToApply.push(scope);
+
+                return true;
             }
 
-            var dropdownElement = openScope.getDropdownElement();
-            if (evt && openScope.getAutoClose() === 'outsideClick' &&
-                dropdownElement && dropdownElement[0].contains(evt.target)) {
-                return;
+            if (openScope && openScope.isOpen) {
+                if (closeIfApplicable(openScope)) {
+                    openScope.focusToggleElement();
+                }
             }
 
-            openScope.focusToggleElement();
-            openScope.isOpen = false;
+            // close all the others too
+            const scopesToKeep = [];
+            oldOpenScopes.forEach(scope => {
+                if (!closeIfApplicable(scope)) {
+                    scopesToKeep.push(scope);
+                }
+            });
+            oldOpenScopes.splice(0, oldOpenScopes.length, ...scopesToKeep);
 
+            // and apply
             if (!$rootScope.$$phase) {
-                openScope.$apply();
+                scopesToApply.forEach(s => s.$apply());
             }
         };