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 61b2a07 [TE] frontend - harleyjj/aiavailability - implements AI availability table UI (#4510) 61b2a07 is described below commit 61b2a0715cd6d3be26691e78611310072885c181 Author: Harley Jackson <hjack...@linkedin.com> AuthorDate: Fri Aug 9 11:23:13 2019 -0700 [TE] frontend - harleyjj/aiavailability - implements AI availability table UI (#4510) --- .../app/pods/aiavailability/controller.js | 102 +++++++++++ .../app/pods/aiavailability/route.js | 186 +++++++++++++++++++++ .../app/pods/aiavailability/template.hbs | 60 +++++++ thirdeye/thirdeye-frontend/app/router.js | 2 + .../styles/components/range-pill-selectors.scss | 4 + thirdeye/thirdeye-frontend/app/utils/anomaly.js | 16 +- .../thirdeye-frontend/app/utils/api/anomaly.js | 13 +- thirdeye/thirdeye-frontend/app/utils/utils.js | 5 +- 8 files changed, 382 insertions(+), 6 deletions(-) diff --git a/thirdeye/thirdeye-frontend/app/pods/aiavailability/controller.js b/thirdeye/thirdeye-frontend/app/pods/aiavailability/controller.js new file mode 100644 index 0000000..e5a8841 --- /dev/null +++ b/thirdeye/thirdeye-frontend/app/pods/aiavailability/controller.js @@ -0,0 +1,102 @@ +import _ from 'lodash'; +import moment from 'moment'; +import { computed, set } from '@ember/object'; +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import { toIso } from 'thirdeye-frontend/utils/utils'; + +const DATE_FORMAT = 'MMM DD, YYYY'; + +export default Controller.extend({ + + /** + * Make duration service accessible + */ + durationCache: service('services/duration'), + + formattedTime: computed( + 'startDate', + 'endDate', + function() { + const { + startDate, + endDate + } = this.getProperties('startDate', 'endDate'); + const times = {}; + times.start = `${moment(toIso(startDate)).format(DATE_FORMAT)}`; + times.end = `${moment(toIso(endDate)).format(DATE_FORMAT)}`; + return times; + } + ), + + /** + * List of anomalies to render as rows + * @type {Array} + */ + rows: computed( + 'tableData', + 'tableHeaders', + 'selectedSortMode', + function() { + const { + tableData, + selectedSortMode + } = this.getProperties('tableData', 'selectedSortMode'); + let allRows = tableData; + if (selectedSortMode) { + let [ sortHeader, sortDir ] = selectedSortMode.split(':'); + + if (sortDir === 'up') { + allRows = _.sortBy(allRows, sortHeader); + } else { + allRows = _.sortBy(allRows, sortHeader).reverse(); + } + } + + return allRows; + } + ), + + actions: { + + /** + * 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 }); + }, + + /** + * Handle sorting for each sortable table column + * @param {String} sortKey - stringified start date + */ + toggleSortDirection(sortKey) { + const tableHeaders = this.get('tableHeaders'); + const propName = 'sortColumn' + sortKey.capitalize() + 'Up' || ''; + let direction = ''; + this.toggleProperty(propName); + direction = this.get(propName) ? 'up' : 'down'; + for (let i = 0; i < tableHeaders.length; i++) { + if (tableHeaders[i].text === sortKey) { + set(tableHeaders[i], 'sort', direction); + break; + } + } + this.set('selectedSortMode', `${sortKey}:${direction}`); + } + } +}); diff --git a/thirdeye/thirdeye-frontend/app/pods/aiavailability/route.js b/thirdeye/thirdeye-frontend/app/pods/aiavailability/route.js new file mode 100644 index 0000000..cefbbba --- /dev/null +++ b/thirdeye/thirdeye-frontend/app/pods/aiavailability/route.js @@ -0,0 +1,186 @@ +/** + * Handles the 'aiavailability' route. + * @module aiavailability/route + * @exports aiavailability model + */ + +import moment from 'moment'; +import Route from '@ember/routing/route'; +import { + hash +} from 'rsvp'; +import { + buildDateEod +} from 'thirdeye-frontend/utils/utils'; +import { setUpTimeRangeOptions } from 'thirdeye-frontend/utils/manage-alert-utils'; +import { getAiAvailability } from 'thirdeye-frontend/utils/anomaly'; +import { inject as service } from '@ember/service'; + +/** + * Time range-related constants + */ +const displayDateFormat = 'YYYY-MM-DD HH:mm'; +const defaultDurationObj = { + duration: '1w', + startDate: buildDateEod(1, 'week').valueOf(), + endDate: moment().startOf('day').valueOf() +}; + +/** + * Setup for query param behavior + */ +const queryParamsConfig = { + refreshModel: true, + replace: true +}; + +const detectionConfigId = '116702779'; // detectionConfigId for getting table data + +export default Route.extend({ + session: service(), + queryParams: { + duration: queryParamsConfig, + startDate: queryParamsConfig, + endDate: queryParamsConfig + }, + + beforeModel(transition) { + const { duration, startDate } = transition.queryParams; + // Default to 1 week of anomalies to show if no dates present in query params + if (!duration || !startDate) { + this.transitionTo({ queryParams: defaultDurationObj }); + } + }, + + /** + * Model hook for the create alert route. + * @method model + * @return {Object} + */ + model(transition) { + // Get duration data + const { + duration, + startDate, + endDate + } = transition; + + return hash({ + // Fetch table data + tableData: getAiAvailability(detectionConfigId, startDate, endDate), + duration, + startDate, + endDate + }); + }, + + afterModel(model) { + this._super(model); + + let tableData = (Array.isArray(model.tableData) && model.tableData.length > 0) ? model.tableData : [{ 'No Data Returned' : 'N/A'}]; + let tableHeaders = [...Object.keys(tableData[0]), 'link to RCA']; + tableHeaders = tableHeaders.map(header => { + return { + text: header, + sort: 'down' + }; + }); + // augment response for direct mapping of keys to headers and fields to table cell + const newTableData = []; + tableData.forEach(flow => { + const anomaliesArray = Object.keys(flow.comment); + if (anomaliesArray.length > 0) { + anomaliesArray.forEach(anomalyId => { + const row = flow; + row['link to RCA'] = anomalyId; + row.comment = row.comment.anomalyId; + // build array of strings to put on DOM + row.columns = []; + tableHeaders.forEach(header => { + const cellValue = row[header.text]; + if (header.text === 'link to RCA') { + const domString = `<a href="https://thirdeye.corp.linkedin.com/app/#/rootcause?anomalyId=${cellValue}">Comment of the anomaly</a>`; + row.columns.push(domString); + } else if (header.text === 'url') { + const domString = `<a href="${cellValue}">Flow Link</a>`; + row.columns.push(domString); + } else { + row.columns.push(cellValue); + } + }); + newTableData.push(row); + }); + } else { + const row = flow; + row['link to RCA'] = null; + row.comment = null; + // build array of strings to put on DOM + row.columns = []; + tableHeaders.forEach(header => { + const cellValue = row[header.text]; + if (header.text === 'link to RCA') { + const domString = 'N/A'; + row.columns.push(domString); + } else if (header.text === 'url') { + const domString = `<a href="${cellValue}">Flow Link</a>`; + row.columns.push(domString); + } else { + row.columns.push(cellValue); + } + }); + newTableData.push(row); + } + }); + Object.assign(model, { tableData: newTableData, tableHeaders }); + }, + + setupController(controller, model) { + this._super(controller, model); + + const { + startDate, + endDate, + duration, + tableData, + tableHeaders + } = model; + + + // Display loading banner + controller.setProperties({ + timeRangeOptions: setUpTimeRangeOptions(['1w', '1m'], duration), + activeRangeStart: moment(Number(startDate)).format(displayDateFormat), + activeRangeEnd: moment(Number(endDate)).format(displayDateFormat), + uiDateFormat: "MMM D, YYYY hh:mm a", + timePickerIncrement: 5, + // isDataLoading: true, + tableData, + tableHeaders + }); + }, + + 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() { + return true; + }, + + /** + * Refresh route's model. + * @method refreshModel + * @return {undefined} + */ + refreshModel() { + this.refresh(); + } + } +}); diff --git a/thirdeye/thirdeye-frontend/app/pods/aiavailability/template.hbs b/thirdeye/thirdeye-frontend/app/pods/aiavailability/template.hbs new file mode 100644 index 0000000..166b39f --- /dev/null +++ b/thirdeye/thirdeye-frontend/app/pods/aiavailability/template.hbs @@ -0,0 +1,60 @@ +{{#if isDataLoading}} + <div class="te-alert-page-pending"> + <img src="{{rootURL}}assets/images/te-alert-{{if isDataLoadingError "error" "pending"}}.png" class="te-alert-page-pending__image {{if isDataLoadingError "te-alert-page-pending__image--error"}}" alt="alertData.Setup is Processing"> + <h2 class="te-alert-page-pending__title">{{if isDataLoadingError "Oops, something went wrong." "Aggregating anomaly performance data..."}}</h2> + {{#if (not isDataLoadingError)}}<div class="te-alert-page-pending__loader"></div>{{/if}} + <p class="te-alert-page-pending__text"> + {{#if isDataLoadingError}} + The AI availability data cannot be retrieved.<br/>{{errMsg}} + {{else}} + Fetching all AI availability data for all alerts may take up to a minute. <br/>Hang in there! + {{/if}} + </p> + </div> +{{else}} + <div class="manage-alert-tune"> + + <div class="range-pill-selectors__ai"> + {{!-- Date range selector --}} + {{range-pill-selectors + title="Select Time Range" + uiDateFormat=uiDateFormat + activeRangeEnd=activeRangeEnd + activeRangeStart=activeRangeStart + timeRangeOptions=timeRangeOptions + timePickerIncrement=timePickerIncrement + selectAction=(action "onRangeSelection") + }} + </div> + + <div class="te-content-block"> + <h4 class="te-alert-page__subtitle"><strong>AI Availability Data</strong> from <strong>{{formattedTime.start}}</strong> to <strong>{{formattedTime.end}}</strong></h4> + + {{!-- Alert anomaly table --}} + <table class="te-anomaly-table te-anomaly-table--performance"> + <thead> + <tr class="te-anomaly-table__row te-anomaly-table__head"> + {{#each tableHeaders as |header|}} + <th class="te-anomaly-table__cell-head"> + <a class="te-anomaly-table__cell-link" {{action "toggleSortDirection" header.text}}> + {{header.text}} + <i class="te-anomaly-table__icon te-anomaly-table__icon--small glyphicon glyphicon-menu-{{header.sort}}"></i> + </a> + </th> + {{/each}} + </tr> + </thead> + + <tbody> + {{#each rows as |row|}} + <tr class="te-anomaly-table__row"> + {{#each row.columns as |column|}} + <td class="te-anomaly-table__cell">{{{column}}}</td> + {{/each}} + </tr> + {{/each}} + </tbody> + </table> + </div> + </div> +{{/if}} diff --git a/thirdeye/thirdeye-frontend/app/router.js b/thirdeye/thirdeye-frontend/app/router.js index b62e2be..b222611 100644 --- a/thirdeye/thirdeye-frontend/app/router.js +++ b/thirdeye/thirdeye-frontend/app/router.js @@ -18,6 +18,8 @@ Router.map(function() { this.route('share-dashboard'); }); + this.route('aiavailability'); + this.route('anomalies'); this.route('manage', function() { diff --git a/thirdeye/thirdeye-frontend/app/styles/components/range-pill-selectors.scss b/thirdeye/thirdeye-frontend/app/styles/components/range-pill-selectors.scss index 93235a5..30453b8 100644 --- a/thirdeye/thirdeye-frontend/app/styles/components/range-pill-selectors.scss +++ b/thirdeye/thirdeye-frontend/app/styles/components/range-pill-selectors.scss @@ -62,4 +62,8 @@ color: app-shade(black, 7); display: inline-block; } + + &__ai { + margin-left: 25px; + } } diff --git a/thirdeye/thirdeye-frontend/app/utils/anomaly.js b/thirdeye/thirdeye-frontend/app/utils/anomaly.js index 5d1156b..5b50891 100644 --- a/thirdeye/thirdeye-frontend/app/utils/anomaly.js +++ b/thirdeye/thirdeye-frontend/app/utils/anomaly.js @@ -14,7 +14,8 @@ import { getAnomaliesByAlertIdUrl, getAnomalyFiltersByTimeRangeUrl, getAnomalyFiltersByAnomalyIdUrl, - getBoundsUrl + getBoundsUrl, + getAiAvailabilityUrl } from 'thirdeye-frontend/utils/api/anomaly'; /** @@ -125,6 +126,19 @@ export function getBounds(detectionId, startTime, endTime) { } /** + * Get table data for AI Availability + * @method getAiAvailability + * @param {Number} detectionConfigId - the config id for the table data's alert + * @param {Number} startDate - start time of analysis range + * @param {Number} endDate - end time of analysis range + * @return {Ember.RSVP.Promise} + */ +export function getAiAvailability(detectionConfigId, startDate, endDate) { + const url = getAiAvailabilityUrl(startDate, endDate); + return fetch(url).then(checkStatus); +} + +/** * Get anomalies for a given detection id over a specified time range * @method getAnomaliesByAlertId * @param {Number} alertId - the alert id aka detection config id diff --git a/thirdeye/thirdeye-frontend/app/utils/api/anomaly.js b/thirdeye/thirdeye-frontend/app/utils/api/anomaly.js index 9dfd386..fc11e39 100644 --- a/thirdeye/thirdeye-frontend/app/utils/api/anomaly.js +++ b/thirdeye/thirdeye-frontend/app/utils/api/anomaly.js @@ -65,13 +65,24 @@ export function getAnomalyFiltersByAnomalyIdUrl(startTime, endTime, anomalyIds) return `/anomalies/search/anomalyIds/${startTime}/${endTime}/1?anomalyIds=${encodeURIComponent(anomalyIds)}`; } +/** + * Returns the url for getting ai availability table + * @param {Number} startDate - beginning of time range of interest + * @param {Number} endDate - end of time range of interest + * @example getAiAvailabilityUrl(1, 1508472700000, 1508472800000) // yields => /thirdeye/table?detectionConfigId=1&start=1508472700000&end=1508472800000 + */ +export function getAiAvailabilityUrl(startDate, endDate) { + return `/thirdeye/table?metricIds=128856623,128856625&start=${startDate}&end=${endDate}&dimensionKeys=grid,flow,project,owner,managers,sla,url`; +} + export const anomalyApiUrls = { getAnomalyDataUrl, getAnomaliesForYamlPreviewUrl, getAnomaliesByAlertIdUrl, getAnomalyFiltersByTimeRangeUrl, getAnomalyFiltersByAnomalyIdUrl, - getBoundsUrl + getBoundsUrl, + getAiAvailabilityUrl }; export default { diff --git a/thirdeye/thirdeye-frontend/app/utils/utils.js b/thirdeye/thirdeye-frontend/app/utils/utils.js index 6515ebe..f53aa08 100644 --- a/thirdeye/thirdeye-frontend/app/utils/utils.js +++ b/thirdeye/thirdeye-frontend/app/utils/utils.js @@ -103,10 +103,7 @@ export function humanizeScore(f) { * Helps with shorthand for repetitive date generation */ export function buildDateEod(unit, type) { - if (unit === 1) { - return makeTime().startOf('day'); - } - return makeTime().subtract(unit-1, type).startOf('day'); + return makeTime().subtract(unit, type).startOf('day'); } /** --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@pinot.apache.org For additional commands, e-mail: commits-h...@pinot.apache.org