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