You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pinot.apache.org by ap...@apache.org on 2018/11/15 23:51:41 UTC

[incubator-pinot] branch master updated: [TE] alert filter UX improvements (#3489)

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

apucher pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new e8516cb  [TE] alert filter UX improvements (#3489)
e8516cb is described below

commit e8516cb5740a200a7666c86b3c6e4f6a01aae3b1
Author: Steve McClung <st...@gmail.com>
AuthorDate: Thu Nov 15 17:51:37 2018 -0600

    [TE] alert filter UX improvements (#3489)
    
    Two main UX improvements here:
    
    - Preserve filter states when navigating back to alert filter page
    - When finding an alert by name, rather than disable fields, set filters according to alert properties and enable field manipulation as per normal (includes the ability to filter for properties which are "null" or non-existent, for example, "find all alerts with no subscription group or application")
---
 .../app/pods/components/entity-filter/component.js |  33 +++++--
 .../app/pods/components/entity-filter/template.hbs |   1 -
 .../app/pods/manage/alerts/index/controller.js     | 103 +++++++++++++++------
 .../app/pods/manage/alerts/index/route.js          |   3 +
 .../app/pods/manage/alerts/index/template.hbs      |  30 +++---
 5 files changed, 118 insertions(+), 52 deletions(-)

diff --git a/thirdeye/thirdeye-frontend/app/pods/components/entity-filter/component.js b/thirdeye/thirdeye-frontend/app/pods/components/entity-filter/component.js
index 8c50063..276a6e7 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/entity-filter/component.js
+++ b/thirdeye/thirdeye-frontend/app/pods/components/entity-filter/component.js
@@ -35,7 +35,8 @@ import {
   get,
   computed,
   getProperties,
-  setProperties
+  setProperties,
+  getWithDefault
 } from '@ember/object';
 import { isPresent } from '@ember/utils';
 import { later } from '@ember/runloop';
@@ -60,6 +61,7 @@ export default Component.extend({
   didReceiveAttrs() {
     this._super(...arguments);
     const filterBlocks = get(this, 'filterBlocks');
+    const filterStateObj = get(this, 'currentFilterState');
     let multiSelectKeys = {}; // new filter object
 
     // Set up filter block object
@@ -68,10 +70,15 @@ export default Component.extend({
       let filterKeys = [];
       let tag = block.name.camelize();
       let matchWidth = block.matchWidth ? block.matchWidth : false;
+
+      // Initially load existing state of selected filters if available
+      if (filterStateObj && filterStateObj[tag] && filterStateObj[tag].length) {
+        block.selected = filterStateObj[tag];
+      }
       // If any pre-selected items, bring them into the new filter object
       multiSelectKeys[tag] = block.selected ? block.selected : null;
       // Dedupe and remove null or empty values
-      filterKeys = Array.from(new Set(block.filterKeys.filter(value => isPresent(value))));
+      filterKeys = Array.from(new Set(block.filterKeys.filter(value => isPresent(value) && value !== 'undefined')));
       // Generate a name and Id for each one based on provided filter keys
       if (block.type !== 'select') {
         filterKeys.forEach((filterName, index) => {
@@ -84,7 +91,13 @@ export default Component.extend({
         });
       }
       // Now add new initialized props to block item
-      setProperties(block, { filtersArray, filterKeys, isHidden: false, tag, matchWidth });
+      setProperties(block, {
+        tag,
+        filterKeys,
+        matchWidth,
+        filtersArray,
+        isHidden: false
+      });
     });
     set(this, 'multiSelectKeys', multiSelectKeys);
   },
@@ -110,23 +123,29 @@ export default Component.extend({
     onFilterSelection(filterObj, selectedItems) {
       const selectKeys = get(this, 'multiSelectKeys');
       let selectedArr = selectedItems;
+
+      // Handle 'status' field toggling rules
       if (filterObj.tag === 'status' && filterObj.selected) {
+        // Toggle selected status
         set(selectedItems, 'isActive', !selectedItems.isActive);
+        // Map selected status to array - will add to filter map
+        selectedArr = filterObj.filtersArray.filterBy('isActive').mapBy('name');
+        // Make sure 'Active' is selected by default when both are un-checked
         if (filterObj.filtersArray.filter(item => item.isActive).length === 0) {
+          selectedArr = ['Active'];
           const activeItem = filterObj.filtersArray.find(item => item.id === 'active');
-          later(() => {
-            set(activeItem, 'isActive', true);
-          });
         }
-        selectedArr = filterObj.filtersArray.filterBy('isActive').mapBy('name');
       }
+      // Handle 'global' or 'primary' filter field toggling
       if (filterObj.tag === 'primary') {
         filterObj.filtersArray.forEach(filter => set(filter, 'isActive', false));
         const activeFilter = filterObj.filtersArray.find(filter => filter.name === selectedItems);
         set(activeFilter, 'isActive', true);
       }
+      // Sets the 'alertFilters' object in parent
       set(selectKeys, filterObj.tag, selectedArr);
       set(selectKeys, 'triggerType', filterObj.type);
+      // Send action up to parent controller
       this.get('onSelectFilter')(selectKeys);
     },
 
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/entity-filter/template.hbs b/thirdeye/thirdeye-frontend/app/pods/components/entity-filter/template.hbs
index aafbb8c..076ba60 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/entity-filter/template.hbs
+++ b/thirdeye/thirdeye-frontend/app/pods/components/entity-filter/template.hbs
@@ -40,7 +40,6 @@
           <li class="entity-filter__group-filter entity-filter__group-filter--link {{if filter.isActive 'entity-filter__group-filter--selected'}}" {{action "onFilterSelection" block filter.name}}>
             {{filter.name}} <span>({{filter.total}})</span>
           </li>
-
         {{/each}}
       {{/if}}
 
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/alerts/index/controller.js b/thirdeye/thirdeye-frontend/app/pods/manage/alerts/index/controller.js
index a743ff5..bd9b4fd 100644
--- a/thirdeye/thirdeye-frontend/app/pods/manage/alerts/index/controller.js
+++ b/thirdeye/thirdeye-frontend/app/pods/manage/alerts/index/controller.js
@@ -63,10 +63,16 @@ export default Controller.extend({
   /**
    * Filter settings
    */
+  alertFilters: [],
   resetFiltersGlobal: null,
   resetFiltersLocal: null,
   alertFoundByName: null,
 
+  /**
+   * The first and broadest entity search property
+   */
+  topSearchKeyName: 'application',
+
   // Total displayed alerts
   totalFilteredAlerts: 0,
 
@@ -128,14 +134,21 @@ export default Controller.extend({
     function() {
       const {
         alertFilters,
+        topSearchKeyName,
         filterBlocksLocal,
         alertFoundByName,
         filterToPropertyMap,
         originalAlerts: initialAlerts
-      } = getProperties(this, 'alertFilters', 'filterBlocksLocal', 'alertFoundByName', 'filterToPropertyMap', 'originalAlerts');
+      } = getProperties(this, 'alertFilters', 'topSearchKeyName', 'filterBlocksLocal', 'alertFoundByName', 'filterToPropertyMap', 'originalAlerts');
       const filterBlocksCopy = _.cloneDeep(filterBlocksLocal);
+      const selectFieldKeys = Object.keys(filterToPropertyMap);
+      const fieldsByState = (state) => alertFilters ? selectFieldKeys.filter((key) => {
+        return (state === 'active') ? isPresent(alertFilters[key]) : isBlank(alertFilters[key]);
+      }) : [];
+      const inactiveFields = fieldsByState('inactive');
+      const activeFields = fieldsByState('active');
+      // Recalculate only 'select' filters when we have a change in them
       const canRecalcFilterOptions = alertFilters && alertFilters.triggerType !== 'checkbox';
-      const inactiveFields = alertFilters ? Object.keys(alertFilters).filter(key => isBlank(alertFilters[key])) : [];
       const filtersToRecalculate = filterBlocksCopy.filter(block => block.type === 'select');
       const nonSelectFilters = filterBlocksCopy.filter(block => block.type !== 'select');
       let filteredAlerts = initialAlerts;
@@ -151,19 +164,19 @@ export default Controller.extend({
             Object.assign(blockItem, { selected: alertFilters[blockItem.name] });
             // We are recalculating each field where options have not been selected
             if (inactiveFields.includes(blockItem.name) || !inactiveFields.length) {
-              const alertPropsAsKeys = filteredAlerts.map(alert => alert[filterToPropertyMap[blockItem.name]]);
-              const filterKeys = [ ...new Set(powerSort(alertPropsAsKeys, null)) ];
-              Object.assign(blockItem, { filterKeys });
+              Object.assign(blockItem, { filterKeys: this._recalculateFilterKeys(filteredAlerts, blockItem) });
+            }
+            // For better UX: restore top field options if its the only active field. In our case the top field is 'applications'
+            if (blockItem.name === topSearchKeyName && activeFields.join('') === topSearchKeyName) {
+              Object.assign(blockItem, { filterKeys: this._recalculateFilterKeys(initialAlerts, blockItem) });
             }
           });
-
           // Preserve selected state for filters that initially have a "selected" property
           if (nonSelectFilters.length) {
             nonSelectFilters.forEach((filter) => {
               filter.selected = alertFilters[filter.name] ? alertFilters[filter.name] : filter.selected;
             });
           }
-
           // Be sure to update the filter options object once per pass
           once(() => {
             set(this, 'filterBlocksLocal', filterBlocksCopy);
@@ -257,6 +270,26 @@ export default Controller.extend({
   ),
 
   /**
+   * We are recalculating the options of each selection field. The values come from the aggregated
+   * properties across all filtered alerts. For example, it returns all possible values for 'application'
+   * @method _recalculateFilterKeys
+   * @param {Array} alertsCollection - array of alerts we are extracting values from
+   * @param {Object} blockItem - the current search filter object
+   * @returns {Array} - a deduped array of values to use as select options
+   * @private
+   */
+  _recalculateFilterKeys(alertsCollection, blockItem) {
+    const filterToPropertyMap = get(this, 'filterToPropertyMap');
+    // Aggregate all existing values for our target properties in the current array collection
+    const alertPropsAsKeys = alertsCollection.map(alert => alert[filterToPropertyMap[blockItem.name]]);
+    // Add 'none' select option if allowed
+    const canInsertNullOption = alertPropsAsKeys.includes(undefined) && blockItem.hasNullOption;
+    if (canInsertNullOption) { alertPropsAsKeys.push('none'); }
+    // Return a deduped array containing all of the values for this property in the current set of alerts
+    return [ ...new Set(powerSort(alertPropsAsKeys.filter(val => isPresent(val)), null)) ];
+  },
+
+  /**
    * This is the core filtering method which acts upon a set of initial alerts to return a subset
    * @method _filterAlerts
    * @param {Array} initialAlerts - array of all alerts to start with
@@ -273,12 +306,10 @@ export default Controller.extend({
    */
   _filterAlerts(initialAlerts, filters) {
     const filterToPropertyMap = get(this, 'filterToPropertyMap');
-
     // A click on a primary alert filter will reset 'filteredAlerts'
     if (filters.primary) {
       this._processPrimaryFilters(initialAlerts, filters.primary);
     }
-
     // Pick up cached alert array for the secondary filters
     let filteredAlerts = get(this, 'filteredAlerts');
 
@@ -288,23 +319,23 @@ export default Controller.extend({
       if (filterValueArray && filterValueArray.length) {
         let newAlerts = filteredAlerts.filter(alert => {
           // See 'filterToPropertyMap' in route. For filterKey = 'owner' this would map alerts by alert['createdBy'] = x
-          const targetAlertObject = alert[filterToPropertyMap[filterKey]];
-          return targetAlertObject && filterValueArray.includes(targetAlertObject);
+          const targetAlertPropertyValue = alert[filterToPropertyMap[filterKey]];
+          const alertMeetsCriteria = targetAlertPropertyValue && filterValueArray.includes(targetAlertPropertyValue);
+          const isMatchForNone = !alert.hasOwnProperty(filterToPropertyMap[filterKey]) && filterValueArray.includes('none');
+          return alertMeetsCriteria || isMatchForNone;
         });
         filteredAlerts = newAlerts;
       }
     });
 
+    // If status filter is present, we re-build the results array to contain only active alerts, inactive alerts, or both.
     if (filters.status) {
-      // !filters.status.length forces an 'Active' default if user tries to de-select both
-      // Depending on the desired UX, remove it if you want to allow user to select NO active and NO inactive.
       const concatStatus = filters.status.length ? filters.status.join().toLowerCase() : 'active';
       const requireAll = filters.status.includes('Active') && filters.status.includes('Inactive');
       const alertsByState = {
         active: filteredAlerts.filter(alert => alert.isActive),
         inactive: filteredAlerts.filter(alert => !alert.isActive)
       };
-      // We re-build the alerts array to contain only active alerts, inactive alerts, or both.
       filteredAlerts = requireAll ? [ ...alertsByState.active, ...alertsByState.inactive ] : alertsByState[concatStatus];
     }
 
@@ -347,24 +378,32 @@ export default Controller.extend({
    * @returns {undefined}
    * @private
    */
-  _resetFilters(isSelectDisabled) {
+  _resetLocalFilters(alert) {
+    let alertFilters = {};
+    const filterToPropertyMap = get(this, 'filterToPropertyMap');
+    const newFilterBlocksLocal = _.cloneDeep(get(this, 'initialFiltersLocal'));
+
+    // Set new select field options (filterKeys) to our found alert properties
+    Object.keys(filterToPropertyMap).forEach((filterKey) => {
+      let targetAlertProp = alert[filterToPropertyMap[filterKey]];
+      alertFilters[filterKey] = targetAlertProp ? [ targetAlertProp ] : ['none'];
+      newFilterBlocksLocal.find(filter => filter.name === filterKey).filterKeys = alertFilters[filterKey];
+    });
+
+    // Do not highlight any of the primary filters
+    Object.assign(alertFilters, { primary: 'none' });
+
+    // Set correct status on current alert
+    const alertStatus = alert.isActive ? 'Active' : 'Inactive';
+    newFilterBlocksLocal.find(filter => filter.name === 'status').selected = [ alertStatus ];
+
     // Reset local (secondary) filters, and set select fields to 'disabled'
     setProperties(this, {
-      filterBlocksLocal: _.cloneDeep(get(this, 'initialFiltersLocal')),
+      filterBlocksLocal: newFilterBlocksLocal,
       resetFiltersLocal: moment().valueOf(),
-      isSelectDisabled
+      allowFilterSummary: false,
+      alertFilters
     });
-    // Reset global (primary) filters, and de-activate any selections
-    if (isSelectDisabled) {
-      const origFiltersGlobal = get(this, 'initialFiltersGlobal');
-      origFiltersGlobal.forEach((filter) => {
-        set(filter, 'selected', []);
-      });
-      setProperties(this, {
-        filterBlocksGlobal: origFiltersGlobal,
-        resetFiltersGlobal: moment().valueOf()
-      });
-    }
   },
 
   actions: {
@@ -372,18 +411,22 @@ export default Controller.extend({
     onSelectAlertByName(alert) {
       if (!alert) { return; }
       set(this, 'alertFoundByName', alert);
-      this._resetFilters(true);
+      this._resetLocalFilters(alert);
     },
 
     // Handles filter selections (receives array of filter options)
     userDidSelectFilter(filterArr) {
       setProperties(this, {
         filtersTriggered: true,
+        allowFilterSummary: true,
         alertFilters: filterArr
       });
       // Reset secondary filters component instance if a primary filter was selected
       if (Object.keys(filterArr).includes('primary')) {
-        this._resetFilters(false);
+        setProperties(this, {
+          filterBlocksLocal: _.cloneDeep(get(this, 'initialFiltersLocal')),
+          resetFiltersLocal: moment().valueOf()
+        });
       }
     },
 
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/alerts/index/route.js b/thirdeye/thirdeye-frontend/app/pods/manage/alerts/index/route.js
index 0e46995..cac8f9e 100644
--- a/thirdeye/thirdeye-frontend/app/pods/manage/alerts/index/route.js
+++ b/thirdeye/thirdeye-frontend/app/pods/manage/alerts/index/route.js
@@ -88,11 +88,13 @@ export default Route.extend({
         title: 'Applications',
         type: 'select',
         matchWidth: true,
+        hasNullOption: true, // allow searches for 'none'
         filterKeys: []
       },
       {
         name: 'subscription',
         title: 'Subscription Groups',
+        hasNullOption: true, // allow searches for 'none'
         type: 'select',
         filterKeys: []
       },
@@ -121,6 +123,7 @@ export default Route.extend({
     filterBlocksLocal.filter(block => block.type === 'select').forEach((filter) => {
       const alertPropertyArray = model.alerts.map(alert => alert[filterToPropertyMap[filter.name]]);
       const filterKeys = [ ...new Set(powerSort(alertPropertyArray, null))];
+      // Add filterKeys prop to each facet or filter block
       Object.assign(filter, { filterKeys });
     });
 
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/alerts/index/template.hbs b/thirdeye/thirdeye-frontend/app/pods/manage/alerts/index/template.hbs
index 23f474e..6ec41bb 100644
--- a/thirdeye/thirdeye-frontend/app/pods/manage/alerts/index/template.hbs
+++ b/thirdeye/thirdeye-frontend/app/pods/manage/alerts/index/template.hbs
@@ -3,6 +3,7 @@
     {{entity-filter
       title="Quick Filters"
       maxStrLen=25
+      currentFilterState=alertFilters
       resetFilters=resetFiltersGlobal
       filterBlocks=filterBlocksGlobal
       onSelectFilter=(action "userDidSelectFilter")
@@ -10,7 +11,7 @@
     {{entity-filter
       title="Filters"
       maxStrLen=25
-      selectDisabled=isSelectDisabled
+      currentFilterState=alertFilters
       resetFilters=resetFiltersLocal
       filterBlocks=filterBlocksLocal
       onSelectFilter=(action "userDidSelectFilter")
@@ -53,24 +54,25 @@
   </div>
 
   <div class="manage-alert-results">
-    {{#if paginatedSelectedAlerts}}
-      <section class="te-search-header">
-        <span class="te-search-title">Alerts ({{totalFilteredAlerts}})</span>
-        {{#if allowFilterSummary}}
-          <span class="te-search-filters">{{activeFiltersString}}</span>
-          {{#if (gt activeFiltersString.length maxFilterStrngLength)}}
-            <span class="te-search-header__icon">
-              <i class="glyphicon glyphicon-resize-full"></i>
-              {{#popover-on-element side="left" class="te-search-header__tooltip te-tooltip"}}{{activeFiltersString}}{{/popover-on-element}}
-            </span>
-          {{/if}}
+    <section class="te-search-header">
+      <span class="te-search-title">Alerts ({{totalFilteredAlerts}})</span>
+      {{#if allowFilterSummary}}
+        <span class="te-search-filters">{{activeFiltersString}}</span>
+        {{#if (gt activeFiltersString.length maxFilterStrngLength)}}
+          <span class="te-search-header__icon">
+            <i class="glyphicon glyphicon-resize-full"></i>
+            {{#popover-on-element side="left" class="te-search-header__tooltip te-tooltip"}}{{activeFiltersString}}{{/popover-on-element}}
+          </span>
         {{/if}}
+      {{/if}}
+      {{#if paginatedSelectedAlerts}}
         <span class="te-search-header__displaynum pull-right">
           Showing {{paginatedSelectedAlerts.length}} of
           {{totalFilteredAlerts}} {{if (gt totalFilteredAlerts 1) 'alerts' 'alert'}}
         </span>
-      </section>
-    {{/if}}
+      {{/if}}
+    </section>
+
     {{#if isLoading}}
       <div class="spinner-wrapper-self-serve spinner-wrapper-self-serve--fixed">{{ember-spinner}}</div>
     {{/if}}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org