You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pinot.apache.org by xh...@apache.org on 2019/10/25 19:49:07 UTC
[incubator-pinot] branch master updated: [TE] frontend -
harleyjj/alert-details - fix more preview comparison bugs (#4738)
This is an automated email from the ASF dual-hosted git repository.
xhsun 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 5af88ef [TE] frontend - harleyjj/alert-details - fix more preview comparison bugs (#4738)
5af88ef is described below
commit 5af88ef909f1c2d71f20ce815372b3eec408ad1b
Author: Harley Jackson <hj...@linkedin.com>
AuthorDate: Fri Oct 25 12:48:58 2019 -0700
[TE] frontend - harleyjj/alert-details - fix more preview comparison bugs (#4738)
---
.../app/pods/anomalies/controller.js | 15 +-
.../app/pods/components/alert-details/component.js | 154 ++++--
.../app/pods/components/alert-details/template.hbs | 2 +-
.../pods/components/detection-yaml/component.js | 41 +-
.../pods/components/detection-yaml/template.hbs | 8 -
.../pods/components/subscription-yaml/component.js | 24 +-
.../pods/components/subscription-yaml/template.hbs | 8 -
.../app/pods/components/yaml-editor/component.js | 565 ---------------------
.../app/pods/components/yaml-editor/template.hbs | 174 -------
.../app/pods/manage/alerts/index/route.js | 3 +-
.../app/pods/manage/explore/route.js | 13 +-
.../app/pods/manage/yaml/route.js | 20 +-
.../app/pods/self-serve/create-alert/route.js | 6 +-
thirdeye/thirdeye-frontend/app/styles/app.scss | 3 +-
.../{yaml-editor.scss => detection-yaml.scss} | 4 +-
.../{yaml-editor.scss => subscription-yaml.scss} | 4 +-
thirdeye/thirdeye-frontend/app/utils/anomaly.js | 2 +-
thirdeye/thirdeye-frontend/app/utils/constants.js | 74 ---
thirdeye/thirdeye-frontend/app/utils/utils.js | 72 +--
thirdeye/thirdeye-frontend/app/utils/yaml-tools.js | 347 +++++++++++++
.../acceptance/self-serve-alert-tuning-test.js | 2 +-
.../component-test.js | 31 +-
.../components/subscription-yaml/component-test.js | 57 +++
23 files changed, 578 insertions(+), 1051 deletions(-)
diff --git a/thirdeye/thirdeye-frontend/app/pods/anomalies/controller.js b/thirdeye/thirdeye-frontend/app/pods/anomalies/controller.js
index 6578247..8011f91 100644
--- a/thirdeye/thirdeye-frontend/app/pods/anomalies/controller.js
+++ b/thirdeye/thirdeye-frontend/app/pods/anomalies/controller.js
@@ -14,8 +14,7 @@ import {
import { inject as service } from '@ember/service';
import { isPresent, isEmpty } from '@ember/utils';
import Controller from '@ember/controller';
-import yamljs from 'yamljs';
-import jsyaml from 'js-yaml';
+import { redundantParse } from 'thirdeye-frontend/utils/yaml-tools';
import { reads } from '@ember/object/computed';
import { toastOptions } from 'thirdeye-frontend/utils/constants';
import { setUpTimeRangeOptions, powerSort } from 'thirdeye-frontend/utils/manage-alert-utils';
@@ -396,21 +395,13 @@ export default Controller.extend({
selectedSubGroupObjects.forEach(group => {
let yamlAsObject;
try {
- yamlAsObject = yamljs.parse(group.get('yaml'));
+ yamlAsObject = redundantParse(group.get('yaml'));
if (Array.isArray(yamlAsObject.subscribedDetections)) {
additionalAlertNames = [ ...additionalAlertNames, ...yamlAsObject.subscribedDetections];
}
}
catch(error){
- try {
- // use jsyaml package to try parsing again, since yamljs doesn't parse some edge cases
- yamlAsObject = jsyaml.safeLoad(group.get('yaml'));
- if (Array.isArray(yamlAsObject.subscribedDetections)) {
- additionalAlertNames = [ ...additionalAlertNames, ...yamlAsObject.subscribedDetections];
- }
- } catch (error) {
- notifications.error(`Failed to retrieve alert names for subscription group: ${group.get('name')}`, 'Error', toastOptions);
- }
+ notifications.error(`Failed to retrieve alert names for subscription group: ${group.get('name')}`, 'Error', toastOptions);
}
});
// add the alert names extracted from groups to any that are already present
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/alert-details/component.js b/thirdeye/thirdeye-frontend/app/pods/components/alert-details/component.js
index e5137b5..64519f5 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/alert-details/component.js
+++ b/thirdeye/thirdeye-frontend/app/pods/components/alert-details/component.js
@@ -16,12 +16,17 @@
import Component from '@ember/component';
import { computed, set, get, getProperties } from '@ember/object';
import { later } from '@ember/runloop';
-import { checkStatus, humanizeFloat, postProps, stripNonFiniteValues } from 'thirdeye-frontend/utils/utils';
+import { checkStatus,
+ humanizeFloat,
+ postProps,
+ replaceNonFiniteWithCurrent,
+ stripNonFiniteValues } from 'thirdeye-frontend/utils/utils';
import { toastOptions } from 'thirdeye-frontend/utils/constants';
import { colorMapping, makeTime, toMetricLabel, extractTail } from 'thirdeye-frontend/utils/rca-utils';
import { getYamlPreviewAnomalies,
getAnomaliesByAlertId,
getBounds } from 'thirdeye-frontend/utils/anomaly';
+import { getValueFromYaml } from 'thirdeye-frontend/utils/yaml-tools';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';
import floatToPercent from 'thirdeye-frontend/utils/float-to-percent';
@@ -84,6 +89,8 @@ export default Component.extend({
granularity: null,
alertYaml: null,
dimensionExploration: null,
+ // cachedMetric holds the last metric of anomalies fetched, so that state can be reset for comparison if metric changes
+ cachedMetric: null,
getAnomaliesError:false, // stops the component from fetching more anomalies until user changes state
detectionHealth: null, // result of call to detection/health/{id}, passed in by parent
timeWindowSize: null, // passed in by parent, which retrieves from endpoint. Do not set
@@ -268,9 +275,9 @@ export default Component.extend({
/**
* Return state of anomalies and time series for updating state correctly
- * 1 - set to old (Alert Overview or Create Alert Preview w/o old)
+ * 1 - set to old (Alert Overview or Create Alert Preview)
* 2 - set to new (Edit Alert Preview with old or Create Alert Preview w/o new)
- * 3 - shuffle then set to new (Create Alert Preview with 2 sets already)
+ * 3 - shuffle then set to new (Create Alert Preview with 2 sets already) (not used for now)
* 4 - get alert anomalies only - no time series (Edit Alert Preview w/o any anomalies loaded yet)
* 5 - error getting anomalies
* @type {Number}
@@ -520,7 +527,8 @@ export default Component.extend({
if (baseline && !_.isEmpty(baseline.upper_bound)) {
series['Upper and lower bound'] = {
timestamps: baseline.timestamp,
- values: stripNonFiniteValues(baseline.upper_bound),
+ values: replaceNonFiniteWithCurrent(baseline.upper_bound,
+ showRules ? timeseries.current : timeseries.value),
type: 'line',
color: 'screenshot-bounds'
};
@@ -529,7 +537,8 @@ export default Component.extend({
if (baseline && !_.isEmpty(baseline.lower_bound)) {
series['lowerBound'] = {
timestamps: baseline.timestamp,
- values: stripNonFiniteValues(baseline.lower_bound),
+ values: replaceNonFiniteWithCurrent(baseline.lower_bound,
+ showRules ? timeseries.current : timeseries.value),
type: 'line',
color: 'screenshot-bounds'
};
@@ -824,10 +833,10 @@ export default Component.extend({
let falsePositives = 0;
let falseNegatives = 0;
let numberOfAnomalies = 0;
- anomaliesOld.forEach(function (attr) {
+ anomaliesOld.forEach(function (anomaly) {
numberOfAnomalies++;
- if(attr.anomaly && attr.anomaly.statusClassification) {
- const classification = attr.anomaly.statusClassification;
+ if(anomaly && anomaly.statusClassification) {
+ const classification = anomaly.statusClassification;
if (classification !== 'NONE') {
respondedAnomaliesCount++;
if (classification === 'TRUE_POSITIVE') {
@@ -960,8 +969,9 @@ export default Component.extend({
}
anomalies = applicationAnomalies;
}
+ set(this, 'cachedMetric', getValueFromYaml('metric', alertYaml, 'string'));
} catch (error) {
- notifications.error(`_getAnomalies failed: ${error}`, 'Error', toastOptions);
+ notifications.error(`_getAnomalies failed: ${error.body.message}`, 'Error', toastOptions);
this.set('getAnomaliesError', true);
}
@@ -998,36 +1008,63 @@ export default Component.extend({
}
},
- _formattedModifiedBy(feedback) {
- let result;
- if (feedback && typeof feedback === 'object') {
- if (feedback.updatedBy && feedback.updatedBy !== 'no-auth-user') {
- result = feedback.updatedBy.split('@')[0];
- } else {
- result = '--';
+ /**
+ * Helper to reset state if the user is previewing a different metric
+ * returns true if the metrics are different
+ * @method checkMetricIfCreateAlertPreview
+ * @return {boolean}
+ */
+ _checkMetricIfCreateAlertPreview() {
+ let isMetricNew = false;
+ const {
+ stateOfAnomaliesAndTimeSeries,
+ cachedMetric,
+ alertYaml
+ } = this.getProperties('stateOfAnomaliesAndTimeSeries', 'cachedMetric', 'alertYaml');
+ if (stateOfAnomaliesAndTimeSeries === 2 || stateOfAnomaliesAndTimeSeries === 3) {
+ if (!this.get('isEditMode')) {
+ // is Create Alert preview
+ isMetricNew = !(cachedMetric === getValueFromYaml('metric', alertYaml, 'string'));
}
}
- return result;
+ return isMetricNew;
},
- _formattedRule(properties) {
- let result;
- if (properties && typeof properties === 'object') {
- if (properties.detectorComponentName) {
- result = properties.detectorComponentName.split(':')[0];
- } else {
- result = '--';
- }
- }
- return result;
- },
+ _fetchAnomalies() {
+ set(this, 'getAnomaliesError', false);
- _formatAnomaly(anomaly) {
- return `${moment(anomaly.startTime).format(TABLE_DATE_FORMAT)}`;
- },
+ // If the user is running the detection with a new metric, we should reset the state of time series and anomalies for comparison
+ if (this._checkMetricIfCreateAlertPreview()) {
+ this.setProperties({
+ anomaliesOld: [],
+ anomaliesOldSet: false,
+ anomaliesNew: [],
+ anomaliesNewSet: false
+ });
+ }
- _filterAnomalies(rows) {
- return rows.filter(row => (row.startTime && row.endTime && !row.child));
+ try {
+ // in Edit Alert Preview, we want the original yaml used for comparisons
+ const content = (get(this, 'isEditMode') && !(get(this, 'anomaliesOldSet'))) ? get(this, 'originalYaml') : get(this, 'alertYaml');
+ return this.get('_getAnomalies').perform(content)
+ .then(results => this._setAnomaliesAndTimeSeries(results))
+ .then(() => {
+ if (get(this, 'metricUrn')) {
+ this._fetchTimeseries();
+ } else {
+ throw new Error('Unable to get MetricUrn from response');
+ }
+ })
+ .catch(error => {
+ if (error.name !== 'TaskCancelation') {
+ this.get('notifications').error(error, 'Error', toastOptions);
+ set(this, 'getAnomaliesError', true);
+ }
+ });
+ } catch (error) {
+ this.get('notifications').error(error, 'Error', toastOptions);
+ set(this, 'getAnomaliesError', true);
+ }
},
_fetchTimeseries() {
@@ -1090,31 +1127,36 @@ export default Component.extend({
set(this, 'errorBaseline', null);
},
- _fetchAnomalies() {
- set(this, 'getAnomaliesError', false);
+ _filterAnomalies(rows) {
+ return rows.filter(row => (row.startTime && row.endTime && !row.child));
+ },
- try {
- // in Edit Alert Preview, we want the original yaml used for comparisons
- const content = (get(this, 'isEditMode') && !(get(this, 'anomaliesOldSet'))) ? get(this, 'originalYaml') : get(this, 'alertYaml');
- return this.get('_getAnomalies').perform(content)
- .then(results => this._setAnomaliesAndTimeSeries(results))
- .then(() => {
- if (get(this, 'metricUrn')) {
- this._fetchTimeseries();
- } else {
- throw new Error('Unable to get MetricUrn from response');
- }
- })
- .catch(error => {
- if (error.name !== 'TaskCancelation') {
- this.get('notifications').error(error, 'Error', toastOptions);
- set(this, 'getAnomaliesError', true);
- }
- });
- } catch (error) {
- this.get('notifications').error(error, 'Error', toastOptions);
- set(this, 'getAnomaliesError', true);
+ _formatAnomaly(anomaly) {
+ return `${moment(anomaly.startTime).format(TABLE_DATE_FORMAT)}`;
+ },
+
+ _formattedModifiedBy(feedback) {
+ let result;
+ if (feedback && typeof feedback === 'object') {
+ if (feedback.updatedBy && feedback.updatedBy !== 'no-auth-user') {
+ result = feedback.updatedBy.split('@')[0];
+ } else {
+ result = '--';
+ }
}
+ return result;
+ },
+
+ _formattedRule(properties) {
+ let result;
+ if (properties && typeof properties === 'object') {
+ if (properties.detectorComponentName) {
+ result = properties.detectorComponentName.split(':')[0];
+ } else {
+ result = '--';
+ }
+ }
+ return result;
},
/**
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/alert-details/template.hbs b/thirdeye/thirdeye-frontend/app/pods/components/alert-details/template.hbs
index ad46a5c..cacda52 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/alert-details/template.hbs
+++ b/thirdeye/thirdeye-frontend/app/pods/components/alert-details/template.hbs
@@ -269,7 +269,7 @@
{{/if}}
</div>
{{else}}
- <div class="yaml-editor-msg">Alert configuration has changed.</div>
+ <div class="detection-yaml-msg">Alert configuration has changed.</div>
{{range-pill-selectors
title="Show me"
uiDateFormat=pill.uiDateFormat
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/detection-yaml/component.js b/thirdeye/thirdeye-frontend/app/pods/components/detection-yaml/component.js
index f23b3a2..e097034 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/detection-yaml/component.js
+++ b/thirdeye/thirdeye-frontend/app/pods/components/detection-yaml/component.js
@@ -1,6 +1,6 @@
/**
* Component to render the detection configuration yaml editor.
- * @module components/detection-editor
+ * @module components/detection-yaml
* @property {Number} alertId - alertId needed in edit mode, to submit changes to alert config
* @property {boolean} isEditMode - to activate the edit mode
* @property {String} detectionYaml - the detection yaml
@@ -16,11 +16,10 @@
*/
import Component from '@ember/component';
-import {computed, set, get, getProperties, setProperties} from '@ember/object';
-import {checkStatus} from 'thirdeye-frontend/utils/utils';
-import {yamlAlertProps, toastOptions} from 'thirdeye-frontend/utils/constants';
-import yamljs from 'yamljs';
-import jsyaml from 'js-yaml';
+import { set, get, getProperties, setProperties } from '@ember/object';
+import { checkStatus } from 'thirdeye-frontend/utils/utils';
+import { toastOptions } from 'thirdeye-frontend/utils/constants';
+import { defaultDetectionYaml, redundantParse } from 'thirdeye-frontend/utils/yaml-tools';
import RSVP from "rsvp";
import fetch from 'fetch';
import {
@@ -30,32 +29,23 @@ import {inject as service} from '@ember/service';
import config from 'thirdeye-frontend/config/environment';
export default Component.extend({
- classNames: ['yaml-editor'],
+ classNames: ['detection-yaml'],
notifications: service('toast'),
/**
- * Properties we expect to receive for the yaml-editor
+ * Properties we expect to receive for detection-yaml
*/
currentMetric: null,
isYamlParseable: true,
alertTitle: 'Define detection configuration',
isEditMode: false,
disableYamlSave: true,
- detectionMsg: '', //General alert failures
detectionYaml: null, // The YAML for the anomaly detection
- currentYamlAlertOriginal: yamlAlertProps,
+ currentYamlAlertOriginal: defaultDetectionYaml,
alertId: null, // only needed in edit mode
setDetectionYaml: null, // bubble up detectionYaml changes to parent
- isDetectionMsg: computed(
- 'detectionMsg',
- function() {
- const detectionMsg = get(this, 'detectionMsg');
- return detectionMsg !== '';
- }
- ),
-
init() {
this._super(...arguments);
const {
@@ -208,18 +198,11 @@ export default Component.extend({
} = getProperties(this, 'detectionYaml', 'noResultsArray');
let yamlAsObject = {};
try {
- yamlAsObject = yamljs.parse(detectionYaml);
+ yamlAsObject = redundantParse(detectionYaml);
set(this, 'isYamlParseable', true);
- }
- catch(err){
- try {
- // use jsyaml package to try parsing again, since yamljs doesn't parse some edge cases
- yamlAsObject = jsyaml.safeLoad(detectionYaml);
- set(this, 'isYamlParseable', true);
- } catch (error) {
- set(this, 'isYamlParseable', false);
- return noResultsArray;
- }
+ } catch (error) {
+ set(this, 'isYamlParseable', false);
+ return noResultsArray;
}
// if editor.metricId field contains a value, metric was just chosen. Populate caches for filters and dimensions
if(editor.metricId){
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/detection-yaml/template.hbs b/thirdeye/thirdeye-frontend/app/pods/components/detection-yaml/template.hbs
index 0cdee45..47ee730 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/detection-yaml/template.hbs
+++ b/thirdeye/thirdeye-frontend/app/pods/components/detection-yaml/template.hbs
@@ -49,11 +49,3 @@
</div>
{{/if}}
</div>
-<div class="col-xs-12">
- {{#if isDetectionMsg}}
- <div class="yaml-editor-msg">
- <p class="yaml-editor-msg__icon"><i class="yaml-editor-msg__icon--error glyphicon glyphicon-remove-circle"></i>Error in alert yaml</p>
- <p>Message: {{detectionMsg}}</p>
- </div>
- {{/if}}
-</div>
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/subscription-yaml/component.js b/thirdeye/thirdeye-frontend/app/pods/components/subscription-yaml/component.js
index ed49a1f..2fa40dd 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/subscription-yaml/component.js
+++ b/thirdeye/thirdeye-frontend/app/pods/components/subscription-yaml/component.js
@@ -3,8 +3,8 @@
* @module components/subscription-yaml
* @property {number} subscriptionGroupId - the subscription group id in edit mode
* @property {boolean} isEditMode - to activate the edit mode
- * @property {Object} subscriptionGroupNames - the list of subscription groups
- * @property {Object} subscriptionYaml - the subscription group yaml
+ * @property {Array} subscriptionGroupNames - the list of subscription groups
+ * @property {String} subscriptionYaml - the subscription group yaml
* @property {function} updateSubscriptionYaml - bubble up the subscription group yaml to parent
* @example
{{subscription-yaml
@@ -21,16 +21,16 @@
*/
import Component from '@ember/component';
-import {computed, get, set} from '@ember/object';
-import {yamlAlertSettings} from 'thirdeye-frontend/utils/constants';
-import {inject as service} from '@ember/service';
+import { get, set } from '@ember/object';
+import { defaultSubscriptionYaml } from 'thirdeye-frontend/utils/yaml-tools';
+import { inject as service } from '@ember/service';
import config from 'thirdeye-frontend/config/environment';
export default Component.extend({
- classNames: ['yaml-editor'],
+ classNames: ['subscription-yaml'],
notifications: service('toast'),
/**
- * Properties we expect to receive for the yaml-editor
+ * Properties we expect to receive for the subscription-yaml
*/
currentMetric: null,
isYamlParseable: true,
@@ -40,7 +40,7 @@ export default Component.extend({
disableSubGroupSave: true,
subscriptionMsg: '', //General subscription failures
subscriptionYaml: null, // The YAML for the subscription group
- currentYamlSettingsOriginal: yamlAlertSettings,
+ currentYamlSettingsOriginal: defaultSubscriptionYaml,
showAnomalyModal: false,
showNotificationModal: false,
setSubscriptionYaml: null, // function passed in from parent
@@ -48,14 +48,6 @@ export default Component.extend({
- isSubscriptionMsg: computed(
- 'subscriptionMsg',
- function() {
- const subscriptionMsg = get(this, 'subscriptionMsg');
- return subscriptionMsg !== '';
- }
- ),
-
init() {
this._super(...arguments);
const {
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/subscription-yaml/template.hbs b/thirdeye/thirdeye-frontend/app/pods/components/subscription-yaml/template.hbs
index 732bb5a..32c1b26 100644
--- a/thirdeye/thirdeye-frontend/app/pods/components/subscription-yaml/template.hbs
+++ b/thirdeye/thirdeye-frontend/app/pods/components/subscription-yaml/template.hbs
@@ -46,11 +46,3 @@
mode="ace/mode/yaml"
}}
</div>
-<div class="col-xs-12">
- {{#if isSubscriptionMsg}}
- <div class="yaml-editor-msg">
- <p class="yaml-editor-msg__icon"><i class="yaml-editor-msg__icon--error glyphicon glyphicon-remove-circle"></i>Error in the subscription yaml</p>
- <p>Message: {{subscriptionMsg}}</p>
- </div>
- {{/if}}
-</div>
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/yaml-editor/component.js b/thirdeye/thirdeye-frontend/app/pods/components/yaml-editor/component.js
deleted file mode 100644
index 7d31990..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/components/yaml-editor/component.js
+++ /dev/null
@@ -1,565 +0,0 @@
-/**
- * Component to render the alert and subscription group yaml editors.
- * @module components/yaml-editor
- * @property {number} alertId - the alert id
- * @property {number} subscriptionGroupId - the subscription group id
- * @property {boolean} isEditMode - to activate the edit mode
- * @property {boolean} showSettings - to show the subscriber groups yaml editor
- * @property {Object} subscriptionGroupNames - the list of subscription groups
- * @property {Object} detectionYaml - the detection yaml to display
- * @property {Object} subscriptionYaml - the subscription group yaml to display
- * @example
- {{yaml-editor
- alertId=model.alertId
- subscriptionGroupId=model.subscriptionGroupId
- isEditMode=true
- showSettings=true
- subscriptionGroupNames=model.subscriptionGroupNames
- detectionYaml=model.detectionYaml
- subscriptionYaml=model.subscriptionYaml
- }}
- * @author lohuynh
- */
-
-import Component from '@ember/component';
-import {computed, set, get, getProperties, setProperties} from '@ember/object';
-import {checkStatus} from 'thirdeye-frontend/utils/utils';
-import {yamlAlertProps, yamlAlertSettings, toastOptions} from 'thirdeye-frontend/utils/constants';
-import yamljs from 'yamljs';
-import RSVP from "rsvp";
-import fetch from 'fetch';
-import {
- selfServeApiGraph, selfServeApiCommon
-} from 'thirdeye-frontend/utils/api/self-serve';
-import {inject as service} from '@ember/service';
-import {task} from 'ember-concurrency';
-import config from 'thirdeye-frontend/config/environment';
-
-const CREATE_GROUP_TEXT = 'Create a new subscription group';
-
-export default Component.extend({
- classNames: ['yaml-editor'],
- notifications: service('toast'),
- /**
- * Properties we expect to receive for the yaml-editor
- */
- currentMetric: null,
- isYamlParseable: true,
- alertTitle: 'Define detection configuration',
- alertSettingsTitle: 'Define subscription configuration',
- isEditMode: false,
- showSettings: true,
- disableYamlSave: true,
- disableSubGroupSave: true,
- detectionMsg: '', //General alert failures
- subscriptionMsg: '', //General subscription failures
- detectionYaml: null, // The YAML for the anomaly detection
- subscriptionYaml: null, // The YAML for the subscription group
- currentYamlAlertOriginal: yamlAlertProps,
- currentYamlSettingsOriginal: yamlAlertSettings,
- showAnomalyModal: false,
- showNotificationModal: false,
- toggleCollapsed: true,
- alertDataIsCurrent: true,
-
-
-
- init() {
- this._super(...arguments);
- const subscriptionGroupNamesDisplay = get(this, 'subscriptionGroupNamesDisplay');
- // Checks to make sure there is a subscription group array with at least one subscription group
- if (subscriptionGroupNamesDisplay && Array.isArray(subscriptionGroupNamesDisplay) && subscriptionGroupNamesDisplay.length > 0) {
- const firstGroup = subscriptionGroupNamesDisplay[0];
- set(this, 'subscriptionYaml', firstGroup.yaml);
- set(this, 'groupName', firstGroup);
- set(this, 'subscriptionGroupId', firstGroup.id);
- }
- },
-
- /**
- * populates subscription group dropdown with options from fetch or model
- * @method subscriptionGroupNamesDisplay
- * @return {Object}
- */
- subscriptionGroupNamesDisplay: computed(
- 'subscriptionGroupNames',
- async function() {
- const {
- isEditMode,
- subscriptionGroupNames
- } = this.getProperties('isEditMode', 'subscriptionGroupNames');
- const createGroup = {
- name: CREATE_GROUP_TEXT,
- id: 'n/a',
- yaml: yamlAlertSettings
- };
- const moddedArray = [createGroup];
- if (isEditMode) {
- return [...moddedArray, ...subscriptionGroupNames];
- }
- const subscriptionGroups = await get(this, '_fetchSubscriptionGroups').perform();
- return [...moddedArray, ...subscriptionGroups];
- }
- ),
-
- /**
- * Flag to trigger special case of no existing subscription groups for an alert
- * @method noExistingSubscriptionGroup
- * @return {Boolean}
- */
- noExistingSubscriptionGroup: computed(
- 'subscriptionGroupNames',
- function() {
- const subscriptionGroupNames = get(this, 'subscriptionGroupNames');
- if (subscriptionGroupNames && Array.isArray(subscriptionGroupNames) && subscriptionGroupNames.length > 0) {
- return false;
- }
- return true;
- }
- ),
-
- /**
- * Change subscription group button text depending on whether creating or updating
- * @method subGroupButtonText
- * @return {String}
- */
- subGroupButtonText: computed(
- 'noExistingSubscriptionGroup',
- 'groupName',
- function() {
- const {
- noExistingSubscriptionGroup,
- groupName
- } = this.getProperties('noExistingSubscriptionGroup', 'groupName');
- return (noExistingSubscriptionGroup || !groupName || groupName.name === CREATE_GROUP_TEXT) ? "Create Group" : "Update Group";
- }
- ),
-
- /**
- * sets Yaml value displayed to contents of detectionYaml or currentYamlAlertOriginal
- * @method currentYamlAlert
- * @return {String}
- */
- currentYamlAlert: computed(
- 'detectionYaml',
- function() {
- const inputYaml = get(this, 'detectionYaml');
- return inputYaml || get(this, 'currentYamlAlertOriginal');
- }
- ),
-
- /**
- * sets Yaml value displayed to contents of subscriptionYaml or currentYamlSettingsOriginal
- * @method currentYamlAlert
- * @return {String}
- */
- currentSubscriptionYaml: computed(
- 'subscriptionYaml',
- function() {
- const subscriptionYaml = get(this, 'subscriptionYaml');
- return subscriptionYaml || get(this, 'currentYamlSettingsOriginal');
- }
- ),
-
-
- isDetectionMsg: computed(
- 'detectionMsg',
- function() {
- const detectionMsg = get(this, 'detectionMsg');
- return detectionMsg !== '';
- }
- ),
-
- isSubscriptionMsg: computed(
- 'subscriptionMsg',
- function() {
- const subscriptionMsg = get(this, 'subscriptionMsg');
- return subscriptionMsg !== '';
- }
- ),
-
- _fetchSubscriptionGroups: task(function* () {
- //dropdown of subscription groups
- const url2 = `/detection/subscription-groups`;
- const postProps2 = {
- method: 'get',
- headers: { 'content-type': 'application/json' }
- };
- const notifications = get(this, 'notifications');
-
- try {
- const response = yield fetch(url2, postProps2);
- const json = yield response.json();
- return json.filterBy('yaml');
- } catch (error) {
- notifications.error('Failed to retrieve subscription groups.', 'Error', toastOptions);
- }
- }).drop(),
-
- /**
- * Calls api's for specific metric's autocomplete
- * @method _loadAutocompleteById
- * @return Promise
- */
- _loadAutocompleteById(metricId) {
- const promiseHash = {
- filters: fetch(selfServeApiGraph.metricFilters(metricId)).then(res => checkStatus(res, 'get', true)),
- dimensions: fetch(selfServeApiGraph.metricDimensions(metricId)).then(res => checkStatus(res, 'get', true))
- };
- return RSVP.hash(promiseHash);
- },
-
- /**
- * Get autocomplete suggestions from relevant api
- * @method _buildYamlSuggestions
- * @return Promise
- */
- _buildYamlSuggestions(currentMetric, yamlAsObject, prefix, noResultsArray,
- filtersCache, dimensionsCache, position) {
- // holds default result to return if all checks fail
- let defaultReturn = Promise.resolve(noResultsArray);
- // when metric is being autocompleted, entire text field will be replaced and metricId stored in editor
- if (yamlAsObject.metric === prefix) {
- return fetch(selfServeApiCommon.metricAutoComplete(prefix))
- .then(checkStatus)
- .then(metrics => {
- if (metrics && metrics.length > 0) {
- return metrics.map(metric => {
- const [dataset, metricname] = metric.alias.split('::');
- return {
- value: metricname,
- caption: metric.alias,
- row: position.row,
- column: position.column,
- metricname,
- dataset,
- id: metric.id,
- completer:{
- insertMatch: (editor, data) => {
- // replace metric row with selected metric
- editor.session.replace({
- start: { row: data.row, column: 0 },
- end: { row: data.row, column: Number.MAX_VALUE }},
- `metric: ${data.metricname}`);
- // find dataset: field in text
- const datasetLocation = editor.find('dataset:');
- // if found, replace with dataset
- if (datasetLocation) {
- editor.session.replace({
- start: { row: datasetLocation.start.row, column: 0},
- end: { row: datasetLocation.end.row, column: Number.MAX_VALUE }},
- `dataset: ${data.dataset}`);
- // otherwise, add it to the line below the metric field
- } else {
- editor.session.insert({
- row: data.row + 1, column: 0 },
- `dataset: ${data.dataset}\n`);
- }
- editor.metricId = data.id;
- }
- }};
- });
- }
- return noResultsArray;
- })
- .catch(() => {
- return noResultsArray;
- });
- }
- // if a currentMetric has been stored, we can check autocomplete filters and dimensions
- if (currentMetric) {
- const dimensionValues = yamlAsObject.dimensionExploration.dimensions;
- const filterTypes = typeof yamlAsObject.filters === "object" ? Object.keys(yamlAsObject.filters) : [];
- if (Array.isArray(dimensionValues) && dimensionValues.includes(prefix)) {
- if (dimensionsCache.length > 0) {
- // wraps result in Promise.resolve because return of Promise is expected by yamlSuggestions
- return Promise.resolve(dimensionsCache.map(dimension => {
- return {
- value: dimension
- };
- }));
- }
- }
- let filterKey = '';
- let i = 0;
- while (i < filterTypes.length) {
- if (filterTypes[i] === prefix){
- i = filterTypes.length;
- // wraps result in Promise.resolve because return of Promise is expected by yamlSuggestions
- return Promise.resolve(Object.keys(filtersCache).map(filterType => {
- return {
- value: `${filterType}:`,
- caption: `${filterType}:`,
- snippet: filterType
- };
- }));
- }
- if (Array.isArray(yamlAsObject.filters[filterTypes[i]]) && yamlAsObject.filters[filterTypes[i]].includes(prefix)) {
- filterKey = filterTypes[i];
- }
- i++;
- }
- if (filterKey) {
- // wraps result in Promise.resolve because return of Promise is expected by yamlSuggestions
- return Promise.resolve(filtersCache[filterKey].map(filterParam => {
- return {
- value: filterParam
- };
- }));
- }
- }
- return defaultReturn;
- },
-
- // Method for handling subscription group, whether there are any or not
- async _handleSubscriptionGroup(subscriptionYaml, notifications, subscriptionGroupId) {
- const {
- noExistingSubscriptionGroup,
- groupName
- } = this.getProperties('noExistingSubscriptionGroup', 'groupName');
- if (noExistingSubscriptionGroup || !groupName || groupName.name === CREATE_GROUP_TEXT) {
- //POST settings
- const setting_url = '/yaml/subscription';
- const settingsPostProps = {
- method: 'POST',
- body: subscriptionYaml,
- headers: { 'content-type': 'text/plain' }
- };
- try {
- const settings_result = await fetch(setting_url, settingsPostProps);
- const settings_status = get(settings_result, 'status');
- const settings_json = await settings_result.json();
- if (settings_status !== 200) {
- set(this, 'errorMsg', get(settings_json, 'message'));
- notifications.error(`Failed to save the subscription configuration due to: ${settings_json.message}.`, 'Error', toastOptions);
- } else {
- notifications.success('Subscription configuration saved successfully', 'Done', toastOptions);
- }
- } catch (error) {
- notifications.error('Error while saving subscription config.', error, toastOptions);
- }
- } else {
- //PUT settings
- const setting_url = `/yaml/subscription/${subscriptionGroupId}`;
- const settingsPostProps = {
- method: 'PUT',
- body: subscriptionYaml,
- headers: { 'content-type': 'text/plain' }
- };
- try {
- const settings_result = await fetch(setting_url, settingsPostProps);
- const settings_status = get(settings_result, 'status');
- const settings_json = await settings_result.json();
- if (settings_status !== 200) {
- set(this, 'errorMsg', get(settings_json, 'message'));
- notifications.error(`Failed to save the subscription configuration due to: ${settings_json.message}.`, 'Error', toastOptions);
- } else {
- notifications.success('Subscription configuration saved successfully', 'Done', toastOptions);
- }
- } catch (error) {
- notifications.error('Error while saving subscription config.', error, toastOptions);
- }
- }
- },
-
- actions: {
- changeAccordion() {
- set(this, 'toggleCollapsed', !get(this, 'toggleCollapsed'));
- },
-
- /**
- * resets given yaml field to default value for creation mode and server value for edit mode
- */
- resetYAML(field) {
- const isEditMode = get(this, 'isEditMode');
- if (field === 'anomaly') {
- if(isEditMode) {
- set(this, 'detectionYaml', get(this, 'currentYamlAlertOriginal'));
- } else {
- const currentYamlAlertOriginal = get(this, 'currentYamlAlertOriginal');
- set(this, 'detectionYaml', currentYamlAlertOriginal);
- }
- } else if (field === 'subscription') {
- if(isEditMode) {
- set(this, 'subscriptionYaml', get(this, 'currentYamlSettingsOriginal'));
- } else {
- const currentYamlSettingsOriginal = get(this, 'currentYamlSettingsOriginal');
- set(this, 'subscriptionYaml', currentYamlSettingsOriginal);
- }
- }
- },
-
- /**
- * Brings up appropriate modal, based on which yaml field is clicked
- */
- triggerDoc(field) {
- if (field === 'Anomaly') {
- window.open(config.docs.detectionConfig);
- } else {
- window.open(config.docs.subscriptionConfig);
- }
- },
-
- /**
- * returns array of suggestions for Yaml editor autocompletion
- */
- yamlSuggestions(editor, session, position, prefix) {
- const {
- detectionYaml,
- noResultsArray
- } = getProperties(this, 'detectionYaml', 'noResultsArray');
- let yamlAsObject = {};
- try {
- yamlAsObject = yamljs.parse(detectionYaml);
- set(this, 'isYamlParseable', true);
- }
- catch(err){
- set(this, 'isYamlParseable', false);
- return noResultsArray;
- }
- // if editor.metricId field contains a value, metric was just chosen. Populate caches for filters and dimensions
- if(editor.metricId){
- const currentMetric = set(this, 'currentMetric', editor.metricId);
- editor.metricId = '';
- return get(this, '_loadAutocompleteById')(currentMetric)
- .then(resultObj => {
- const { filters, dimensions } = resultObj;
- setProperties(this, {
- dimensionsCache: dimensions,
- filtersCache: filters
- });
- })
- .then(() => {
- return get(this, '_buildYamlSuggestions')(currentMetric,
- yamlAsObject, prefix, noResultsArray, get(this, 'filtersCache'),
- get(this, 'dimensionsCache'), position)
- .then(results => results);
- });
- }
- const currentMetric = get(this, 'currentMetric');
- // deals with no metricId, which could be autocomplete for metric or for filters and dimensions already cached
- return get(this, '_buildYamlSuggestions')(currentMetric, yamlAsObject,
- prefix, noResultsArray, get(this, 'filtersCache'),
- get(this, 'dimensionsCache'), position)
- .then(results => results);
-
- },
-
- /**
- * Activates 'Create changes' button and stores YAML content in detectionYaml
- */
- onEditingDetectionYamlAction(value) {
- setProperties(this, {
- disableYamlSave: false,
- detectionYaml: value,
- detectionMsg: '',
- subscriptionMsg: '',
- alertDataIsCurrent: false
- });
- },
-
- /**
- * Activates 'Create changes' button and stores YAML content in subscriptionYaml
- */
- onEditingSubscriptionYamlAction(value) {
- setProperties(this, {
- disableSubGroupSave: false,
- subscriptionYaml: value
- });
- },
-
- /**
- * Updates the subscription settings yaml with user section
- */
- onSubscriptionGroupSelectionAction(value) {
- if(value.yaml) {
- set(this, 'subscriptionYaml', value.yaml);
- set(this, 'groupName', value);
- set(this, 'subscriptionGroupId', value.id);
- }
- },
-
- /**
- * Fired by create button in YAML UI
- * Grabs YAML content and sends it
- */
- createAlertYamlAction() {
- const content = {
- detection: get(this, 'detectionYaml'),
- subscription: get(this, 'subscriptionYaml')
- };
- const url = '/yaml/create-alert';
- const postProps = {
- method: 'post',
- body: JSON.stringify(content),
- headers: { 'content-type': 'application/json' }
- };
- const notifications = get(this, 'notifications');
-
- fetch(url, postProps).then((res) => {
- res.json().then((result) => {
- if(result){
- if (result.detectionMsg) {
- set(this, 'detectionMsg', result.detectionMsg);
- }
- if (result.subscriptionMsg) {
- set(this, 'subscriptionMsg', result.subscriptionMsg);
- }
- if (result.detectionAlertConfigId && result.detectionConfigId) {
- notifications.success('Created alert successfully.', 'Created', toastOptions);
- }
- }
- });
- }).catch((error) => {
- notifications.error('Create alert failed.', error, toastOptions);
- });
- },
-
- /**
- * Fired by alert button in YAML UI in edit mode
- * Grabs alert yaml and puts it to the backend.
- */
- async submitAlertEdit() {
- const {
- detectionYaml,
- notifications,
- alertId
- } = getProperties(this, 'detectionYaml', 'notifications', 'alertId');
-
- //PUT alert
- const alert_url = `/yaml/${alertId}`;
- const alertPostProps = {
- method: 'PUT',
- body: detectionYaml,
- headers: { 'content-type': 'text/plain' }
- };
- try {
- const alert_result = await fetch(alert_url, alertPostProps);
- const alert_status = get(alert_result, 'status');
- const alert_json = await alert_result.json();
- if (alert_status !== 200) {
- set(this, 'errorMsg', get(alert_json, 'message'));
- notifications.error(`Failed to save the detection configuration due to: ${alert_json.message}.`, 'Error', toastOptions);
- } else {
- notifications.success('Detection configuration saved successfully', 'Done', toastOptions);
- }
- } catch (error) {
- notifications.error('Error while saving detection config.', error, toastOptions);
- }
- },
-
- /**
- * Fired by subscription group button in YAML UI in edit mode
- * Grabs subscription group yaml and posts or puts it to the backend.
- */
- async submitSubscriptionGroup() {
- const {
- subscriptionYaml,
- notifications,
- subscriptionGroupId
- } = getProperties(this, 'subscriptionYaml', 'notifications', 'subscriptionGroupId');
- // If there is no existing subscription group, this method will handle it
- this._handleSubscriptionGroup(subscriptionYaml, notifications, subscriptionGroupId);
- }
- }
-});
diff --git a/thirdeye/thirdeye-frontend/app/pods/components/yaml-editor/template.hbs b/thirdeye/thirdeye-frontend/app/pods/components/yaml-editor/template.hbs
deleted file mode 100644
index 174f690..0000000
--- a/thirdeye/thirdeye-frontend/app/pods/components/yaml-editor/template.hbs
+++ /dev/null
@@ -1,174 +0,0 @@
-<fieldset class="te-form__section te-form__section--first row">
- <div class="col-xs-12">
- <legend class="te-form__section-title">{{alertTitle}}</legend>
- </div>
- <div class="col-xs-12 {{if isEditMode "bottom-margin"}}">
- {{#unless isEditMode}}
- <label for="select-metric" class="control-label te-label te-label--taller required">Can't find your metric?
- {{#link-to "self-serve.import-metric" class="thirdeye-link-secondary thirdeye-link-secondary--inside"}}
- Import a Metric From InGraphs
- {{/link-to}}
- </label>
- {{/unless}}
- <div class="pull-right">
- {{bs-button
- defaultText="Reset"
- type="outline-primary"
- buttonType="reset"
- onClick=(action "resetYAML" "anomaly")
- class="te-button te-button--link"
- }}
- {{bs-button
- defaultText="View Docs & Examples"
- type="outline-primary"
- buttonType="link"
- onClick=(action "triggerDoc" "Anomaly")
- class="te-button te-button--cancel"
- }}
- </div>
- </div>
- <div class="col-xs-12 {{if isEditMode "bottom-margin"}}">
- {{ember-ace
- lines=35
- value=currentYamlAlert
- suggestCompletions=(action 'yamlSuggestions')
- enableLiveAutocompletion=true
- update=(action "onEditingDetectionYamlAction")
- mode="ace/mode/yaml"
- }}
- {{#if isEditMode}}
- <div class="pull-right">
- {{bs-button
- defaultText="Update Alert"
- type="primary"
- buttonType="submit"
- disabled=disableYamlSave
- onClick=(action "submitAlertEdit")
- class="te-button te-button--submit"
- }}
- </div>
- {{/if}}
- </div>
- <div class="col-xs-12">
- {{#if isDetectionMsg}}
- <div class="yaml-editor-msg">
- <p class="yaml-editor-msg__icon"><i class="yaml-editor-msg__icon--error glyphicon glyphicon-remove-circle"></i>Error in alert yaml</p>
- <p>Message: {{detectionMsg}}</p>
- </div>
- {{/if}}
- </div>
- <div class="col-xs-12">
- {{#bs-accordion onChange=(action "changeAccordion") as |acc|}}
- {{#acc.item value=preview as |aitem|}}
- {{#aitem.title}}
- <section class="dashboard-container__title thirdeye-link-secondary">Preview alert {{if toggleCollapsed "/ Enter YAML configuration to preview alert." ""}}
- <span class="pull-right"><i class="glyphicon glyphicon-menu-{{if toggleCollapsed "down" "up"}}"></i></span>
- </section>
- {{/aitem.title}}
- {{#aitem.body}}
- {{#alert-details
- isPreviewMode=true
- alertYaml=detectionYaml
- alertId=alertId
- dataIsCurrent=alertDataIsCurrent
- }}
- {{yield}}
- {{/alert-details}}
- {{/aitem.body}}
- {{/acc.item}}
- {{/bs-accordion}}
- </div>
-
- <div class="col-xs-12">
- <hr/>
- </div>
- {{#if showSettings}}
- <div class="col-xs-12">
- <legend class="te-form__section-title">{{alertSettingsTitle}}</legend>
- </div>
- <div class="col-xs-4">
- {{#if isEditMode}}
- <label class="te-label te-label--small">
- {{#if noExistingSubscriptionGroup}}
- Create a subscription group for this alert
- {{else}}
- Edit a subscription group already subscribed to this alert
- {{/if}}</label>
- {{else}}
- <label class="te-label te-label--small">Add this alert to a subscription group</label>
- {{/if}}
-
- {{!-- subscription group --}}
- {{#power-select
- placeholder="Create a new subscription group"
- options=subscriptionGroupNamesDisplay
- selected=groupName
- searchField="name"
- onchange=(action 'onSubscriptionGroupSelectionAction')
- as |groupName|
- }}
- {{groupName.name}} ({{groupName.id}})
- {{/power-select}}
- </div>
- <div class="col-xs-12">
- <label for="select-metric" class="control-label te-label te-label--taller required">Can't find your team? Contact
- <a class="thirdeye-link-secondary" target="_blank" href="mailto:ask_thirdeye@linkedin.com">ask_thirdeye@linkedin.com</a>
- </label>
- <div class="pull-right">
- {{bs-button
- defaultText="Reset"
- type="outline-primary"
- buttonType="reset"
- onClick=(action "resetYAML" "subscription")
- class="te-button te-button--link"
- }}
- {{bs-button
- defaultText="View Docs & Examples"
- type="outline-primary"
- buttonType="link"
- onClick=(action "triggerDoc" "subscription")
- class="te-button te-button--cancel"
- }}
- </div>
- </div>
- <div class="col-xs-12">
- {{!-- subscription settings editor --}}
- {{ember-ace
- lines=25
- value=currentSubscriptionYaml
- update=(action "onEditingSubscriptionYamlAction")
- mode="ace/mode/yaml"
- }}
- </div>
- <div class="col-xs-12">
- {{#if isSubscriptionMsg}}
- <div class="yaml-editor-msg">
- <p class="yaml-editor-msg__icon"><i class="yaml-editor-msg__icon--error glyphicon glyphicon-remove-circle"></i>Error in the subscription yaml</p>
- <p>Message: {{subscriptionMsg}}</p>
- </div>
- {{/if}}
- </div>
- {{/if}}
-</fieldset>
-
-<fieldset class="te-form__section-submit">
- {{#if isEditMode}}
- {{bs-button
- defaultText=subGroupButtonText
- type="primary"
- buttonType="submit"
- disabled=disableSubGroupSave
- onClick=(action "submitSubscriptionGroup")
- class="te-button te-button--submit"
- }}
- {{else}}
- {{bs-button
- defaultText="Create alert"
- type="primary"
- buttonType="submit"
- disabled=disableYamlSave
- onClick=(action "createAlertYamlAction")
- class="te-button te-button--submit"
- }}
- {{/if}}
-</fieldset>
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/alerts/index/route.js b/thirdeye/thirdeye-frontend/app/pods/manage/alerts/index/route.js
index 06a731f..a6c2133 100644
--- a/thirdeye/thirdeye-frontend/app/pods/manage/alerts/index/route.js
+++ b/thirdeye/thirdeye-frontend/app/pods/manage/alerts/index/route.js
@@ -3,7 +3,8 @@ import Route from '@ember/routing/route';
import fetch from 'fetch';
import { get, getWithDefault } from '@ember/object';
import { inject as service } from '@ember/service';
-import { checkStatus, formatYamlFilter } from 'thirdeye-frontend/utils/utils';
+import { checkStatus } from 'thirdeye-frontend/utils/utils';
+import { formatYamlFilter} from 'thirdeye-frontend/utils/yaml-tools';
import { powerSort } from 'thirdeye-frontend/utils/manage-alert-utils';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/explore/route.js b/thirdeye/thirdeye-frontend/app/pods/manage/explore/route.js
index 84c2ee5..751a136 100644
--- a/thirdeye/thirdeye-frontend/app/pods/manage/explore/route.js
+++ b/thirdeye/thirdeye-frontend/app/pods/manage/explore/route.js
@@ -8,9 +8,7 @@ import RSVP from 'rsvp';
import { set, get } from '@ember/object';
import { inject as service } from '@ember/service';
import { toastOptions } from 'thirdeye-frontend/utils/constants';
-import { formatYamlFilter } from 'thirdeye-frontend/utils/utils';
-import yamljs from 'yamljs';
-import jsyaml from 'js-yaml';
+import { formatYamlFilter, redundantParse } from 'thirdeye-frontend/utils/yaml-tools';
import moment from 'moment';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
@@ -41,14 +39,9 @@ export default Route.extend(AuthenticatedRouteMixin, {
if (detection_json.yaml) {
let detectionInfo;
try {
- detectionInfo = yamljs.parse(detection_json.yaml);
+ detectionInfo = redundantParse(detection_json.yaml);
} catch (error) {
- try {
- // use jsyaml package to try parsing again, since yamljs doesn't parse some edge cases
- detectionInfo = jsyaml.safeLoad(detection_json.yaml);
- } catch (error) {
- throw new Error('yaml parsing error');
- }
+ throw new Error('yaml parsing error');
}
const lastDetection = new Date(detection_json.lastTimestamp);
Object.assign(detectionInfo, {
diff --git a/thirdeye/thirdeye-frontend/app/pods/manage/yaml/route.js b/thirdeye/thirdeye-frontend/app/pods/manage/yaml/route.js
index ac319e9..4c871d0 100644
--- a/thirdeye/thirdeye-frontend/app/pods/manage/yaml/route.js
+++ b/thirdeye/thirdeye-frontend/app/pods/manage/yaml/route.js
@@ -7,11 +7,10 @@ import Route from '@ember/routing/route';
import RSVP from 'rsvp';
import { set, get } from '@ember/object';
import { inject as service } from '@ember/service';
-import yamljs from 'yamljs';
-import jsyaml from 'js-yaml';
import moment from 'moment';
-import { yamlAlertSettings, toastOptions } from 'thirdeye-frontend/utils/constants';
-import { formatYamlFilter } from 'thirdeye-frontend/utils/utils';
+import { toastOptions } from 'thirdeye-frontend/utils/constants';
+import { defaultSubscriptionYaml } from 'thirdeye-frontend/utils/yaml-tools';
+import { formatYamlFilter, redundantParse } from 'thirdeye-frontend/utils/yaml-tools';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
const CREATE_GROUP_TEXT = 'Create a new subscription group';
@@ -44,14 +43,9 @@ export default Route.extend(AuthenticatedRouteMixin, {
if (detection_json.yaml) {
let detectionInfo;
try {
- detectionInfo = yamljs.parse(detection_json.yaml);
+ detectionInfo = redundantParse(detection_json.yaml);
} catch (error) {
- try {
- // use jsyaml package to try parsing again, since yamljs doesn't parse some edge cases
- detectionInfo = jsyaml.safeLoad(detection_json.yaml);
- } catch (error) {
- throw new Error('yaml parsing error');
- }
+ throw new Error('yaml parsing error');
}
const lastDetection = new Date(detection_json.lastTimestamp);
Object.assign(detectionInfo, {
@@ -144,7 +138,7 @@ export default Route.extend(AuthenticatedRouteMixin, {
const createGroup = {
name: CREATE_GROUP_TEXT,
id: 'n/a',
- yaml: yamlAlertSettings
+ yaml: defaultSubscriptionYaml
};
const moddedArray = [createGroup];
const subscriptionGroups = this.get('store')
@@ -164,7 +158,7 @@ export default Route.extend(AuthenticatedRouteMixin, {
{ groupName: 'Other Groups', options: subscriptionGroups}
];
- let subscriptionYaml = yamlAlertSettings;
+ let subscriptionYaml = defaultSubscriptionYaml;
let groupName = createGroup;
let subscriptionGroupId = createGroup.id;
diff --git a/thirdeye/thirdeye-frontend/app/pods/self-serve/create-alert/route.js b/thirdeye/thirdeye-frontend/app/pods/self-serve/create-alert/route.js
index 24d3a02..ad35170 100644
--- a/thirdeye/thirdeye-frontend/app/pods/self-serve/create-alert/route.js
+++ b/thirdeye/thirdeye-frontend/app/pods/self-serve/create-alert/route.js
@@ -9,7 +9,7 @@ import Route from '@ember/routing/route';
import { selfServeApiOnboard } from 'thirdeye-frontend/utils/api/self-serve';
import { postProps, checkStatus } from 'thirdeye-frontend/utils/utils';
import { inject as service } from '@ember/service';
-import { yamlAlertSettings } from 'thirdeye-frontend/utils/constants';
+import { defaultSubscriptionYaml } from 'thirdeye-frontend/utils/yaml-tools';
const CREATE_GROUP_TEXT = 'Create a new subscription group';
@@ -39,7 +39,7 @@ export default Route.extend({
const createGroup = {
name: CREATE_GROUP_TEXT,
id: 'n/a',
- yaml: yamlAlertSettings
+ yaml: defaultSubscriptionYaml
};
const moddedArray = [createGroup];
const subscriptionGroups = this.get('store')
@@ -54,7 +54,7 @@ export default Route.extend({
};
});
const subscriptionGroupNamesDisplay = [...moddedArray, ...subscriptionGroups];
- let subscriptionYaml = yamlAlertSettings;
+ let subscriptionYaml = defaultSubscriptionYaml;
let groupName = createGroup;
if (subscriptionGroupNamesDisplay && Array.isArray(subscriptionGroupNamesDisplay) && subscriptionGroupNamesDisplay.length > 0) {
const firstGroup = subscriptionGroupNamesDisplay[0];
diff --git a/thirdeye/thirdeye-frontend/app/styles/app.scss b/thirdeye/thirdeye-frontend/app/styles/app.scss
index 96ee0a6..1fdd6c9 100644
--- a/thirdeye/thirdeye-frontend/app/styles/app.scss
+++ b/thirdeye/thirdeye-frontend/app/styles/app.scss
@@ -66,7 +66,8 @@ body {
@import 'components/timeseries-chart';
@import 'components/range-pill-selectors';
@import 'components/shared/common-tabs';
-@import 'components/yaml-editor';
+@import 'components/detection-yaml';
+@import 'components/subscription-yaml';
// Pod Pages
@import 'ember-power-select/themes/bootstrap';
diff --git a/thirdeye/thirdeye-frontend/app/styles/components/yaml-editor.scss b/thirdeye/thirdeye-frontend/app/styles/components/detection-yaml.scss
similarity index 87%
copy from thirdeye/thirdeye-frontend/app/styles/components/yaml-editor.scss
copy to thirdeye/thirdeye-frontend/app/styles/components/detection-yaml.scss
index 9268e2e..9da2d9e 100644
--- a/thirdeye/thirdeye-frontend/app/styles/components/yaml-editor.scss
+++ b/thirdeye/thirdeye-frontend/app/styles/components/detection-yaml.scss
@@ -1,5 +1,5 @@
-.yaml-editor {
- .yaml-editor-msg {
+.detection-yaml {
+ .detection-yaml-msg {
color: #e75858;
&__icon {
diff --git a/thirdeye/thirdeye-frontend/app/styles/components/yaml-editor.scss b/thirdeye/thirdeye-frontend/app/styles/components/subscription-yaml.scss
similarity index 85%
rename from thirdeye/thirdeye-frontend/app/styles/components/yaml-editor.scss
rename to thirdeye/thirdeye-frontend/app/styles/components/subscription-yaml.scss
index 9268e2e..97e347f 100644
--- a/thirdeye/thirdeye-frontend/app/styles/components/yaml-editor.scss
+++ b/thirdeye/thirdeye-frontend/app/styles/components/subscription-yaml.scss
@@ -1,5 +1,5 @@
-.yaml-editor {
- .yaml-editor-msg {
+.subscription-yaml {
+ .subscription-yaml-msg {
color: #e75858;
&__icon {
diff --git a/thirdeye/thirdeye-frontend/app/utils/anomaly.js b/thirdeye/thirdeye-frontend/app/utils/anomaly.js
index 861dc7c..3b7a457 100644
--- a/thirdeye/thirdeye-frontend/app/utils/anomaly.js
+++ b/thirdeye/thirdeye-frontend/app/utils/anomaly.js
@@ -4,9 +4,9 @@ import _ from 'lodash';
import {
checkStatus,
postProps,
- postYamlProps,
getProps
} from 'thirdeye-frontend/utils/utils';
+import { postYamlProps } from 'thirdeye-frontend/utils/yaml-tools';
import fetch from 'fetch';
import {
anomalyApiUrls,
diff --git a/thirdeye/thirdeye-frontend/app/utils/constants.js b/thirdeye/thirdeye-frontend/app/utils/constants.js
index 4fd9836..261ac9b 100644
--- a/thirdeye/thirdeye-frontend/app/utils/constants.js
+++ b/thirdeye/thirdeye-frontend/app/utils/constants.js
@@ -5,85 +5,11 @@ export const deleteProps = {
credentials: 'include'
};
-export const yamlAlertProps = `# Below is a sample template. You may refer the documentation for more examples and update the fields accordingly.
-
-# Give a name for this anomaly detection pipeline (should be unique).
-detectionName: name_of_the_detection
-
-# Tell the alert recipients what it means if this alert is fired.
-description: If this alert fires then it means so-and-so and check so-and-so for irregularities
-
-# The metric you want to do anomaly detection on. You may type a few characters and look ahead (ctrl + space) to auto-fill.
-metric: metric_name
-
-# The dataset or UMP table name to which the metric belongs. Look ahead should auto populate this field.
-dataset: dataset_name
-
-rules: # Can configure multiple rules with "OR" relationship.
-- detection:
- - name: detection_rule_1
- type: ALGORITHM # Configure the detection type here. See doc for more details.
- params: # The parameters for this rule. Different rules have different params.
- configuration:
- bucketPeriod: P1D # Use PT1H for hourly and PT5M for minute level (ingraph metrics) data.
- pValueThreshold: 0.05 # Higher value means more sensitive to small changes.
- mlConfig: true # The machine learning auto config to select and maintain the configuration with the best performance.
- filter: # Filter out anomalies detected by rules to reduce noise.
- - name: filter_rule_1
- type: PERCENTAGE_CHANGE_FILTER
- params:
- pattern: UP_OR_DOWN # Other patterns: "UP","DOWN".
- threshold: 0.05 # Filter out all changes less than 5% compared to baseline.
-`;
-
-export const yamlAlertSettings = `# Below is a sample subscription group template. You may refer the documentation and update accordingly.
-
-# The name of the subscription group. You may choose an existing or a provide a new subscription group name
-subscriptionGroupName: test_subscription_group
-
-# Every alert in ThirdEye is attached to an application. Please specify the registered application name here. You may request for a new application by dropping an email to ask_thirdeye
-application: thirdeye-internal
-
-# The default notification type. See additional settings for details and exploring other notification types like dimension alerter.
-type: DEFAULT_ALERTER_PIPELINE
-
-# List of detection names that you want to subscribe. Copy-paste the detection name from the above anomaly detection config here.
-subscribedDetections:
- - name_of_the_detection_above
-
-# Configure how you want to be alerted. You can receive the standard ThirdEye email alert (recommended)
-# or for advanced critical use-cases setup Iris alert by referring to the documentation
-alertSchemes:
-- type: EMAIL
-recipients:
- to:
- - "me@company.com" # Specify alert recipient email address here
- - "me@company.com"
- cc:
- - "cc_email@company.com"
-fromAddress: thirdeye-dev@linkedin.com
-
-# The frequency at which you want to be notified. Typically you want to be notified immediately after
-# an anomaly is detected. The below cron runs every 5 minutes. Use online cronmaker to compute this.
-cron: "0 0/5 * 1/1 * ? *"
-
-# Enable or disable notification of alert
-active: true
-
-# The below links will appear in the email alerts. This will help alert recipients to quickly refer and act on.
-referenceLinks:
- "Oncall Runbook": "http://go/oncall"
- "Thirdeye FAQs": "http://go/thirdeyefaqs"
-
-`;
-
export const toastOptions = {
timeOut: 10000
};
export default {
deleteProps,
- yamlAlertProps,
- yamlAlertSettings,
toastOptions
};
diff --git a/thirdeye/thirdeye-frontend/app/utils/utils.js b/thirdeye/thirdeye-frontend/app/utils/utils.js
index f53aa08..127bcc1 100644
--- a/thirdeye/thirdeye-frontend/app/utils/utils.js
+++ b/thirdeye/thirdeye-frontend/app/utils/utils.js
@@ -2,6 +2,7 @@ import d3 from 'd3';
import { isNone } from '@ember/utils';
import moment from 'moment';
import { splitFilterFragment, toFilterMap, makeTime } from 'thirdeye-frontend/utils/rca-utils';
+import _ from 'lodash';
/**
* The Promise returned from fetch() won't reject on HTTP error status even if the response is an HTTP 404 or 500.
@@ -164,19 +165,6 @@ export function getProps() {
}
/**
- * Preps post object for Yaml payload
- * @param {string} text to post
- * @returns {Object}
- */
-export function postYamlProps(postData) {
- return {
- method: 'post',
- body: postData,
- headers: { 'content-type': 'text/plain' }
- };
-}
-
-/**
* Format conversion helper
* @param {String} dateStr - date to convert
*/
@@ -185,6 +173,27 @@ export function toIso(dateStr) {
}
/**
+ * Replace all Infinity and NaN with value from second time series
+ * @param {Array} series1 - time series to modify
+ * @param {Array} series2 - time series to get replacement values from
+ */
+export function replaceNonFiniteWithCurrent(series1, series2) {
+ if (_.isEmpty(series2)) {
+ return stripNonFiniteValues(series1);
+ }
+ for (let i = 0; i < series1.length; i++) {
+ if (!isFinite(series1[i]) || !series1[i]) {
+ let newValue = null;
+ if (i < series2.length) {
+ newValue = isFinite(series2[i]) ? series2[i] : null;
+ }
+ series1[i] = newValue;
+ }
+ }
+ return series1;
+}
+
+/**
* Replace all Infinity and NaN with null in array of numbers
* @param {Array} timeSeries - time series to modify
*/
@@ -194,40 +203,6 @@ export function stripNonFiniteValues(timeSeries) {
});
}
-/**
- * The yaml filters formatter. Convert filters in the yaml file in to a legacy filters string
- * For example, filters = {
- * "country": ["us", "cn"],
- * "browser": ["chrome"]
- * }
- * will be convert into "country=us;country=cn;browser=chrome"
- *
- * @method _formatYamlFilter
- * @param {Map} filters multimap of filters
- * @return {String} - formatted filters string
- */
-export function formatYamlFilter(filters) {
- if (filters){
- const filterStrings = [];
- Object.keys(filters).forEach(
- function(filterKey) {
- const filter = filters[filterKey];
- if (filter && Array.isArray(filter)) {
- filter.forEach(
- function (filterValue) {
- filterStrings.push(filterKey + '=' + filterValue);
- }
- );
- } else {
- filterStrings.push(filterKey + '=' + filter);
- }
- }
- );
- return filterStrings.join(';');
- }
- return '';
-}
-
export default {
checkStatus,
humanizeFloat,
@@ -237,8 +212,7 @@ export default {
parseProps,
postProps,
toIso,
+ replaceNonFiniteWithCurrent,
stripNonFiniteValues,
- postYamlProps,
- formatYamlFilter,
getProps
};
diff --git a/thirdeye/thirdeye-frontend/app/utils/yaml-tools.js b/thirdeye/thirdeye-frontend/app/utils/yaml-tools.js
new file mode 100644
index 0000000..f78549f
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/app/utils/yaml-tools.js
@@ -0,0 +1,347 @@
+import yamljs from 'yamljs';
+import jsyaml from 'js-yaml';
+
+export const defaultDetectionYaml = `# Below is a sample template. You may refer the documentation for more examples and update the fields accordingly.
+# Give a name for this anomaly detection pipeline (should be unique).
+detectionName: name_of_the_detection
+# Tell the alert recipients what it means if this alert is fired.
+description: If this alert fires then it means so-and-so and check so-and-so for irregularities
+# The metric you want to do anomaly detection on. You may type a few characters and look ahead (ctrl + space) to auto-fill.
+metric: metric_name
+# The dataset or UMP table name to which the metric belongs. Look ahead should auto populate this field.
+dataset: dataset_name
+rules: # Can configure multiple rules with "OR" relationship.
+- detection:
+ - name: detection_rule_1
+ type: ALGORITHM # Configure the detection type here. See doc for more details.
+ params: # The parameters for this rule. Different rules have different params.
+ configuration:
+ bucketPeriod: P1D # Use PT1H for hourly and PT5M for minute level (ingraph metrics) data.
+ pValueThreshold: 0.05 # Higher value means more sensitive to small changes.
+ mlConfig: true # The machine learning auto config to select and maintain the configuration with the best performance.
+ filter: # Filter out anomalies detected by rules to reduce noise.
+ - name: filter_rule_1
+ type: PERCENTAGE_CHANGE_FILTER
+ params:
+ pattern: UP_OR_DOWN # Other patterns: "UP","DOWN".
+ threshold: 0.05 # Filter out all changes less than 5% compared to baseline.
+`;
+
+export const defaultSubscriptionYaml = `# Below is a sample subscription group template. You may refer the documentation and update accordingly.
+# The name of the subscription group. You may choose an existing or a provide a new subscription group name
+subscriptionGroupName: test_subscription_group
+# Every alert in ThirdEye is attached to an application. Please specify the registered application name here. You may request for a new application by dropping an email to ask_thirdeye
+application: thirdeye-internal
+# List of detection names that you want to subscribe. Copy-paste the detection name from the above anomaly detection config here.
+subscribedDetections:
+ - name_of_the_detection_above
+# Configure how you want to be alerted. You can receive the standard ThirdEye email alert (recommended)
+# or for advanced critical use-cases setup Iris alert by referring to the documentation
+alertSchemes:
+- type: EMAIL
+recipients:
+ to:
+ - "me@company.com" # Specify alert recipient email address here
+ - "me@company.com"
+ cc:
+ - "cc_email@company.com"
+fromAddress: thirdeye-dev@linkedin.com
+# Enable or disable notification of alert
+active: true
+# The below links will appear in the email alerts. This will help alert recipients to quickly refer and act on.
+referenceLinks:
+ "Oncall Runbook": "http://go/oncall"
+ "Thirdeye FAQs": "http://go/thirdeyefaqs"
+`;
+
+/**
+ * Recursive helper function for merging two javascript objects
+ * @method mergeContents
+ * @param {Object} OR {Array} fieldObject taken from field
+ * @param {Object} OR {Array} yamlObject piece of parsed yaml
+ */
+function mergeContents(fieldObject, yamlObject) {
+ let output = yamlObject; // default to just return original value
+ if (Array.isArray(fieldObject) && Array.isArray(yamlObject)) {
+ // for arrays, we keep old and add new
+ output = [...new Set([...fieldObject, ...yamlObject])];
+ } else if (typeof fieldObject === 'string' && typeof yamlObject === 'string') {
+ // for strings, we replace old with new
+ output = fieldObject;
+ } else if (typeof fieldObject === 'object' && typeof yamlObject === 'object') {
+ // if both are objects, match keys and recurse
+ output = {};
+ const fieldKeys = Object.keys(fieldObject);
+ const yamlKeys = yamlObject ? Object.keys(yamlObject) : [];
+ if (yamlKeys.length > 0) {
+ fieldKeys.forEach(key => {
+ if (yamlKeys.includes(key)) {
+ // merge yaml contents and passed in contents
+ output[key] = mergeContents(fieldObject[key], yamlObject[key]);
+ }
+ });
+ }
+ }
+ return output;
+}
+
+/**
+ * helper for parsing yaml with backup
+ * @method redundantParse
+ * @param {String} yamlString (the yaml string to parse)
+ */
+export function redundantParse(yamlString) {
+ let yamlAsObject = {};
+ try {
+ yamlAsObject = yamljs.parse(yamlString);
+ } catch(error) {
+ try {
+ // use jsyaml package to try parsing again, since yamljs doesn't parse some edge cases
+ yamlAsObject = jsyaml.safeLoad(yamlString);
+ } catch (error) {
+ throw new Error("yaml parsing error");
+ }
+ }
+ return yamlAsObject;
+}
+
+/**
+ * This function will update yaml according to form input arrays and strings from the user
+ * @method fieldsToYaml
+ * @param {Object} fields (assumes keys are same as target fields in yaml)
+ * @param {String} yamlString (the yaml string to be updated)
+ * @return {String} - updatedYaml
+ */
+export function fieldsToYaml(fields, yamlString) {
+ // parse yamlString to JSON
+ let yamlAsObject = {};
+ try {
+ yamlAsObject = redundantParse(yamlString);
+ }
+ catch(err) {
+ return null;
+ }
+ const fieldKeys = Object.keys(fields);
+ const yamlKeys = yamlAsObject ? Object.keys(yamlAsObject) : [];
+ if (yamlKeys.length > 0) {
+ yamlKeys.forEach(key => {
+ if (fieldKeys.includes(key) && fields[key]) {
+ // merge yaml contents and passed in contents, then map stringified version
+ yamlAsObject[key] = mergeContents(fields[key], yamlAsObject[key]);
+ }
+ });
+ }
+ // insert comments back into yaml.
+ yamlString = addBackComments(yamlAsObject, yamlString);
+ return yamlString;
+}
+
+/**
+ * Recursive helper function for getting values from yaml
+ * @method traverseYaml
+ * @param {String} field (field to look for in Yaml)
+ * @param {Object} yamlObject (the yaml object to search in)
+ * @param {String} type (the type of value expected)
+ */
+function traverseYaml(field, yamlObject, type) {
+ const keys = Object.keys(yamlObject);
+ let value = null;
+ for (let i = 0; i < keys.length; i++) {
+ if (keys[i] === field) {
+ value = (typeof yamlObject[keys[i]] === type) ? yamlObject[keys[i]] : null;
+ } else if (yamlObject[keys[i]] && typeof yamlObject[keys[i]] === 'object') {
+ value = traverseYaml(field, yamlObject[keys[i]]);
+ }
+ if (value) {
+ break;
+ }
+ }
+ return value;
+}
+
+
+
+/**
+ * This function will return the value of a given field in a yaml string if it exists
+ * @method getValueFromYaml
+ * @param {String} field (field to look for in Yaml)
+ * @param {String} yamlString (the yaml string to search in)
+ * @param {String} type (the type of value expected)
+ */
+export function getValueFromYaml(field, yamlString, type) {
+ // parse yamlString to JSON
+ let yamlAsObject = {};
+ try {
+ yamlAsObject = redundantParse(yamlString);
+ }
+ catch(err){
+ return null;
+ }
+ return yamlAsObject ? traverseYaml(field, yamlAsObject, type) : null;
+}
+
+/**
+ * This function will preserve comments and blank lines and do a "best effort" to keep in the correct place
+ * @method addBackComments
+ * @param {Object} fields (assumes keys are same as target fields in yaml)
+ * @param {String} yamlString (the yaml string to be updated)
+ * @return {String} - updatedYaml
+ */
+export function addBackComments(yamlAsObject, yamlString) {
+ // stringify new yaml
+ let newYaml;
+ let yamlKeys = Object.keys(yamlAsObject); // this should not change since we only update fields
+ try {
+ newYaml = yamljs.stringify(yamlAsObject, 20, 1);
+ }
+ catch(err) {
+ return null;
+ }
+ // split yamls into array of strings, line by line
+ let oldLines = yamlString.split('\n');
+ let newLines = newYaml.split('\n');
+ const mergedLines = [];
+ // iterate through all keys of yaml, so we can replace all lines allocated to a key
+ let newYamlIndex = 0;
+ let oldYamlIndex = 0; // this will point at the lines of old yaml
+ let keyIndex = 0; // this is for iterating through the keys
+ // iterate through split new yaml
+ while (newYamlIndex < newLines.length && oldYamlIndex < oldLines.length && keyIndex < yamlKeys.length) {
+ let nextKey = ((keyIndex + 1) < yamlKeys.length) ? keyIndex + 1 : keyIndex;
+ if (newLines[newYamlIndex].includes(`${yamlKeys[keyIndex]}:`)) {
+ let foundStart = false;
+ // loop through old yaml string and add comments and/or blank lines as needed
+ while (!foundStart && newYamlIndex < newLines.length && oldYamlIndex < oldLines.length) {
+ if (oldLines[oldYamlIndex].includes(`${yamlKeys[keyIndex]}:`) && oldLines[oldYamlIndex].charAt(0) !== '#') {
+ // start of same key
+ foundStart = true;
+ let foundEndOld = false;
+ let foundEndNew = false;
+ while (!foundEndOld && !foundEndNew && oldYamlIndex < oldLines.length && newYamlIndex < newLines.length) {
+ if (oldLines[oldYamlIndex].charAt(0) !== '#') {
+ const thisLine = oldLines[oldYamlIndex].split('#');
+ if (thisLine.length > 1) {
+ // there are comments, keep them
+ mergedLines.push(newLines[newYamlIndex] + ' #' + thisLine.slice(1).join('#'));
+ newYamlIndex++;
+ oldYamlIndex++;
+ } else {
+ // replace whole line
+ mergedLines.push(newLines[newYamlIndex]);
+ newYamlIndex++;
+ oldYamlIndex++;
+ }
+ } else {
+ // whole line is comment
+ mergedLines.push(oldLines[oldYamlIndex]);
+ oldYamlIndex++;
+ }
+ if (oldYamlIndex < oldLines.length && oldLines[oldYamlIndex].includes(`${yamlKeys[nextKey]}:`) && oldLines[oldYamlIndex].charAt(0) !== '#') {
+ foundEndOld = true;
+ }
+ if (newYamlIndex < newLines.length && newLines[newYamlIndex].includes(`${yamlKeys[nextKey]}:`)) {
+ foundEndNew = true;
+ }
+ }
+ // take care of remaining lines
+ if (!foundEndOld) {
+ while (!foundEndOld && oldYamlIndex < oldLines.length) {
+ if (oldLines[oldYamlIndex].charAt(0) === '#') {
+ // additional comment line, add it
+ mergedLines.push(oldLines[oldYamlIndex]);
+ oldYamlIndex++;
+ } else {
+ const thisLine = oldLines[oldYamlIndex].split('#');
+ if (thisLine.length > 1) {
+ mergedLines.push(['', thisLine.slice(1).join('#')].join('#'));
+ }
+ oldYamlIndex++;
+ }
+ if (oldYamlIndex < oldLines.length && oldLines[oldYamlIndex].includes(`${yamlKeys[nextKey]}:`) && oldLines[oldYamlIndex].charAt(0) !== '#') {
+ foundEndOld = true;
+ }
+ }
+ }
+ if (!foundEndNew) {
+ while (!foundEndNew && newYamlIndex < newLines.length) {
+ // for new, we just add the lines until the next key or end of array
+ mergedLines.push(newLines[newYamlIndex]);
+ newYamlIndex++;
+ if (newYamlIndex < newLines.length && newLines[newYamlIndex].includes(`${yamlKeys[nextKey]}:`)) {
+ foundEndNew = true;
+ }
+ }
+ }
+ } else {
+ // blank line or comment - add to mergedLines
+ mergedLines.push(oldLines[oldYamlIndex]);
+ oldYamlIndex++;
+ }
+ }
+ keyIndex++;
+ } else {
+ // totally new line, insert to merged result
+ mergedLines.push(newLines[newYamlIndex]);
+ newYamlIndex++;
+ }
+ }
+ yamlString = mergedLines.join('\n');
+ return yamlString;
+}
+
+/**
+ * The yaml filters formatter. Convert filters in the yaml file in to a legacy filters string
+ * For example, filters = {
+ * "country": ["us", "cn"],
+ * "browser": ["chrome"]
+ * }
+ * will be convert into "country=us;country=cn;browser=chrome"
+ *
+ * @method formatYamlFilter
+ * @param {Map} filters multimap of filters
+ * @return {String} - formatted filters string
+ */
+export function formatYamlFilter(filters) {
+ if (filters){
+ const filterStrings = [];
+ Object.keys(filters).forEach(
+ function(filterKey) {
+ const filter = filters[filterKey];
+ if (filter && Array.isArray(filter)) {
+ filter.forEach(
+ function (filterValue) {
+ filterStrings.push(filterKey + '=' + filterValue);
+ }
+ );
+ } else {
+ filterStrings.push(filterKey + '=' + filter);
+ }
+ }
+ );
+ return filterStrings.join(';');
+ }
+ return '';
+}
+
+/**
+ * Preps post object for Yaml payload
+ * @param {string} text to post
+ * @returns {Object}
+ */
+export function postYamlProps(postData) {
+ return {
+ method: 'post',
+ body: postData,
+ headers: { 'content-type': 'text/plain' }
+ };
+}
+
+export default {
+ defaultDetectionYaml,
+ defaultSubscriptionYaml,
+ formatYamlFilter,
+ getValueFromYaml,
+ fieldsToYaml,
+ postYamlProps,
+ redundantParse
+};
diff --git a/thirdeye/thirdeye-frontend/tests/acceptance/self-serve-alert-tuning-test.js b/thirdeye/thirdeye-frontend/tests/acceptance/self-serve-alert-tuning-test.js
index 02377e7..1220632 100644
--- a/thirdeye/thirdeye-frontend/tests/acceptance/self-serve-alert-tuning-test.js
+++ b/thirdeye/thirdeye-frontend/tests/acceptance/self-serve-alert-tuning-test.js
@@ -22,7 +22,7 @@ module('Acceptance | tune alert settings', function(hooks) {
'Subscription Group'
];
- test(`visiting alert page to test self-serve tuning flow`, async (assert) => {
+ test(`check whether alerts page shows correct number of alerts`, async (assert) => {
server.createList('alert', 5);
await visit(`/manage/alerts`);
diff --git a/thirdeye/thirdeye-frontend/tests/integration/pods/components/yaml-editor/component-test.js b/thirdeye/thirdeye-frontend/tests/integration/pods/components/detection-yaml/component-test.js
similarity index 65%
rename from thirdeye/thirdeye-frontend/tests/integration/pods/components/yaml-editor/component-test.js
rename to thirdeye/thirdeye-frontend/tests/integration/pods/components/detection-yaml/component-test.js
index 26e9492..04f76f2 100644
--- a/thirdeye/thirdeye-frontend/tests/integration/pods/components/yaml-editor/component-test.js
+++ b/thirdeye/thirdeye-frontend/tests/integration/pods/components/detection-yaml/component-test.js
@@ -1,17 +1,14 @@
/**
- * Integration tests for the yaml-editor component.
+ * Integration tests for the detection-yaml component.
* @property {number} alertId - the alert id
* @property {Array} subscriptionGroupNames - an array of objects containing subscription group information
* @property {boolean} isEditMode - to activate the edit mode
* @property {boolean} showSettings - to show the subscriber groups yaml editor
* @property {string} detectionYaml - the detection yaml to display
* @example
- {{yaml-editor
+ {{detection-yaml
alertId=1
- subscriptionGroupId=1
isEditMode=true
- showSettings=true
- subscriptionGroupNames=subscriptionGroupNames
detectionYaml=detectionYaml
}}
* @author hjackson
@@ -22,7 +19,7 @@ import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
-module('Integration | Component | yaml-editor', function(hooks) {
+module('Integration | Component | detection-yaml', function(hooks) {
setupRenderingTest(hooks);
const testText = 'default yaml';
@@ -34,11 +31,9 @@ module('Integration | Component | yaml-editor', function(hooks) {
});
await render(hbs`
- {{yaml-editor
- alertId=alertId
+ {{detection-yaml
isEditMode=true
- showSettings=true
- subscriptionGroupNames=subscriptionGroups
+ alertId=alertId
detectionYaml=detectionYaml
}}
`);
@@ -49,25 +44,11 @@ module('Integration | Component | yaml-editor', function(hooks) {
const defaultText = '# Below is a sample template. You may refer the documentation for more examples and update the fields accordingly.';
await render(hbs`
- {{yaml-editor
+ {{detection-yaml
isEditMode=false
- showSettings=true
}}
`);
assert.ok(this.$('.ace_line')[0].children[0].textContent === defaultText);
});
-
- test(`displays default yaml file of subscription group in create mode`, async function(assert) {
-
- const defaultText = '# Below is a sample subscription group template. You may refer the documentation and update accordingly.';
- await render(hbs`
- {{yaml-editor
- isEditMode=false
- showSettings=true
- }}
- `);
-
- assert.ok(this.$('.ace_line')[30].children[0].textContent === defaultText);
- });
});
diff --git a/thirdeye/thirdeye-frontend/tests/integration/pods/components/subscription-yaml/component-test.js b/thirdeye/thirdeye-frontend/tests/integration/pods/components/subscription-yaml/component-test.js
new file mode 100644
index 0000000..ebe4a20
--- /dev/null
+++ b/thirdeye/thirdeye-frontend/tests/integration/pods/components/subscription-yaml/component-test.js
@@ -0,0 +1,57 @@
+/**
+ * Integration tests for the subscription-yaml component.
+ * @property {Array} subscriptionGroupNames - an array of objects containing subscription group information
+ * @property {boolean} isEditMode - to activate the edit mode
+ * @example
+ {{subscription-yaml
+ isEditMode=true
+ subscriptionYaml=subscriptionYaml
+ subscriptionMsg=""
+ subscriptionGroupNamesDisplay=subscriptionGroupNamesDisplay
+ groupName=groupName
+ createGroup=createGroup
+ }}
+ * @author hjackson
+ */
+
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+
+module('Integration | Component | subscription-yaml', function(hooks) {
+ setupRenderingTest(hooks);
+ const testText = 'default yaml';
+
+ test(`displays yaml of subscription group in edit mode`, async function(assert) {
+ this.setProperties({
+ subscriptionGroups: [],
+ subscriptionYaml: testText
+ });
+
+ await render(hbs`
+ {{subscription-yaml
+ isEditMode=true
+ subscriptionYaml=subscriptionYaml
+ subscriptionMsg=""
+ subscriptionGroupNamesDisplay=subscriptionGroupNamesDisplay
+ groupName=groupName
+ createGroup=createGroup
+ }}
+ `);
+ assert.ok(this.$('.ace_line')[0].innerText === testText);
+ });
+
+ test(`displays default subscription group yaml in create mode`, async function(assert) {
+
+ const defaultText = '# Below is a sample subscription group template. You may refer the documentation and update accordingly.';
+ await render(hbs`
+ {{subscription-yaml
+ isEditMode=false
+ showSettings=true
+ }}
+ `);
+
+ assert.ok(this.$('.ace_line')[0].children[0].textContent === defaultText);
+ });
+});
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org