apucher closed pull request #3489: [TE] frontend - alert filter UX improvements
URL: https://github.com/apache/incubator-pinot/pull/3489
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

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 8c5006326a..276a6e70b1 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 aafbb8ce53..076ba6046a 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 a743ff56fe..bd9b4fdb83 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);
@@ -256,6 +269,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
@@ -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 0e46995106..cac8f9e0b8 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 23f474e7ce..6ec41bbe58 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}}


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
[email protected]


With regards,
Apache Git Services

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to