You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pinot.apache.org by ji...@apache.org on 2020/08/05 18:11:46 UTC

[incubator-pinot] 01/01: ui for new anomalies page

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

jihao pushed a commit to branch new-anomalies-page-ui
in repository https://gitbox.apache.org/repos/asf/incubator-pinot.git

commit 20a747fbbef2e7af177985317aaf800b3fca8d39
Author: Jihao Zhang <ji...@linkedin.com>
AuthorDate: Wed Aug 5 11:11:17 2020 -0700

    ui for new anomalies page
---
 .../app/pods/anomalies/controller.js               | 446 +++++----------------
 .../thirdeye-frontend/app/pods/anomalies/route.js  | 144 ++-----
 .../app/pods/anomalies/template.hbs                |   2 +-
 .../pods/components/anomaly-summary/component.js   |  41 +-
 .../pods/components/anomaly-summary/template.hbs   |  14 +-
 .../app/pods/components/entity-filter/component.js |   9 +-
 thirdeye/thirdeye-frontend/app/utils/anomaly.js    |  54 ++-
 7 files changed, 239 insertions(+), 471 deletions(-)

diff --git a/thirdeye/thirdeye-frontend/app/pods/anomalies/controller.js b/thirdeye/thirdeye-frontend/app/pods/anomalies/controller.js
index 8011f91..05fe75e 100644
--- a/thirdeye/thirdeye-frontend/app/pods/anomalies/controller.js
+++ b/thirdeye/thirdeye-frontend/app/pods/anomalies/controller.js
@@ -3,22 +3,13 @@
  * @module manage/alerts/controller
  * @exports alerts controller
  */
-import _ from 'lodash';
-import {
-  set,
-  get,
-  computed,
-  getProperties,
-  setProperties
-} from '@ember/object';
-import { inject as service } from '@ember/service';
-import { isPresent, isEmpty } from '@ember/utils';
+import {computed, get, getProperties, set, setProperties} from '@ember/object';
+import {inject as service} from '@ember/service';
+import {isPresent} from '@ember/utils';
 import Controller from '@ember/controller';
-import { redundantParse } from 'thirdeye-frontend/utils/yaml-tools';
-import { reads } from '@ember/object/computed';
-import { toastOptions } from 'thirdeye-frontend/utils/constants';
-import { setUpTimeRangeOptions, powerSort } from 'thirdeye-frontend/utils/manage-alert-utils';
-import {  anomalyResponseObjNew } from 'thirdeye-frontend/utils/anomaly';
+import {reads} from '@ember/object/computed';
+import {setUpTimeRangeOptions} from 'thirdeye-frontend/utils/manage-alert-utils';
+import {searchAnomalyWithFilters} from 'thirdeye-frontend/utils/anomaly';
 import moment from 'moment';
 
 const TIME_PICKER_INCREMENT = 5; // tells date picker hours field how granularly to display time
@@ -29,8 +20,7 @@ const TIME_RANGE_OPTIONS = ['1d', '1w', '1m', '3m'];
 
 export default Controller.extend({
 
-  queryParams: ['testMode'],
-  store: service('store'),
+  queryParams: ['testMode'], store: service('store'),
 
   notifications: service('toast'),
 
@@ -69,9 +59,7 @@ export default Controller.extend({
   /**
    * Filter settings
    */
-  anomalyFilters: {},
-  resetFiltersLocal: null,
-  alertFoundByName: null,
+  anomalyFilters: {}, resetFiltersLocal: null, alertFoundByName: null,
 
   /**
    * The first and broadest entity search property
@@ -85,354 +73,139 @@ export default Controller.extend({
   pageSize: 10,
 
   // Number of pages to display
-  paginationSize: computed(
-    'pagesNum',
-    'pageSize',
-    function() {
-      const pagesNum = this.get('pagesNum');
-      const pageSize = this.get('pageSize');
-
-      return Math.min(pagesNum, pageSize/2);
-    }
-  ),
+  paginationSize: computed('pagesNum', 'pageSize', function () {
+    const pagesNum = this.get('pagesNum');
+    const pageSize = this.get('pageSize');
+
+    return Math.min(pagesNum, pageSize / 2);
+  }),
 
   // Total Number of pages to display
-  pagesNum: computed(
-    'totalAnomalies',
-    'pageSize',
-    function() {
-      const { pageSize, totalAnomalies } = getProperties(this, 'pageSize', 'totalAnomalies');
-      return Math.ceil(totalAnomalies/pageSize);
-    }
-  ),
+  pagesNum: computed('totalAnomalies', 'pageSize', function () {
+    const {pageSize, totalAnomalies} = getProperties(this, 'pageSize', 'totalAnomalies');
+    return Math.ceil(totalAnomalies / pageSize);
+  }),
 
   // creates the page Array for view
-  viewPages: computed(
-    'pages',
-    'currentPage',
-    'paginationSize',
-    'pageNums',
-    function() {
-      const size = this.get('paginationSize');
-      const currentPage = this.get('currentPage');
-      const max = this.get('pagesNum');
-      const step = Math.floor(size / 2);
-
-      if (max === 1) { return; }
-
-      const startingNumber = ((max - currentPage) < step)
-        ? Math.max(max - size + 1, 1)
-        : Math.max(currentPage - step, 1);
-
-      return [...new Array(size)].map((page, index) =>  startingNumber + index);
-    }
-  ),
-
-  // return list of anomalyIds according to filter(s) applied
-  selectedAnomalies: computed(
-    'anomalyFilters',
-    'anomalyIdList',
-    'activeFiltersString',
-    function() {
-      const {
-        anomalyIdList,
-        anomalyFilters,
-        anomaliesById,
-        activeFiltersString
-      } = this.getProperties('anomalyIdList', 'anomalyFilters', 'anomaliesById', 'activeFiltersString');
-      const filterMaps = ['statusFilterMap', 'functionFilterMap', 'datasetFilterMap', 'metricFilterMap', 'dimensionFilterMap'];
-      if (activeFiltersString === 'All Anomalies') {
-        // no filter applied, just return all
-        return anomalyIdList;
-      }
-      let selectedAnomalies = anomalyIdList;
-      filterMaps.forEach(map => {
-        const selectedFilters = anomalyFilters[map];
-        // When a filter gets deleted, it leaves an empty array behind.  We need to treat null and empty array the same here
-        if (!isEmpty(selectedFilters)) {
-          // a filter is selected, grab relevant anomalyIds
-          selectedAnomalies = this._intersectOfArrays(selectedAnomalies, this._unionOfArrays(anomaliesById, map, anomalyFilters[map]));
-        }
-      });
-      return selectedAnomalies;
+  viewPages: computed('pages', 'currentPage', 'paginationSize', 'pageNums', function () {
+    const size = this.get('paginationSize');
+    const currentPage = this.get('currentPage');
+    const max = this.get('pagesNum');
+    const step = Math.floor(size / 2);
+
+    if (max === 1) {
+      return;
     }
-  ),
 
-  totalAnomalies: computed(
-    'selectedAnomalies',
-    function() {
-      return get(this, 'selectedAnomalies').length;
-    }
-  ),
+    const startingNumber = ((max - currentPage) < step) ? Math.max(max - size + 1, 1) : Math.max(currentPage - step, 1);
 
-  noAnomalies: computed(
-    'totalAnomalies',
-    function() {
-      return (get(this, 'totalAnomalies') === 0);
-    }
-  ),
-
-  paginatedSelectedAnomalies: computed(
-    'selectedAnomalies.@each',
-    'filtersTriggered',
-    'pageSize',
-    'currentPage',
-    function() {
-      const {
-        pageSize,
-        currentPage
-      } = getProperties(this, 'pageSize', 'currentPage');
-      // Initial set of anomalies
-      let anomalies = this.get('selectedAnomalies');
-      // Return one page of sorted anomalies
-      return anomalies.slice((currentPage - 1) * pageSize, currentPage * pageSize);
-    }
-  ),
+    return [...new Array(size)].map((page, index) => startingNumber + index);
+  }),
+
+  totalAnomalies: computed('searchResult', function () {
+    return get(this, 'searchResult').count;
+  }),
+
+  noAnomalies: computed('totalAnomalies', function () {
+    return (get(this, 'totalAnomalies') === 0);
+  }),
+
+  paginatedSelectedAnomalies: computed('searchResult', function () {
+    // Return one page of sorted anomalies
+    return get(this, 'searchResult').elements;
+  }),
 
   /**
    * Date types to display in the pills
    * @type {Object[]} - array of objects, each of which represents each date pill
    */
-  pill: computed(
-    'anomaliesRange', 'startDate', 'endDate', 'duration',
-    function() {
-      const anomaliesRange = get(this, 'anomaliesRange');
-      const startDate = Number(anomaliesRange[0]);
-      const endDate = Number(anomaliesRange[1]);
-      const duration = get(this, 'duration') || DEFAULT_ACTIVE_DURATION;
-      const predefinedRanges = {
-        'Today': [moment().startOf('day'), moment().startOf('day').add(1, 'days')],
-        'Last 24 hours': [moment().subtract(1, 'day'), moment()],
-        'Yesterday': [moment().subtract(1, 'day').startOf('day'), moment().startOf('day')],
-        'Last Week': [moment().subtract(1, 'week').startOf('day'), moment().startOf('day')]
-      };
-
-      return {
-        uiDateFormat: UI_DATE_FORMAT,
-        activeRangeStart: moment(startDate).format(DISPLAY_DATE_FORMAT),
-        activeRangeEnd: moment(endDate).format(DISPLAY_DATE_FORMAT),
-        timeRangeOptions: setUpTimeRangeOptions(TIME_RANGE_OPTIONS, duration),
-        timePickerIncrement: TIME_PICKER_INCREMENT,
-        predefinedRanges
-      };
-    }
-  ),
+  pill: computed('anomaliesRange', 'startDate', 'endDate', 'duration', function () {
+    const anomaliesRange = get(this, 'anomaliesRange');
+    const startDate = Number(anomaliesRange[0]);
+    const endDate = Number(anomaliesRange[1]);
+    const duration = get(this, 'duration') || DEFAULT_ACTIVE_DURATION;
+    const predefinedRanges = {
+      'Today': [moment().startOf('day'), moment().startOf('day').add(1, 'days')],
+      'Last 24 hours': [moment().subtract(1, 'day'), moment()],
+      'Yesterday': [moment().subtract(1, 'day').startOf('day'), moment().startOf('day')],
+      'Last Week': [moment().subtract(1, 'week').startOf('day'), moment().startOf('day')]
+    };
+
+    return {
+      uiDateFormat: UI_DATE_FORMAT,
+      activeRangeStart: moment(startDate).format(DISPLAY_DATE_FORMAT),
+      activeRangeEnd: moment(endDate).format(DISPLAY_DATE_FORMAT),
+      timeRangeOptions: setUpTimeRangeOptions(TIME_RANGE_OPTIONS, duration),
+      timePickerIncrement: TIME_PICKER_INCREMENT,
+      predefinedRanges
+    };
+  }),
 
   // String containing all selected filters for display
-  activeFiltersString: computed(
-    'anomalyFilters',
-    'filtersTriggered',
-    function() {
-      const anomalyFilters = get(this, 'anomalyFilters');
-      const filterAbbrevMap = {
-        functionFilterMap: 'function',
-        datasetFilterMap: 'dataset',
-        statusFilterMap: 'status',
-        metricFilterMap: 'metric',
-        dimensionFilterMap: 'dimension'
-      };
-      let filterStr = 'All Anomalies';
-      if (isPresent(anomalyFilters)) {
-        let filterArr = [get(this, 'primaryFilterVal')];
-        Object.keys(anomalyFilters).forEach((filterKey) => {
-          const value = anomalyFilters[filterKey];
-          const isStatusAll = filterKey === 'status' && Array.isArray(value) && value.length > 1;
-          // Only display valid search filters
-          if (filterKey !== 'triggerType' && value !== null && value.length && !isStatusAll) {
-            let concatVal = filterKey === 'status' && !value.length ? 'Active' : value.join(', ');
-            let abbrevKey = filterAbbrevMap[filterKey] || filterKey;
-            filterArr.push(`${abbrevKey}: ${concatVal}`);
-          }
-        });
-        filterStr = filterArr.join(' | ');
-      }
-      return filterStr;
+  activeFiltersString: computed('anomalyFilters', 'filtersTriggered', function () {
+    const anomalyFilters = get(this, 'anomalyFilters');
+    const filterAbbrevMap = {
+      functionFilterMap: 'function',
+      datasetFilterMap: 'dataset',
+      statusFilterMap: 'status',
+      metricFilterMap: 'metric',
+      dimensionFilterMap: 'dimension'
+    };
+    let filterStr = 'All Anomalies';
+    if (isPresent(anomalyFilters)) {
+      let filterArr = [get(this, 'primaryFilterVal')];
+      Object.keys(anomalyFilters).forEach((filterKey) => {
+        const value = anomalyFilters[filterKey];
+        const isStatusAll = filterKey === 'status' && Array.isArray(value) && value.length > 1;
+        // Only display valid search filters
+        if (filterKey !== 'triggerType' && value !== null && value.length && !isStatusAll) {
+          let concatVal = filterKey === 'status' && !value.length ? 'Active' : value.join(', ');
+          let abbrevKey = filterAbbrevMap[filterKey] || filterKey;
+          filterArr.push(`${abbrevKey}: ${concatVal}`);
+        }
+      });
+      filterStr = filterArr.join(' | ');
     }
-  ),
+    return filterStr;
+  }),
 
   // When the user changes the time range, this will fetch the anomaly ids
   _updateVisuals() {
     const {
-      anomaliesRange,
-      updateAnomalies,
-      anomalyIds
-    } = this.getProperties('anomaliesRange', 'updateAnomalies', 'anomalyIds');
-    set(this, 'isLoading', true);
-    const [ start, end ] = anomaliesRange;
-    if (anomalyIds) {
-      set(this, 'anomalyIds', null);
-    } else {
-      updateAnomalies(start, end)
-        .then(res => {
-          this.setProperties({
-            anomaliesById: res,
-            anomalyIdList: res.anomalyIds
-          });
-          this._resetLocalFilters();
-          set(this, 'isLoading', false);
-        })
-        .catch(() => {
-          this._resetLocalFilters();
-          set(this, 'isLoading', false);
-        });
-    }
-  },
-
-  /**
-   * When user chooses to either find an alert by name, or use a global filter,
-   * we should re-set all local filters.
-   * @method _resetFilters
-   * @param {Boolean} isSelectDisabled
-   * @returns {undefined}
-   * @private
-   */
-  _resetLocalFilters() {
-    let anomalyFilters = {};
-    const newFilterBlocksLocal = _.cloneDeep(get(this, 'initialFiltersLocal'));
-    const anomaliesById = get(this, 'anomaliesById');
-
-    // Fill in select options for these filters ('filterKeys') based on alert properties from model.alerts
-    newFilterBlocksLocal.forEach((filter) => {
-      let filterKeys = [];
-      if (filter.name === "dimensionFilterMap" && isPresent(anomaliesById.searchFilters[filter.name])) {
-        const anomalyPropertyArray = Object.keys(anomaliesById.searchFilters[filter.name]);
-        anomalyPropertyArray.forEach(dimensionType => {
-          let group = Object.keys(anomaliesById.searchFilters[filter.name][dimensionType]);
-          group = group.map(dim => `${dimensionType}::${dim}`);
-          filterKeys = [...filterKeys, ...group];
-        });
-      } else if (filter.name === "subscriptionFilterMap"){
-        filterKeys = this.get('store')
-          .peekAll('subscription-groups')
-          .sortBy('name')
-          .filter(group => (group.get('active') && group.get('yaml')))
-          .map(group => group.get('name'));
-      } else if (filter.name === "statusFilterMap" && isPresent(anomaliesById.searchFilters[filter.name])){
-        let anomalyPropertyArray = Object.keys(anomaliesById.searchFilters[filter.name]);
-        anomalyPropertyArray = anomalyPropertyArray.map(prop => {
-          // get the right object
-          const mapping = anomalyResponseObjNew.filter(e => (e.status === prop));
-          // map the status to name
-          return mapping.length > 0 ? mapping[0].name : prop;
-        });
-        filterKeys = [ ...new Set(powerSort(anomalyPropertyArray, null))];
-      } else {
-        if (isPresent(anomaliesById.searchFilters[filter.name])) {
-          const anomalyPropertyArray = Object.keys(anomaliesById.searchFilters[filter.name]);
-          filterKeys = [ ...new Set(powerSort(anomalyPropertyArray, null))];
-        }
-      }
-      // Add filterKeys prop to each facet or filter block
-      Object.assign(filter, { filterKeys });
-    });
-    // Reset local (secondary) filters, and set select fields to 'disabled'
-    setProperties(this, {
-      filterBlocksLocal: newFilterBlocksLocal,
-      resetFiltersLocal: moment().valueOf(),
-      anomalyFilters
-    });
-  },
-
-  // method to union anomalyId arrays for filters applied of same type
-  _unionOfArrays(anomaliesById, filterType, selectedFilters) {
-    //handle dimensions separately, since they are nested
-    let addedIds = [];
-    if (filterType === 'dimensionFilterMap' && isPresent(anomaliesById.searchFilters[filterType])) {
-      selectedFilters.forEach(filter => {
-        const [type, dimension] = filter.split('::');
-        addedIds = [...addedIds, ...anomaliesById.searchFilters.dimensionFilterMap[type][dimension]];
-      });
-    } else if (filterType === 'statusFilterMap' && isPresent(anomaliesById.searchFilters[filterType])){
-      const translatedFilters = selectedFilters.map(f => {
-        // get the right object
-        const mapping = anomalyResponseObjNew.filter(e => (e.name === f));
-        // map the name to status
-        return mapping.length > 0 ? mapping[0].status : f;
-      });
-      translatedFilters.forEach(filter => {
-        addedIds = [...addedIds, ...anomaliesById.searchFilters[filterType][filter]];
-      });
-    } else {
-      if (isPresent(anomaliesById.searchFilters[filterType])) {
-        selectedFilters.forEach(filter => {
-          // If there are no anomalies from the time range with these filters, then the result will be null, so we handle that here
-          // It can happen for functionFilterMap only, because we are using subscription groups to map to alert names (function filters)
-          const anomalyIdsInResponse = anomaliesById.searchFilters[filterType][filter];
-          addedIds = anomalyIdsInResponse ? [...addedIds, ...anomaliesById.searchFilters[filterType][filter]] : addedIds;
-        });
-      }
-    }
-    return addedIds;
-  },
+      anomaliesRange, anomalyIds, pageSize, currentPage, anomalyFilters
+    } = this.getProperties('anomaliesRange', 'anomalyIds', 'pageSize', 'currentPage', 'anomalyFilters');
 
-  // method to intersect anomalyId arrays for filters applied of different types
-  // i.e. we want anomalies that have both characteristics when filter type is different
-  _intersectOfArrays(existingArray, incomingArray) {
-    return existingArray.filter(anomalyId => incomingArray.includes(anomalyId));
-  },
-
-  /**
-   * This will retrieve the subscription groups from Ember Data and extract yaml configs
-   * The yaml configs are used to extract alert names and apply them as filters
-   * @method _subscriptionGroupFilter
-   * @param {Object} filterObj
-   * @returns {Object}
-   * @private
-   */
-  _subscriptionGroupFilter(filterObj) {
-    // get selected subscription groups, if any
-    const notifications = get(this, 'notifications');
-    const selectedSubGroups = filterObj['subscriptionFilterMap'];
-    if (Array.isArray(selectedSubGroups) && selectedSubGroups.length > 0) {
-      // extract selected subscription groups from Ember Data
-      const selectedSubGroupObjects = this.get('store')
-        .peekAll('subscription-groups')
-        .filter(group => {
-          return selectedSubGroups.includes(group.get('name'));
+    set(this, 'isLoading', true);
+    const [start, end] = anomaliesRange;
+    searchAnomalyWithFilters(pageSize * (currentPage - 1), pageSize, anomalyIds ? null : start, anomalyIds ? null : end,
+      anomalyFilters.feedbackStatus, anomalyFilters.subscription, anomalyFilters.alertName, anomalyFilters.metric,
+      anomalyFilters.dataset, anomalyIds)
+      .then(res => {
+        this.setProperties({
+          searchResult: res
         });
-      let additionalAlertNames = [];
-      // for each group, grab yaml, extract alert names for adding to filterObj
-      selectedSubGroupObjects.forEach(group => {
-        let yamlAsObject;
-        try {
-          yamlAsObject = redundantParse(group.get('yaml'));
-          if (Array.isArray(yamlAsObject.subscribedDetections)) {
-            additionalAlertNames = [ ...additionalAlertNames, ...yamlAsObject.subscribedDetections];
-          }
-        }
-        catch(error){
-          notifications.error(`Failed to retrieve alert names for subscription group: ${group.get('name')}`, 'Error', toastOptions);
-        }
+        set(this, 'isLoading', false);
+      })
+      .catch(() => {
+        set(this, 'isLoading', false);
       });
-      // add the alert names extracted from groups to any that are already present
-      let updatedFunctionFilterMap = Array.isArray(filterObj['functionFilterMap']) ? [ ...filterObj['functionFilterMap'], ...additionalAlertNames] : additionalAlertNames;
-      updatedFunctionFilterMap = [ ...new Set(powerSort(updatedFunctionFilterMap, null))];
-      set(filterObj, 'functionFilterMap', updatedFunctionFilterMap);
-    }
-    return filterObj;
   },
 
   actions: {
     // Clears all selected filters at once
     clearFilters() {
-      this._resetLocalFilters();
+      set(this, 'anomalyFilters', {});
+      this._updateVisuals();
     },
 
     // Handles filter selections (receives array of filter options)
     userDidSelectFilter(filterObj) {
-      const filterBlocksLocal = get(this, 'filterBlocksLocal');
-      // handle special case of subscription groups
-      filterObj = this._subscriptionGroupFilter(filterObj);
-      filterBlocksLocal.forEach(block => {
-        block.selected = filterObj[block.name];
-      });
       setProperties(this, {
-        filtersTriggered: true,
-        allowFilterSummary: true,
-        anomalyFilters: filterObj
+        filtersTriggered: true, allowFilterSummary: true, anomalyFilters: filterObj
       });
       // Reset current page
       set(this, 'currentPage', 1);
+      this._updateVisuals();
     },
 
     /**
@@ -442,9 +215,7 @@ export default Controller.extend({
      */
     onRangeSelection(timeRangeOptions) {
       const {
-        start,
-        end,
-        value: duration
+        start, end, value: duration
       } = timeRangeOptions;
 
       const startDate = moment(start).valueOf();
@@ -452,6 +223,8 @@ export default Controller.extend({
       //Update the time range option selected
       set(this, 'anomaliesRange', [startDate, endDate]);
       set(this, 'duration', duration);
+      set(this, 'anomalyIds', null);
+      set(this, 'currentPage', 1)
       this._updateVisuals();
     },
 
@@ -486,6 +259,7 @@ export default Controller.extend({
       }
 
       this.set('currentPage', newPage);
+      this._updateVisuals();
     }
   }
 });
diff --git a/thirdeye/thirdeye-frontend/app/pods/anomalies/route.js b/thirdeye/thirdeye-frontend/app/pods/anomalies/route.js
index e2f3c6a..9c4bf8f 100644
--- a/thirdeye/thirdeye-frontend/app/pods/anomalies/route.js
+++ b/thirdeye/thirdeye-frontend/app/pods/anomalies/route.js
@@ -1,22 +1,17 @@
-import { hash } from 'rsvp';
+import {hash} from 'rsvp';
 import Route from '@ember/routing/route';
 import moment from 'moment';
-import { inject as service } from '@ember/service';
-import { isPresent } from '@ember/utils';
-import { powerSort } from 'thirdeye-frontend/utils/manage-alert-utils';
-import {
-  getAnomalyFiltersByTimeRange,
-  getAnomalyFiltersByAnomalyId,
-  anomalyResponseObjNew } from 'thirdeye-frontend/utils/anomaly';
+import {inject as service} from '@ember/service';
+import {anomalyResponseObj, searchAnomaly} from 'thirdeye-frontend/utils/anomaly';
 import _ from 'lodash';
 import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
 
 const start = moment().subtract(1, 'day').valueOf();
 const end = moment().valueOf();
+const pagesize = 10;
 
 const queryParamsConfig = {
-  refreshModel: true,
-  replace: false
+  refreshModel: true, replace: false
 };
 
 export default Route.extend(AuthenticatedRouteMixin, {
@@ -33,14 +28,15 @@ export default Route.extend(AuthenticatedRouteMixin, {
 
   async model(params) {
     // anomalyIds param allows for clicking into the route from email and listing a specific set of anomalyIds
-    let { anomalyIds } = params;
-    const anomaliesById = anomalyIds ? await getAnomalyFiltersByAnomalyId(start, end, anomalyIds) : await getAnomalyFiltersByTimeRange(start, end);
-    const subscriptionGroups = await this.get('anomaliesApiService').querySubscriptionGroups(); // Get all subscription groups available
+    let {anomalyIds} = params;
+    let searchResult;
+    if (anomalyIds) {
+      anomalyIds = anomalyIds.split(",");
+    }
+    // query anomalies
+    searchResult = searchAnomaly(0, pagesize, anomalyIds ? null : start, anomalyIds ? null : end, anomalyIds);
     return hash({
-      updateAnomalies:  getAnomalyFiltersByTimeRange,
-      anomaliesById,
-      subscriptionGroups,
-      anomalyIds
+      updateAnomalies: searchAnomaly, anomalyIds, searchResult
     });
   },
 
@@ -51,87 +47,28 @@ export default Route.extend(AuthenticatedRouteMixin, {
     const defaultParams = {
       anomalyIds
     };
-    Object.assign(model, { ...defaultParams});
+    Object.assign(model, {...defaultParams});
     return model;
   },
 
   setupController(controller, model) {
-
     // This filter category is "secondary". To add more, add an entry here and edit the controller's "filterToPropertyMap"
-    const filterBlocksLocal = [
-      {
-        name: 'statusFilterMap',
-        title: 'Feedback Status',
-        type: 'select',
-        matchWidth: true,
-        filterKeys: []
-      },
-      {
-        name: 'functionFilterMap',
-        title: 'Alert Names',
-        type: 'select',
-        filterKeys: []
-      },
-      {
-        name: 'datasetFilterMap',
-        title: 'Dataset',
-        type: 'select',
-        filterKeys: []
-      },
-      {
-        name: 'metricFilterMap',
-        title: 'Metric',
-        type: 'select',
-        filterKeys: []
-      },
-      {
-        name: 'dimensionFilterMap',
-        title: 'Dimension',
-        type: 'select',
-        matchWidth: true,
-        filterKeys: []
-      },
-      {
-        name: 'subscriptionFilterMap',
-        title: 'Subscription Groups',
-        type: 'select',
-        filterKeys: []
-      }
-    ];
-
-    // Fill in select options for these filters ('filterKeys') based on alert properties from model.alerts
-    filterBlocksLocal.forEach((filter) => {
-      let filterKeys = [];
-      if (filter.name === "dimensionFilterMap" && isPresent(model.anomaliesById.searchFilters[filter.name])) {
-        const anomalyPropertyArray = Object.keys(model.anomaliesById.searchFilters[filter.name]);
-        anomalyPropertyArray.forEach(dimensionType => {
-          let group = Object.keys(model.anomaliesById.searchFilters[filter.name][dimensionType]);
-          group = group.map(dim => `${dimensionType}::${dim}`);
-          filterKeys = [...filterKeys, ...group];
-        });
-      } else if (filter.name === "statusFilterMap" && isPresent(model.anomaliesById.searchFilters[filter.name])){
-        let anomalyPropertyArray = Object.keys(model.anomaliesById.searchFilters[filter.name]);
-        anomalyPropertyArray = anomalyPropertyArray.map(prop => {
-          // get the right object
-          const mapping = anomalyResponseObjNew.filter(e => (e.status === prop));
-          // map the status to name
-          return mapping.length > 0 ? mapping[0].name : prop;
-        });
-        filterKeys = [ ...new Set(powerSort(anomalyPropertyArray, null))];
-      } else if (filter.name === "subscriptionFilterMap"){
-        filterKeys = this.get('store')
-          .peekAll('subscription-groups')
-          .sortBy('name')
-          .filter(group => (group.get('active') && group.get('yaml')))
-          .map(group => group.get('name'));
-      } else {
-        if (isPresent(model.anomaliesById.searchFilters[filter.name])) {
-          const anomalyPropertyArray = Object.keys(model.anomaliesById.searchFilters[filter.name]);
-          filterKeys = [ ...new Set(powerSort(anomalyPropertyArray, null))];
-        }
-      }
-      Object.assign(filter, { filterKeys });
-    });
+    const filterBlocksLocal = [{
+      name: 'alertName', title: 'Alert Names', type: 'search', filterKeys: []
+    }, {
+      name: 'dataset', title: 'Datasets', type: 'search', filterKeys: []
+    }, {
+      name: 'metric', title: 'Metrics', type: 'search', filterKeys: []
+    }, {
+      name: 'feedbackStatus',
+      title: 'Feedback Status',
+      type: 'select',
+      matchWidth: true,
+      filterKeys: anomalyResponseObj.map(f => f.name)
+    }, {
+      name: 'subscription', title: 'Subscription Groups', hasNullOption: true, // allow searches for 'none'
+      type: 'search', filterKeys: []
+    }];
 
     // Keep an initial copy of the secondary filter blocks in memory
     Object.assign(model, {
@@ -139,15 +76,9 @@ export default Route.extend(AuthenticatedRouteMixin, {
     });
     // Send filters to controller
     controller.setProperties({
-      model,
-      anomaliesById: model.anomaliesById,
-      resultsActive: true,
-      updateAnomalies: model.updateAnomalies,  //requires start and end time in epoch ex updateAnomalies(start, end)
-      filterBlocksLocal,
-      anomalyIdList: model.anomaliesById.anomalyIds,
-      anomaliesRange: [start, end],
-      subscriptionGroups: model.subscriptionGroups,
-      anomalyIds: this.get('anomalyIds')
+      model, resultsActive: true, updateAnomalies: model.updateAnomalies,  //requires start and end time in epoch ex updateAnomalies(start, end)
+      filterBlocksLocal, anomaliesRange: [start, end], anomalyIds: this.get('anomalyIds'), // url params
+      searchResult: model.searchResult
     });
   },
 
@@ -164,8 +95,7 @@ export default Route.extend(AuthenticatedRouteMixin, {
       if (transition.intent.name && transition.intent.name !== 'logout') {
         this.set('session.store.fromUrl', {lastIntentTransition: transition});
       }
-    },
-    error() {
+    }, error() {
       // The `error` hook is also provided the failed
       // `transition`, which can be stored and later
       // `.retry()`d if desired.
@@ -180,9 +110,9 @@ export default Route.extend(AuthenticatedRouteMixin, {
     },
 
     /**
-    * Refresh route's model.
-    * @method refreshModel
-    */
+     * Refresh route's model.
+     * @method refreshModel
+     */
     refreshModel() {
       this.refresh();
     }
diff --git a/thirdeye/thirdeye-frontend/app/pods/anomalies/template.hbs b/thirdeye/thirdeye-frontend/app/pods/anomalies/template.hbs
index fc1bdf5..96ac454 100644
--- a/thirdeye/thirdeye-frontend/app/pods/anomalies/template.hbs
+++ b/thirdeye/thirdeye-frontend/app/pods/anomalies/template.hbs
@@ -63,7 +63,7 @@
           {{#each paginatedSelectedAnomalies as |anomaly|}}
             <section class="te-search-results">
               {{anomaly-summary
-                anomalyId=anomaly
+                anomalyData = anomaly
               }}
             </section>
           {{/each}}
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/anomaly-summary/component.js b/thirdeye/thirdeye-frontend/app/pods/components/anomaly-summary/component.js
index 0be588a..8adf8dc 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/anomaly-summary/component.js
+++ b/thirdeye/thirdeye-frontend/app/pods/components/anomaly-summary/component.js
@@ -16,7 +16,8 @@ import { getFormattedDuration,
   verifyAnomalyFeedback,
   anomalyResponseObj,
   anomalyResponseObjNew,
-  updateAnomalyFeedback
+  updateAnomalyFeedback,
+  anomalyTypeMapping
 } from 'thirdeye-frontend/utils/anomaly';
 import RSVP from "rsvp";
 import fetch from 'fetch';
@@ -211,7 +212,8 @@ export default Component.extend({
           shownChangeRate: humanizeFloat(change),
           anomalyFeedback: a.feedback ? a.feedback.feedbackType : "NONE",
           showResponseSaved: (labelResponse.anomalyId === a.id) ? labelResponse.showResponseSaved : false,
-          showResponseFailed: (labelResponse.anomalyId === a.id) ? labelResponse.showResponseFailed: false
+          showResponseFailed: (labelResponse.anomalyId === a.id) ? labelResponse.showResponseFailed: false,
+          type: anomalyTypeMapping[a.type]
         };
       }
       return tableAnomaly;
@@ -230,34 +232,29 @@ export default Component.extend({
   ),
 
   _fetchAnomalyData() {
-    const anomalyId = get(this, 'anomalyId');
-    const anomalyUrl = `/dashboard/anomalies/view/${anomalyId}`;
+    const anomalyData = get(this, 'anomalyData');
+    const anomalyId = anomalyData.id;
 
+    set(this, 'anomalyId', anomalyId);
     set(this, 'isLoading', true);
 
-    fetch(anomalyUrl)
-      .then(checkStatus)
-      .then(res => {
-        set(this, 'anomalyData', res);
-        const predictedUrl = `/detection/predicted-baseline/${anomalyId}?start=${res.startTime}&end=${res.endTime}&padding=true`;
-        const timeseriesHash = {
-          predicted: fetch(predictedUrl).then(res => checkStatus(res, 'get', true))
-        };
-        return RSVP.hash(timeseriesHash);
-      })
-      .then((res) => {
-        if (!(this.get('isDestroyed') || this.get('isDestroying'))) {
-          set(this, 'current', res.predicted);
-          set(this, 'predicted', res.predicted);
-          set(this, 'isLoading', false);
-        }
-      })
+    const predictedUrl = `/detection/predicted-baseline/${anomalyId}?start=${anomalyData.startTime}&end=${anomalyData.endTime}&padding=true`;
+    const timeseriesHash = {
+      predicted: fetch(predictedUrl).then(res => checkStatus(res, 'get', true))
+    };
+    RSVP.hash(timeseriesHash).then((res) => {
+      if (!(this.get('isDestroyed') || this.get('isDestroying'))) {
+        set(this, 'current', res.predicted);
+        set(this, 'predicted', res.predicted);
+        set(this, 'isLoading', false);
+      }
+    })
       .catch(() => {
         if (!(this.get('isDestroyed') || this.get('isDestroying'))) {
           set(this, 'isLoading', false);
         }
       });
-  },
+    },
 
   _formatAnomaly(anomaly) {
     return `${moment(anomaly.startTime).format(TABLE_DATE_FORMAT)}`;
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/anomaly-summary/template.hbs b/thirdeye/thirdeye-frontend/app/pods/components/anomaly-summary/template.hbs
index 3303c1e..d8b47ae 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/anomaly-summary/template.hbs
+++ b/thirdeye/thirdeye-frontend/app/pods/components/anomaly-summary/template.hbs
@@ -44,6 +44,11 @@
           </th>
           <th class="te-anomaly-table__cell-head">
             <a class="te-anomaly-table__cell-link">
+              Anomaly Type
+            </a>
+          </th>
+          <th class="te-anomaly-table__cell-head">
+            <a class="te-anomaly-table__cell-link">
               Feedback
             </a>
           </th>
@@ -74,7 +79,14 @@
               </li>
             </ul>
            </td>
-           <td class="te-anomaly-table__cell">
+          <td class="te-anomaly-table__cell">
+            <ul class="te-anomaly-table__list te-anomaly-table__list--left">
+              <li class="te-anomaly-table__list-item te-anomaly-table__list-item--stronger">
+                {{anomaly.type}}
+              </li>
+            </ul>
+          </td>
+          <td class="te-anomaly-table__cell">
               {{#if renderStatusIcon}}
                 {{#if anomaly.showResponseSaved}}
                   <i class="te-anomaly-table__icon--status glyphicon glyphicon-ok-circle"></i>
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 2f88193..b641a5c 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/entity-filter/component.js
+++ b/thirdeye/thirdeye-frontend/app/pods/components/entity-filter/component.js
@@ -121,7 +121,7 @@ export default Component.extend({
       case 'metric': {
         return fetch(autocompleteAPI.metric(text))
           .then(checkStatus)
-          .then(metrics => metrics.map(m => m.name));
+          .then(metrics => [...new Set(metrics.map(m => m.name))]);
       }
       case 'application': {
         return fetch(autocompleteAPI.application(text))
@@ -140,7 +140,12 @@ export default Component.extend({
       case 'dataset': {
         return fetch(autocompleteAPI.dataset(text))
           .then(checkStatus)
-          .then(datasets => datasets.map(d => d.name));
+          .then(datasets =>  [...new Set(datasets.map(d => d.name))]);
+      }
+      case 'alertName': {
+        return fetch(autocompleteAPI.alertByName(text))
+          .then(checkStatus)
+          .then(detections => detections.map(d => d.name));
       }
     }
   }),
diff --git a/thirdeye/thirdeye-frontend/app/utils/anomaly.js b/thirdeye/thirdeye-frontend/app/utils/anomaly.js
index 3bbb2b8..3a2542b 100644
--- a/thirdeye/thirdeye-frontend/app/utils/anomaly.js
+++ b/thirdeye/thirdeye-frontend/app/utils/anomaly.js
@@ -79,6 +79,10 @@ export const anomalyResponseObjNew = [
   }
 ];
 
+export const anomalyTypeMapping = {
+  "DEVIATION": "Metric Deviation", "TREND_CHANGE": "Trend Change", "DATA_SLA": "SLA Violation"
+}
+
 /**
  * Mapping for anomalyResponseObj 'status' to 'name' for easy lookup
  */
@@ -95,7 +99,6 @@ anomalyResponseObjNew.forEach((obj) => {
   anomalyResponseMapNew[obj.value] = obj.name;
 });
 
-
 /**
  * Update feedback status on any anomaly
  * @method updateAnomalyFeedback
@@ -243,6 +246,50 @@ export function pluralizeTime(time, unit) {
   return time ? time + ' ' + unitStr : '';
 }
 
+export function searchAnomaly(offset, limit, startTime, endTime, anomalyIds) {
+  return searchAnomalyWithFilters(offset, limit, startTime, endTime, [], [], [], [], [], anomalyIds)
+}
+
+export function searchAnomalyWithFilters(offset, limit, startTime, endTime, feedbackStatuses, subscriptionGroups,
+  detectionNames, metrics, datasets, anomalyIds) {
+  let url = `/anomaly-search?offset=${offset}&limit=${limit}`;
+  if (startTime) {
+    url = url.concat(`&startTime=${startTime}`);
+  }
+  if (endTime) {
+    url = url.concat(`&endTime=${endTime}`);
+  }
+  feedbackStatuses = feedbackStatuses || [];
+  for (const feedbackStatus of feedbackStatuses) {
+    const feedback = anomalyResponseObj.find(feedback => feedback.name === feedbackStatus)
+    if (feedback) {
+      url = url.concat(`&feedbackStatus=${feedback.value}`);
+    }
+  }
+  subscriptionGroups = subscriptionGroups || [];
+  for (const subscriptionGroup of subscriptionGroups) {
+    url = url.concat(`&subscriptionGroup=${subscriptionGroup}`);
+  }
+  detectionNames = detectionNames || [];
+  for (const detectionName of detectionNames) {
+    url = url.concat(`&detectionName=${detectionName}`);
+  }
+  metrics = metrics || [];
+  for (const metric of metrics) {
+    url = url.concat(`&metric=${metric}`);
+  }
+  datasets = datasets || [];
+  for (const dataset of datasets) {
+    url = url.concat(`&dataset=${dataset}`);
+  }
+  anomalyIds = anomalyIds || [];
+  for (const anomalyId of anomalyIds) {
+    url = url.concat(`&anomalyId=${anomalyId}`);
+  }
+  return fetch(url).then(checkStatus);
+}
+
+
 export default {
   anomalyResponseObj,
   anomalyResponseMap,
@@ -255,5 +302,8 @@ export default {
   putAlertActiveStatus,
   getYamlPreviewAnomalies,
   getAnomaliesByAlertId,
-  getBounds
+  getBounds,
+  searchAnomaly,
+  searchAnomalyWithFilters,
+  anomalyTypeMapping
 };


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