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 2019/04/26 00:55:58 UTC

[incubator-pinot] branch master updated: [TE] frontend - harleyjj/anomalies - enable anomalyIds param for email linking to new anomalies route (#4164)

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

jihao 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 b7b6ba3  [TE] frontend - harleyjj/anomalies - enable anomalyIds param for email linking to new anomalies route (#4164)
b7b6ba3 is described below

commit b7b6ba35dc6b5c39e5f4527e063e021c112a877c
Author: Harley Jackson <hj...@linkedin.com>
AuthorDate: Thu Apr 25 17:55:53 2019 -0700

    [TE] frontend - harleyjj/anomalies - enable anomalyIds param for email linking to new anomalies route (#4164)
---
 .../app/pods/anomalies/controller.js               | 121 ++++++++++-----------
 .../thirdeye-frontend/app/pods/anomalies/route.js  |  69 ++++++++----
 .../pods/components/anomaly-summary/component.js   |   8 +-
 .../pods/components/anomaly-summary/template.hbs   |   1 +
 thirdeye/thirdeye-frontend/app/utils/anomaly.js    |  24 +++-
 .../thirdeye-frontend/app/utils/api/anomaly.js     |  20 +++-
 6 files changed, 147 insertions(+), 96 deletions(-)

diff --git a/thirdeye/thirdeye-frontend/app/pods/anomalies/controller.js b/thirdeye/thirdeye-frontend/app/pods/anomalies/controller.js
index 13be4dd..f2bc02b 100644
--- a/thirdeye/thirdeye-frontend/app/pods/anomalies/controller.js
+++ b/thirdeye/thirdeye-frontend/app/pods/anomalies/controller.js
@@ -9,8 +9,7 @@ import {
   get,
   computed,
   getProperties,
-  setProperties,
-  observer
+  setProperties
 } from '@ember/object';
 import { inject as service } from '@ember/service';
 import { isPresent, isEmpty } from '@ember/utils';
@@ -97,33 +96,6 @@ export default Controller.extend({
     }
   ),
 
-  // When the user changes the time range, this will fetch the anomaly ids
-  updateVisuals: observer(
-    'anomaliesRange',
-    function() {
-      const {
-        anomaliesRange,
-        updateAnomalies
-      } = this.getProperties('anomaliesRange', 'updateAnomalies');
-      set(this, 'isLoading', true);
-      const [ start, end ] = anomaliesRange;
-      updateAnomalies(start, end)
-        .then(res => {
-          this.setProperties({
-            anomaliesById: res,
-            anomalyIds: res.anomalyIds
-          });
-          this._resetLocalFilters();
-          set(this, 'isLoading', false);
-        })
-        .catch(() => {
-          this._resetLocalFilters();
-          set(this, 'isLoading', false);
-        });
-
-    }
-  ),
-
   // Total Number of pages to display
   pagesNum: computed(
     'totalAnomalies',
@@ -159,21 +131,21 @@ export default Controller.extend({
   // return list of anomalyIds according to filter(s) applied
   selectedAnomalies: computed(
     'anomalyFilters',
-    'anomalyIds',
+    'anomalyIdList',
     'activeFiltersString',
     function() {
       const {
-        anomalyIds,
+        anomalyIdList,
         anomalyFilters,
         anomaliesById,
         activeFiltersString
-      } = this.getProperties('anomalyIds', '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 anomalyIds;
+        return anomalyIdList;
       }
-      let selectedAnomalies = anomalyIds;
+      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
@@ -283,6 +255,34 @@ export default Controller.extend({
     }
   ),
 
+  // 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.
@@ -298,24 +298,21 @@ export default Controller.extend({
 
     // Fill in select options for these filters ('filterKeys') based on alert properties from model.alerts
     newFilterBlocksLocal.forEach((filter) => {
-      if (filter.name === "dimensionFilterMap") {
+      let filterKeys = [];
+      if (filter.name === "dimensionFilterMap" && isPresent(anomaliesById.searchFilters[filter.name])) {
         const anomalyPropertyArray = Object.keys(anomaliesById.searchFilters[filter.name]);
-        let filterKeys = [];
         anomalyPropertyArray.forEach(dimensionType => {
           let group = Object.keys(anomaliesById.searchFilters[filter.name][dimensionType]);
           group = group.map(dim => `${dimensionType}::${dim}`);
           filterKeys = [...filterKeys, ...group];
         });
-        Object.assign(filter, { filterKeys });
       } else if (filter.name === "subscriptionFilterMap"){
-        const filterKeys = this.get('store')
+        filterKeys = this.get('store')
           .peekAll('subscription-groups')
           .sortBy('name')
           .filter(group => (group.get('active') && group.get('yaml')))
           .map(group => group.get('name'));
-        // Add filterKeys prop to each facet or filter block
-        Object.assign(filter, { filterKeys });
-      } else if (filter.name === "statusFilterMap"){
+      } 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
@@ -323,17 +320,16 @@ export default Controller.extend({
           // map the status to name
           return mapping.length > 0 ? mapping[0].name : prop;
         });
-        const filterKeys = [ ...new Set(powerSort(anomalyPropertyArray, null))];
-        // Add filterKeys prop to each facet or filter block
-        Object.assign(filter, { filterKeys });
+        filterKeys = [ ...new Set(powerSort(anomalyPropertyArray, null))];
       } else {
-        const anomalyPropertyArray = Object.keys(anomaliesById.searchFilters[filter.name]);
-        const filterKeys = [ ...new Set(powerSort(anomalyPropertyArray, null))];
-        // Add filterKeys prop to each facet or filter block
-        Object.assign(filter, { filterKeys });
+        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,
@@ -345,15 +341,13 @@ export default Controller.extend({
   // method to union anomalyId arrays for filters applied of same type
   _unionOfArrays(anomaliesById, filterType, selectedFilters) {
     //handle dimensions separately, since they are nested
-    if (filterType === 'dimensionFilterMap') {
-      let addedIds = [];
+    let addedIds = [];
+    if (filterType === 'dimensionFilterMap' && isPresent(anomaliesById.searchFilters[filterType])) {
       selectedFilters.forEach(filter => {
         const [type, dimension] = filter.split('::');
         addedIds = [...addedIds, ...anomaliesById.searchFilters.dimensionFilterMap[type][dimension]];
       });
-      return addedIds;
-    } else if (filterType === 'statusFilterMap'){
-      let addedIds = [];
+    } 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));
@@ -363,17 +357,17 @@ export default Controller.extend({
       translatedFilters.forEach(filter => {
         addedIds = [...addedIds, ...anomaliesById.searchFilters[filterType][filter]];
       });
-      return addedIds;
     } else {
-      let addedIds = [];
-      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;
+      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;
   },
 
   // method to intersect anomalyId arrays for filters applied of different types
@@ -462,6 +456,7 @@ export default Controller.extend({
       //Update the time range option selected
       set(this, 'anomaliesRange', [startDate, endDate]);
       set(this, 'duration', duration);
+      this._updateVisuals();
     },
 
     // Handles UI sort change
diff --git a/thirdeye/thirdeye-frontend/app/pods/anomalies/route.js b/thirdeye/thirdeye-frontend/app/pods/anomalies/route.js
index 9fb9583..9701ca5 100644
--- a/thirdeye/thirdeye-frontend/app/pods/anomalies/route.js
+++ b/thirdeye/thirdeye-frontend/app/pods/anomalies/route.js
@@ -2,12 +2,22 @@ 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 {  getAnomalyIdsByTimeRange, anomalyResponseObjNew } from 'thirdeye-frontend/utils/anomaly';
+import {
+  getAnomalyFiltersByTimeRange,
+  getAnomalyFiltersByAnomalyId,
+  anomalyResponseObjNew } from 'thirdeye-frontend/utils/anomaly';
+import _ from 'lodash';
 
 const start = moment().subtract(1, 'day').valueOf();
 const end = moment().valueOf();
 
+const queryParamsConfig = {
+  refreshModel: true,
+  replace: false
+};
+
 export default Route.extend({
 
   // Make duration service accessible
@@ -15,17 +25,35 @@ export default Route.extend({
   anomaliesApiService: service('services/api/anomalies'),
   session: service(),
   store: service('store'),
+  queryParams: {
+    anomalyIds: queryParamsConfig
+  },
+  anomalyIds: null,
 
-  async model() {
-    const anomaliesById = await getAnomalyIdsByTimeRange(start, end);
+  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
     return hash({
-      updateAnomalies:  getAnomalyIdsByTimeRange,
+      updateAnomalies:  getAnomalyFiltersByTimeRange,
       anomaliesById,
-      subscriptionGroups
+      subscriptionGroups,
+      anomalyIds
     });
   },
 
+  afterModel(model) {
+    // If we set anomalyIds to null in the controller, the route will refresh and clear the params from url
+    const anomalyIds = model.anomalyIds || null;
+    this.set('anomalyIds', anomalyIds);
+    const defaultParams = {
+      anomalyIds
+    };
+    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"
@@ -72,16 +100,15 @@ export default Route.extend({
 
     // Fill in select options for these filters ('filterKeys') based on alert properties from model.alerts
     filterBlocksLocal.forEach((filter) => {
-      if (filter.name === "dimensionFilterMap") {
+      let filterKeys = [];
+      if (filter.name === "dimensionFilterMap" && isPresent(model.anomaliesById.searchFilters[filter.name])) {
         const anomalyPropertyArray = Object.keys(model.anomaliesById.searchFilters[filter.name]);
-        let filterKeys = [];
         anomalyPropertyArray.forEach(dimensionType => {
           let group = Object.keys(model.anomaliesById.searchFilters[filter.name][dimensionType]);
           group = group.map(dim => `${dimensionType}::${dim}`);
           filterKeys = [...filterKeys, ...group];
         });
-        Object.assign(filter, { filterKeys });
-      } else if (filter.name === "statusFilterMap"){
+      } 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
@@ -89,28 +116,25 @@ export default Route.extend({
           // map the status to name
           return mapping.length > 0 ? mapping[0].name : prop;
         });
-        const filterKeys = [ ...new Set(powerSort(anomalyPropertyArray, null))];
-        // Add filterKeys prop to each facet or filter block
-        Object.assign(filter, { filterKeys });
+        filterKeys = [ ...new Set(powerSort(anomalyPropertyArray, null))];
       } else if (filter.name === "subscriptionFilterMap"){
-        const filterKeys = this.get('store')
+        filterKeys = this.get('store')
           .peekAll('subscription-groups')
           .sortBy('name')
           .filter(group => (group.get('active') && group.get('yaml')))
           .map(group => group.get('name'));
-        // Add filterKeys prop to each facet or filter block
-        Object.assign(filter, { filterKeys });
       } else {
-        const anomalyPropertyArray = Object.keys(model.anomaliesById.searchFilters[filter.name]);
-        const filterKeys = [ ...new Set(powerSort(anomalyPropertyArray, null))];
-        // Add filterKeys prop to each facet or filter block
-        Object.assign(filter, { filterKeys });
+        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 });
     });
 
     // Keep an initial copy of the secondary filter blocks in memory
     Object.assign(model, {
-      initialFiltersLocal: filterBlocksLocal
+      initialFiltersLocal: _.cloneDeep(filterBlocksLocal)
     });
     // Send filters to controller
     controller.setProperties({
@@ -119,9 +143,10 @@ export default Route.extend({
       resultsActive: true,
       updateAnomalies: model.updateAnomalies,  //requires start and end time in epoch ex updateAnomalies(start, end)
       filterBlocksLocal,
-      anomalyIds: model.anomaliesById.anomalyIds,
+      anomalyIdList: model.anomaliesById.anomalyIds,
       anomaliesRange: [start, end],
-      subscriptionGroups: model.subscriptionGroups
+      subscriptionGroups: model.subscriptionGroups,
+      anomalyIds: this.get('anomalyIds')
     });
   },
 
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 0f3e4fb..6e90868 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/anomaly-summary/component.js
+++ b/thirdeye/thirdeye-frontend/app/pods/components/anomaly-summary/component.js
@@ -10,7 +10,7 @@ import {
   computed,
   getProperties
 } from '@ember/object';
-import { colorMapping, toColor, makeTime } from 'thirdeye-frontend/utils/rca-utils';
+import { colorMapping, makeTime } from 'thirdeye-frontend/utils/rca-utils';
 import { getFormattedDuration,
   anomalyResponseMapNew,
   verifyAnomalyFeedback,
@@ -61,10 +61,14 @@ export default Component.extend({
     rescale: true
   },
 
+  // legend and point are for the graph
   legend: {
     show: true,
     position: 'right'
   },
+  point: {
+    show: false
+  },
   isLoading: false,
   feedbackOptions: ['Not reviewed yet', 'Yes - unexpected', 'Expected temporary change', 'Expected permanent change', 'No change observed'],
   labelMap: anomalyResponseMapNew,
@@ -216,7 +220,7 @@ export default Component.extend({
         set(this, 'predicted', res.predicted);
         set(this, 'isLoading', false);
       })
-      .catch(err => {
+      .catch(() => {
         set(this, 'isLoading', false);
       });
   },
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 f8f3bfe..b0bbdb2 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/anomaly-summary/template.hbs
+++ b/thirdeye/thirdeye-frontend/app/pods/components/anomaly-summary/template.hbs
@@ -16,6 +16,7 @@
       zoom=zoom
       subchart=subchart
       legend=legend
+      point=point
     }}
   </div>
   {{!-- Alert anomaly table --}}
diff --git a/thirdeye/thirdeye-frontend/app/utils/anomaly.js b/thirdeye/thirdeye-frontend/app/utils/anomaly.js
index b7ec6f4..fe16601 100644
--- a/thirdeye/thirdeye-frontend/app/utils/anomaly.js
+++ b/thirdeye/thirdeye-frontend/app/utils/anomaly.js
@@ -11,7 +11,8 @@ import {
   anomalyApiUrls,
   getAnomaliesForYamlPreviewUrl,
   getAnomaliesByAlertIdUrl,
-  getAnomalyIdsByTimeRangeUrl
+  getAnomalyFiltersByTimeRangeUrl,
+  getAnomalyFiltersByAnomalyIdUrl
 } from 'thirdeye-frontend/utils/api/anomaly';
 
 /**
@@ -122,14 +123,27 @@ export function getAnomaliesByAlertId(alertId, startTime, endTime) {
 }
 
 /**
- * Get anomaly ids over a specified time range
- * @method getAnomalyIdsByTimeRange
+ * Get anomaly filters over a specified time range
+ * @method getAnomalyFiltersByTimeRange
  * @param {Number} startTime - start time of query range
  * @param {Number} endTime - end time of query range
  * @return {Ember.RSVP.Promise}
  */
-export function getAnomalyIdsByTimeRange(startTime, endTime) {
-  const url = getAnomalyIdsByTimeRangeUrl(startTime, endTime);
+export function getAnomalyFiltersByTimeRange(startTime, endTime) {
+  const url = getAnomalyFiltersByTimeRangeUrl(startTime, endTime);
+  return fetch(url).then(checkStatus);
+}
+
+/**
+ * Get anomaly filters for specified anomaly ids
+ * @method getAnomalyFiltersByAnomalyId
+ * @param {Number} startTime - start time of query range
+ * @param {Number} endTime - end time of query range
+ * @param {String} anomalyIds - string of comma delimited anomaly ids
+ * @return {Ember.RSVP.Promise}
+ */
+export function getAnomalyFiltersByAnomalyId(startTime, endTime, anomalyIds) {
+  const url = getAnomalyFiltersByAnomalyIdUrl(startTime, endTime, anomalyIds);
   return fetch(url).then(checkStatus);
 }
 
diff --git a/thirdeye/thirdeye-frontend/app/utils/api/anomaly.js b/thirdeye/thirdeye-frontend/app/utils/api/anomaly.js
index d17c1d1..5d4d077 100644
--- a/thirdeye/thirdeye-frontend/app/utils/api/anomaly.js
+++ b/thirdeye/thirdeye-frontend/app/utils/api/anomaly.js
@@ -32,20 +32,32 @@ export function getAnomaliesByAlertIdUrl(alertId, startTime, endTime) {
 }
 
 /**
- * Returns the url for getting anomaly ids of all anomalies over the specified time range
+ * Returns the url for getting anomaly filters of all anomalies over the specified time range
  * @param {Number} startTime - beginning of time range of interest
  * @param {Number} endTime - end of time range of interest
- * @example getAnomalyIdsByTimeRangeUrl(1508472700000, 1508472800000) // yields => /anomalies/search/time/1508472700000/1508472800000/1?filterOnly=true
+ * @example getAnomalyFiltersByTimeRangeUrl(1508472700000, 1508472800000) // yields => /anomalies/search/time/1508472700000/1508472800000/1?filterOnly=true
  */
-export function getAnomalyIdsByTimeRangeUrl(startTime, endTime) {
+export function getAnomalyFiltersByTimeRangeUrl(startTime, endTime) {
   return `/anomalies/search/time/${startTime}/${endTime}/1?filterOnly=true`;
 }
 
+/**
+ * Returns the url for getting anomaly filters of anomalies with given id's
+ * @param {Number} startTime - beginning of time range of interest
+ * @param {Number} endTime - end of time range of interest
+ * @param {String} anomalyIds - string of comma delimitedanomaly ids
+ * @example getAnomalyFiltersByAnomalyIdUrl(1508472700000, 1508472800000, anomalyIds) // yields => /anomalies/search/anomalyIds/1508472700000/1508472800000/1?anomalyIds={anomalyIds}
+ */
+export function getAnomalyFiltersByAnomalyIdUrl(startTime, endTime, anomalyIds) {
+  return `/anomalies/search/anomalyIds/${startTime}/${endTime}/1?anomalyIds=${encodeURIComponent(anomalyIds)}`;
+}
+
 export const anomalyApiUrls = {
   getAnomalyDataUrl,
   getAnomaliesForYamlPreviewUrl,
   getAnomaliesByAlertIdUrl,
-  getAnomalyIdsByTimeRangeUrl
+  getAnomalyFiltersByTimeRangeUrl,
+  getAnomalyFiltersByAnomalyIdUrl
 };
 
 export default {


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