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 56b86f503de7adce72dc5c8f88803dda064676df Author: Jihao Zhang <jihzh...@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-unsubscr...@pinot.apache.org For additional commands, e-mail: commits-h...@pinot.apache.org