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]