You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pinot.apache.org by ak...@apache.org on 2020/05/12 17:03:15 UTC
[incubator-pinot] branch master updated: [TE] frontend -
harleyjj/deprecated - remove dead routes from frontend (#5315)
This is an automated email from the ASF dual-hosted git repository.
akshayrai09 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 e88f6c2 [TE] frontend - harleyjj/deprecated - remove dead routes from frontend (#5315)
e88f6c2 is described below
commit e88f6c29d13c8f78f94815ad819c086500b5bce8
Author: Harley Jackson <hj...@linkedin.com>
AuthorDate: Tue May 12 10:03:06 2020 -0700
[TE] frontend - harleyjj/deprecated - remove dead routes from frontend (#5315)
---
.../app/pods/auto-onboard/controller.js | 280 -------
.../app/pods/auto-onboard/route.js | 24 -
.../app/pods/auto-onboard/template.hbs | 140 ----
.../app/pods/manage/alert/controller.js | 52 --
.../app/pods/manage/alert/edit/controller.js | 215 ------
.../app/pods/manage/alert/edit/route.js | 164 -----
.../app/pods/manage/alert/edit/template.hbs | 86 ---
.../app/pods/manage/alert/explore/controller.js | 816 ---------------------
.../app/pods/manage/alert/explore/route.js | 605 ---------------
.../app/pods/manage/alert/explore/template.hbs | 364 ---------
.../app/pods/manage/alert/route.js | 171 -----
.../app/pods/manage/alert/template.hbs | 59 --
.../app/pods/manage/alert/tune/controller.js | 446 -----------
.../app/pods/manage/alert/tune/route.js | 487 ------------
.../app/pods/manage/alert/tune/template.hbs | 257 -------
.../app/pods/preview/controller.js | 478 ------------
.../thirdeye-frontend/app/pods/preview/route.js | 24 -
.../app/pods/preview/template.hbs | 82 ---
thirdeye/thirdeye-frontend/app/router.js | 7 -
19 files changed, 4757 deletions(-)
diff --git a/thirdeye/thirdeye-frontend/app/pods/auto-onboard/controller.js b/thirdeye/thirdeye-frontend/app/pods/auto-onboard/controller.js
deleted file mode 100644
index 4f0657f..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/auto-onboard/controller.js
+++ /dev/null
@@ -1,280 +0,0 @@
-import {observer, computed, set, get} from '@ember/object';
-import Controller from '@ember/controller';
-import {checkStatus} from 'thirdeye-frontend/utils/utils';
-import fetch from 'fetch';
-
-export default Controller.extend({
- detectionConfigId: null,
-
- detectionConfigName: null,
-
- datasetName: null,
-
- datasets: null,
-
- dimensions: null,
-
- selectedMetric: null,
-
- filterOptions: null,
-
- metrics: null,
-
- metricsProperties: null,
-
- metricUrn: null,
-
- filterMap: null,
-
- toggleChecked: false,
-
- output: null,
-
- topk: null,
-
- minContribution: null,
-
- minValue: null,
-
- idToNames: null,
-
- selectedDimensions: '{}',
-
- selectedFilters: '{}',
-
- queryParams: ['detectionId'],
-
- generalFieldsEnabled: computed.or('dimensions'),
-
- metricsFieldEnabled: computed.or('metrics'),
-
- // TODO: replace with ember data
- hasDetectionId: observer('detectionId', async function () {
- const detectionId = this.get('detectionId');
- const res = await fetch(`/dataset-auto-onboard/` + detectionId).then(checkStatus);
- const nestedProperties = res['properties']['nested'];
- const nestedProperty = nestedProperties[0];
-
- // fill in values:
- this.setProperties({
- detectionConfigId: res['id'],
- detectionConfigName: res['name'],
- topk: nestedProperty['k'],
- minValue: nestedProperty['minValue'],
- minContribution: nestedProperty['minContribution'],
- datasetName: res['properties']['datasetName']
- });
-
- await this._datasetNameChanged();
-
- let dimensionBreakdownUrn = null;
- const idToNames = this.get('idToNames');
- const metricsProperties = get(this, 'metricsProperties');
-
- // fill in dimensions
- this.set('selectedDimensions', JSON.stringify({
- 'dimensions': nestedProperty['dimensions']
- }));
-
- // fill in filters
- let urnPieces = nestedProperty['metricUrn'].split(':');
- const filters = {};
- let i;
- for (i = 3; i < urnPieces.length; i++) {
- const filter = urnPieces[i].split('=');
- if (filter[0] in filters) {
- filters[filter[0]].push(filter[1]);
- } else {
- filters[filter[0]] = [filter[1]];
- }
- }
- this.set('selectedFilters', JSON.stringify(filters));
- this._updateFilters();
-
- // fill in selected metrics
- const metricIds = nestedProperties.reduce(function (obj, property) {
- let urn;
- if ('nestedMetricUrn' in property) {
- urn = property['nestedMetricUrn'];
- dimensionBreakdownUrn = property['metricUrn'];
- } else {
- urn = property['metricUrn'];
- }
- obj.push(urn.split(':')[2]);
- return obj;
- }, []);
-
- Object.keys(metricsProperties).forEach(function (key) {
- if (metricIds.indexOf(metricsProperties[key]['id'].toString()) == -1) {
- set(metricsProperties[key], 'monitor', false);
- }
- });
-
- // fill in dimension breakdown metric
- this.set('selectedMetric', idToNames[this._metricUrnToId(dimensionBreakdownUrn)]);
- }),
-
- // TODO: replace with ember data
- _writeDetectionConfig(detectionConfigBean) {
- const jsonString = JSON.stringify(detectionConfigBean);
- return fetch(`/thirdeye/entity?entityType=DETECTION_CONFIG`, {method: 'POST', body: jsonString})
- .then(checkStatus)
- .then(res => set(this, 'output', `saved '${detectionConfigBean.name}' as id ${res}`))
- .catch(err => set(this, 'output', err));
- },
-
- _metricUrnToId(metricUrn) {
- return metricUrn.split(':')[2];
- },
-
- _updateFilters() {
- const filters = this.get('selectedFilters');
- const metricsProperties = get(this, 'metricsProperties');
- const filterMap = JSON.parse(filters);
- Object.keys(metricsProperties).forEach(function (key) {
- const metricProperty = metricsProperties[key];
- let metricUrn = "thirdeye:metric:" + metricProperty['id'];
- Object.keys(filterMap).forEach(function (key) {
- filterMap[key].forEach(function (value) {
- metricUrn = metricUrn + ":" + key + "=" + value;
- });
- });
- metricsProperties[key]['urn'] = metricUrn;
- });
- },
-
- // TODO: replace with ember data
- async _datasetNameChanged() {
- const url = `/dataset-auto-onboard/metrics?dataset=` + get(this, 'datasetName');
- const res = await fetch(url).then(checkStatus);
- const metricsProperties = res.reduce(function (obj, metric) {
- obj[metric["name"]] = {
- "id": metric['id'], "urn": "thirdeye:metric:" + metric['id'], "monitor": true
- };
- return obj;
- }, {});
- const metricUrn = metricsProperties[Object.keys(metricsProperties)[0]]['id'];
- const idToNames = {};
- Object.keys(metricsProperties).forEach(function (key) {
- idToNames[metricsProperties[key]['id']] = key;
- });
-
- this.setProperties({
- metricsProperties: metricsProperties,
- metrics: Object.keys(metricsProperties),
- idToNames: idToNames,
- metricUrn: metricUrn
- });
-
- const result = await fetch(`/data/autocomplete/filters/metric/${metricUrn}`).then(checkStatus);
-
- this.setProperties({
- filterOptions: result, dimensions: {dimensions: Object.keys(result)}
- });
- },
-
- actions: {
- onSave(dataset) {
- this.set('datasetName', dataset);
- this._datasetNameChanged();
- },
-
- toggleCheckBox(name) {
- const metricsProperties = get(this, 'metricsProperties');
- set(metricsProperties[name], 'monitor', !metricsProperties[name]['monitor']);
- },
-
- onChangeValue(property, value) {
- this.set(property, value);
- },
-
- onFilters(filters) {
- this.set('selectedFilters', filters);
- this._updateFilters();
- },
-
- onSubmit() {
- const metricsProperties = get(this, 'metricsProperties');
- const nestedProperties = [];
- const selectedMetric = this.get('selectedMetric');
- const detectionConfigId = this.get('detectionConfigId');
- const selectedDimensions = JSON.parse(this.get('selectedDimensions'));
- const topk = this.get('topk');
- const minValue = this.get('minValue');
- const minContribution = this.get('minContribution');
- Object.keys(metricsProperties).forEach(function (key) {
- const properties = metricsProperties[key];
- if (!properties['monitor']) {
- return;
- }
- const detectionConfig = {
- className: 'org.apache.pinot.thirdeye.detection.algorithm.DimensionWrapper', nested: [{
- className: 'org.apache.pinot.thirdeye.detection.algorithm.MovingWindowAlgorithm',
- baselineWeeks: 4,
- windowSize: '4 weeks',
- changeDuration: '7d',
- outlierDuration: '12h',
- aucMin: -10,
- zscoreMin: -4,
- zscoreMax: 4
- }]
- };
- if (selectedMetric == null) {
- detectionConfig['metricUrn'] = properties['urn'];
- } else {
- detectionConfig['metricUrn'] = metricsProperties[selectedMetric]['urn'];
- detectionConfig['nestedMetricUrn'] = properties['urn'];
- }
- if (selectedDimensions != null) {
- detectionConfig['dimensions'] = selectedDimensions['dimensions'];
- }
- if (topk != null) {
- detectionConfig['k'] = parseInt(topk);
- }
- if (minValue != null) {
- detectionConfig['minValue'] = parseFloat(minValue);
- }
- if (minContribution != null) {
- detectionConfig['minContribution'] = parseFloat(minContribution);
- }
- nestedProperties.push(detectionConfig);
- });
-
- const configResult = {
- "cron": "45 10/15 * * * ? *", "name": get(this, 'detectionConfigName'), "lastTimestamp": 0, "properties": {
- "className": "org.apache.pinot.thirdeye.detection.algorithm.MergeWrapper",
- "maxGap": 7200000,
- "nested": nestedProperties,
- "datasetName": get(this, 'datasetName')
- }
- };
-
- if (detectionConfigId != null) {
- configResult['id'] = detectionConfigId;
- }
- this._writeDetectionConfig(configResult);
- },
-
- onSelectDimension(dims) {
- this.set('selectedDimensions', dims);
- },
-
- onChangeName(name) {
- this.set('detectionConfigName', name);
- },
-
- onSelectMetric(name) {
- this.set('selectedMetric', name);
- },
-
- async onLoadDatasets() {
- const url = `/thirdeye/entity/DATASET_CONFIG`;
- const res = await fetch(url).then(checkStatus);
- const datasets = res.reduce(function (obj, datasetConfig) {
- obj.push(datasetConfig['dataset']);
- return obj;
- }, []);
- this.set('datasets', datasets);
- }
- }
-});
diff --git a/thirdeye/thirdeye-frontend/app/pods/auto-onboard/route.js b/thirdeye/thirdeye-frontend/app/pods/auto-onboard/route.js
deleted file mode 100644
index 8cdaf28..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/auto-onboard/route.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import Route from '@ember/routing/route';
-import { inject as service } from '@ember/service';
-
-export default Route.extend({
- session: service(),
- actions: {
- /**
- * save session url for transition on login
- * @method willTransition
- */
- willTransition(transition) {
- //saving session url - TODO: add a util or service - lohuynh
- if (transition.intent.name && transition.intent.name !== 'logout') {
- this.set('session.store.fromUrl', {lastIntentTransition: transition});
- }
- },
- error() {
- // The `error` hook is also provided the failed
- // `transition`, which can be stored and later
- // `.retry()`d if desired.
- return true;
- },
- }
-});
diff --git a/thirdeye/thirdeye-frontend/app/pods/auto-onboard/template.hbs b/thirdeye/thirdeye-frontend/app/pods/auto-onboard/template.hbs
deleted file mode 100644
index 5771497..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/auto-onboard/template.hbs
+++ /dev/null
@@ -1,140 +0,0 @@
-<div class="container">
- <h3>Dataset auto onboard</h3>
- {{#power-select
- classNames="te-input"
- options=datasets
- selected=datasetName
- onchange=(action "onSave")
- onopen=(action "onLoadDatasets")
- loadingMessage="Waiting for the server...."
- placeholder="Choose a dataset to onboard"
- searchPlaceholder="Type to filter..."
- triggerId="select-dataset"
- triggerClass="te-form__select"
- disabled=false
- as |dataset|
- }}
- {{dataset}}
- {{/power-select}}
- {{#if metricUrn}}
- <div class="row">
- <label class="control-label te-label te-label--taller">
- Detection Name:
- </label>
- </div>
- {{input
- type="text"
- id="anomaly-form-function-name"
- class="form-control te-input te-input--read-only"
- placeholder="Add a descriptive name"
- value=detectionConfigName
- key-up=(action "onChangeName" detectionConfigName)
- }}
- <div class="row">
- <label class="control-label te-label te-label--taller">
- Choose the metrics:
- </label>
- </div>
- {{#each-in metricsProperties as |name metric|}}
- <div>
- {{name}}
- <input type="checkbox"
- checked={{metric.monitor}}
- onclick={{action "toggleCheckBox" name}}/>
- </div>
- {{/each-in}}
- <div class="row">
- <label class="control-label te-label te-label--taller">
- Filter metric by(Optional):
- <span>
- <i class="glyphicon glyphicon-question-sign"></i>
- {{#tooltip-on-element class="te-tooltip"}}
- For example, filter on countryCode::US implies only anomalies in US will be detected.
- {{/tooltip-on-element}}
- </span>
- </label>
- </div>
- {{filter-select
- options=filterOptions
- selected=selectedFilters
- triggerId="select-filters"
- onChange=(action "onFilters")
- }}
- <div class="row">
- <label for="select-dimension" class="control-label te-label te-label--taller">
- Create an alert for each dimension value in: (optional)
- <span>
- <i class="glyphicon glyphicon-question-sign"></i>
- {{#tooltip-on-element class="te-tooltip"}}
- For example, selecting Continent means anomalies on each continent will be monitored.
- {{/tooltip-on-element}}
- </span>
- </label>
- {{filter-select
- selected=selectedDimensions
- options=dimensions
- triggerId="select-dimensions"
- onChange=(action "onSelectDimension")
- }}
- </div>
- <div class="row">
- <label class="control-label te-label te-label--taller">
- Set up the business rules(Optional):
- <span>
- <i class="glyphicon glyphicon-question-sign"></i>
- {{#tooltip-on-element class="te-tooltip"}}
- For example, selecting page_view metric and set top k dimensions to 5 means the dimensions that have top 5 page_view will be monitored.
- {{/tooltip-on-element}}
- </span>
- </label>
- </div>
- <label for="select-dimension" class="control-label te-label te-label--taller">
- Metric used in dimension breakdown
- </label>
- {{#power-select
- classNames="te-input"
- options=metrics
- selected=selectedMetric
- onchange=(action "onSelectMetric")
- loadingMessage="Waiting for the server...."
- placeholder="Choose metric for rules"
- searchPlaceholder="Type to filter..."
- triggerId="select-metric"
- triggerClass="te-form__select"
- disabled=(not metricsFieldEnabled)
- as |metric|
- }}
- {{metric}}
- {{/power-select}}
- <div class="col-xs-4">
- <label class="control-label te-label te-label--taller">
- Explore top k dimensions:
- </label>
- <input type="text"
- value={{topk}}
- onChange={{action (action "onChangeValue" "topk") value="target.value"}}/>
- </div>
- <div class="col-xs-4">
- <label class="control-label te-label te-label--taller">
- Min value:
- </label>
- <input type="text"
- value={{minValue}}
- onChange={{action (action "onChangeValue" "minValue") value="target.value"}}/>
- </div>
- <div class="col-xs-4">
- <label class="control-label te-label te-label--taller">
- Min contribution:
- </label>
- <input type="text"
- value={{minContribution}}
- onChange={{action (action "onChangeValue" "minContribution") value="target.value"}}/>
- </div>
- <div class="row">
- <button onClick={{action "onSubmit"}}>submit</button>
- </div>
- {{/if}}
- <div class="row">
- {{output}}
- </div>
-</div>
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/alert/controller.js b/thirdeye/thirdeye-frontend/app/pods/manage/alert/controller.js
deleted file mode 100644
index a5db82b..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/manage/alert/controller.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Controller for Alert Landing and Details Page
- * @module manage/alert
- * @exports manage/alert
- */
-import Controller from '@ember/controller';
-import { setProperties } from '@ember/object';
-
-export default Controller.extend({
- /**
- * Set up to receive prompt to trigger page mode change.
- * When replay id is received it indicates that we need to check replay status
- * before displaying alert function performance data.
- */
- queryParams: ['jobId', 'functionName'],
- jobId: null,
- functionName: null,
- isOverviewLoaded: true,
-
- actions: {
-
- /**
- * Placeholder for subscribe button click action
- */
- onClickAlertSubscribe() {
- // TODO: Set user as watcher for this alert when API ready
- },
-
- /**
- * Handle conditions for display of appropriate alert nav link (overview or edit)
- */
- setEditModeActive() {
- this.set('isEditModeActive', true);
- },
-
- /**
- * Handle navigation to edit route
- */
- onClickEdit() {
- this.set('isEditModeActive', true);
- this.transitionToRoute('manage.alert.edit', this.get('id'), { queryParams: { refresh: true }});
- },
-
- /**
- * Navigate to Alert Page
- */
- onClickNavToOverview() {
- this.set('isEditModeActive', false);
- this.transitionToRoute('manage.alert.explore', this.get('id'));
- }
- }
-});
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/alert/edit/controller.js b/thirdeye/thirdeye-frontend/app/pods/manage/alert/edit/controller.js
deleted file mode 100644
index 4757b8b..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/manage/alert/edit/controller.js
+++ /dev/null
@@ -1,215 +0,0 @@
-/**
- * Handles alert edit form
- * @module manage/alert/edit
- * @exports manage/alert/edit
- */
-import { reads, or } from '@ember/object/computed';
-import fetch from 'fetch';
-import Controller from '@ember/controller';
-import {
- set,
- get,
- computed,
- setProperties
-} from '@ember/object';
-import {
- checkStatus,
- postProps
-} from 'thirdeye-frontend/utils/utils';
-import {
- selfServeApiCommon,
- selfServeApiOnboard
-} from 'thirdeye-frontend/utils/api/self-serve';
-
-export default Controller.extend({
-
- /**
- * Optional query param to refresh model
- */
- queryParams: ['refresh'],
- refresh: null,
-
- /**
- * Important initializations
- */
- isEditAlertSuccess: false, // alert save success
- isProcessingForm: false, // to trigger submit disable
- isExiting: false, // exit detection
- showManageGroupsModal: false, // manage group modal
-
- /**
- * The config group that the current alert belongs to
- * @type {Object}
- */
- originalConfigGroup: reads('model.originalConfigGroup'),
-
- /**
- * Mapping alertFilter's pattern to human readable strings
- * @returns {String}
- */
- pattern: computed('alertProps', function() {
- const props = this.get('alertProps');
- const patternObj = props.find(prop => prop.name === 'pattern');
- const pattern = patternObj ? decodeURIComponent(patternObj.value) : 'Up and Down';
-
- return pattern;
- }),
-
- /**
- * Extracting Weekly Effect from alert Filter
- * @returns {String}
- */
- weeklyEffect: computed('alertFilters.weeklyEffectModeled', function() {
- const weeklyEffect = this.getWithDefault('alertFilters.weeklyEffectModeled', true);
-
- return weeklyEffect;
- }),
-
- /**
- * Extracting sensitivity from alert Filter and maps it to human readable values
- * @returns {String}
- */
- sensitivity: computed('alertProps', function() {
- const props = this.get('alertProps');
- const sensitivityObj = props.find(prop => prop.name === 'sensitivity');
- const sensitivity = sensitivityObj ? decodeURIComponent(sensitivityObj.value) : 'MEDIUM';
-
- const sensitivityMapping = {
- LOW: 'Robust (Low)',
- MEDIUM: 'Medium',
- HIGH: 'Sensitive (High)'
- };
-
- return sensitivityMapping[sensitivity];
- }),
-
- /**
- * Disable submit under these circumstances
- * @method isSubmitDisabled
- * @return {Boolean} show/hide submit
- */
- isSubmitDisabled: or('{isProcessingForm,isAlertNameDuplicate}'),
-
- /**
- * Fetches an alert function record by name.
- * Use case: when user names an alert, make sure no duplicate already exists.
- * @method _fetchAlertByName
- * @param {String} functionName - name of alert or function
- * @return {Promise}
- */
- _fetchAlertByName(functionName) {
- const url = selfServeApiCommon.alertFunctionByName(functionName);
- return fetch(url).then(checkStatus);
- },
-
- /**
- * Display success banners while model reloads
- * @method confirmEditSuccess
- * @return {undefined}
- */
- confirmEditSuccess() {
- this.set('isEditAlertSuccess', true);
- },
-
- /**
- * Reset fields to model init state
- * @method clearAll
- * @return {undefined}
- */
- clearAll() {
- this.setProperties({
- model: null,
- isExiting: true,
- isSubmitDisabled: false,
- isEmailError: false,
- isDuplicateEmail: false,
- isEditAlertSuccess: false,
- isNewConfigGroupSaved: false,
- isProcessingForm: false,
- isActive: false,
- isLoadError: false,
- updatedRecipients: [],
- granularity: null,
- alertFilters: null,
- alertFunctionName: null,
- alertId: null,
- loadErrorMessage: null
- });
- },
-
- /**
- * Actions for edit alert form view
- */
- actions: {
-
- /**
- * Make sure alert name does not already exist in the system
- * Either add or clear the "is duplicate name" banner
- * @method validateAlertName
- * @param {String} userProvidedName - The new alert name
- * @return {undefined}
- */
- validateAlertName(userProvidedName) {
- this._fetchAlertByName(userProvidedName).then(matchingAlerts => {
- const isDuplicateName = matchingAlerts.find(alert => alert.functionName === userProvidedName);
- this.set('isAlertNameDuplicate', isDuplicateName);
- });
- },
-
- /**
- * Action handler for displaying groups modal
- * @returns {undefined}
- */
- onShowManageGroupsModal() {
- set(this, 'showManageGroupsModal', true);
- },
-
- /**
- * Action handler for CANCEL button - simply reset all fields
- * @returns {undefined}
- */
- onCancel() {
- const alertId = get(this, 'alertId');
- this.send('refreshModel');
- this.transitionToRoute('manage.alert.explore', alertId);
- },
-
- /**
- * Action handler for form submit
- * MVP Version: Can activate/deactivate and update alert name and edit config group data
- * @returns {Promise}
- */
- onSubmit() {
- const {
- isActive,
- alertFunctionName,
- alertData: postFunctionBody
- } = this.getProperties(
- 'isActive',
- 'alertFunctionName',
- 'alertData'
- );
-
- // Disable submit for now and make sure we're clear of email errors
- set(this, 'isProcessingForm', true);
-
- // Assign these fresh editable values to the Alert object currently being edited
- setProperties(postFunctionBody, {
- isActive,
- functionName: alertFunctionName
- });
-
- // Step 1: Save any edits to the Alert entity in our DB
- return fetch(selfServeApiOnboard.editAlert, postProps(postFunctionBody))
- .then(res => checkStatus(res, 'post'))
- .then(() => {
- this.send('confirmSaveStatus', true);
- set(this, 'isProcessingForm', false);
- })
- .catch(() => {
- this.send('confirmSaveStatus', false);
- this.clearAll();
- });
- }
- }
-});
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/alert/edit/route.js b/thirdeye/thirdeye-frontend/app/pods/manage/alert/edit/route.js
deleted file mode 100644
index c31d7d53..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/manage/alert/edit/route.js
+++ /dev/null
@@ -1,164 +0,0 @@
-/**
- * Handles the 'edit' route for manage alert
- * @module manage/alert/edit/edit
- * @exports manage/alert/edit/edit
- */
-import RSVP from 'rsvp';
-import fetch from 'fetch';
-import Route from '@ember/routing/route';
-import { task } from 'ember-concurrency';
-import { get } from '@ember/object';
-import { checkStatus } from 'thirdeye-frontend/utils/utils';
-import { selfServeApiCommon } from 'thirdeye-frontend/utils/api/self-serve';
-import { inject as service } from '@ember/service';
-
-export default Route.extend({
- session: service(),
- notifications: service('toast'),
-
- /**
- * Optional params to load a fresh view
- */
- queryParams: {
- refresh: {
- refreshModel: true,
- replace: false
- }
- },
-
- async model(params, transition) {
- const {
- id,
- alertData
- } = this.modelFor('manage.alert');
-
- if (!id) { return; }
-
- const alertGroups = await fetch(selfServeApiCommon.configGroupByAlertId(id)).then(checkStatus);
-
- return RSVP.hash({
- alertGroups,
- alertData
- });
- },
-
- afterModel(model) {
- const {
- alertData,
- alertGroups
- } = model;
-
- const {
- properties: alertProps
- } = alertData;
-
- // Add a parsed properties array to the model
- const propsArray = alertProps.split(';').map((prop) => {
- const [ name, value ] = prop.split('=');
- return { name, value: decodeURIComponent(value) };
- });
-
- Object.assign(model, {
- propsArray,
- alertGroups
- });
- },
-
- setupController(controller, model) {
- this._super(controller, model);
-
- const {
- alertData,
- alertGroups,
- propsArray: alertProps,
- loadError: isLoadError,
- loadErrorMsg: loadErrorMessage
- } = model;
-
- const {
- isActive,
- bucketSize,
- bucketUnit,
- id: alertId,
- filters: alertFilters,
- functionName: alertFunctionName
- } = alertData;
-
- controller.setProperties({
- model,
- alertData,
- alertFilters,
- alertProps,
- alertFunctionName,
- alertId,
- alertGroups,
- isActive,
- isLoadError,
- loadErrorMessage,
- granularity: `${bucketSize}_${bucketUnit}`
- });
- },
-
- /**
- * Fetch alert data for each function id that the currently selected group watches
- * @method fetchAlertDataById
- * @param {Object} functionIds - alert ids included in the currently selected group
- * @return {RSVP.hash} A new list of functions (alerts)
- */
- fetchAlertDataById: task(function * (functionIds) {
- const functionArray = yield functionIds.map(id => fetch(selfServeApiCommon.alertById(id)).then(checkStatus));
- return RSVP.hash(functionArray);
- }),
-
- actions: {
- /**
- * save session url for transition on login
- * @method willTransition
- */
- willTransition(transition) {
- //saving session url - TODO: add a util or service - lohuynh
- if (transition.intent.name && transition.intent.name !== 'logout') {
- this.set('session.store.fromUrl', {lastIntentTransition: transition});
- }
- },
-
- /**
- * Handle any errors occurring in model/afterModel in parent route
- * https://www.emberjs.com/api/ember/2.16/classes/Route/events/error?anchor=error
- * https://guides.emberjs.com/v2.18.0/routing/loading-and-error-substates/#toc_the-code-error-code-event
- */
- error() {
- return true;
- },
-
- /**
- * Toast confirmation of save status
- */
- confirmSaveStatus(isSuccess) {
- const notifications = this.get('notifications');
- const toastOptions = {
- timeOut: '4000',
- positionClass: 'toast-bottom-right'
- };
- if (isSuccess) {
- notifications.success('Alert options saved successfully', 'Done', toastOptions);
- } else {
- notifications.error('Alert options failed to save', 'Error', toastOptions);
- }
- },
-
- /**
- * Action called on submission to reload the route's model
- */
- refreshModel() {
- this.refresh();
- },
-
- /**
- * Refresh anomaly data when changes are made
- */
- loadFunctionsTable(selectedConfigGroup) {
- get(this, 'prepareAlertList').perform(selectedConfigGroup);
- }
- }
-});
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/alert/edit/template.hbs b/thirdeye/thirdeye-frontend/app/pods/manage/alert/edit/template.hbs
deleted file mode 100644
index 2d07114..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/manage/alert/edit/template.hbs
+++ /dev/null
@@ -1,86 +0,0 @@
-<main class="alert-create card-container card-container--padded te-form">
- <fieldset class="te-form__section row">
- <div class="col-xs-12">
- <legend class="te-form__section-title">Alert Details</legend>
- </div>
-
- {{!-- Field: Alert Name --}}
- <div class="form-group col-xs-10">
- <label for="anomaly-form-function-name" class="control-label te-label required">
- Alert Name
- <div class="te-form__sub-label">(Please follow this naming convention: <span class="te-form__sub-label--strong">productName_metricName_dimensionName_other</span>)</div>
- </label>
- {{#if isAlertNameDuplicate}}
- <div class="te-form__alert--warning alert-warning">Warning: <strong>{{alertFunctionName}}</strong> already exists. Please try another name.</div>
- {{/if}}
- {{input
- type="text"
- id="anomaly-form-function-name"
- class="form-control te-input te-input--read-only"
- placeholder="Add a descriptive alert name"
- value=alertFunctionName
- key-up=(action "validateAlertName" alertFunctionName)
- }}
- </div>
-
- {{!-- Field: Active --}}
- <div class="form-group col-xs-2">
- <label for="select-status" class="control-label te-label required">
- Status
- <div class="te-form__sub-label">Toggles detection on/off</div>
- </label>
- {{#x-toggle
- value=isActive
- classNames="te-toggle te-toggle--form te-toggle--left"
- theme='ios'
- showLabels=true
- name="activeToggle"
- onToggle=(action (mut isActive))
- as |toggle|}}
- {{#toggle.label value=isActive}}
- <span class="te-label te-label--flush">{{if isActive 'Active' 'Inactive'}}</span>
- {{/toggle.label}}
- {{toggle.switch theme='ios' onLabel='diff on' offLabel='diff off'}}
- {{/x-toggle}}
- </div>
- </fieldset>
-
- <fieldset class="te-form__section row">
- <div class="col-xs-12">
- <legend class="te-form__section-title">Notification Settings</legend>
- </div>
-
- {{!-- Button: Edit --}}
- <div class="form-group col-xs-12">
- Alerts can be part of multiple different subscription groups. Each group will send out the alert once according to schedule.
- <div>
- <button {{action "onShowManageGroupsModal"}}>Edit Notification Settings</button>
- </div>
- </div>
- </fieldset>
-
- <fieldset class="te-form__section-submit">
- {{bs-button
- type="outline-primary"
- buttonType="Cancel"
- defaultText="Cancel"
- onClick=(action "onCancel")
- class="te-button te-button--cancel"
- }}
-
- {{bs-button
- defaultText="Save"
- type="primary"
- onClick=(action "onSubmit")
- buttonType="submit"
- disabled=isSubmitDisabled
- class="te-button te-button--submit"
- }}
-
- </fieldset>
-</main>
-
-{{modals/manage-groups-modal
- showManageGroupsModal=showManageGroupsModal
- preselectedFunctionId=alertId
-}}
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/alert/explore/controller.js b/thirdeye/thirdeye-frontend/app/pods/manage/alert/explore/controller.js
deleted file mode 100644
index 2a9dcf8..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/manage/alert/explore/controller.js
+++ /dev/null
@@ -1,816 +0,0 @@
-/**
- * Controller for Alert Details Page: Overview Tab
- * @module manage/alert/explore
- * @exports manage/alert/explore
- */
-import _ from 'lodash';
-import fetch from 'fetch';
-import moment from 'moment';
-import { later } from "@ember/runloop";
-import { isPresent } from "@ember/utils";
-import Controller from '@ember/controller';
-import { task, timeout } from 'ember-concurrency';
-import {
- set,
- get,
- computed,
- setProperties,
- getProperties,
- getWithDefault
-} from '@ember/object';
-import {
- checkStatus,
- postProps,
- buildDateEod
-} from 'thirdeye-frontend/utils/utils';
-import {
- buildAnomalyStats,
- extractSeverity
-} from 'thirdeye-frontend/utils/manage-alert-utils';
-import { inject as service } from '@ember/service';
-import config from 'thirdeye-frontend/config/environment';
-import floatToPercent from 'thirdeye-frontend/utils/float-to-percent';
-import * as anomalyUtil from 'thirdeye-frontend/utils/anomaly';
-
-export default Controller.extend({
- /**
- * Be ready to receive time span for anomalies via query params
- */
- queryParams: ['duration', 'startDate', 'endDate', 'repRunStatus'],
- duration: null,
- startDate: null,
- endDate: null,
- repRunStatus: null,
- openReport: false,
-
- /**
- * Mapping anomaly table column names to corresponding prop keys
- */
- sortMap: {
- number: 'index',
- start: 'anomalyStart',
- score: 'severityScore',
- change: 'changeRate',
- resolution: 'anomalyFeedback'
- },
-
- /**
- * Make duration service accessible
- */
- durationCache: service('services/duration'),
-
- /**
- * Date format for date range picker
- */
- serverDateFormat: 'YYYY-MM-DD HH:mm',
-
- /**
- * Set initial view values
- * @method initialize
- * @param {Boolean} isReplayNeeded
- * @return {undefined}
- */
- initialize() {
- const {
- repRunStatus,
- openReport
- } = this.getProperties('repRunStatus', 'openReport');
-
- this.setProperties({
- filters: {},
- loadedWowData: [],
- topDimensions: [],
- predefinedRanges: {},
- missingAnomalyProps: {},
- selectedSortMode: '',
- replayErrorMailtoStr: '',
- selectedTimeRange: '',
- selectedFilters: JSON.stringify({}),
- timePickerIncrement: 5,
- renderStatusIcon: true,
- openReportModal: false,
- isAlertReady: false,
- isGraphReady: false,
- isReportSuccess: false,
- isReportFailure: false,
- isPageLoadFailure: false,
- isAnomalyArrayChanged: false,
- sortColumnStartUp: false,
- sortColumnScoreUp: false,
- sortColumnChangeUp: false,
- sortColumnNumberUp: true,
- isAnomalyListFiltered: false,
- isDimensionFetchDone: false,
- sortColumnResolutionUp: false,
- checkReplayInterval: 2000, // 2 seconds
- selectedDimension: 'All Dimensions',
- selectedResolution: 'All Resolutions',
- labelMap: anomalyUtil.anomalyResponseMap,
- currentPage: 1,
- pageSize: 10
- });
-
- // Start checking for replay to end if a jobId is present
- if (this.get('isReplayPending')) {
- this.set('replayStartTime', moment());
- this.get('checkReplayStatus').perform(this.get('jobId'));
- }
-
- // If a replay is still running, reload when done
- if (repRunStatus) {
- this.get('checkForNewAnomalies').perform(repRunStatus);
- }
-
- // If query param is set, auto-open report anomaly modal
- if (openReport) {
- this.triggerOpenReportModal();
- }
- },
-
- /**
- * newLink: Id of new alert (for migrated alerts)
- * @type {String}
- */
- newId: computed(
- 'alertData',
- function() {
- const alertData = get(this, 'alertData');
- if(alertData && alertData.functionName) {
- let pieces = alertData.functionName.split('_');
- if (pieces.length > 0) {
- return pieces[pieces.length-1];
- }
- }
- return null;
- }
- ),
-
- /**
- * Table pagination: number of pages to display
- * @type {Number}
- */
- paginationSize: computed(
- 'pagesNum',
- 'pageSize',
- function() {
- const { pagesNum, pageSize } = this.getProperties('pagesNum', 'pageSize');
- return Math.min(pagesNum, pageSize/2);
- }
- ),
-
- /**
- * Table pagination: total Number of pages to display
- * @type {Number}
- */
- pagesNum: computed(
- 'filteredAnomalies',
- 'pageSize',
- function() {
- const { filteredAnomalies, pageSize } = this.getProperties('filteredAnomalies', 'pageSize');
- const anomalyCount = filteredAnomalies.length || 0;
- return Math.ceil(anomalyCount/pageSize);
- }
- ),
-
- /**
- * Table pagination: creates the page Array for view
- * @type {Array}
- */
- viewPages: computed(
- 'pages',
- 'currentPage',
- 'paginationSize',
- 'pagesNum',
- function() {
- const {
- currentPage,
- pagesNum: max,
- paginationSize: size
- } = this.getProperties('currentPage', 'pagesNum', 'paginationSize');
- 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);
- }
- ),
-
- /**
- * Table pagination: pre-filtered and sorted anomalies with pagination
- * @type {Array}
- */
- paginatedFilteredAnomalies: computed(
- 'filteredAnomalies.@each',
- 'pageSize',
- 'currentPage',
- 'loadedWoWData',
- 'selectedSortMode',
- 'selectedResolution',
- function() {
- let anomalies = this.get('filteredAnomalies');
- const { pageSize, currentPage, selectedSortMode } = getProperties(this, 'pageSize', 'currentPage', 'selectedSortMode');
-
- if (selectedSortMode) {
- let [ sortKey, sortDir ] = selectedSortMode.split(':');
-
- if (sortDir === 'up') {
- anomalies = anomalies.sortBy(this.get('sortMap')[sortKey]);
- } else {
- anomalies = anomalies.sortBy(this.get('sortMap')[sortKey]).reverse();
- }
- }
-
- return anomalies.slice((currentPage - 1) * pageSize, currentPage * pageSize);
- }
- ),
-
- /**
- * date-time-picker: indicates the date format to be used based on granularity
- * @type {String}
- */
- uiDateFormat: computed('alertData.windowUnit', function() {
- const rawGranularity = this.get('alertData.bucketUnit');
- const granularity = rawGranularity ? rawGranularity.toLowerCase() : '';
-
- switch(granularity) {
- case 'days':
- return 'MMM D, YYYY';
- case 'hours':
- return 'MMM D, YYYY h a';
- default:
- return 'MMM D, YYYY hh:mm a';
- }
- }),
-
- /**
- * Preps a mailto link containing the currently selected metric name
- * @method graphMailtoLink
- * @return {String} the URI-encoded mailto link
- */
- graphMailtoLink: computed(
- 'alertData',
- function() {
- const alertData = this.get('alertData');
- const fullMetricName = `${alertData.collection}::${alertData.metric}`;
- const recipient = config.email;
- const subject = 'TE Self-Serve Alert Page: error loading metric and/or alert records';
- const body = `TE Team, please look into a possible inconsistency issue with [ ${fullMetricName} ] in alert page for alert id ${alertData.id}
- Alert page: ${location.href}`;
- const mailtoString = `mailto:${recipient}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
- return mailtoString;
- }
- ),
-
- /**
- * Determines whether there is a discrepancy between anomaly ids detected and anomaly records loaded
- * @type {Boolean}
- */
- isAnomalyLoadError: computed(
- 'totalAnomalies',
- 'anomalyData.length',
- function() {
- const { totalAnomalies, anomalyData } = getProperties(this, 'totalAnomalies', 'anomalyData');
- const totalsMatching = anomalyData ? (totalAnomalies !== anomalyData.length) : true;
- return totalsMatching;
- }
- ),
-
- /**
- * Data needed to render the stats 'cards' above the anomaly graph for this alert
- * @type {Object}
- */
- anomalyStats: computed(
- 'alertData',
- 'alertEvalMetrics',
- 'alertEvalMetrics.projected',
- function() {
- const {
- alertData,
- alertEvalMetrics,
- DEFAULT_SEVERITY: defaultSeverity
- } = this.getProperties('alertData', 'alertEvalMetrics', 'DEFAULT_SEVERITY');
- const features = getWithDefault(alertData, 'alertFilter.features', null);
- const mttdStr = _.has(alertData, 'alertFilter.mttd') ? alertData.alertFilter.mttd.split(';') : null;
- const severityUnit = (!mttdStr || mttdStr && mttdStr[1].split('=')[0] !== 'deviation') ? '%' : '';
- const mttdWeight = Number(extractSeverity(alertData, defaultSeverity));
- const convertedWeight = severityUnit === '%' ? mttdWeight * 100 : mttdWeight;
- const statsCards = [
- {
- title: 'Number of anomalies',
- key: 'totalAlerts',
- tooltip: false,
- hideProjected: false,
- text: 'Actual number of alerts sent'
- },
- {
- title: 'Review Rate',
- key: 'responseRate',
- units: '%',
- tooltip: false,
- hideProjected: true,
- text: '% of anomalies that are reviewed.'
- },
- {
- title: 'Precision',
- key: 'precision',
- units: '%',
- tooltip: false,
- text: '% of true anomalies among alerted anomalies.'
- },
- {
- title: 'Recall',
- key: 'recall',
- units: '%',
- tooltip: false,
- text: '% of true alerted anomalies among the total true anomalies.'
- },
- {
- title: `MTTD for > ${convertedWeight}${severityUnit} change`,
- key: 'mttd',
- units: 'hrs',
- tooltip: false,
- hideProjected: true,
- text: `Minimum time to detect for anomalies with > ${convertedWeight}${severityUnit} change`
- }
- ];
-
- return buildAnomalyStats(alertEvalMetrics, statsCards, true);
- }
- ),
-
- /**
- * If user selects a dimension from the dropdown, we filter the anomaly results here.
- * NOTE: this is currently set up to support single-dimension filters
- * @type {Object}
- */
- filteredAnomalies: computed(
- 'selectedDimension',
- 'selectedResolution',
- 'anomalyData',
- 'anomaliesLoaded',
- function() {
- const {
- labelMap,
- anomalyData,
- anomaliesLoaded,
- selectedDimension: targetDimension,
- selectedResolution: targetResolution
- } = this.getProperties('labelMap', 'anomalyData', 'selectedDimension', 'selectedResolution', 'anomaliesLoaded');
- let newAnomalies = [];
-
- if (anomaliesLoaded && anomalyData) {
- newAnomalies = this.get('anomalyData');
- if (targetDimension !== 'All Dimensions') {
- // Filter for selected dimension
- newAnomalies = newAnomalies.filter(data => targetDimension === data.dimensionString);
- }
- if (targetResolution !== 'All Resolutions') {
- // Filter for selected resolution
- newAnomalies = newAnomalies.filter(data => targetResolution === labelMap[data.anomalyFeedback]);
- }
- // Let page know whether anomalies viewed are filtered or not
- set(this, 'isAnomalyListFiltered', anomalyData.length !== newAnomalies.length);
- // Add an index number to each row
- newAnomalies.forEach((anomaly, index) => {
- set(anomaly, 'index', ++index);
- });
- }
- return newAnomalies;
- }
- ),
-
- /**
- * All selected dimensions to be loaded into graph
- * @returns {Array}
- */
- selectedDimensions: computed(
- 'topDimensions',
- 'topDimensions.@each.isSelected',
- function() {
- return this.get('topDimensions').filterBy('isSelected');
- }
- ),
-
- /**
- * Find the active baseline option name
- * @type {String}
- */
- baselineTitle: computed(
- 'baselineOptions',
- function() {
- const activeOpName = this.get('baselineOptions').filter(item => item.isActive)[0].name;
- const displayName = `Current/${activeOpName}`;
- return displayName;
- }
- ),
-
- /**
- * Generate date range selection options if needed
- * @method renderDate
- * @param {Number} range - number of days (duration)
- * @return {String}
- */
- renderDate(range) {
- // TODO: enable single day range
- const newDate = buildDateEod(range, 'days').format("DD MM YYY");
- return `Last ${range} Days (${newDate} to Today)`;
- },
-
- /**
- * Concurrency task to ping the job-info endpoint to check status of an ongoing replay job.
- * If there is no progress after a set time, we display an error message.
- * @param {Number} jobId - the id for the newly triggered replay job
- * @return {undefined}
- */
- checkReplayStatus: task(function * (jobId) {
- yield timeout(2000);
-
- const {
- alertId,
- replayStartTime
- } = this.getProperties('alertId', 'replayStartTime');
- const replayStatusList = ['completed', 'failed', 'timeout'];
- const checkStatusUrl = `/detection-onboard/get-status?jobId=${jobId}`;
- let isReplayTimeUp = Number(moment.duration(moment().diff(replayStartTime)).asSeconds().toFixed(0)) > 60;
-
- // In replay status check, continue to display "pending" banner unless we have known success or failure.
- fetch(checkStatusUrl).then(checkStatus)
- .then((jobStatus) => {
- const replayStatusObj = _.has(jobStatus, 'taskStatuses')
- ? jobStatus.taskStatuses.find(status => status.taskName === 'FunctionReplay')
- : null;
- const replayStatus = replayStatusObj ? replayStatusObj.taskStatus.toLowerCase() : '';
- // When either replay is no longer pending or 60 seconds have passed, transition to full alert page.
- if (replayStatusList.includes(replayStatus) || isReplayTimeUp) {
- const repRunStatus = replayStatus === 'running' ? jobId : null;
- // Replay may be complete. Give server time to load anomalies
- later(this, function() {
- this.transitionToRoute('manage.alert', alertId, { queryParams: { jobId: null, repRunStatus }});
- }, 3000);
- } else {
- this.get('checkReplayStatus').perform(jobId);
- }
- })
- .catch(() => {
- // If we have job status failure, go ahead and transition to full alert page.
- this.transitionToRoute('manage.alert', alertId, { queryParams: { jobId: null }});
- });
- }),
-
- /**
- * Concurrency task to reload page once a running replay is complete
- * @param {Number} jobId - the id for the newly triggered replay job
- * @return {undefined}
- */
- checkForNewAnomalies: task(function * (jobId) {
- yield timeout(5000);
-
- // In replay status check, continue to display "pending" banner unless we have known success or failure.
- fetch(`/detection-onboard/get-status?jobId=${jobId}`).then(checkStatus)
- .then((jobStatus) => {
- const replayStatusObj = _.has(jobStatus, 'taskStatuses')
- ? jobStatus.taskStatuses.find(status => status.taskName === 'FunctionReplay')
- : null;
- if (replayStatusObj) {
- if (replayStatusObj.taskStatus.toLowerCase() === 'completed') {
- this.transitionToRoute({ queryParams: { repRunStatus: null }});
- } else {
- this.get('checkForNewAnomalies').perform(jobId);
- }
- }
- })
- .catch(() => {
- // If we have job status failure, go ahead and transition to full alert page.
- this.transitionToRoute('manage.alert', this.get('alertId'), { queryParams: { repRunStatus: null }});
- });
- }),
-
- /**
- * Send a POST request to the report anomaly API (2-step process)
- * http://go/te-ss-alert-flow-api
- * @method reportAnomaly
- * @param {String} id - The alert id
- * @param {Object} data - The input values from 'report new anomaly' modal
- * @return {Promise}
- */
- reportAnomaly(id, data) {
- const reportUrl = `/anomalies/reportAnomaly/${id}?`;
- const updateUrl = `/anomalies/updateFeedbackRange/${data.startTime}/${data.endTime}/${id}?feedbackType=${data.feedbackType}`;
- const requiredProps = ['data.startTime', 'data.endTime', 'data.feedbackType'];
- const missingData = !requiredProps.every(prop => isPresent(prop));
- let queryStringUrl = reportUrl;
-
- if (missingData) {
- return Promise.reject(new Error('missing data'));
- } else {
- Object.entries(data).forEach(([key, value]) => {
- queryStringUrl += `&${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
- });
- // Step 1: Report the anomaly
- return fetch(queryStringUrl, postProps('')).then((res) => checkStatus(res, 'post'))
- .then(() => {
- // Step 2: Automatically update anomaly feedback in that range
- return fetch(updateUrl, postProps('')).then((res) => checkStatus(res, 'post'));
- });
- }
- },
-
- /**
- * Modal opener for "report missing anomaly". Can be triggered from link click or
- * automatically via queryparam "openReport=true"
- * @method triggerOpenReportModal
- * @return {undefined}
- */
- triggerOpenReportModal() {
- this.setProperties({
- isReportSuccess: false,
- isReportFailure: false,
- openReportModal: true
- });
- // We need the C3/D3 graph to render after its containing parent elements are rendered
- // in order to avoid strange overflow effects.
- later(() => {
- this.set('renderModalContent', true);
- });
- },
-
- /**
- * When exiting route, lets kill the replay status check calls
- * @method clearAll
- * @return {undefined}
- */
- clearAll() {
- this.setProperties({
- activeRangeStart: '',
- activeRangeEnd: '',
- alertEvalMetrics: {}
- });
- // Cancel controller concurrency tasks
- this.get('checkReplayStatus').cancelAll();
- this.get('checkForNewAnomalies').cancelAll();
- },
-
- /**
- * Actions for alert page
- */
- actions: {
-
- /**
- * Handle selected dimension filter
- * @method onSelectDimension
- * @param {Object} selectedObj - the user-selected dimension to filter by
- */
- onSelectDimension(selectedObj) {
- setProperties(this, {
- currentPage: 1,
- selectedDimension: selectedObj
- });
- // Select graph dimensions based on filter
- this.get('topDimensions').forEach((dimension) => {
- const isAllSelected = selectedObj === 'All Dimensions';
- const isActive = selectedObj.includes(dimension.name) || isAllSelected;
- set(dimension, 'isSelected', isActive);
- });
- },
-
- /**
- * Handle selected resolution filter
- * @method onSelectResolution
- * @param {Object} selectedObj - the user-selected resolution to filter by
- */
- onSelectResolution(selectedObj) {
- setProperties(this, {
- currentPage: 1,
- selectedResolution: selectedObj
- });
- },
-
- /**
- * Handle dynamically saving anomaly feedback responses
- * @method onChangeAnomalyResponse
- * @param {Object} anomalyRecord - the anomaly being responded to
- * @param {String} selectedResponse - user-selected anomaly feedback option
- * @param {Object} inputObj - the selection object
- */
- onChangeAnomalyResponse: async function(anomalyRecord, selectedResponse, inputObj) {
- const responseObj = anomalyUtil.anomalyResponseObj.find(res => res.name === selectedResponse);
- const labelMap = get(this, 'labelMap');
- const loadedResponsesArr = [];
- const newOptionsArr = [];
- // Update select field
- set(inputObj, 'selected', selectedResponse);
- // Reset status icon
- set(this, 'renderStatusIcon', false);
- try {
- // Save anomaly feedback
- await anomalyUtil.updateAnomalyFeedback(anomalyRecord.anomalyId, responseObj.value);
- // We make a call to ensure our new response got saved
- const anomaly = await anomalyUtil.verifyAnomalyFeedback(anomalyRecord.anomalyId, responseObj.status);
- const filterMap = getWithDefault(anomaly, 'searchFilters.statusFilterMap', null);
- // This verifies that the status change got saved as key in the anomaly statusFilterMap property
- const keyPresent = filterMap && Object.keys(filterMap).find(key => responseObj.status.includes(key));
- if (keyPresent) {
- setProperties(anomalyRecord, {
- anomalyFeedback: responseObj.status,
- showResponseSaved: true,
- showResponseFailed: false
- });
- // Collect all available new labels
- loadedResponsesArr.push(responseObj.status, ...get(this, 'anomalyData').mapBy('anomalyFeedback'));
- loadedResponsesArr.forEach((response) => {
- if (labelMap[response]) { newOptionsArr.push(labelMap[response]) }
- });
- // Update resolutionOptions array - we may have a new option now
- set(this, 'resolutionOptions', [ ...new Set([ 'All Resolutions', ...newOptionsArr ])]);
- } else {
- throw 'Response not saved';
- }
- } catch (err) {
- setProperties(anomalyRecord, {
- showResponseFailed: true,
- showResponseSaved: false
- });
- }
- // Force status icon to refresh
- set(this, 'renderStatusIcon', true);
- },
-
- /**
- * Action handler for page clicks
- * @param {Number|String} page
- */
- onPaginationClick(page) {
- let newPage = page;
- let currentPage = this.get('currentPage');
-
- switch (page) {
- case 'previous':
- if (currentPage > 1) {
- newPage = --currentPage;
- } else {
- newPage = currentPage;
- }
- break;
- case 'next':
- if (currentPage < this.get('pagesNum')) {
- newPage = ++currentPage;
- } else {
- newPage = currentPage;
- }
- break;
- }
-
- this.set('currentPage', newPage);
- },
-
- /**
- * Handle submission of missing anomaly form from alert-report-modal
- */
- onSave() {
- const { alertId, missingAnomalyProps } = this.getProperties('alertId', 'missingAnomalyProps');
- this.reportAnomaly(alertId, missingAnomalyProps)
- .then((result) => {
- const rangeFormat = 'YYYY-MM-DD HH:mm';
- const startStr = moment(missingAnomalyProps.startTime).format(rangeFormat);
- const endStr = moment(missingAnomalyProps.endTime).format(rangeFormat);
- this.setProperties({
- isReportSuccess: true,
- openReportModal: false,
- reportedRange: `${startStr} - ${endStr}`
- });
- // Reload after save confirmation
- later(this, function() {
- this.send('refreshModel');
- }, 1000);
- })
- // If failure, leave modal open and report
- .catch((err) => {
- this.setProperties({
- missingAnomalyProps: {},
- isReportFailure: true
- });
- });
- },
-
- /**
- * Handle missing anomaly modal cancel
- */
- onCancel() {
- this.setProperties({
- isReportSuccess: false,
- isReportFailure: false,
- openReportModal: false,
- renderModalContent: false
- });
- },
-
- /**
- * Open modal for missing anomalies
- */
- onClickReportAnomaly() {
- this.triggerOpenReportModal();
- },
-
- /**
- * Received bubbled-up action from modal
- * @param {Object} all input field values
- */
- onInputMissingAnomaly(inputObj) {
- this.set('missingAnomalyProps', inputObj);
- },
-
- /**
- * Handle display of selected baseline options
- * @param {Object} wowObj - the baseline selection
- */
- onBaselineOptionClick(wowObj) {
- const { anomalyData, baselineOptions } = this.getProperties('anomalyData', 'baselineOptions');
- const isValidSelection = !wowObj.isActive;
- let newOptions = baselineOptions.map((val) => {
- return { name: val.name, isActive: false };
- });
-
- // Set active option
- newOptions.find((val) => val.name === wowObj.name).isActive = true;
- this.set('baselineOptions', newOptions);
-
- // Set new values for each anomaly
- if (isValidSelection) {
- anomalyData.forEach((anomaly) => {
- const wow = anomaly.wowData;
- const wowDetails = wow.compareResults.find(res => res.compareMode.toLowerCase() === wowObj.name.toLowerCase());
- let curr = anomaly.current;
- let base = anomaly.baseline;
- let change = anomaly.changeRate;
-
- if (wowDetails) {
- curr = wow.currentVal.toFixed(2);
- base = wowDetails.baselineValue.toFixed(2);
- change = floatToPercent(wowDetails.change);
- }
-
- // Set displayed value properties. Note: ensure no CP watching these props
- setProperties(anomaly, {
- shownCurrent: curr,
- shownBaseline: base,
- shownChangeRate: change
- });
- });
- }
- },
-
- /**
- * Sets the new custom date range for anomaly coverage
- * @method onRangeSelection
- * @param {Object} rangeOption - the user-selected time range to load
- */
- onRangeSelection(rangeOption) {
- const {
- start,
- end,
- value: duration
- } = rangeOption;
- const durationObj = {
- duration,
- startDate: moment(start).valueOf(),
- endDate: moment(end).valueOf()
- };
- // Cache the new time range and update page with it
- this.get('durationCache').setDuration(durationObj);
- this.transitionToRoute({ queryParams: durationObj });
- },
-
-
- /**
- * Load tuning sub-route and properly toggle alert nav button
- */
- onClickTuneSensitivity() {
- this.send('updateParentLink');
- const { duration, startDate, endDate } = this.model;
- this.transitionToRoute('manage.alert.tune', this.get('alertId'), { queryParams: { duration, startDate, endDate }});
- },
-
- /**
- * Handle sorting for each sortable table column
- * @param {String} sortKey - stringified start date
- */
- toggleSortDirection(sortKey) {
- const propName = 'sortColumn' + sortKey.capitalize() + 'Up' || '';
-
- this.toggleProperty(propName);
- if (this.get(propName)) {
- this.set('selectedSortMode', sortKey + ':up');
- } else {
- this.set('selectedSortMode', sortKey + ':down');
- }
-
- //On sort, set table to first pagination page
- this.set('currentPage', 1);
- }
-
- }
-});
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/alert/explore/route.js b/thirdeye/thirdeye-frontend/app/pods/manage/alert/explore/route.js
deleted file mode 100644
index c67b6a3..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/manage/alert/explore/route.js
+++ /dev/null
@@ -1,605 +0,0 @@
-/**
- * Handles the 'explore' route for manage alert
- * @module manage/alert/edit/explore
- * @exports manage/alert/edit/explore
- */
-import RSVP from 'rsvp';
-import fetch from 'fetch';
-import moment from 'moment';
-import Route from '@ember/routing/route';
-import { later } from '@ember/runloop';
-import { task, timeout } from 'ember-concurrency';
-import { inject as service } from '@ember/service';
-import {
- get,
- setProperties
-} from '@ember/object';
-import { isPresent, isNone, isBlank } from '@ember/utils';
-import {
- checkStatus,
- buildDateEod,
- makeFilterString,
- toIso
-} from 'thirdeye-frontend/utils/utils';
-import {
- enhanceAnomalies,
- toIdGroups,
- setUpTimeRangeOptions,
- getTopDimensions,
- buildMetricDataUrl,
- extractSeverity
-} from 'thirdeye-frontend/utils/manage-alert-utils';
-import {
- selfServeApiCommon,
- selfServeApiGraph
-} from 'thirdeye-frontend/utils/api/self-serve';
-import {
- anomalyResponseObj,
- anomalyResponseMap
-} from 'thirdeye-frontend/utils/anomaly';
-import { getAnomalyDataUrl } from 'thirdeye-frontend/utils/api/anomaly';
-
-/**
- * Shorthand for setting date defaults
- */
-const displayDateFormat = 'YYYY-MM-DD HH:mm';
-
-/**
- * Basic alert page constants
- */
-const DEFAULT_SEVERITY = 0.3;
-const DIMENSION_COUNT = 7;
-const METRIC_DATA_COLOR = 'blue';
-
-/**
- * Basic alert page defaults
- */
-const wowOptions = ['Wow', 'Wo2W', 'Wo3W', 'Wo4W'];
-const durationMap = { m:'month', d:'day', w:'week' };
-const baselineOptions = [{ name: 'Predicted', isActive: true }];
-const defaultDurationObj = {
- duration: '3m',
- startDate: buildDateEod(3, 'month').valueOf(),
- endDate: moment()
-};
-
-/**
- * Build WoW array from basic options
- */
-const newWowList = wowOptions.map((item) => {
- return { name: item, isActive: false };
-});
-
-/**
- * Derives start/end timestamps based on queryparams and user-selected time range with certain fall-backs/defaults
- * @param {String} bucketUnit - is requested range from an hourly or minutely metric?
- * @param {String} duration - the model's processed query parameter for duration ('1m', '2w', etc)
- * @param {String} start - the model's processed query parameter for startDate
- * @param {String} end - the model's processed query parameter for endDate
- * @returns {Object}
- */
-const processRangeParams = (bucketUnit = 'DAYS', duration, start, end) => {
- // To avoid loading too much data, override our time span defaults based on whether the metric is 'minutely'
- const isMetricMinutely = bucketUnit.toLowerCase().includes('minute');
- const defaultQueryUnit = isMetricMinutely ? 'week' : 'month';
- const defaultQuerySize = isMetricMinutely ? 2 : 1;
-
- // We also allow a 'duration' query param to set the time range. For example, duration=15d (last 15 days)
- const qsRegexMatch = duration.match(new RegExp(/^(\d)+([d|m|w])$/i));
- const durationMatch = duration && qsRegexMatch ? qsRegexMatch : [];
-
- // If the duration string is recognized, we use it. Otherwise, we fall back on the defaults above
- const querySize = durationMatch && durationMatch.length ? durationMatch[1] : defaultQuerySize;
- const queryUnit = durationMatch && durationMatch.length ? durationMap[durationMatch[2].toLowerCase()] : defaultQueryUnit;
-
- // If duration = 'custom', we know the user is requesting specific start/end times.
- // In this case, we will use those instead of our parsed duration & defaults
- const isCustomDate = duration === 'custom';
- const baseStart = isCustomDate ? moment(parseInt(start, 10)) : buildDateEod(querySize, queryUnit);
- const baseEnd = isCustomDate ? moment(parseInt(end, 10)) : moment();
-
- // These resulting timestamps are used for our graph and anomaly queries
- const startStamp = baseStart.valueOf();
- const endStamp = baseEnd.valueOf();
-
- return { startStamp, endStamp, baseStart, baseEnd };
-};
-
-/**
- * Setup for query param behavior
- */
-const queryParamsConfig = {
- refreshModel: true,
- replace: false
-};
-
-export default Route.extend({
- queryParams: {
- duration: queryParamsConfig,
- startDate: queryParamsConfig,
- endDate: queryParamsConfig,
- openReport: queryParamsConfig,
- repRunStatus: queryParamsConfig
- },
-
- /**
- * Make duration service accessible
- */
- durationCache: service('services/duration'),
- session: service(),
-
- beforeModel(transition) {
- const { duration, startDate } = transition.queryParams;
- // Default to 1 month of anomalies to show if no dates present in query params
- if (!duration || !startDate) {
- this.transitionTo({ queryParams: defaultDurationObj });
- }
- },
-
- model(params, transition) {
- const { id, alertData, jobId } = this.modelFor('manage.alert');
- const isReplayDone = isNone(jobId) && jobId !== -1;
- if (!id) { return; }
-
- // Get duration data from service
- const {
- duration,
- startDate,
- endDate
- } = this.get('durationCache').getDuration(transition.queryParams, defaultDurationObj);
-
- // Prepare endpoints for eval, mttd, projected metrics calls
- const dateParams = `start=${toIso(startDate)}&end=${toIso(endDate)}`;
- const evalUrl = `/detection-job/eval/filter/${id}?${dateParams}`;
- const mttdUrl = `/detection-job/eval/mttd/${id}?severity=${extractSeverity(alertData, DEFAULT_SEVERITY)}`;
- let performancePromiseHash = {
- current: {},
- projected: {},
- mttd: ''
- };
-
- // Once replay is done or timed out, this route loads all needed data. We load placeholders first.
- if (isReplayDone) {
- performancePromiseHash = {
- current: fetch(`${evalUrl}&isProjected=FALSE`).then(checkStatus),
- projected: fetch(`${evalUrl}&isProjected=TRUE`).then(checkStatus),
- mttd: fetch(mttdUrl).then(checkStatus)
- };
- }
-
- return RSVP.hash(performancePromiseHash)
- .then((alertEvalMetrics) => {
- Object.assign(alertEvalMetrics.current, { mttd: alertEvalMetrics.mttd});
- return {
- id,
- jobId,
- alertData,
- duration,
- startDate,
- evalUrl,
- endDate,
- dateParams,
- alertEvalMetrics,
- isReplayDone
- };
- })
- // Catch is not mandatory here due to our error action, but left it to add more context.
- .catch((error) => {
- return RSVP.reject({ error, location: `${this.routeName}:model`, calls: performancePromiseHash });
- });
- },
-
- afterModel(model) {
- this._super(model);
-
- const {
- id: alertId,
- alertData,
- isReplayDone,
- startDate,
- endDate,
- duration,
- dateParams,
- alertEvalMetrics
- } = model;
-
- // Pull alert properties into context
- const {
- metric: metricName,
- collection: dataset,
- exploreDimensions,
- filters: filtersRaw,
- bucketSize,
- bucketUnit
- } = alertData;
- // Derive start/end time ranges based on querystring input with fallback on default '1 month'
- const {
- startStamp,
- endStamp,
- baseStart,
- baseEnd
- } = processRangeParams(bucketUnit, duration, startDate, endDate);
-
- // Set initial value for metricId for early transition cases
- const config = {
- startStamp,
- endStamp,
- bucketSize,
- bucketUnit,
- baseEnd,
- baseStart,
- exploreDimensions,
- filters: filtersRaw ? makeFilterString(filtersRaw) : ''
- };
-
- // Load endpoints for projected metrics. TODO: consolidate into CP if duplicating this logic
- const anomalyDataUrl = getAnomalyDataUrl(startStamp, endStamp);
- const metricsUrl = selfServeApiCommon.metricAutoComplete(metricName);
- const anomaliesUrl = `/dashboard/anomaly-function/${alertId}/anomalies?${dateParams}&useNotified=true`;
- let anomalyPromiseHash = {
- projectedMttd: 0,
- metricsByName: [],
- anomalyIds: []
- };
-
- // If replay still pending, load placeholders for this data.
- if (isReplayDone) {
- anomalyPromiseHash = {
- projectedMttd: 0, // In overview mode, no projected MTTD value is needed
- metricsByName: fetch(metricsUrl).then(checkStatus),
- anomalyIds: fetch(anomaliesUrl).then(checkStatus)
- };
- }
-
- return RSVP.hash(anomalyPromiseHash)
- .then(async (data) => {
- const metricId = this._locateMetricId(data.metricsByName, alertData);
- const totalAnomalies = data.anomalyIds.length;
- Object.assign(alertEvalMetrics.projected, { mttd: data.projectedMttd });
- Object.assign(config, { id: metricId });
- Object.assign(model, {
- anomalyIds: data.anomalyIds,
- exploreDimensions,
- totalAnomalies,
- anomalyDataUrl,
- anomaliesUrl,
- config
- });
- const maxTimeUrl = selfServeApiGraph.maxDataTime(metricId);
- const maxTime = isReplayDone && metricId ? await fetch(maxTimeUrl).then(checkStatus) : moment().valueOf();
- Object.assign(model, { metricDataUrl: buildMetricDataUrl({
- maxTime,
- endStamp: config.endStamp,
- startStamp: config.startStamp,
- id: metricId,
- filters: config.filters,
- granularity: `${config.bucketSize}_${config.bucketUnit}`,
- dimension: 'All' // NOTE: avoid dimension explosion - config.exploreDimensions ? config.exploreDimensions.split(',')[0] : 'All'
- })});
- })
- // Catch is not mandatory here due to our error action, but left it to add more context
- .catch((err) => {
- return RSVP.reject({ err, location: `${this.routeName}:afterModel`, calls: anomalyPromiseHash });
- });
- },
-
- setupController(controller, model) {
- this._super(controller, model);
-
- const {
- id,
- jobId,
- alertData,
- anomalyIds,
- duration,
- config,
- loadError,
- isReplayDone,
- metricDataUrl,
- anomalyDataUrl,
- totalAnomalies,
- exploreDimensions,
- alertEvalMetrics
- } = model;
-
- // Prime the controller
- controller.setProperties({
- loadError,
- jobId,
- alertData,
- alertId: id,
- DEFAULT_SEVERITY,
- totalAnomalies,
- anomalyDataUrl,
- baselineOptions,
- alertEvalMetrics,
- anomaliesLoaded: false,
- isMetricDataInvalid: false,
- isMetricDataLoading: true,
- alertDimension: exploreDimensions,
- isReplayPending: isPresent(jobId) && jobId !== -1,
- alertHasDimensions: isPresent(exploreDimensions),
- timeRangeOptions: setUpTimeRangeOptions(['3m'], duration),
- baselineOptionsLoading: anomalyIds && anomalyIds.length > 0,
- responseOptions: anomalyResponseObj.map(response => response.name)
- });
- // Kick off controller defaults and replay status check
- controller.initialize();
-
- // Ensure date range picker gets populated correctly
- later(this, () => {
- controller.setProperties({
- activeRangeStart: moment(config.startStamp).format(displayDateFormat),
- activeRangeEnd: moment(config.endStamp).format(displayDateFormat)
- });
- });
-
- // Once replay is finished, begin loading anomaly and graph data as concurrency tasks
- // See https://github.com/linkedin/pinot/pull/2518#discussion-diff-169751380R366
- if (isReplayDone) {
- get(this, 'loadAnomalyData').perform(anomalyIds, exploreDimensions);
- get(this, 'loadGraphData').perform(metricDataUrl, exploreDimensions);
- }
- },
-
- resetController(controller, isExiting) {
- this._super(...arguments);
-
- // Cancel all pending concurrency tasks in controller
- if (isExiting) {
- get(this, 'loadAnomalyData').cancelAll();
- get(this, 'loadGraphData').cancelAll();
- controller.clearAll();
- }
- },
-
- /**
- * Performs the repetitive task of setting graph properties based on
- * returned metric data and dimension data
- * @method _setGraphProperties
- * @param {Object} metricData - returned metric timeseries data
- * @param {String} exploreDimensions - string of metric dimensions
- * @returns {undefined}
- * @private
- */
- _setGraphProperties(metricData, exploreDimensions) {
- const alertDimension = exploreDimensions ? exploreDimensions.split(',')[0] : '';
- Object.assign(metricData, { color: METRIC_DATA_COLOR });
- this.controller.setProperties({
- metricData,
- alertDimension,
- isMetricDataLoading: false
- });
- // If alert has dimensions set, load them into graph once replay is done.
- if (exploreDimensions && !this.controller.isReplayPending) {
- const topDimensions = getTopDimensions(metricData, DIMENSION_COUNT);
- this.controller.setProperties({
- topDimensions,
- isDimensionFetchDone: true,
- availableDimensions: topDimensions.length
- });
- }
- },
-
- /**
- * Tries find a specific metric id based on a common dataset string
- * @method _locateMetricId
- * @param {Array} metricList - list of metrics from metric-by-name lookup
- * @param {Object} alertData - currently loaded alert properties
- * @returns {Number} target metric id
- * @private
- */
- _locateMetricId(metricList, alertData) {
- const metricId = metricList.find((metric) => {
- return (metric.name === alertData.metric) && (metric.dataset === alertData.collection);
- }) || { id: 0 };
- return isBlank(metricList) ? 0 : metricId.id;
- },
-
- /**
- * Returns an aggregate list of all labels found in the currently-loaded anomaly set
- * @method _filterResolutionLabels
- * @param {Array} anomalyData - list of all anomalies for current alert
- * @returns {Array} list of all labels found in anomaly set
- * @private
- */
- _filterResolutionLabels(anomalyData) {
- let availableLabels = [];
- anomalyData.forEach((anomaly) => {
- let mappedLabel = anomalyResponseMap[anomaly.anomalyFeedback];
- if (mappedLabel) { availableLabels.push(mappedLabel); }
- });
- return availableLabels;
- },
-
- /**
- * Fetches all anomaly data for found anomalies - downloads all 'pages' of data from server
- * in order to handle sorting/filtering on the entire set locally. Start/end date are not used here.
- * @param {Array} anomalyIds - list of all found anomaly ids
- * @returns {RSVP promise}
- */
- fetchCombinedAnomalies: task(function * (anomalyIds) {
- yield timeout(300);
- if (anomalyIds.length) {
- const idGroups = toIdGroups(anomalyIds);
- const anomalyPromiseHash = idGroups.map((group, index) => {
- let idStringParams = `anomalyIds=${encodeURIComponent(idGroups[index].toString())}`;
- let url = `/anomalies/search/anomalyIds/0/0/${index + 1}?${idStringParams}`;
- let getAnomalies = get(this, 'fetchAnomalyEntity').perform(url);
- return RSVP.resolve(getAnomalies);
- });
- return RSVP.all(anomalyPromiseHash);
- } else {
- return RSVP.resolve([]);
- }
- }),
-
- /**
- * Fetches change rate data for each available anomaly id
- * @method fetchCombinedAnomalyChangeData
- * @param {Array} anomalyData - array of processed anomalies
- * @returns {RSVP promise}
- */
- fetchCombinedAnomalyChangeData: task(function * (anomalyData) {
- yield timeout(300);
- let promises = [];
-
- anomalyData.forEach((anomaly) => {
- let id = anomaly.anomalyId;
- promises[id] = get(this, 'fetchAnomalyEntity').perform(`/anomalies/${id}`);
- });
-
- return RSVP.hash(promises);
- }),
-
- /**
- * Fetches severity scores for all anomalies
- * TODO: Move this and other shared requests to a common service
- * @param {Array} anomalyIds - list of all found anomaly ids
- * @returns {RSVP promise}
- */
- fetchSeverityScores: task(function * (anomalyIds) {
- yield timeout(300);
- if (anomalyIds && anomalyIds.length) {
- const anomalyPromiseHash = anomalyIds.map((id) => {
- return RSVP.hash({
- id,
- score: get(this, 'fetchAnomalyEntity').perform(`/dashboard/anomalies/score/${id}`)
- });
- });
- return RSVP.allSettled(anomalyPromiseHash);
- } else {
- return RSVP.resolve([]);
- }
- }),
-
- /**
- * Fetch any single entity as a cancellable concurrency task
- * @param {String} url - endpoint for fetch
- * @returns {fetch promise}
- */
- fetchAnomalyEntity: task(function * (url) {
- yield timeout(300);
- return fetch(url).then(checkStatus);
- }),
-
- /**
- * Fetch all anomalies we have Ids for. Enhance the data and populate power-select filter options.
- * Using ember concurrency parent/child tasks. When parent is cancelled, so are children
- * http://ember-concurrency.com/docs/child-tasks.
- * TODO: complete concurrency task error handling and refactor child tasks for cuncurrency.
- * @param {Array} anomalyIds - the IDs of anomalies that have been reported for this alert.
- * @return {undefined}
- */
- loadAnomalyData: task(function * (anomalyIds, exploreDimensions) {
- const dimensionOptions = ['All Dimensions'];
- const hasDimensions = exploreDimensions && exploreDimensions.length;
- // Load data for each anomaly Id
- const rawAnomalies = yield get(this, 'fetchCombinedAnomalies').perform(anomalyIds);
- // Fetch and append severity score to each anomaly record
- const severityScores = yield get(this, 'fetchSeverityScores').perform(anomalyIds);
- // Process anomaly records to make them template-ready
- const anomalyData = yield enhanceAnomalies(rawAnomalies, severityScores);
- // Prepare de-duped power-select option array for anomaly feedback
- const resolutionOptions = ['All Resolutions', ...new Set(this._filterResolutionLabels(anomalyData))];
- // Populate dimensions power-select options if dimensions exist
- if (hasDimensions) {
- dimensionOptions.push(...new Set(anomalyData.map(anomaly => anomaly.dimensionString)));
- }
- // Push anomaly data into controller
- this.controller.setProperties({
- anomalyData,
- dimensionOptions,
- resolutionOptions,
- anomaliesLoaded: true,
- totalLoadedAnomalies: anomalyData.length,
- baselineOptionsLoading: false
- });
- // Fetch and append extra WoW data for each anomaly record
- const wowData = yield get(this, 'fetchCombinedAnomalyChangeData').perform(anomalyData);
- anomalyData.forEach((anomaly) => {
- anomaly.wowData = wowData[anomaly.anomalyId] || {};
- });
- // Load enhanced dataset into controller (WoW options will appear)
- this.controller.setProperties({
- anomalyData,
- baselineOptions: [baselineOptions[0], ...newWowList]
- });
- // We use .cancelOn('deactivate') to make sure the task cancels when the user leaves the route.
- // We use restartable to ensure that only one instance of the task is running at a time, hence
- // any time setupController performs the task, any prior instances are canceled.
- }).cancelOn('deactivate').restartable(),
-
- /**
- * Concurrenty task to ping the job-info endpoint to check status of an ongoing replay job.
- * If there is no progress after a set time, we display an error message.
- * @param {Number} jobId - the id for the newly triggered replay job
- * @param {String} functionName - user-provided new function name (used to validate creation)
- * @return {undefined}
- */
- loadGraphData: task(function * (metricDataUrl, exploreDimensions) {
- try {
- // Fetch and load graph metric data from either local store or API
- const metricData = yield fetch(metricDataUrl).then(checkStatus);
- // Load graph with metric data from timeseries API
- yield this._setGraphProperties(metricData, exploreDimensions);
- } catch (e) {
- this.controller.setProperties({
- isMetricDataInvalid: true,
- isMetricDataLoading: false
- });
- }
- }).cancelOn('deactivate').restartable(),
-
- actions: {
- /**
- * save session url for transition on login
- * @method willTransition
- */
- willTransition(transition) {
- //saving session url - TODO: add a util or service - lohuynh
- if (transition.intent.name && transition.intent.name !== 'logout') {
- this.set('session.store.fromUrl', {lastIntentTransition: transition});
- }
- },
-
- /**
- * Refresh route's model.
- */
- refreshModel() {
- this.replaceWith({ queryParams: { openReport: false } });
- },
-
- /**
- * Refresh anomaly data when changes are made
- */
- refreshAnomalyTable() {
- const { anomalyIds, exploreDimensions } = this.currentModel;
- if (anomalyIds && anomalyIds.length) {
- get(this, 'loadAnomalyData').perform(anomalyIds, exploreDimensions);
- }
- },
-
- /**
- * Change link state in parent controller to reflect transition to tuning route
- */
- updateParentLink() {
- setProperties(this.controllerFor('manage.alert'), {
- isOverViewModeActive: false,
- isEditModeActive: true
- });
- // Cancel route's main concurrency tasks
- get(this, 'loadAnomalyData').cancelAll();
- get(this, 'loadGraphData').cancelAll();
- },
-
- /**
- * Handle any errors occurring in model/afterModel in parent route
- * https://www.emberjs.com/api/ember/2.16/classes/Route/events/error?anchor=error
- * https://guides.emberjs.com/v2.18.0/routing/loading-and-error-substates/#toc_the-code-error-code-event
- */
- error() {
- return true;
- }
- }
-});
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/alert/explore/template.hbs b/thirdeye/thirdeye-frontend/app/pods/manage/alert/explore/template.hbs
deleted file mode 100644
index 0539b1a..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/manage/alert/explore/template.hbs
+++ /dev/null
@@ -1,364 +0,0 @@
-<div class="manage-alert-explore">
- {{#if (not isReplayPending)}}
- {{!-- Date range selector --}}
- {{range-pill-selectors
- title="Showing"
- maxTime=maxTime
- uiDateFormat=uiDateFormat
- activeRangeEnd=activeRangeEnd
- activeRangeStart=activeRangeStart
- timeRangeOptions=timeRangeOptions
- timePickerIncrement=timePickerIncrement
- selectAction=(action "onRangeSelection")
- }}
-
- {{#if isPageLoadFailure}}
- {{#bs-alert type="danger" class="te-form__banner te-form__banner--failure"}}
- <strong>Error:</strong> Failed to load performance data.
- {{/bs-alert}}
- {{/if}}
-
- <div class="te-horizontal-cards te-content-block">
- <h4 class="te-self-serve__block-title">Alert Performance</h4>
- <p class="te-self-serve__block-subtext te-self-serve__block-subtext--narrow">All estimated performance numbers are based on reviewed anomalies.</p>
- <a class="te-self-serve__side-link" {{action "onClickTuneSensitivity" this}}>
- <i class="glyphicon glyphicon-cog te-icon__inline-link"></i> Customize sensitivity
- </a>
- <div class="te-horizontal-cards__container">
- {{!-- Alert anomaly stats cards --}}
- {{anomaly-stats-block
- isTunePreviewActive=isTunePreviewActive
- displayMode="explore"
- anomalyStats=anomalyStats
- }}
- </div>
- {{#if repRunStatus}}
- <p class="te-self-serve__block-subtext te-self-serve__block-subtext--normal">Replay in progress. Please check back later...</p>
- {{/if}}
- </div>
-
- {{#if isReportSuccess}}
- {{#bs-alert type="success" class="te-form__banner te-form__banner--success"}}
- <strong>Success:</strong> Anomaly reported for dates <strong>{{reportedRange}}</strong>. Reloading anomalies...
- {{/bs-alert}}
- {{/if}}
-
- {{#if isReportFailure}}
- {{#bs-alert type="danger" class="te-form__banner te-form__banner--failure"}}
- <strong>Error:</strong> Failed to save reported anomaly.
- {{/bs-alert}}
- {{/if}}
-
- {{#if (and anomaliesLoaded isAnomalyLoadError)}}
- {{#bs-alert type="danger" class="te-form__banner te-form__banner--failure"}}
- <strong>Warning:</strong> We are not able to load data for {{model.totalAnomalies}} anomalies.
- Please <a class="thirdeye-link-secondary thirdeye-link-secondary--warning" target="_blank" href="{{graphMailtoLink}}">ask_thirdeye</a> for confirmation.
- {{/bs-alert}}
- {{/if}}
-
- <div class="te-content-block">
- <h4 class="te-self-serve__block-title">Anomalies over time (
- {{#if anomaliesLoaded}}
- {{filteredAnomalies.length}}
- {{#if isAnomalyListFiltered}}
- of {{totalLoadedAnomalies}}
- {{/if}}
- {{else}}
- ...loading anomalies
- {{/if}})
- </h4>
- <a class="te-self-serve__side-link te-self-serve__side-link--high" {{action "onClickReportAnomaly" this}}>Report missing anomaly</a>
-
- {{!-- Dimension selector --}}
- {{#if alertHasDimensions}}
- <div class="te-form__select te-form__select--wider col-md-3">
- {{#power-select
- triggerId="select-dimension"
- triggerClass="te-form__select"
- options=dimensionOptions
- searchEnabled=true
- searchPlaceholder="Type to filter..."
- matchTriggerWidth=true
- matchContentWidth=true
- selected=selectedDimension
- onchange=(action "onSelectDimension")
- as |dimension|
- }}
- {{dimension}}
- {{/power-select}}
- </div>
- {{/if}}
- {{!-- Resolution selector --}}
- {{#if totalAnomalies}}
- <div class="col-md-3 {{if (not alertHasDimensions) "te-form__select--left"}}">
- {{#power-select
- triggerId="select-resolution"
- triggerClass="te-form__select"
- options=resolutionOptions
- searchEnabled=false
- matchTriggerWidth=true
- matchContentWidth=true
- selected=selectedResolution
- onchange=(action "onSelectResolution")
- as |resolution|
- }}
- {{resolution}}
- {{/power-select}}
- </div>
- {{/if}}
-
- {{!-- Redirect Modal --}}
- {{#te-modal
- isShowingModal=true
- headerText="Your Alert Has Been Migrated"
- noButtons=true
- isCancellable=false
- }}
- <main class="te-form alert-report-modal__redirect">
- <legend class="te-report-title">This alert has been deprecated.</legend>
- <div class="te-form__note">Please click the following link and update any bookmarks:
- {{#if newId}}
- {{#link-to "manage.explore" newId class="thirdeye-link-secondary"}}
- Migrated Alert
- {{/link-to}}
- {{else}}
- {{#link-to "manage.alerts" class="thirdeye-link-secondary"}}
- Migrated Alert
- {{/link-to}}
- {{/if}}
- </div>
- </main>
- {{/te-modal}}
-
- {{!-- Missing anomaly modal --}}
- {{#te-modal
- cancelButtonText="Cancel"
- submitButtonText="Report"
- submitAction=(action "onSave")
- cancelAction=(action "onCancel")
- isShowingModal=openReportModal
- headerText="Report Undetected Anomaly"
- }}
- {{#if renderModalContent}}
- {{alert-report-modal
- maxTime=maxTime
- showTimePicker=true
- metricData=metricData
- uiDateFormat=uiDateFormat
- metricName=alertData.metric
- viewRegionEnd=viewRegionEnd
- topDimensions=topDimensions
- alertDimension=alertDimension
- graphMailtoLink=graphMailtoLink
- viewRegionStart=viewRegionStart
- alertName=alertData.functionName
- predefinedRanges=predefinedRanges
- dimensionOptions=dimensionOptions
- timePickerIncrement=timePickerIncrement
- isDimensionFetchDone=isDimensionFetchDone
- isMetricDataLoading=isMetricDataLoading
- isMetricDataInvalid=isMetricDataInvalid
- inputAction=(action "onInputMissingAnomaly")
- }}
- {{else}}
- {{ember-spinner}}
- {{/if}}
- {{/te-modal}}
-
- {{!-- Alert page graph --}}
- {{self-serve-graph
- metricData=metricData
- isMetricSelected=true
- componentId='alert-page'
- topDimensions=topDimensions
- isTopDimensionsAllowed=false
- selectedDimension=alertDimension
- selectedDimensions=selectedDimensions
- graphMailtoLink=graphMailtoLink
- isMetricDataLoading=isMetricDataLoading
- isMetricDataInvalid=isMetricDataInvalid
- isDimensionFetchDone=isDimensionFetchDone
- }}
-
- {{#if filteredAnomalies}}
- {{!-- Baseline type selector --}}
- {{range-pill-selectors
- title="Baseline"
- timeRangeOptions=baselineOptions
- selectAction=(action "onBaselineOptionClick")
- }}
- {{/if}}
-
- {{!-- Alert anomaly table --}}
- <div class="te-block-container">
- {{#if baselineOptionsLoading}}
- <div class="spinner-wrapper-self-serve spinner-wrapper-self-serve--custom">{{ember-spinner}}</div>
- {{/if}}
- <table class="te-anomaly-table">
- {{#if filteredAnomalies}}
- <thead>
- <tr class="te-anomaly-table__row te-anomaly-table__head">
- <th class="te-anomaly-table__cell-head te-anomaly-table__cell-head--left">
- <a class="te-anomaly-table__cell-link" {{action "toggleSortDirection" "number"}}>#
- <i class="te-anomaly-table__icon glyphicon {{if sortColumnNumberUp "glyphicon-menu-down" "glyphicon-menu-up"}}"></i>
- </a>
- </th>
- <th class="te-anomaly-table__cell-head te-anomaly-table__cell-head--left">
- <a class="te-anomaly-table__cell-link" {{action "toggleSortDirection" "start"}}>
- Start/Duration (PDT)
- <i class="te-anomaly-table__icon glyphicon {{if sortColumnStartUp "glyphicon-menu-up" "glyphicon-menu-down"}}"></i>
- </a>
- </th>
- {{#if alertHasDimensions}}
- <th class="te-anomaly-table__cell-head te-anomaly-table__cell-head--fixed">Dimensions</th>
- {{/if}}
- <th class="te-anomaly-table__cell-head">
- <a class="te-anomaly-table__cell-link" {{action "toggleSortDirection" "score"}}>
- Severity Score
- <i class="te-anomaly-table__icon glyphicon {{if sortColumnScoreUp "glyphicon-menu-up" "glyphicon-menu-down"}}"></i>
- </a>
- </th>
- <th class="te-anomaly-table__cell-head">
- <a class="te-anomaly-table__cell-link" {{action "toggleSortDirection" "change"}}>
- {{baselineTitle}}
- <i class="te-anomaly-table__icon glyphicon {{if sortColumnChangeUp "glyphicon-menu-up" "glyphicon-menu-down"}}"></i>
- </a>
- </th>
- <th class="te-anomaly-table__cell-head">
- <a class="te-anomaly-table__cell-link" {{action "toggleSortDirection" "resolution"}}>
- Resolution
- <i class="te-anomaly-table__icon glyphicon {{if sortColumnResolutionUp "glyphicon-menu-up" "glyphicon-menu-down"}}"></i>
- </a>
- </th>
- <th class="te-anomaly-table__cell-head"></th>
- </tr>
- </thead>
- {{/if}}
- <tbody>
- {{#each paginatedFilteredAnomalies as |anomaly|}}
- <tr class="te-anomaly-table__row">
- <td class="te-anomaly-table__cell te-anomaly-table__cell--index">{{anomaly.index}}</td>
- <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">
- <a target="_blank" class="te-anomaly-table__link" href="/app/#/rootcause?anomalyId={{anomaly.anomalyId}}">
- {{anomaly.startDateStr}}
- </a>
- </li>
- <li class="te-anomaly-table__list-item te-anomaly-table__list-item--lighter">{{anomaly.durationStr}}</li>
- </ul>
- </td>
- {{#if alertHasDimensions}}
- <td class="te-anomaly-table__cell">
- <ul class="te-anomaly-table__list">
- {{#each anomaly.dimensionList as |dimension|}}
- <li class="te-anomaly-table__list-item te-anomaly-table__list-item--smaller" title="{{dimension.dimensionVal}}">
- {{dimension.dimensionKey}}: <span class="stronger">{{dimension.dimensionVal}}</span>
- </li>
- {{else}}
- -
- {{/each}}
- </ul>
- </td>
- {{/if}}
- <td class="te-anomaly-table__cell">{{anomaly.severityScore}}</td>
- <td class="te-anomaly-table__cell">
- <ul class="te-anomaly-table__list">
- <li>{{anomaly.shownCurrent}} / {{anomaly.shownBaseline}}</li>
- <li class="te-anomaly-table__value-label te-anomaly-table__value-label--{{calculate-direction anomaly.shownChangeRate}}">
- {{#if (not anomaly.isNullChangeRate)}}
- ({{anomaly.shownChangeRate}}%)
- {{else}}
- (N/A)
- {{/if}}
- </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>
- {{/if}}
- {{#if anomaly.showResponseFailed}}
- <i class="te-anomaly-table__icon--status te-anomaly-table__icon--error glyphicon glyphicon-remove-circle"></i>
- {{/if}}
- {{/if}}
-
- {{#if anomaly.isUserReported}}
- <div class="te-anomaly-table__text te-anomaly-table__text--explore">User Reported</div>
- <div class="te-anomaly-table__comment">
- <i class="glyphicon glyphicon-th-list"></i>
- {{#tooltip-on-element class="te-anomaly-table__tooltip"}}
- {{anomaly.anomalyFeedbackComments}}
- {{/tooltip-on-element}}
- </div>
- {{else}}
- {{#power-select
- triggerId=anomaly.anomalyId
- triggerClass="te-anomaly-table__select"
- options=responseOptions
- searchEnabled=false
- selected=(get labelMap anomaly.anomalyFeedback)
- onchange=(action "onChangeAnomalyResponse" anomaly)
- as |response|
- }}
- {{response}}
- {{/power-select}}
- {{/if}}
- </td>
- <td class="te-anomaly-table__cell te-anomaly-table__cell--feedback">
- <div class="te-anomaly-table__link-wrapper">
- {{#link-to 'rootcause' (query-params anomalyId=anomaly.anomalyId) target="_blank" class="te-anomaly-table__link"}}
- Investigate
- {{/link-to}}
- </div>
- </td>
- </tr>
- {{/each}}
- </tbody>
- </table>
- </div>
-
- {{!--pagination--}}
- {{#if (gt pagesNum 1)}}
- <nav class="text-center" aria-label="Page navigation">
- <ul class="pagination">
- <li class={{if (eq currentPage 1) 'active disabled'}} >
- <a href="#" {{action "onPaginationClick" 1}} aria-label="First">
- <span aria-hidden="true">First</span>
- </a>
- </li>
- <li class={{if (eq currentPage 1) 'active disabled'}}>
- <a href="#" {{action "onPaginationClick" "previous"}} aria-label="Previous">
- <span aria-hidden="true">Previous</span>
- </a>
- </li>
- {{#each viewPages as |page|}}
- <li class={{if (eq page currentPage) 'active'}}><a href="#" {{action "onPaginationClick" page}}>{{page}}</a></li>
- {{/each}}
- <li class={{if (eq currentPage pagesNum) 'disabled'}} >
- <a href="#" {{action "onPaginationClick" "next"}} aria-label="Next">
- <span aria-hidden="true">Next</span>
- </a>
- </li>
- <li class={{if (eq currentPage pagesNum) 'disabled'}} >
- <a href="#" {{action "onPaginationClick" pagesNum}} aria-label="Last">
- <span aria-hidden="true">Last</span>
- </a>
- </li>
- </ul>
- </nav>
- {{/if}}
- </div>
-
- {{else}}
- <div class="te-alert-page-pending">
- <img src="{{rootURL}}assets/images/te-alert-pending.png" class="te-alert-page-pending__image" alt="alert setup processing">
- <h2 class="te-alert-page-pending__title">Setting up your alert</h2>
- <div class="te-alert-page-pending__loader"></div>
- <p class="te-alert-page-pending__text">
- This may take up to a minute<br/>We will send you an email when it's done!
- </p>
- </div>
- {{/if}}
-</div>
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/alert/route.js b/thirdeye/thirdeye-frontend/app/pods/manage/alert/route.js
deleted file mode 100644
index 2182f36..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/manage/alert/route.js
+++ /dev/null
@@ -1,171 +0,0 @@
-/**
- * Handles the 'alert details' route.
- * @module manage/alert/route
- * @exports manage alert model
- */
-import RSVP from 'rsvp';
-import fetch from 'fetch';
-import moment from 'moment';
-import { later } from '@ember/runloop';
-import { isPresent } from "@ember/utils";
-import Route from '@ember/routing/route';
-import { inject as service } from '@ember/service';
-import config from 'thirdeye-frontend/config/environment';
-import { checkStatus, buildDateEod } from 'thirdeye-frontend/utils/utils';
-import { selfServeApiCommon } from 'thirdeye-frontend/utils/api/self-serve';
-
-// Setup for query param behavior
-const queryParamsConfig = {
- refreshModel: true,
- replace: false
-};
-
-export default Route.extend({
- queryParams: {
- jobId: queryParamsConfig
- },
-
- durationCache: service('services/duration'),
- session: service(),
-
- beforeModel(transition) {
- const id = transition.params['manage.alert'].alert_id;
- const { jobId, functionName } = transition.queryParams;
- const duration = '3m';
- const startDate = buildDateEod(3, 'month').valueOf();
- const endDate = moment().utc().valueOf();
-
- // Enter default 'explore' route with defaults loaded in URI
- // An alert Id of 0 means there is an alert creation error to display
- if (transition.targetName === 'manage.alert.index' && Number(id) !== -1) {
- this.transitionTo('manage.alert.explore', id, { queryParams: {
- duration,
- startDate,
- endDate,
- functionName: null,
- jobId
- }});
-
- // Save duration to service object for session availability
- this.get('durationCache').setDuration({ duration, startDate, endDate });
- }
- },
-
- model(params, transition) {
- const { alert_id: id, jobId, functionName } = params;
- if (!id) { return; }
-
- // Fetch all the basic alert data needed in manage.alert subroutes
- // Apply calls from go/te-ss-alert-flow-api
- return RSVP.hash({
- id,
- jobId,
- functionName: functionName || 'Unknown',
- isLoadError: Number(id) === -1,
- destination: transition.targetName,
- alertData: fetch(selfServeApiCommon.alertById(id)).then(checkStatus),
- email: fetch(selfServeApiCommon.configGroupByAlertId(id)).then(checkStatus),
- allConfigGroups: fetch(selfServeApiCommon.allConfigGroups).then(checkStatus),
- allAppNames: fetch(selfServeApiCommon.allApplications).then(checkStatus)
- });
- },
-
- resetController(controller, isExiting) {
- this._super(...arguments);
- if (isExiting) {
- controller.set('alertData', {});
- }
- },
-
- setupController(controller, model) {
- this._super(controller, model);
-
- const {
- id,
- alertData,
- pathInfo,
- jobId,
- isLoadError,
- functionName,
- destination,
- allConfigGroups
- } = model;
-
- const newAlertData = !alertData ? {} : alertData;
- let errorText = '';
-
- // Itereate through config groups to enhance all alerts with extra properties (group name, application)
- allConfigGroups.forEach((config) => {
- let groupFunctionIds = config.emailConfig && config.emailConfig.functionIds ? config.emailConfig.functionIds : [];
- let foundMatch = groupFunctionIds.find(funcId => funcId === Number(id));
- if (foundMatch) {
- Object.assign(newAlertData, {
- application: config.application,
- group: config.name
- });
- }
- });
-
- const isEditModeActive = destination.includes('edit') || destination.includes('tune');
- const pattern = newAlertData.alertFilter ? newAlertData.alertFilter.pattern : 'N/A';
- const granularity = newAlertData.bucketSize && newAlertData.bucketUnit ? `${newAlertData.bucketSize}_${newAlertData.bucketUnit}` : 'N/A';
- Object.assign(newAlertData, { pattern, granularity });
-
- // We do not have a valid alertId. Set error state.
- if (isLoadError) {
- Object.assign(newAlertData, { functionName, isActive: false });
- errorText = `${functionName.toUpperCase()} has failed to create. Please try again or email ${config.email}`;
- }
-
- controller.setProperties({
- id,
- pathInfo,
- errorText,
- isLoadError,
- isEditModeActive,
- alertData: newAlertData,
- isTransitionDone: true,
- isReplayPending: isPresent(jobId)
- });
- },
-
- actions: {
- /**
- * Set loader on start of transition
- */
- willTransition(transition) {
- //saving session url - TODO: add a util or service - lohuynh
- if (transition.intent.name && transition.intent.name !== 'logout') {
- this.set('session.store.fromUrl', {lastIntentTransition: transition});
- }
-
- this.controller.set('isTransitionDone', false);
- if (transition.targetName === 'manage.alert.index') {
- this.refresh();
- }
- },
-
- /**
- * Once transition is complete, remove loader
- */
- didTransition() {
- this.controller.set('isTransitionDone', true);
- // This is needed in order to update the links in this parent route,
- // giving the "currentRouteName" time to resolve
- later(this, () => {
- if (this.router.currentRouteName.includes('explore')) {
- this.controller.set('isEditModeActive', false);
- }
- });
- },
-
- // // Sub-route errors will bubble up to this
- error() {
- if (this.controller) {
- this.controller.set('isLoadError', true);
- }
- return true;//pass up stream
- }
- }
-
-});
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/alert/template.hbs b/thirdeye/thirdeye-frontend/app/pods/manage/alert/template.hbs
deleted file mode 100644
index f297baf..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/manage/alert/template.hbs
+++ /dev/null
@@ -1,59 +0,0 @@
-<section class="te-page__top te-search-results {{if isEditModeActive "te-search-results--slim"}}">
- <div class="container">
-
- {{#self-serve-alert-details
- alertData=alertData
- isLoadError=isLoadError
- displayMode="single"
- }}
-
- {{#if (not isReplayPending)}}
- <div class="te-search-results__cta">
- {{#if isEditModeActive}}
- <button {{action "onClickNavToOverview"}} class="te-button te-button--outline">Back to overview</button>
- {{else}}
- <button class="te-button te-button--outline" {{action "onClickEdit"}}>Edit</button>
- {{/if}}
- </div>
- {{/if}}
-
- {{/self-serve-alert-details}}
-
- {{#if isEditModeActive}}
- <div class="te-topcard-subnav">
- <div class="te-topcard-subnav__item">
- <span {{action "setEditModeActive"}}>
- {{#link-to "manage.alert.edit" alertData.id (query-params refresh=true) class="thirdeye-link thirdeye-link--smaller thirdeye-link--nav" activeClass="thirdeye-link--active"}}
- Edit alert settings
- {{/link-to}}
- </span>
- </div>
- <div class="te-topcard-subnav__item">
- <span {{action "setEditModeActive"}}>
- {{#link-to "manage.alert.tune" alertData.id class="thirdeye-link thirdeye-link--smaller thirdeye-link--nav" activeClass="thirdeye-link--active"}}
- Tune alert sensitivity
- {{/link-to}}
- </span>
- </div>
- </div>
- {{/if}}
-
- </div>
-</section>
-
-<section class="te-page__bottom">
- <div class="container">
- {{#if isLoadError}}
- <div class="te-alert-page-pending">
- <img src="{{rootURL}}assets/images/te-alert-error.png" class="te-alert-page-pending__image te-alert-page-pending__image--error" alt="error">
- <h2 class="te-alert-page-pending__title">Oops, something went wrong</h2>
- <p class="te-alert-page-pending__text">{{errorText}}</p>
- </div>
- {{else}}
- {{#if (not isTransitionDone)}}
- <div class="spinner-wrapper-self-serve spinner-wrapper-self-serve__content-block">{{ember-spinner}}</div>
- {{/if}}
- {{outlet}}
- {{/if}}
- </div>
-</section>
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/alert/tune/controller.js b/thirdeye/thirdeye-frontend/app/pods/manage/alert/tune/controller.js
deleted file mode 100644
index 6cfe919..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/manage/alert/tune/controller.js
+++ /dev/null
@@ -1,446 +0,0 @@
-/**
- * Controller for Alert Details Page: Tune Sensitivity Tab
- * @module manage/alert/tune
- * @exports manage/alert/tune
- */
-import _ from 'lodash';
-import moment from 'moment';
-import Controller from '@ember/controller';
-import { later } from '@ember/runloop';
-import { isPresent } from "@ember/utils";
-import { computed, set, get, getProperties, setProperties } from '@ember/object';
-import { inject as service } from '@ember/service';
-import { buildDateEod } from 'thirdeye-frontend/utils/utils';
-import { anomalyResponseMap } from 'thirdeye-frontend/utils/anomaly';
-import { buildAnomalyStats } from 'thirdeye-frontend/utils/manage-alert-utils';
-
-export default Controller.extend({
- /**
- * Be ready to receive time span for anomalies via query params
- */
- queryParams: ['duration', 'startDate', 'endDate'],
- duration: null,
- startDate: null,
- endDate: null,
-
- /**
- * Make duration service accessible
- */
- durationCache: service('services/duration'),
-
- /**
- * Make toast service accessible
- */
- notifications: service('toast'),
-
- /**
- * Set initial view values
- * @method initialize
- * @return {undefined}
- */
- initialize() {
- this.setProperties({
- filterBy: 'All',
- isGraphReady: false,
- isTunePreviewActive: false,
- isTuneSaveSuccess: false,
- isTuneSaveFailure: false,
- selectedTuneType: 'current',
- predefinedRanges: {},
- today: moment(),
- selectedSortMode: '',
- activeRangeStart: '',
- activeRangeEnd: '',
- removedAnomalies: 0,
- sortColumnStartUp: false,
- sortColumnScoreUp: false,
- sortColumnChangeUp: false,
- isAnomalyTableLoading: false,
- sortColumnResolutionUp: false,
- isPerformanceDataLoading: false,
- customMttdClasses: 'form-control te-input'
- });
- },
-
- /**
- * Severity power-select options
- * @type {Array}
- */
- tuneSeverityOptions: computed('severityMap', function() {
- return Object.keys(this.get('severityMap'));
- }),
-
- /**
- * Returns selectable pattern options for power-select
- * @type {Array}
- */
- tunePatternOptions: computed('patternMap', function() {
- return Object.keys(this.get('patternMap'));
- }),
-
- /**
- * Mapping anomaly table column names to corresponding prop keys
- */
- sortMap: {
- start: 'anomalyStart',
- score: 'severityScore',
- change: 'changeRate',
- resolution: 'anomalyFeedback'
- },
-
- /**
- * Conditional formatting for tuning fields
- * @type {Boolean}
- */
- isTuneAmountPercent: computed('selectedSeverityOption', function() {
- return this.get('selectedSeverityOption') !== 'Absolute Value of Change';
- }),
-
- /**
- * Builds the new autotune filter from custom tuning options
- * @type {String}
- */
- customTuneQueryString: computed(
- 'selectedSeverityOption',
- 'customPercentChange',
- 'customMttdChange',
- 'selectedTunePattern',
- function() {
- const {
- severityMap,
- patternMap,
- customPercentChange: amountChange,
- selectedTunePattern: selectedPattern,
- selectedSeverityOption: selectedSeverity
- } = this.getProperties('severityMap', 'patternMap', 'customPercentChange', 'selectedTunePattern', 'selectedSeverityOption');
- const isPercent = selectedSeverity === 'Percentage of Change';
- const mttdVal = Number(this.get('customMttdChange')).toFixed(2);
- const severityThresholdVal = isPercent ? (Number(amountChange)/100).toFixed(2) : amountChange;
- const featureString = `window_size_in_hour,${severityMap[selectedSeverity]}`;
- const mttdString = `window_size_in_hour=${mttdVal};${severityMap[selectedSeverity]}=${severityThresholdVal}`;
- const patternString = patternMap[selectedPattern] ? `&pattern=${encodeURIComponent(patternMap[selectedPattern])}` : '';
- const configString = `&tuningFeatures=${encodeURIComponent(featureString)}&mttd=${encodeURIComponent(mttdString)}${patternString}`;
- return { configString, severityVal: severityThresholdVal };
- }
- ),
-
- /**
- * Indicates the allowed date range picker increment based on granularity
- * @type {Number}
- */
- timePickerIncrement: computed('alertData.windowUnit', function() {
- const granularity = this.get('alertData.windowUnit').toLowerCase();
-
- switch(granularity) {
- case 'days':
- return 1440;
- case 'hours':
- return 60;
- default:
- return 5;
- }
- }),
-
- /**
- * Allows us to enable/disable the custom tuning options
- * @type {Boolean}
- */
- isCustomFieldsDisabled: computed('selectedTuneType', function() {
- return this.get('selectedTuneType') === 'current';
- }),
-
- /**
- * date-time-picker: indicates the date format to be used based on granularity
- * @type {String}
- */
- uiDateFormat: computed('alertData.windowUnit', function() {
- const granularity = this.get('alertData.windowUnit').toLowerCase();
-
- switch(granularity) {
- case 'days':
- return 'MMM D, YYYY';
- case 'hours':
- return 'MMM D, YYYY h a';
- default:
- return 'MMM D, YYYY hh:mm a';
- }
- }),
-
- /**
- * Data needed to render the stats 'cards' above the anomaly graph for this alert
- * NOTE: buildAnomalyStats util currently requires both 'current' and 'projected' props to be present.
- * @type {Object}
- */
- anomalyStats: computed(
- 'alertEvalMetrics',
- 'isTuneAmountPercent',
- 'customPercentChange',
- 'alertEvalMetrics.projected',
- function() {
- const {
- isTuneAmountPercent,
- alertEvalMetrics: metrics,
- customPercentChange: severity
- } = this.getProperties(
- 'isTuneAmountPercent',
- 'alertEvalMetrics',
- 'customPercentChange'
- );
- const severityUnit = isTuneAmountPercent ? '%' : '';
- const isPerfDataReady = _.has(metrics, 'current');
- const statsCards = [
- {
- title: 'Estimated number of anomalies',
- key: 'totalAlerts',
- tooltip: false,
- text: 'Estimated number of anomalies based on alert settings'
- },
- {
- title: 'Estimated precision',
- key: 'precision',
- units: '%',
- tooltip: false,
- text: 'Among all anomalies sent by the alert, the % of them that are true.'
- },
- {
- title: 'Estimated recall',
- key: 'recall',
- units: '%',
- tooltip: false,
- text: 'Among all anomalies that happened, the % of them sent by the alert.'
- },
- {
- title: `MTTD for > ${severity}${severityUnit} change`,
- key: 'mttd',
- units: 'hrs',
- tooltip: false,
- text: `Minimum time to detect for anomalies with > ${severity}${severityUnit} change`
- }
- ];
-
- return isPerfDataReady ? buildAnomalyStats(metrics, statsCards, false) : [];
- }
- ),
-
- /**
- * Data needed to render the stats 'cards' above the anomaly graph for this alert
- * @type {Object}
- */
- diffedAnomalies: computed(
- 'anomalyData',
- 'filterBy',
- 'selectedSortMode',
- function() {
- const {
- anomalyData: anomalies,
- filterBy: activeFilter,
- selectedSortMode
- } = this.getProperties('anomalyData', 'filterBy', 'selectedSortMode');
- let filterKey = '';
- let filteredAnomalies = anomalies || [];
- let num = 1;
-
- switch (activeFilter) {
- case 'True Anomalies':
- filterKey = 'True Anomaly';
- break;
- case 'False Alarms':
- filterKey = 'False Alarm';
- break;
- case 'User Reported':
- filterKey = 'New Trend';
- break;
- default:
- filterKey = '';
- }
-
- // Filter anomalies in table according to filterkey
- if (activeFilter !== 'All') {
- filteredAnomalies = anomalies.filter(anomaly => anomaly.anomalyFeedback === filterKey);
- }
- if (selectedSortMode) {
- let [ sortKey, sortDir ] = selectedSortMode.split(':');
- if (sortDir === 'up') {
- filteredAnomalies = filteredAnomalies.sortBy(this.get('sortMap')[sortKey]);
- } else {
- filteredAnomalies = filteredAnomalies.sortBy(this.get('sortMap')[sortKey]).reverse();
- }
- }
-
- // Number the list
- filteredAnomalies.forEach((anomaly) => {
- set(anomaly, 'index', num);
- setProperties(anomaly, {
- index: num,
- feedbackLabel: anomalyResponseMap[anomaly.anomalyFeedback] || anomaly.anomalyFeedback
- });
- num++;
- });
-
- return filteredAnomalies;
- }
- ),
-
- /**
- * Reset the controller values on exit
- * @method clearAll
- */
- clearAll() {
- this.setProperties({
- alertEvalMetrics: {}
- });
- },
-
- actions: {
-
- /**
- * This field will not accept empty input - default back to the original value
- * @method onChangeSeverityValue
- * @param {String} severity - the custom tuning severity input
- */
- onChangeSeverityValue(severity) {
- if (!isPresent(severity)) {
- this.set('customPercentChange', this.model.customPercentChange);
- }
- },
-
- /**
- * This field will not accept empty input - default back to the original value
- * @method onChangeMttdValue
- * @param {String} mttd - the custom tuning mttd input
- */
- onChangeMttdValue(mttd) {
- if (!isPresent(mttd)) {
- this.set('customMttdChange', this.model.customMttdChange);
- }
- },
-
- /**
- * Sets the new custom date range for anomaly coverage
- * @method onRangeSelection
- * @param {Object} rangeOption - the user-selected time range to load
- */
- onRangeSelection(rangeOption) {
- const {
- start,
- end,
- value: duration
- } = rangeOption;
- const durationObj = {
- duration,
- startDate: moment(start).valueOf(),
- endDate: moment(end).valueOf()
- };
- // Cache the new time range and update page with it
- this.get('durationCache').setDuration(durationObj);
- this.transitionToRoute({ queryParams: durationObj });
- },
-
- /**
- * Save the currently loaded tuning options
- */
- onSubmitTuning() {
- this.send('submitTuningRequest', this.get('autoTuneId'));
- },
-
- /**
- * Handle "reset" click - reload the model
- */
- onResetPage() {
- this.initialize();
- this.set('alertEvalMetrics.projected', this.get('originalProjectedMetrics'));
- this.send('resetTuningParams', this.get('alertData'));
- },
-
- /**
- * Replaces the 'tableStats' object with a new one with selected filter
- * activated and triggers table filtering.
- * @param {String} metric - label of the currently selected category
- */
- toggleCategory(metric) {
- const stats = this.get('tableStats');
- const newStats = stats.map((cat) => {
- return {
- count: cat.count,
- label: cat.label,
- isActive: false
- };
- });
- // Activate selected metric in our new stats object
- newStats.find(cat => cat.label === metric).isActive = true;
- // Apply new table stats object and trigger re-render of filtered anomalies
- this.setProperties({
- tableStats: newStats,
- filterBy: metric
- });
- },
-
- /**
- * Handle sorting for each sortable table column
- * @param {String} sortKey - stringified start date
- */
- toggleSortDirection(sortKey) {
- const propName = `sortColumn${sortKey.capitalize()}Up` || '';
-
- this.toggleProperty(propName);
- if (this.get(propName)) {
- this.set('selectedSortMode', `${sortKey}:up`);
- } else {
- this.set('selectedSortMode', `${sortKey}:down`);
- }
- // On sort, set table to first pagination page
- this.set('currentPage', 1);
- },
-
- /**
- * On "preview" click, display the resulting anomaly table and trigger
- * tuning if we have custom settings (tuning data for default option is already loaded)
- */
- onClickPreviewPerformance() {
- const defaultConfig = { configString: '' };
- const { customMttdClasses, mttdMinimums, alertData, notifications, customMttdChange } = getProperties(this,
- 'customMttdClasses',
- 'mttdMinimums',
- 'alertData',
- 'notifications',
- 'customMttdChange'
- );
- const granularityBucket = alertData.bucketUnit ? alertData.bucketUnit.toLowerCase() : null;
- const isBucketDefaultPresent = granularityBucket && mttdMinimums.hasOwnProperty(granularityBucket);
- const isMttdSetTooLow = isBucketDefaultPresent ? Number(customMttdChange) < Number(mttdMinimums[granularityBucket]) : false;
- const currentMinimumMttd = mttdMinimums[granularityBucket];
- const mttdUnit = granularityBucket === 'hours' ? 'hour' : 'hours';
- const mttdErrMsg = `MTTD is set too low for metric granularity. Please enter a value of at least ${currentMinimumMttd} ${mttdUnit}.`;
- const toastOptions = {
- timeOut: '10000',
- positionClass: 'toast-top-right'
- };
-
- // Check if MTTD is set below minimums and display error message
- if (isMttdSetTooLow) {
- set(this, 'customMttdClasses', `${customMttdClasses} te-input--error`);
- notifications.error(mttdErrMsg, 'MTTD range error', toastOptions);
- document.querySelector('#custom-tune-mttd').select();
- return;
- } else {
- notifications.clear();
- setProperties(this, {
- customMttdClasses: 'form-control te-input',
- isPerformanceDataLoading: true
- });
- }
-
- if (this.get('selectedTuneType') === 'custom') {
- // Trigger preview with custom params
- this.send('triggerTuningSequence', this.get('customTuneQueryString'));
- } else {
- // When user wants to preview using "current" settings, our request does not contain custom params.
- this.send('triggerTuningSequence', defaultConfig);
- }
-
- // Reset table filter
- set(this, 'filterBy', 'All');
- }
- }
-
-});
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/alert/tune/route.js b/thirdeye/thirdeye-frontend/app/pods/manage/alert/tune/route.js
deleted file mode 100644
index 63380c1..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/manage/alert/tune/route.js
+++ /dev/null
@@ -1,487 +0,0 @@
-/**
- * Handles the 'explore' route for manage alert
- * @module manage/alert/edit/explore
- * @exports manage/alert/edit/explore
- */
-import RSVP from "rsvp";
-import _ from 'lodash';
-import fetch from 'fetch';
-import moment from 'moment';
-import Route from '@ember/routing/route';
-import { isPresent } from "@ember/utils";
-import { later } from "@ember/runloop";
-import { task } from 'ember-concurrency';
-import {
- checkStatus,
- postProps,
- buildDateEod,
- toIso
-} from 'thirdeye-frontend/utils/utils';
-import {
- enhanceAnomalies,
- setUpTimeRangeOptions,
- toIdGroups,
- extractSeverity
-} from 'thirdeye-frontend/utils/manage-alert-utils';
-import { inject as service } from '@ember/service';
-
-/**
- * Basic alert page defaults
- */
-const durationDefault = '3m';
-const defaultSeverity = '0.3';
-const dateFormat = 'YYYY-MM-DD';
-const displayDateFormat = 'YYYY-MM-DD HH:mm';
-const defaultDurationObj = {
- duration: '3m',
- startDate: buildDateEod(3, 'month').valueOf(),
- endDate: moment().valueOf()
-};
-
-/**
- * Pattern display options (power-select) and values
- */
-const patternMap = {
- 'Up and Down': 'UP,DOWN',
- 'Up Only': 'UP',
- 'Down Only': 'DOWN'
-};
-
-/**
- * Severity display options (power-select) and values
- */
-const severityMap = {
- 'Percentage of Change': 'weight',
- 'Absolute Value of Change': 'deviation'
-};
-
-/**
- * If no filter data is set for sensitivity, use this
- */
-const sensitivityDefaults = {
- defaultMttdVal: '5',
- selectedSeverityOption: 'Percentage of Change',
- selectedTunePattern: 'Up and Down',
- defaultPercentChange: '0.3',
- // Set granularity minimums in number of hours
- mttdGranularityMinimums: {
- days: 24,
- hours: 1,
- minutes: 0.25
- }
-};
-
-/**
- * Build the object to populate anomaly table feedback categories
- * @param {Array} anomalies - list of all deduped and filtered anomalies
- * @returns {Object}
- */
-const anomalyTableStats = (anomalies) => {
- const trueAnomalies = anomalies ? anomalies.filter(anomaly => anomaly.anomalyFeedback === 'True anomaly') : 0;
- const falseAnomalies = anomalies ? anomalies.filter(anomaly => anomaly.anomalyFeedback === 'False Alarm') : 0;
- const userAnomalies = anomalies ? anomalies.filter(anomaly => anomaly.anomalyFeedback === 'Confirmed - New Trend') : 0;
-
- return [
- {
- count: anomalies.length,
- label: 'All',
- isActive: true
- },
- {
- count: trueAnomalies.length,
- label: 'True Anomalies',
- isActive: false
- },
- {
- count: falseAnomalies.length,
- label: 'False Alarms',
- isActive: false
- },
- {
- count: userAnomalies.length,
- label: 'User Created',
- isActive: false
- }
- ];
-};
-
-/**
- * Set up select & input field defaults for sensitivity settings
- * @param {Array} alertData - properties for the currently loaded alert
- * @returns {Object}
- */
-const processDefaultTuningParams = (alertData) => {
- let {
- defaultMttdVal,
- selectedSeverityOption,
- selectedTunePattern,
- defaultPercentChange,
- mttdGranularityMinimums
- } = sensitivityDefaults;
-
- // Cautiously derive tuning data from alert filter properties
- const featureString = 'window_size_in_hour';
- const alertFilterObj = alertData.alertFilter || null;
- const alertPattern = alertFilterObj ? alertFilterObj.pattern : null;
- const isFeaturesPropFormatted = _.has(alertFilterObj, 'features') && alertFilterObj.features.includes(featureString);
- const isMttdPropFormatted = _.has(alertFilterObj, 'mttd') && alertFilterObj.mttd.includes(`${featureString}=`);
- const alertFeatures = isFeaturesPropFormatted ? alertFilterObj.features.split(',')[1] : null;
- const alertMttd = isMttdPropFormatted ? alertFilterObj.mttd.split(';') : null;
- const granularityBucket = alertData.bucketUnit ? alertData.bucketUnit.toLowerCase() : null;
- const isBucketDefaultPresent = granularityBucket && mttdGranularityMinimums.hasOwnProperty(granularityBucket);
- const defaultMttdChange = isBucketDefaultPresent ? mttdGranularityMinimums[granularityBucket] : defaultMttdVal;
-
- // Load saved pattern into pattern options
- const savedTunePattern = alertPattern ? alertPattern : 'UP,DOWN';
- for (var patternKey in patternMap) {
- if (savedTunePattern === patternMap[patternKey]) {
- selectedTunePattern = patternKey;
- }
- }
-
- // TODO: enable once issue resolved in backend (not saving selection to new feature string)
- const savedSeverityPattern = alertMttd ? alertMttd[1].split('=')[0] : 'weight';
- const isAbsValue = savedSeverityPattern === 'deviation';
- for (var severityKey in severityMap) {
- if (savedSeverityPattern === severityMap[severityKey]) {
- selectedSeverityOption = severityKey;
- }
- }
-
- // Load saved mttd
- const mttdValue = alertMttd ? alertMttd[0].split('=')[1] : 'N/A';
- const customMttdChange = !isNaN(mttdValue) ? Math.round(Number(mttdValue)) : defaultMttdChange;
-
- // Load saved severity value
- const severityValue = alertMttd ? alertMttd[1].split('=')[1] : 'N/A';
- const rawPercentChange = !isNaN(severityValue) ? Number(severityValue) : defaultPercentChange;
- const customPercentChange = isAbsValue ? rawPercentChange : rawPercentChange * 100;
-
- return { selectedSeverityOption, selectedTunePattern, customPercentChange, customMttdChange };
-};
-
-/**
- * Fetches all anomaly data for found anomalies - downloads all 'pages' of data from server
- * in order to handle sorting/filtering on the entire set locally. Start/end date are not used here.
- * @param {Array} anomalyIds - list of all found anomaly ids
- * @returns {RSVP promise}
- */
-const fetchCombinedAnomalies = (anomalyIds) => {
- if (anomalyIds.length) {
- const idGroups = toIdGroups(anomalyIds);
- const anomalyPromiseHash = idGroups.map((group, index) => {
- let idStringParams = `anomalyIds=${encodeURIComponent(idGroups[index].toString())}`;
- let getAnomalies = fetch(`/anomalies/search/anomalyIds/0/0/${index + 1}?${idStringParams}`).then(checkStatus);
- return RSVP.resolve(getAnomalies);
- });
- return RSVP.all(anomalyPromiseHash);
- } else {
- return RSVP.resolve([]);
- }
-};
-
-/**
- * Fetches severity scores for all anomalies
- * TODO: Move this and other shared requests to a common service
- * @param {Array} anomalyIds - list of all found anomaly ids
- * @returns {RSVP promise}
- */
-const fetchSeverityScores = (anomalyIds) => {
- if (anomalyIds.length) {
- const anomalyPromiseHash = anomalyIds.map((id) => {
- return RSVP.hash({
- id,
- score: fetch(`/dashboard/anomalies/score/${id}`).then(checkStatus)
- });
- });
- return RSVP.allSettled(anomalyPromiseHash);
- } else {
- return RSVP.resolve([]);
- }
-};
-
-/**
- * Returns a promise hash to fetch to fetch fresh projected anomaly data after tuning adjustments
- * @param {Date} startDate - start of date range
- * @param {Date} endDate - end of date range
- * @param {Sting} tuneId - current autotune filter Id
- * @param {String} alertId - current alert Id
- * @returns {Object} containing fetch promises
- */
-const tuningPromiseHash = (startDate, endDate, tuneId, alertId, severity = defaultSeverity) => {
- const baseStart = moment(Number(startDate));
- const baseEnd = moment(Number(endDate));
- const tuneParams = `start=${toIso(startDate)}&end=${toIso(endDate)}`;
- const qsParams = `start=${baseStart.utc().format(dateFormat)}&end=${baseEnd.utc().format(dateFormat)}&useNotified=true`;
- const projectedUrl = `/detection-job/eval/autotune/${tuneId}?${tuneParams}`;
- const projectedMttdUrl = `/detection-job/eval/projected/mttd/${tuneId}?severity=${severity}`;
- const anomaliesUrlA = `/dashboard/anomaly-function/${alertId}/anomalies?${qsParams}`;
- const anomaliesUrlB =`/detection-job/eval/projected/anomalies/${tuneId}?${qsParams}`;
-
- return {
- projectedMttd: fetch(projectedMttdUrl).then(checkStatus),
- projectedEval: fetch(projectedUrl).then(checkStatus),
- idListA: fetch(anomaliesUrlA).then(checkStatus),
- idListB: fetch(anomaliesUrlB).then(checkStatus)
- };
-};
-
-/**
- * Returns a bi-directional diff given "before" and "after" tuning anomaly Ids
- * @param {Array} listA - list of all anomaly ids BEFORE tuning
- * @param {Array} listB - list of all anomaly ids AFTER tuning
- * @returns {Object}
- */
-const anomalyDiff = (listA, listB) => {
- return {
- idsRemoved: listA.filter(id => !listB.includes(id)),
- idsAdded: listB.filter(id => !listA.includes(id))
- };
-};
-
-/**
- * Setup for query param behavior
- */
-const queryParamsConfig = {
- refreshModel: true,
- replace: true
-};
-
-export default Route.extend({
- queryParams: {
- duration: queryParamsConfig,
- startDate: queryParamsConfig,
- endDate: queryParamsConfig
- },
-
- /**
- * Make duration service accessible
- */
- durationCache: service('services/duration'),
- session: service(),
-
- beforeModel(transition) {
- const { duration, startDate } = transition.queryParams;
-
- // Default to 3 months of anomalies to show if no dates present in query params
- if (!duration || (duration !== 'custom' && duration !== '3m') || !startDate) {
- this.transitionTo({ queryParams: defaultDurationObj });
- }
- },
-
- model(params, transition) {
- const { id, alertData } = this.modelFor('manage.alert');
- if (!id) { return; }
-
- // Get duration data
- const {
- duration,
- startDate,
- endDate
- } = this.get('durationCache').getDuration(transition.queryParams, defaultDurationObj);
-
- // Prepare endpoints for the initial eval, mttd, projected metrics calls
- const tuneParams = `start=${toIso(startDate)}&end=${toIso(endDate)}`;
- const tuneIdUrl = `/detection-job/autotune/filter/${id}?${tuneParams}`;
- const evalUrl = `/detection-job/eval/filter/${id}?${tuneParams}&isProjected=TRUE`;
- const mttdUrl = `/detection-job/eval/mttd/${id}?severity=${extractSeverity(alertData, defaultSeverity)}`;
- const initialPromiseHash = {
- current: fetch(evalUrl).then(checkStatus),
- mttd: fetch(mttdUrl).then(checkStatus)
- };
-
- return RSVP.hash(initialPromiseHash)
- .then((alertEvalMetrics) => {
- Object.assign(alertEvalMetrics.current, { mttd: alertEvalMetrics.mttd});
- return {
- id,
- alertData,
- duration,
- tuneIdUrl,
- startDate,
- endDate,
- tuneParams,
- alertEvalMetrics
- };
- })
- .catch((error) => {
- return RSVP.reject({ error, location: `${this.routeName}:model`, calls: initialPromiseHash });
- });
- },
-
- setupController(controller, model) {
- this._super(controller, model);
-
- const {
- id,
- alertData,
- duration,
- loadError,
- startDate,
- endDate,
- alertEvalMetrics
- } = model;
-
- // Conditionally add select option for severity
- if (alertData.toCalculateGlobalMetric) {
- severityMap['Site Wide Impact'] = 'site_wide_impact';
- }
-
- // Prepare sensitivity default values to populate tuning options from alert data
- const {
- selectedSeverityOption,
- selectedTunePattern,
- customPercentChange,
- customMttdChange
- } = processDefaultTuningParams(alertData);
- Object.assign(model, { customPercentChange, customMttdChange });
-
- controller.setProperties({
- alertData,
- loadError,
- patternMap,
- severityMap,
- alertId: id,
- autoTuneId: '',
- customMttdChange,
- customPercentChange,
- alertEvalMetrics,
- selectedTunePattern,
- selectedSeverityOption,
- mttdMinimums: sensitivityDefaults.mttdGranularityMinimums,
- alertHasDimensions: isPresent(alertData.exploreDimensions),
- timeRangeOptions: setUpTimeRangeOptions([durationDefault], duration)
- });
- controller.initialize();
-
- // Ensure date range picker gets populated correctly
- later(this, () => {
- controller.setProperties({
- activeRangeStart: moment(Number(startDate)).format(displayDateFormat),
- activeRangeEnd: moment(Number(endDate)).format(displayDateFormat)
- });
- });
- },
-
- resetController(controller, isExiting) {
- this._super(...arguments);
-
- if (isExiting) {
- this.get('triggerTuningSequence').cancelAll();
- controller.clearAll();
- }
- },
-
- saveAutoTuneSettings(id) {
- return fetch(`/detection-job/update/filter/${id}`, postProps('')).then(checkStatus);
- },
-
- /**
- * This concurrency task fetches anomaly performance metrics and data for all anomalies which
- * would not be included in the notification set under the user-selected tuning settings.
- * @method triggerTuningSequence
- * @param {Object} configObj - the user-selected type and value of tuning severity thresholds
- * @return {undefined}
- */
- triggerTuningSequence: task(function * (configObj) {
- const { configString, severityVal} = configObj;
- const {
- id: alertId,
- startDate,
- endDate,
- tuneIdUrl
- } = this.currentModel;
- try {
- // Send the new tuning settings to backend to get an auto-tune Id
- const tuneId = yield fetch(tuneIdUrl + configString, postProps('')).then(checkStatus);
- // Use the autotune Id to fetch new performance metrics for this alert, and load them into the template
- const performanceData = yield RSVP.hash(tuningPromiseHash(startDate, endDate, tuneId[0], alertId, severityVal));
- const idsRemoved = anomalyDiff(performanceData.idListA, performanceData.idListB).idsRemoved;
- const projectedStats = performanceData.projectedEval;
- Object.assign(projectedStats, { mttd: performanceData.projectedMttd });
- this.controller.set('alertEvalMetrics.projected', projectedStats);
- this.controller.setProperties({
- removedAnomalies: idsRemoved.length,
- isTunePreviewActive: true,
- isAnomalyTableLoading: true,
- isPerformanceDataLoading: false
- });
- // Fetch all anomaly data for the list of removed anomalies
- const rawAnomalyData = yield fetchCombinedAnomalies(idsRemoved);
- // Fetch severity scores for each anomaly
- const severityScores = yield fetchSeverityScores(idsRemoved);
- const anomalyData = enhanceAnomalies(rawAnomalyData, severityScores);
- this.controller.setProperties({
- anomalyData,
- autoTuneId: tuneId[0],
- isAnomalyTableLoading: false,
- tableStats: anomalyTableStats(anomalyData)
- });
- } catch(error) {
- this.controller.setProperties({
- loadError: true,
- loadErrorMsg: error
- });
- }
- }).cancelOn('deactivate').restartable(),
-
- actions: {
- /**
- * save session url for transition on login
- * @method willTransition
- */
- willTransition(transition) {
- //saving session url - TODO: add a util or service - lohuynh
- if (transition.intent.name && transition.intent.name !== 'logout') {
- this.set('session.store.fromUrl', {lastIntentTransition: transition});
- }
- },
-
- /**
- * Handle any errors occurring in model/afterModel in parent route
- * https://www.emberjs.com/api/ember/2.16/classes/Route/events/error?anchor=error
- * https://guides.emberjs.com/v2.18.0/routing/loading-and-error-substates/#toc_the-code-error-code-event
- */
- error() {
- return true;
- },
-
- // User clicks reset button
- resetPage() {
- this.transitionTo({ queryParams: defaultDurationObj });
- },
-
- // User resets settings
- resetTuningParams(alertData) {
- const {
- selectedSeverityOption,
- selectedTunePattern,
- customPercentChange,
- customMttdChange
- } = processDefaultTuningParams(alertData);
- this.controller.setProperties({
- selectedSeverityOption,
- selectedTunePattern,
- customPercentChange,
- customMttdChange
- });
- },
-
- // User clicks "save" on previewed tune settings
- submitTuningRequest(tuneId) {
- this.saveAutoTuneSettings(tuneId)
- .then((result) => {
- this.controller.set('isTuneSaveSuccess', true);
- })
- .catch((error) => {
- this.controller.set('isTuneSaveFailure', true);
- this.controller.set('failureMessage', error);
- });
- },
-
- // User clicks "preview", having configured performance settings
- triggerTuningSequence(configObj) {
- this.get('triggerTuningSequence').perform(configObj);
- }
- }
-});
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/alert/tune/template.hbs b/thirdeye/thirdeye-frontend/app/pods/manage/alert/tune/template.hbs
deleted file mode 100644
index 345c00e..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/manage/alert/tune/template.hbs
+++ /dev/null
@@ -1,257 +0,0 @@
-<div class="manage-alert-tune">
- {{!-- Date range selector --}}
- {{range-pill-selectors
- title="Showing"
- maxTime=maxTime
- uiDateFormat=uiDateFormat
- activeRangeEnd=activeRangeEnd
- activeRangeStart=activeRangeStart
- timeRangeOptions=timeRangeOptions
- timePickerIncrement=timePickerIncrement
- selectAction=(action "onRangeSelection")
- }}
-
- <div class="te-content-block">
- <fieldset class="te-form__section te-form__section--first te-form__section--slim row">
- <div class="col-xs-12">
- <h4 class="te-self-serve__block-title">Tune alert notifications</h4>
- <p class="te-self-serve__block-subtext">All configurations and outputs will be based on reviewed anomalies.</p>
- </div>
- <ul class="te-form__list col-xs-12 col-sm-8">
- <li class="te-form__list-item">
- {{#radio-button value="current" groupValue=selectedTuneType}}
- <div class="te-form__list-label">Automatically tune alert settings</div>
- {{/radio-button}}
- </li>
- <li class="te-form__list-item">
- {{#radio-button value="custom" groupValue=selectedTuneType}}
- <div class="te-form__list-label">Customize alert settings</div>
- {{/radio-button}}
- </li>
- </ul>
-
- <div class="te-form__indent-block col-xs-8">
- <label for="alert-tune-pattern" class="te-form__list-label">Anomaly Pattern</label>
- <div class="te-form__tune-field">
- {{#power-select
- triggerId="alert-tune-pattern"
- triggerClass="te-form__tune-field"
- verticalPosition="below"
- renderInPlace=true
- options=tunePatternOptions
- searchEnabled=false
- selected=selectedTunePattern
- placeHolder="Select Pattern"
- onchange=(action (mut selectedTunePattern))
- disabled=isCustomFieldsDisabled
- as |pattern|
- }}
- {{pattern}}
- {{/power-select}}
- </div>
- </div>
-
- <div class="te-form__indent-block col-xs-10">
- <label for="alert-tune-pattern" class="te-form__list-label">When</label>
- <div class="te-form__tune-field">
- {{#power-select
- triggerId="alert-tune-severity"
- triggerClass="te-form__tune-field"
- verticalPosition="below"
- renderInPlace=true
- options=tuneSeverityOptions
- searchEnabled=false
- selected=selectedSeverityOption
- onchange=(action (mut selectedSeverityOption))
- disabled=isCustomFieldsDisabled
- as |severity|
- }}
- {{severity}}
- {{/power-select}}
- </div>
-
- <label for="custom-tune-percent" class="te-form__list-label">is >=</label>
- <div class="te-form__tune-field te-form__tune-field--amount">
- {{input
- type="text"
- id="custom-tune-percent"
- class="form-control te-input te-input"
- focus-out="onChangeSeverityValue"
- value=customPercentChange
- disabled=isCustomFieldsDisabled
- }}
- </div>
- {{#if isTuneAmountPercent}}
- <span class="te-form__list-label--suffix">%, </span>
- {{/if}}
-
- <label for="custom-tune-mttd" class="te-form__list-label">MTTD should be no more than</label>
- <div class="te-form__tune-field te-form__tune-field--amount">
- {{input
- type="text"
- id="custom-tune-mttd"
- class=customMttdClasses
- value=customMttdChange
- focus-out="onChangeMttdValue"
- disabled=isCustomFieldsDisabled
- }}
- </div>
- <span class="te-form__list-label">hours</span>
- </div>
-
- <div class="te-form__cta-row">
- <a class="te-button te-button--link" {{action "onResetPage" preventDefault=false}}>Reset</a>
- {{bs-button
- defaultText="Preview performance"
- type="outline-primary"
- onClick=(action "onClickPreviewPerformance")
- class="te-button te-button--outline"
- }}
- </div>
- </fieldset>
- </div>
-
- <div class="te-horizontal-cards te-content-block">
- <h4 class="te-self-serve__block-title">{{if isTunePreviewActive "Compare" "Tuned"}} Alert Performance</h4>
- <div class="te-horizontal-cards__container">
- {{#if isPerformanceDataLoading}}
- <div class="spinner-wrapper-self-serve spinner-wrapper-self-serve__content-block">{{ember-spinner}}</div>
- {{/if}}
- {{!-- Alert anomaly stats cards --}}
- {{anomaly-stats-block
- isTunePreviewActive=isTunePreviewActive
- displayMode="tune"
- anomalyStats=anomalyStats
- }}
- </div>
- {{#if isTunePreviewActive}}
- <div class="te-form__cta-button">
- {{bs-button
- defaultText="Save tuning"
- type="primary"
- onClick=(action "onSubmitTuning")
- buttonType="submit"
- class="te-button te-button--submit"
- }}
- </div>
- {{/if}}
-
- {{#if isTuneSaveSuccess}}
- {{#bs-alert type="success" class="te-form__banner te-form__banner--success"}}
- <div class="te-form__banner-title">Success:</div> Filters modified.
- {{/bs-alert}}
- {{/if}}
-
- {{#if isTuneSaveFailure}}
- {{#bs-alert type="danger" class="te-form__banner te-form__banner--failure"}}
- <span class="stronger">Error:</span> {{failureMessage}}
- {{/bs-alert}}
- {{/if}}
-
- </div>
-
- {{#if isTunePreviewActive}}
- <div class="te-horizontal-cards te-content-block">
- <h4 class="te-self-serve__block-title">{{removedAnomalies}} Anomalies removed with these settings</h4>
- {{#if (gt removedAnomalies 0)}}
- <p class="te-self-serve__block-subtext">The following previously sent anomalies <span class="stronger">would not have been sent</span> under the new settings.</p>
- {{else}}
- <p class="te-self-serve__block-subtext">These settings would result in <span class="stronger">NO reduction</span> of sent anomalies</p>
- {{/if}}
- {{#if isAnomalyTableLoading}}
- <div class="spinner-wrapper-self-serve spinner-wrapper-self-serve__content-block">{{ember-spinner}}</div>
- {{/if}}
-
- {{!-- Alert anomaly table --}}
- <table class="te-anomaly-table te-anomaly-table-tuning">
- <thead>
- <tr class="te-anomaly-table__row te-anomaly-table__row--metrics">
- <td colspan="5">
- <ul class="te-anomaly-table-stats">
- {{#each tableStats as |metric|}}
- <li class="te-anomaly-table-stats__category {{if metric.isActive "te-anomaly-table-stats__category--active"}}" {{action "toggleCategory" metric.label}}>
- <div class="te-anomaly-table-stats__content te-anomaly-table-stats__content--number">
- {{metric.count}}
- </div>
- <div class="te-anomaly-table-stats__content te-anomaly-table-stats__content--text">
- {{metric.label}}
- </div>
- </li>
- {{/each}}
- </ul>
- </td>
- </tr>
- <tr class="te-anomaly-table__row te-anomaly-table__head">
- <th class="te-anomaly-table__cell-head">
- <a class="te-anomaly-table__cell-link" {{action "toggleSortDirection" "start"}}>
- Start/Duration (PDT)
- <i class="te-anomaly-table__icon glyphicon {{if sortColumnStartUp "glyphicon-menu-up" "glyphicon-menu-down"}}"></i>
- </a>
- </th>
- {{#if alertHasDimensions}}
- <th class="te-anomaly-table__cell-head">Dimensions</th>
- {{/if}}
- <th class="te-anomaly-table__cell-head">
- <a class="te-anomaly-table__cell-link" {{action "toggleSortDirection" "score"}}>
- Severity Score
- <i class="te-anomaly-table__icon glyphicon {{if sortColumnScoreUp "glyphicon-menu-up" "glyphicon-menu-down"}}"></i>
- </a>
- </th>
- <th class="te-anomaly-table__cell-head">
- <a class="te-anomaly-table__cell-link" {{action "toggleSortDirection" "change"}}>
- Current/WoW
- <i class="te-anomaly-table__icon glyphicon {{if sortColumnChangeUp "glyphicon-menu-up" "glyphicon-menu-down"}}"></i>
- </a>
- </th>
- <th class="te-anomaly-table__cell-head">
- <a class="te-anomaly-table__cell-link" {{action "toggleSortDirection" "resolution"}}>
- Resolution
- <i class="te-anomaly-table__icon glyphicon {{if sortColumnResolutionUp "glyphicon-menu-up" "glyphicon-menu-down"}}"></i>
- </a>
- </th>
- </tr>
- </thead>
-
- <tbody>
- {{#each diffedAnomalies as |anomaly|}}
- <tr class="te-anomaly-table__row">
- <td class="te-anomaly-table__cell">
- <ul class="te-anomaly-table__list">
- <li class="te-anomaly-table__list-item te-anomaly-table__list-item--shadow">{{anomaly.index}}</li>
- <li class="te-anomaly-table__list-item te-anomaly-table__list-item--stronger">{{anomaly.startDateStr}}</li>
- <li class="te-anomaly-table__list-item te-anomaly-table__list-item--lighter">{{anomaly.durationStr}}</li>
- </ul>
- </td>
- {{#if alertHasDimensions}}
- <td class="te-anomaly-table__cell">
- <ul class="te-anomaly-table__list">
- {{#each anomaly.dimensionList as |dimension|}}
- <li class="te-anomaly-table__list-item te-anomaly-table__list-item--smaller">
- {{dimension.dimensionKey}}: <span class="stronger">{{dimension.dimensionVal}}</span>
- </li>
- {{else}}
- -
- {{/each}}
- </ul>
- </td>
- {{/if}}
- <td class="te-anomaly-table__cell">{{anomaly.severityScore}}</td>
- <td class="te-anomaly-table__cell">
- <ul class="te-anomaly-table__list">
- <li>{{anomaly.shownCurrent}} / {{anomaly.shownBaseline}}</li>
- <li class="te-anomaly-table__value-label te-anomaly-table__value-label--{{calculate-direction anomaly.shownChangeRate}}">
- ({{anomaly.shownChangeRate}}%)
- </li>
- </ul>
- </td>
- <td class="te-anomaly-table__cell">
- {{anomaly.feedbackLabel}}
- </td>
- </tr>
- {{/each}}
- </tbody>
- </table>
- </div>
- {{/if}}
-
-</div>
diff --git a/thirdeye/thirdeye-frontend/app/pods/preview/controller.js b/thirdeye/thirdeye-frontend/app/pods/preview/controller.js
deleted file mode 100644
index 9430b53..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/preview/controller.js
+++ /dev/null
@@ -1,478 +0,0 @@
-import { observer, computed, set, get, getProperties } from '@ember/object';
-import { later, debounce } from '@ember/runloop';
-import { reads, gt, or } from '@ember/object/computed';
-import { inject as service } from '@ember/service';
-import Controller from '@ember/controller';
-import {
- filterObject,
- filterPrefix,
- hasPrefix,
- toBaselineUrn,
- toBaselineRange,
- toCurrentUrn,
- toOffsetUrn,
- toFilters,
- toFilterMap,
- appendFilters,
- dateFormatFull,
- colorMapping,
- stripTail,
- extractTail,
- toColor
-} from 'thirdeye-frontend/utils/rca-utils';
-import EVENT_TABLE_COLUMNS from 'thirdeye-frontend/shared/eventTableColumns';
-import filterBarConfig from 'thirdeye-frontend/shared/filterBarConfig';
-import moment from 'moment';
-import config from 'thirdeye-frontend/config/environment';
-import _ from 'lodash';
-import { checkStatus } from 'thirdeye-frontend/utils/utils';
-import fetch from 'fetch';
-
-const PREVIEW_DATE_FORMAT = 'MMM DD, hh:mm a';
-
-export default Controller.extend({
- detectionConfig: null,
-
- detectionConfigName: null,
-
- detectionConfigCron: null,
-
- metricUrn: null,
-
- output: 'nothing here',
-
- anomalies: null,
-
- diagnostics: null,
-
- diagnosticsPath: null,
-
- diagnosticsValues: null,
-
- timeseries: null,
-
- baseline: null,
-
- analysisRange: [moment().subtract(2, 'month').startOf('hour').valueOf(), moment().startOf('hour').valueOf()],
-
- displayRange: [moment().subtract(3, 'month').startOf('hour').valueOf(), moment().startOf('hour').valueOf()],
-
- compareMode: 'wo1w',
-
- compareModeOptions: [
- 'wo1w',
- 'wo2w',
- 'wo3w',
- 'wo4w',
- 'mean4w',
- 'median4w',
- 'min4w',
- 'max4w',
- 'none'
- ],
-
- errorTimeseries: null,
-
- errorBaseline: null,
-
- errorAnomalies: null,
-
- colorMapping: colorMapping,
-
- axis: {
- y: {
- show: true
- },
- y2: {
- show: false
- },
- x: {
- type: 'timeseries',
- show: true,
- tick: {
- fit: false
- }
- }
- },
-
- zoom: {
- enabled: true,
- rescale: true
- },
-
- legend: {
- show: false
- },
-
- anomalyMetricUrns: computed('anomalies', function () {
- const anomalies = get(this, 'anomalies') || [];
- const metricUrns = new Set(anomalies.map(anomaly => stripTail(anomaly.metricUrn)));
-
- // TODO refactor this side-effect
- this._fetchEntities(metricUrns)
- .then(res => set(this, 'metricEntities', res));
-
- return metricUrns;
- }),
-
- metricEntities: null,
-
- anomalyMetricEntities: computed('anomalyMetricUrns', 'metricEntities', function () {
- const { anomalyMetricUrns, metricEntities } = getProperties(this, 'anomalyMetricUrns', 'metricEntities');
- if (_.isEmpty(anomalyMetricUrns) || _.isEmpty(metricEntities)) { return []; }
- return [...anomalyMetricUrns].filter(urn => urn in metricEntities).map(urn => metricEntities[urn]).sortBy('name');
- }),
-
- anomalyMetricUrnDimensions: computed('anomalies', function () {
- const anomalies = get(this, 'anomalies');
- const urn2dimensions = {};
- anomalies.forEach(anomaly => {
- const baseUrn = stripTail(anomaly.metricUrn);
- if (!_.isEqual(baseUrn, anomaly.metricUrn)) {
- urn2dimensions[baseUrn] = urn2dimensions[baseUrn] || new Set();
- urn2dimensions[baseUrn].add(anomaly.metricUrn);
- }
- });
- return urn2dimensions;
- }),
-
- anomalyMetricUrnDimensionLabels: computed('anomalies', function () {
- const anomalies = get(this, 'anomalies');
- const metricUrns = new Set(anomalies.map(anomaly => anomaly.metricUrn));
-
- const urn2count = {};
- [...anomalies].forEach(anomaly => {
- const urn = anomaly.metricUrn;
- urn2count[urn] = (urn2count[urn] || 0) + 1;
- });
-
- const urn2labels = {};
- [...metricUrns].forEach(urn => {
- const filters = toFilters(urn);
- urn2labels[urn] = filters.map(arr => arr[1]).join(", ") + ` (${urn2count[urn]})`;
- });
- return urn2labels;
- }),
-
- anomaliesByMetricUrn: computed('anomalies', function () {
- const anomalies = get(this, 'anomalies');
- const urn2anomalies = {};
- anomalies.forEach(anomaly => {
- const urn = anomaly.metricUrn;
- urn2anomalies[urn] = (urn2anomalies[urn] || []).concat([anomaly]);
- });
- return urn2anomalies;
- }),
-
- series: computed(
- 'anomalies',
- 'timeseries',
- 'baseline',
- 'diagnosticsSeries',
- 'analysisRange',
- 'displayRange',
- function () {
- const metricUrn = get(this, 'metricUrn');
- const anomalies = get(this, 'anomalies');
- const timeseries = get(this, 'timeseries');
- const baseline = get(this, 'baseline');
- const diagnosticsSeries = get(this, 'diagnosticsSeries');
- const analysisRange = get(this, 'analysisRange');
- const displayRange = get(this, 'displayRange');
-
- const series = {};
-
- if (!_.isEmpty(anomalies)) {
-
- anomalies
- .filter(anomaly => anomaly.metricUrn === metricUrn)
- .forEach(anomaly => {
- const key = this._formatAnomaly(anomaly);
- series[key] = {
- timestamps: [anomaly.startTime, anomaly.endTime],
- values: [1, 1],
- type: 'line',
- color: 'teal',
- axis: 'y2'
- };
- series[key + '-region'] = Object.assign({}, series[key], {
- type: 'region',
- color: 'orange'
- });
- });
- }
-
- if (timeseries && !_.isEmpty(timeseries.value)) {
- series['current'] = {
- timestamps: timeseries.timestamp,
- values: timeseries.value,
- type: 'line',
- color: toColor(metricUrn)
- };
- }
-
- if (baseline && !_.isEmpty(baseline.value)) {
- series['baseline'] = {
- timestamps: baseline.timestamp,
- values: baseline.value,
- type: 'line',
- color: 'light-' + toColor(metricUrn)
- };
- }
-
- // detection range
- if (timeseries && !_.isEmpty(timeseries.value)) {
- series['pre-detection-region'] = {
- timestamps: [displayRange[0], analysisRange[0]],
- values: [1, 1],
- type: 'region',
- color: 'grey'
- };
- }
-
- return Object.assign(series, diagnosticsSeries);
- }
- ),
-
- diagnosticsSeries: computed(
- 'diagnostics',
- 'diagnosticsPath',
- 'diagnosticsValues',
- function () {
- const diagnosticsPath = get(this, 'diagnosticsPath');
- const diagnosticsValues = get(this, 'diagnosticsValues') || [];
-
- const series = {};
-
- diagnosticsValues.forEach(value => {
- const diagnostics = this._makeDiagnosticsSeries(diagnosticsPath, value);
- if (!_.isEmpty(diagnostics)) {
- series[`diagnostics-${value}`] = diagnostics;
- }
- });
-
- const changeSeries = this._makeDiagnosticsPoints(diagnosticsPath);
- Object.keys(changeSeries).forEach(key => {
- series[`diagnostics-${key}`] = changeSeries[key];
- });
-
- return series;
- }
- ),
-
- diagnosticsPathOptions: computed('diagnostics', function () {
- const diagnostics = get(this, 'diagnostics');
- return this._makePaths('', diagnostics);
- }),
-
- diagnosticsValueOptions: computed('diagnostics', 'diagnosticsPath', function () {
- const diagnosticsPath = get(this, 'diagnosticsPath');
- const diagnostics = get(this, 'diagnostics.' + diagnosticsPath);
- if (_.isEmpty(diagnostics)) { return []; }
- return Object.keys(diagnostics.data);
- }),
-
- _makePaths(prefix, diagnostics) {
- if (_.isEmpty(diagnostics)) { return []; }
-
- const directPaths = Object.keys(diagnostics)
- .filter(key => key.startsWith('thirdeye:metric:'))
- .map(key => prefix + `${key}`);
-
- const nestedPaths = Object.keys(diagnostics)
- .filter(key => !key.startsWith('thirdeye:metric:'))
- .map(key => this._makePaths(`${prefix}${key}.`, diagnostics[key]))
- .reduce((agg, paths) => agg.concat(paths), []);
-
- return directPaths.concat(nestedPaths);
- },
-
- _makeKey(dimensions) {
- return Object.values(dimensions).join(', ')
- },
-
- _formatAnomaly(anomaly) {
- return `${moment(anomaly.startTime).format(PREVIEW_DATE_FORMAT)} (${this._makeKey(anomaly.dimensions)})`;
- },
-
- _filterAnomalies(rows) {
- return rows.filter(row => (row.startTime && row.endTime && !row.child));
- },
-
- _makeDiagnosticsSeries(path, key) {
- try {
- const source = get(this, 'diagnostics.' + path + '.data');
-
- if (_.isEmpty(source.timestamp) || _.isEmpty(source[key])) { return; }
-
- return {
- timestamps: source.timestamp,
- values: source[key],
- type: 'line',
- axis: 'y2'
- }
-
- } catch (err) {
- return undefined;
- }
- },
-
- _makeDiagnosticsPoints(path) {
- try {
- const changepoints = get(this, 'diagnostics.' + path + '.changepoints');
-
- if (_.isEmpty(changepoints)) { return {}; }
-
- const out = {};
- changepoints.forEach((p, i) => {
- out[`changepoint-${i}`] = {
- timestamps: [p, p + 1],
- values: [1, 0],
- type: 'line',
- color: 'red',
- axis: 'y2'
- };
-
- out[`changepoint-${i}-region`] = {
- timestamps: [p, p + 3600000 * 24],
- values: [1, 1],
- type: 'region',
- color: 'red',
- axis: 'y2'
- };
- });
-
- return out;
-
- } catch (err) {
- return {};
- }
- },
-
- _fetchTimeseries() {
- const metricUrn = get(this, 'metricUrn');
- const range = get(this, 'displayRange');
- const granularity = '15_MINUTES';
- const timezone = moment.tz.guess();
-
- set(this, 'errorTimeseries', null);
-
- const urlCurrent = `/rootcause/metric/timeseries?urn=${metricUrn}&start=${range[0]}&end=${range[1]}&offset=current&granularity=${granularity}&timezone=${timezone}`;
- fetch(urlCurrent)
- .then(checkStatus)
- .then(res => set(this, 'timeseries', res))
- .then(res => set(this, 'output', 'got timeseries'))
- // .catch(err => set(this, 'errorTimeseries', err));
-
- set(this, 'errorBaseline', null);
-
- const offset = get(this, 'compareMode');
- const urlBaseline = `/rootcause/metric/timeseries?urn=${metricUrn}&start=${range[0]}&end=${range[1]}&offset=${offset}&granularity=${granularity}&timezone=${timezone}`;
- fetch(urlBaseline)
- .then(checkStatus)
- .then(res => set(this, 'baseline', res))
- .then(res => set(this, 'output', 'got baseline'))
- // .catch(err => set(this, 'errorBaseline', err));
- },
-
- _fetchAnomalies() {
- const analysisRange = get(this, 'analysisRange');
- const url = `/detection/preview?start=${analysisRange[0]}&end=${analysisRange[1]}&diagnostics=true`;
-
- const jsonString = get(this, 'detectionConfig');
-
- set(this, 'errorAnomalies', null);
-
- fetch(url, { method: 'POST', body: jsonString })
- .then(checkStatus)
- .then(res => {
- set(this, 'anomalies', this._filterAnomalies(res.anomalies));
- set(this, 'diagnostics', res.diagnostics);
- })
- .then(res => set(this, 'output', 'got anomalies'))
- // .catch(err => set(this, 'errorAnomalies', err));
- },
-
- _fetchEntities(urns) {
- const urnString = [...urns].join(',');
- const url = `/rootcause/raw?framework=identity&urns=${urnString}`;
- return fetch(url)
- .then(checkStatus)
- .then(res => res.reduce((agg, entity) => {
- agg[entity.urn] = entity;
- return agg;
- }, {}));
- },
-
- _writeDetectionConfig() {
- const detectionConfigBean = {
- name: get(this, 'detectionConfigName'),
- cron: get(this, 'detectionConfigCron'),
- properties: JSON.parse(get(this, 'detectionConfig')),
- lastTimestamp: 0
- };
-
- const jsonString = JSON.stringify(detectionConfigBean);
-
- return fetch(`/thirdeye/entity?entityType=DETECTION_CONFIG`, { method: 'POST', body: jsonString })
- .then(checkStatus)
- .then(res => set(this, 'output', `saved '${detectionConfigBean.name}' as id ${res}`))
- .catch(err => set(this, 'errorAnomalies', err));
- },
-
- actions: {
- onPreview() {
- set(this, 'output', 'loading anomalies ...');
-
- this._fetchAnomalies();
- },
-
- onMetricChange(updates) {
- set(this, 'output', 'fetching time series ...');
-
- const metricUrns = filterPrefix(Object.keys(updates), 'thirdeye:metric:');
-
- if (_.isEmpty(metricUrns)) { return; }
-
- const metricUrn = metricUrns[0];
-
- set(this, 'metricUrn', metricUrn);
-
- this._fetchTimeseries();
- },
-
- onMetricLink(metricUrn) {
- set(this, 'metricUrn', metricUrn);
- this._fetchTimeseries();
-
- // select matching diagnostics
- const diagnosticsPathOptions = get(this, 'diagnosticsPathOptions');
- const candidate = diagnosticsPathOptions.find(path => path.includes(metricUrn));
- if (!_.isEmpty(candidate)) {
- set(this, 'diagnosticsPath', candidate);
- }
- },
-
- onCompareMode(compareMode) {
- set(this, 'output', 'fetching time series ...');
-
- set(this, 'compareMode', compareMode);
-
- this._fetchTimeseries();
- },
-
- onDiagnosticsPath(diagnosticsPath) {
- set(this, 'diagnosticsPath', diagnosticsPath);
- },
-
- onDiagnosticsValues(diagnosticsValues) {
- set(this, 'diagnosticsValues', diagnosticsValues);
- },
-
- onSave() {
- set(this, 'output', 'saving detection config ...');
-
- this._writeDetectionConfig();
- }
- }
-});
diff --git a/thirdeye/thirdeye-frontend/app/pods/preview/route.js b/thirdeye/thirdeye-frontend/app/pods/preview/route.js
deleted file mode 100644
index 8cdaf28..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/preview/route.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import Route from '@ember/routing/route';
-import { inject as service } from '@ember/service';
-
-export default Route.extend({
- session: service(),
- actions: {
- /**
- * save session url for transition on login
- * @method willTransition
- */
- willTransition(transition) {
- //saving session url - TODO: add a util or service - lohuynh
- if (transition.intent.name && transition.intent.name !== 'logout') {
- this.set('session.store.fromUrl', {lastIntentTransition: transition});
- }
- },
- error() {
- // The `error` hook is also provided the failed
- // `transition`, which can be stored and later
- // `.retry()`d if desired.
- return true;
- },
- }
-});
diff --git a/thirdeye/thirdeye-frontend/app/pods/preview/template.hbs b/thirdeye/thirdeye-frontend/app/pods/preview/template.hbs
deleted file mode 100644
index a993e11..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/preview/template.hbs
+++ /dev/null
@@ -1,82 +0,0 @@
-<div class="container">
- <div class="row">
- <div class="col-xs-12">
- ({{metricUrn}})
- </div>
- </div>
- <div class="row">
- <div class="col-xs-12 preview-chart">
- {{timeseries-chart
- series=series
- colorMapping=colorMapping
- axis=axis
- zoom=zoom
- legend=legend
- }}
- </div>
- </div>
- <div class="row">
- <div class="col-xs-4">
- <label>diagnostics path</label>
- {{#power-select
- selected=diagnosticsPath
- options=diagnosticsPathOptions
- searchEnabled=false
- triggerId="select-diagnostics-path"
- onchange=(action "onDiagnosticsPath")
- as |path|
- }}
- {{path}}
- {{/power-select}}
- </div>
- <div class="col-xs-4">
- <label>value key</label>
- {{#power-select-multiple
- selected=diagnosticsValues
- options=diagnosticsValueOptions
- triggerId="select-diagnostics-values"
- onchange=(action "onDiagnosticsValues")
- as |value|
- }}
- {{value}}
- {{/power-select-multiple}}
- </div>
-
- </div>
- <div class="row">
- <div class="col-xs-6">
- <label>Name</label>{{input value=detectionConfigName}}
- <label>Cron</label>{{input value=detectionConfigCron}}
- <button onClick={{action "onSave"}}>save</button>
- </div>
- <div class="col-xs-6">
- {{output}}
- </div>
- </div>
- <div class="row">
- <div class="col-xs-6">
- {{textarea-autosize
- placeholder="Enter detection config here ..."
- value=detectionConfig
- cols=70
- rows=16
- }}
- <button onClick={{action "onPreview"}}>preview</button>
- </div>
- <div class="col-xs-6">
- <p>{{errorTimeseries}}</p>
- <p>{{errorBaseline}}</p>
- <p>{{errorAnomalies}}</p>
- {{#each anomalyMetricEntities as | baseMetric |}}
- <p>
- <a {{action "onMetricLink" baseMetric.urn}}>{{baseMetric.label}}{{get-safe anomalyMetricUrnDimensionLabels baseMetric.urn}}</a>
- <ol>
- {{#each (get-safe anomalyMetricUrnDimensions baseMetric.urn) as | metric |}}
- <li><a {{action "onMetricLink" metric}}>{{get-safe anomalyMetricUrnDimensionLabels metric}}</a></li>
- {{/each}}
- </ol>
- </p>
- {{/each}}
- </div>
- </div>
-</div>
\ No newline at end of file
diff --git a/thirdeye/thirdeye-frontend/app/router.js b/thirdeye/thirdeye-frontend/app/router.js
index b222611..a9d895b 100644
--- a/thirdeye/thirdeye-frontend/app/router.js
+++ b/thirdeye/thirdeye-frontend/app/router.js
@@ -23,11 +23,6 @@ Router.map(function() {
this.route('anomalies');
this.route('manage', function() {
- this.route('alert', { path: 'alert/:alert_id' }, function() {
- this.route('explore');
- this.route('tune');
- this.route('edit');
- });
this.route('alerts', function() {
this.route('performance');
});
@@ -43,8 +38,6 @@ Router.map(function() {
this.route('screenshot', { path: 'screenshot/:anomaly_id' });
this.route('rootcause');
- this.route('preview');
- this.route('auto-onboard');
});
export default Router;
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org