From 75de15bb8f5ad2a45dc3ad7f55ef706102e744de Mon Sep 17 00:00:00 2001 From: mgrauer Date: Tue, 8 Mar 2016 17:02:33 +0000 Subject: [PATCH 1/2] UI for tracker aggregate metrics notified users --- .../controllers/ProducerController.php | 2 +- .../css/producer/producer.aggregatemetric.css | 2 +- .../js/producer/producer.aggregateMetric.js | 595 +++++++++++++----- .../producer/producer.aggregatemetric.scss | 123 +++- .../views/producer/aggregatemetric.phtml | 89 ++- 5 files changed, 604 insertions(+), 207 deletions(-) diff --git a/modules/tracker/controllers/ProducerController.php b/modules/tracker/controllers/ProducerController.php index 53e902b60..cc1bbec95 100644 --- a/modules/tracker/controllers/ProducerController.php +++ b/modules/tracker/controllers/ProducerController.php @@ -277,7 +277,7 @@ public function aggregatemetricAction() /** @var array $aggregateMetricSpecs */ $aggregateMetricSpecs = $this->Tracker_AggregateMetricSpec->getAggregateMetricSpecsForProducer($producerDao); $this->view->producer = $producerDao; - $this->view->aggregateMetricSpecs = $aggregateMetricSpecs; $this->view->distinctTrendNames = $distinctTrendNames; + $this->view->trackerJson = json_encode(array('aggregateMetricSpecs' => $aggregateMetricSpecs)); } } diff --git a/modules/tracker/public/css/producer/producer.aggregatemetric.css b/modules/tracker/public/css/producer/producer.aggregatemetric.css index dabf6b36e..e8da582a0 100644 --- a/modules/tracker/public/css/producer/producer.aggregatemetric.css +++ b/modules/tracker/public/css/producer/producer.aggregatemetric.css @@ -1 +1 @@ -a.aggregateMetricSpecAction{color:#56758b}a.aggregateMetricSpecAction:hover{background-color:#e5e5e5;cursor:pointer}div#addAggregateMetricSpec{margin-top:10px}div#aggregateMetricSpecList{margin-top:10px}div.aggregateMetricSectionTitle{border-top:1px solid #d7d7d7;color:#555;font-size:13px;margin-top:15px;margin-bottom:10px;padding-top:10px;text-align:center}div.resultingMetricText{color:#555;font-size:12px;margin-top:10px;text-align:center}div#aggregateMetricSpecSaveState{margin-top:20px}div#aggregateMetricSpecSaveState input{background:#f6f9fe;border:1px solid #808080;color:#2e6e9e;cursor:pointer;float:right;margin-left:10px}div#aggregateMetricSpecSaveState input:disabled{color:#c0c0c0;cursor:not-allowed}label#aggregateMetricSpecValidationError{color:crimson}img#aggregateMetricSpecSaveLoading{float:left;margin-right:15px}img#aggregateMetricSpecDeleteLoading{float:right;margin-right:40px}table#aggreagteMetricSpecListTable{table-layout:fixed;border:2px solid #ddd;width:100%}table#aggreagteMetricSpecListTable th{border-bottom:solid 1px black}table#aggreagteMetricSpecListTable th:first-of-type{width:70%;border-right:dotted 1px black}table#aggreagteMetricSpecListTable td.specName{border-right:dotted 1px black}table#aggreagteMetricSpecListTable tr.odd{background-color:#f4f4f4}table#aggreagteMetricSpecListTable span.actionsList{display:flex;justify-content:space-around} +a.aggregateMetricSpecAction{color:#56758b}a.aggregateMetricSpecAction:hover{background-color:#e5e5e5;cursor:pointer}a.alertedUsersAction{color:#56758b}a.alertedUsersAction:hover{background-color:#e5e5e5;cursor:pointer}div#addAggregateMetricSpec{margin-top:10px}div#aggregateMetricSpecList{margin-top:10px}div.aggregateMetricSpecCreate{border-top:2px solid darkblue !important}div.aggregateMetricSpecEdit{border-top:2px solid darkblue !important}div#aggregateMetricUserAlerts{border-top:2px solid darkblue;color:#555;font-size:13px;margin-top:15px;margin-bottom:10px;padding-top:10px;text-align:center}div#aggregateMetricUserAlerts div#aggregateMetricUserAlertsThreshold{margin-top:15px;margin-bottom:10px}div#aggregateMetricUserAlerts div#aggregateMetricUserAlertsThreshold table#aggregateMetricUserAlertsThresholdDefinition{margin-top:10px}div#aggregateMetricUserAlerts div#aggregateMetricUserAlertsThreshold table#aggregateMetricUserAlertsThresholdDefinition td:first-of-type{text-align:left}div#aggregateMetricUserAlerts input#aggregateMetricUserAlertsSpec{margin-top:10px;width:100%}div#aggregateMetricUserAlerts div#addAggregateMetricSpecAlertUser{margin-top:15px;text-align:center}div#aggregateMetricUserAlerts div.alertUserSearch{margin-top:10px}div#aggregateMetricUserAlerts div.alertUserSearch input{width:100%}div.aggregateMetricSectionTitle{border-top:1px solid #d7d7d7;color:#555;font-size:13px;margin-top:15px;margin-bottom:10px;padding-top:10px;text-align:center}div.resultingMetricText{color:#555;font-size:12px;margin-top:10px;text-align:center}div#aggregateMetricSpecUserAlertsSaveState{margin-top:20px}div#aggregateMetricSpecUserAlertsSaveState input{background:#f6f9fe;border:1px solid #808080;color:#2e6e9e;cursor:pointer;float:right;margin-left:10px}div#aggregateMetricSpecSaveState{margin-top:20px}div#aggregateMetricSpecSaveState input{background:#f6f9fe;border:1px solid #808080;color:#2e6e9e;cursor:pointer;float:right;margin-left:10px}div#aggregateMetricSpecSaveState input:disabled{color:#c0c0c0;cursor:not-allowed}label#aggregateMetricSpecValidationError{color:crimson}img#aggregateMetricSpecSaveLoading{float:left;margin-right:15px}img#aggregateMetricSpecDeleteLoading{float:right;margin-right:40px}img#aggregateMetricSpecAlertsLoading{float:left}table#aggregateMetricSpecAlertedUsers{table-layout:fixed;border:2px solid #ddd;width:100%}table#aggregateMetricSpecAlertedUsers th{border-bottom:solid 1px black;text-align:center}table#aggregateMetricSpecAlertedUsers th:first-of-type{width:70%;border-right:dotted 1px black}table#aggregateMetricSpecAlertedUsers tr.even{background-color:#f4f4f4}table#aggregateMetricSpecAlertedUsers td.userName{border-right:dotted 1px black;text-align:left}table#aggregateMetricSpecAlertedUsers tr.activeRow{border:2px solid darkblue;background-color:#c0d1fe}table#aggregateMetricSpecListTable{table-layout:fixed;border:2px solid #ddd;width:100%}table#aggregateMetricSpecListTable th{border-bottom:solid 1px black;text-align:center}table#aggregateMetricSpecListTable th:first-of-type{width:70%;border-right:dotted 1px black}table#aggregateMetricSpecListTable td.specName{border-right:dotted 1px black}table#aggregateMetricSpecListTable tr.even{background-color:#f4f4f4}table#aggregateMetricSpecListTable tr.activeRow{border:2px solid darkblue;background-color:#c0d1fe}table#aggregateMetricSpecListTable span.actionsList{display:flex;justify-content:space-around}table#aggregateMetricSpecListTable span.actionsList a:first-of-type{padding-left:5px} diff --git a/modules/tracker/public/js/producer/producer.aggregateMetric.js b/modules/tracker/public/js/producer/producer.aggregateMetric.js index 31eca41eb..ac0dd404c 100644 --- a/modules/tracker/public/js/producer/producer.aggregateMetric.js +++ b/modules/tracker/public/js/producer/producer.aggregateMetric.js @@ -8,16 +8,7 @@ var midas = midas || {}; $(document).ready(function () { 'use strict'; - function addClassesToSpecTableRows() { - $('#aggreagteMetricSpecListTable tbody tr').each(function (ind, elem) { - // Set the first (0th) row to be odd, 'even' and 'odd' are switched from their - // expected places. - $(this).removeClass('even odd').addClass(ind % 2 ? 'even' : 'odd'); - }); - } - - // Initialize the table rows. - addClassesToSpecTableRows(); + //:~ Server API interfaces /** * Interface to server side AggregateMetricSpec REST API. @@ -60,25 +51,19 @@ $(document).ready(function () { } /** - * Displays the loading image, updates the branch list based on - * the current metric name, then hides the loading image. - * @param successCallback callback on success, not passed any value + * Additional wrapper around ajaxWebApi.ajax to provide some defaults. + * @param string jsonMethod the midas json method + * @param string httpMethod The HTTP method {'POST'|'PUT'|'DELETE'|'GET'} + * @param string args The args to pass to the API call + * @param function successCb success callback, passed the return value (Optional) */ - function updateBranchList(successCallback) { - var producerId = $('#producerId').val(); - var metricName = $('select#aggregateMetricSpecMetricName').val(); - $('img#aggregateMetricSpecSaveLoading').show(); + function callAjaxWebApi(jsonMethod, httpMethod, args, successCb) { ajaxWebApi.ajax({ - method: 'midas.tracker.branchesformetricname.list', - args: 'producerId=' + producerId + '&trendMetricName='+metricName, + method: jsonMethod, + type: httpMethod, + args: args, success: function (retVal) { - $('#aggregateMetricSpecBranch').find('option').remove(); - var branches = retVal.data; - $.each(branches, function (key, value) { - $('#aggregateMetricSpecBranch').append(''); - }); - $('img#aggregateMetricSpecSaveLoading').hide(); - if (successCallback) { successCallback(); } + if (successCb) { successCb(retVal); } }, error: function (retVal) { midas.createNotice(retVal.message, 3000, 'error'); @@ -88,42 +73,37 @@ $(document).ready(function () { }); } - /** Clear all spec inputs of any value. */ - function clearSpecInputs() { - $('.amsField').val(''); - $('#aggregateMetricSpecValidationError').text(''); - $('#aggregateMetricSpecSpec').val(''); - $('#aggregateMetricSpecMetricName option:disabled').attr('selected', 'selected'); - $('#aggregateMetricSpecComparison option:disabled').attr('selected', 'selected'); - // Remove branches, add the placeholder. - $('#aggregateMetricSpecBranch').find('option').remove(); - $('#aggregateMetricSpecBranch').append(''); - } + //:~ Utility functions - /** Update display of disabled composite spec input from individual elements. */ - function updateSpec() { - var metricName = $('#aggregateMetricSpecMetricName').val(); - var metric = $('#aggregateMetricSpecAggregateMetric').val(); - var param = $('#aggregateMetricSpecParam').val(); - $('#aggregateMetricSpecSpec').val(metric + "('" + metricName + "', " + param + ")"); + /** + * Helper function to assign correct row classes to spec table. + * @param string tableId the DOM id of the table. + */ + function addClassesToTableRows(tableId) { + $('#' + tableId + ' tbody tr').each(function (ind, elem) { + $(this).removeClass('even odd').addClass(ind % 2 ? 'odd' : 'even'); + }); } /** - * Parse individual elements from composite spec string. - * @param spec string containing the aggregeate metric spec + * Utility function to search if a given value exists as an option on the select + * with the passed in id. + * @param id the id of the select element + * @param value the value sought as an option of the select */ - function parseMetricSpec(spec) { - // Expected to be like: - // percentile('Optimal distance', 95) - var specParts = /(.*)\('(.*)',\s*(.*)\)/.exec(spec); - var specParts = { - 'metricName': specParts[2], - 'metric': specParts[1], - 'param': specParts[3] - }; - return specParts; + function selectValueFound(id, value) { + var found = false; + var options = document.getElementById(id).options; + for (var i = 0; i < options.length; i++) { + if (value === options[i].value) { + found = true; + } + } + return found; } + //:~ Aggregate Metric Specs main panel + /** * Reset and display the spec details panel, defaults to create mode. * @param {bool} true if edit mode, false (the default) for create mode @@ -131,7 +111,7 @@ $(document).ready(function () { function showDetailsPanel(editMode) { clearSpecInputs(); $('div#aggregateMetricSpecCreateEdit').show(); - $('img#aggregateMetricSpecSaveLoading').hide(); + $('#aggregateMetricSpecSaveLoading').hide(); $('div#aggregateMetricSpecSaveState input').prop('disabled', false); if (editMode) { $('.aggregateMetricSpecCreate').hide(); @@ -142,20 +122,45 @@ $(document).ready(function () { } } + /** Remove highlight from active row. */ + function unhighlightActiveRow() { + $('#aggregateMetricSpecListTable tbody tr').each(function (ind, elem) { + $(this).removeClass('activeRow'); + }); + } + + /** + * Highlight the row which houses the current action in the Aggregate Metric Spec table. + * @param element actionLink the anchor element that was clicked. + */ + function activateRow(actionLink) { + var aggregateMetricSpecId = actionLink.data('aggregate_metric_spec_id'); + $('#aggregateMetricSpecEditId').val(aggregateMetricSpecId); + unhighlightActiveRow(); + actionLink.closest('tr').addClass('activeRow'); + $('#aggregateMetricSpecCreateEdit').hide(); + $('#aggregateMetricUserAlerts').hide(); + } + /** * Add a row to the aggregateMetricSpec table for the passed in aggregateMetricSpec. - * @param aggregateMetricSpec object with AggregateMetricSpecDao key value pairs + * @param object aggregateMetricSpec with AggregateMetricSpecDao key value pairs */ function addToSpecTable(aggregateMetricSpec) { - var row = '' + aggregateMetricSpec.name + ''; - row += ''; - row += ' '; - row += ' Edit'; - row += ' '; - row += ' Delete'; + + function createActionLink(qtip, actionClass, imgPath, label) { + var actionLink = ' '; + actionLink += ' '+label+''; + return actionLink; + } + + var row = '' + aggregateMetricSpec.name + ''; + row += createActionLink('Edit aggregate metric spec', 'editAggregateMetricSpec', '/public/images/icons/edit.png', 'Edit'); + row += createActionLink('Edit user notifications', 'editAggregateMetricSpecNotificationUsers', '/public/images/icons/email_error.png', 'Alerts'); + row += createActionLink('Remove aggregate metric spec', 'removeAggregateMetricSpec', '/public/images/icons/close.png', 'Delete'); row += ''; - $('#aggreagteMetricSpecListTable tbody').append(row); - addClassesToSpecTableRows(); + $('#aggregateMetricSpecListTable tbody').append(row); + addClassesToTableRows('aggregateMetricSpecListTable'); } /** @@ -172,94 +177,7 @@ $(document).ready(function () { $('td:first', row).text(name); } - /** - * Return an object with of the current input values from the spec details panel, - * namespaced as 'aggregateMetricSpec' for those key-values directly - * from the AggregateMetricSpecDao and 'specInputs' for those input - * values contributing to the value of aggregateMetricSpec.spec. - * - * @return {object} with namespaces 'aggregateMetricSpec' and 'specInputs' - */ - function getSpecInputsValues() { - var specValues = { - 'aggregateMetricSpec': { - 'producer_id': $('#producerId').val(), - 'name': $('#aggregateMetricSpecName').val(), - 'description': $('#aggregateMetricSpecDescription').val(), - 'branch': $('#aggregateMetricSpecBranch').val(), - 'spec': $('#aggregateMetricSpecSpec').val(), - 'value': $('#aggregateMetricSpecValue').val(), - 'comparison': $('#aggregateMetricSpecComparison').val() - }, - 'specInputs': { - 'metricName': $('#aggregateMetricSpecMetricName').val(), - 'aggregateMetric': $('#aggregateMetricSpecAggregateMetric').val(), - 'param': $('#aggregateMetricSpecParam').val() - } - }; - return specValues; - } - - /** - * Utility function to search if a given value exists as an option on the select - * with the passed in id. - * @param id the id of the select element - * @param value the value sought as an option of the select - */ - function selectValueFound(id, value) { - var found = false; - var options = document.getElementById(id).options; - for (var i = 0; i < options.length; i++) { - if (value === options[i].value) { - found = true; - } - } - return found; - } - - /** - * Populate the input elements with the values from the passed in aggregateMetricSpec, - * including some validation in case the passed in spec was created out of data that is - * no longer valid, e.g. a branch name that no longer has scalars tied to the metric_name. - * @param aggregateMetricSpec object with AggregateMetricSpecDao key value pairs - */ - function populateSpecInputs(aggregateMetricSpec) { - $('#aggregateMetricSpecEditId').val(aggregateMetricSpec.aggregate_metric_spec_id); - $('#aggregateMetricSpecName').val(aggregateMetricSpec.name); - $('#aggregateMetricSpecDescription').val(aggregateMetricSpec.description); - $('#aggregateMetricSpecSpec').val(aggregateMetricSpec.spec); - // Skip value if it is 0 and comparison is empty, this was added as a DB - // default and wouldn't be allowed by the UI validation logic. - if (aggregateMetricSpec.value && - (aggregateMetricSpec.value != 0 || aggregateMetricSpec.comparison)) { - $('#aggregateMetricSpecValue').val(aggregateMetricSpec.value); - } - if (aggregateMetricSpec.comparison) { - $('#aggregateMetricSpecComparison').val(aggregateMetricSpec.comparison); - } - var specParts = parseMetricSpec(aggregateMetricSpec.spec); - $('#aggregateMetricSpecParam').val(specParts.param); - $('#aggregateMetricSpecAggregateMetric').val(specParts.metric); - var metricNameFound = selectValueFound('aggregateMetricSpecMetricName', specParts.metricName); - if (!metricNameFound) { - // Don't set the branch name because the metric name is invalid, - // and that determines the set of possible branches. - $('#aggregateMetricSpecValidationError').text("Loaded metric name '"+specParts.metricName+"' is invalid"); - $('img#aggregateMetricSpecSaveLoading').hide(); - } else { - $('#aggregateMetricSpecMetricName').val(specParts.metricName); - // Don't need to hide the loading image because updateBranchList will. - var successCallback = function () { - var branchNameFound = selectValueFound('aggregateMetricSpecBranch', aggregateMetricSpec.branch); - if (!branchNameFound) { - $('#aggregateMetricSpecValidationError').text("Loaded branch '"+aggregateMetricSpec.branch+"' is invalid"); - } else { - $('#aggregateMetricSpecBranch').val(aggregateMetricSpec.branch); - } - }; - updateBranchList(successCallback); - } - } + // Aggregate Metric Specs main panel handlers /** * Handler for Delete action, delete an Aggregate Metric Spec on the server @@ -267,13 +185,15 @@ $(document).ready(function () { * to a static parent as the links can be dynamically generated through * creation of new aggregate metric specs. */ - $('#aggreagteMetricSpecListTable').on('click', 'a.removeAggregateMetricSpec', function(){ - var aggregateMetricSpecId = $(event.target).data('aggregate_metric_spec_id'); - var row = $(event.target).closest('tr'); + $('#aggregateMetricSpecListTable').on('click', 'a.removeAggregateMetricSpec', function(){ + activateRow($(this)); + + var aggregateMetricSpecId = $(this).data('aggregate_metric_spec_id'); + var row = $(this).closest('tr'); var sCb = function (data) { row.remove(); $('#aggregateMetricSpecDeleteLoading').hide(); - addClassesToSpecTableRows(); + addClassesToTableRows('aggregateMetricSpecListTable'); }; $('#aggregateMetricSpecDeleteLoading').show(); aggregatemetricspecRest('DELETE', aggregateMetricSpecId, null, sCb, null, null); @@ -281,6 +201,7 @@ $(document).ready(function () { /** Handler for Add action, open the details panel in Create state. */ $('div#addAggregateMetricSpec').click(function () { + unhighlightActiveRow(); showDetailsPanel(); }); @@ -290,21 +211,53 @@ $(document).ready(function () { * to a static parent as the links can be dynamically generated through * creation of new aggregate metric specs. */ - $('#aggreagteMetricSpecListTable').on('click', 'a.editAggregateMetricSpec', function(){ - var aggregateMetricSpecId = $(event.target).data('aggregate_metric_spec_id'); + $('#aggregateMetricSpecListTable').on('click', 'a.editAggregateMetricSpec', function() { + activateRow($(this)); + var aggregateMetricSpecId = $(this).data('aggregate_metric_spec_id'); showDetailsPanel(true); - $('img#aggregateMetricSpecSaveLoading').show(); + $('#aggregateMetricSpecSaveLoading').show(); var successCallback = function (aggregateMetricSpec) { populateSpecInputs(aggregateMetricSpec); } aggregatemetricspecRest('GET', aggregateMetricSpecId, null, successCallback); }); - /** Handler for Cancel button, hide the details panel. */ - $('input#aggregateMetricSpecCancel').click(function () { - $('div#aggregateMetricSpecCreateEdit').hide(); + /** Handler for Alerts button, show the panel to edit the alerted users. */ + $('#aggregateMetricSpecListTable').on('click', 'a.editAggregateMetricSpecNotificationUsers', function() { + activateRow($(this)); + var amsName = $('td:first', $(this).closest('tr')).text(); + $('#aggregateMetricUserAlerts').show(); + var aggregateMetricSpecId = $(this).data('aggregate_metric_spec_id'); + $('#aggregateMetricUserAlertsSpecName').text(amsName); + $('#aggregateMetricSpecAlertValue').val(''); + $('#aggregateMetricSpecAlertComparison').val(''); + $('#aggregateMetricUserAlertsSpec').val(''); + $('#aggregateMetricSpecAlertedUsers').find('tr:gt(0)').remove(); + $('#aggregateMetricSpecAlertsLoading').show(); + $('#addAlertUserSearch').val('Start typing a name or email address...'); + $('#addAlertUserSearchValue').val('init'); + var successCallback = function (aggregateMetricSpec) { + $('#aggregateMetricUserAlertsSpecName').text(aggregateMetricSpec.name); + $('#aggregateMetricSpecAlertValue').val(aggregateMetricSpec.value); + $('#aggregateMetricSpecAlertComparison').val(aggregateMetricSpec.comparison); + $('#aggregateMetricUserAlertsSpec').val(aggregateMetricSpec.spec); + + var jsonMethod = 'midas.tracker.aggregatemetricspecnotifiedusers.list'; + var args = 'aggregateMetricSpecId=' + aggregateMetricSpecId; + callAjaxWebApi(jsonMethod, 'GET', args, function (retVal) { + for (var userInd = 0; userInd < retVal.data.length; userInd++) { + var user = retVal.data[userInd]; + addToAlertedUsersTable(user.firstname + ' ' + user.lastname, user.user_id); + } + addClassesToTableRows('aggregateMetricSpecAlertedUsers'); + $('#aggregateMetricSpecAlertsLoading').hide(); + }); + }; + aggregatemetricspecRest('GET', aggregateMetricSpecId, null, successCallback); }); + //:~ Spec Details Panel + /** * Save an aggregateMetricSpec, either as a new Dao or update an existing one, * depending on whether aggregateMetricSpecId is passed, will perform validation @@ -355,7 +308,7 @@ $(document).ready(function () { } // Save the AMS on the server. - $('img#aggregateMetricSpecSaveLoading').show(); + $('#aggregateMetricSpecSaveLoading').show(); var successCallback = function (aggregateMetricSpec) { if (aggregateMetricSpecId) { updateSpecInTable(aggregateMetricSpec); @@ -363,12 +316,164 @@ $(document).ready(function () { addToSpecTable(aggregateMetricSpec); } $('div#aggregateMetricSpecCreateEdit').hide(); + unhighlightActiveRow(); } var method = aggregateMetricSpecId ? 'PUT' : 'POST'; aggregateMetricSpecId = aggregateMetricSpecId ? aggregateMetricSpecId : null; aggregatemetricspecRest(method, aggregateMetricSpecId, specValues.aggregateMetricSpec, successCallback); } + /** + * Displays the loading image, updates the branch list based on + * the current metric name, then hides the loading image. + * @param successCallback callback on success, not passed any value + */ + function updateBranchList(successCallback) { + var producerId = $('#producerId').val(); + var metricName = $('select#aggregateMetricSpecMetricName').val(); + $('#aggregateMetricSpecSaveLoading').show(); + + var jsonMethod = 'midas.tracker.branchesformetricname.list'; + var args = 'producerId=' + producerId + '&trendMetricName='+metricName; + callAjaxWebApi(jsonMethod, 'GET', args, function (retVal) { + $('#aggregateMetricSpecBranch').find('option').remove(); + var branches = retVal.data; + $.each(branches, function (key, value) { + $('#aggregateMetricSpecBranch').append(''); + }); + $('#aggregateMetricSpecSaveLoading').hide(); + if (successCallback) { successCallback(); } + }); + } + + /** Clear all spec inputs of any value. */ + function clearSpecInputs() { + $('.amsField').val(''); + $('#aggregateMetricSpecValidationError').text(''); + $('#aggregateMetricSpecSpec').val(''); + $('#aggregateMetricSpecMetricName option:disabled').attr('selected', 'selected'); + $('#aggregateMetricSpecComparison option:disabled').attr('selected', 'selected'); + // Remove branches, add the placeholder. + $('#aggregateMetricSpecBranch').find('option').remove(); + $('#aggregateMetricSpecBranch').append(''); + } + + /** Update display of disabled composite spec input from individual elements. */ + function updateSpec() { + var metricName = $('#aggregateMetricSpecMetricName').val(); + var metric = $('#aggregateMetricSpecAggregateMetric').val(); + var param = $('#aggregateMetricSpecParam').val(); + $('#aggregateMetricSpecSpec').val(metric + "('" + metricName + "', " + param + ")"); + } + + /** + * Parse individual elements from composite spec string. + * @param spec string containing the aggregeate metric spec + */ + function parseMetricSpec(spec) { + // Expected to be like: + // percentile('Optimal distance', 95) + var specParts = /(.*)\('(.*)',\s*(.*)\)/.exec(spec); + var specParts = { + 'metricName': specParts[2], + 'metric': specParts[1], + 'param': specParts[3] + }; + return specParts; + } + + /** + * Return an object with the current input values from the spec details panel, + * namespaced as 'aggregateMetricSpec' for those key-values directly + * from the AggregateMetricSpecDao and 'specInputs' for those input + * values contributing to the value of aggregateMetricSpec.spec. + * + * @return {object} with namespaces 'aggregateMetricSpec' and 'specInputs' + */ + function getSpecInputsValues() { + var specValues = { + 'aggregateMetricSpec': { + 'producer_id': $('#producerId').val(), + 'name': $('#aggregateMetricSpecName').val(), + 'description': $('#aggregateMetricSpecDescription').val(), + 'branch': $('#aggregateMetricSpecBranch').val(), + 'spec': $('#aggregateMetricSpecSpec').val(), + 'value': $('#aggregateMetricSpecValue').val(), + 'comparison': $('#aggregateMetricSpecComparison').val() + }, + 'specInputs': { + 'metricName': $('#aggregateMetricSpecMetricName').val(), + 'aggregateMetric': $('#aggregateMetricSpecAggregateMetric').val(), + 'param': $('#aggregateMetricSpecParam').val() + } + }; + return specValues; + } + + /** + * Populate the input elements with the values from the passed in aggregateMetricSpec, + * including some validation in case the passed in spec was created out of data that is + * no longer valid, e.g. a branch name that no longer has scalars tied to the metric_name. + * @param aggregateMetricSpec object with AggregateMetricSpecDao key value pairs + */ + function populateSpecInputs(aggregateMetricSpec) { + $('#aggregateMetricSpecEditId').val(aggregateMetricSpec.aggregate_metric_spec_id); + $('#aggregateMetricSpecName').val(aggregateMetricSpec.name); + $('#aggregateMetricSpecDescription').val(aggregateMetricSpec.description); + $('#aggregateMetricSpecSpec').val(aggregateMetricSpec.spec); + // Skip value if it is 0 and comparison is empty, this was added as a DB + // default and wouldn't be allowed by the UI validation logic. + if (aggregateMetricSpec.value && + (aggregateMetricSpec.value != 0 || aggregateMetricSpec.comparison)) { + $('#aggregateMetricSpecValue').val(aggregateMetricSpec.value); + } + if (aggregateMetricSpec.comparison) { + $('#aggregateMetricSpecComparison').val(aggregateMetricSpec.comparison); + } + var specParts = parseMetricSpec(aggregateMetricSpec.spec); + $('#aggregateMetricSpecParam').val(specParts.param); + $('#aggregateMetricSpecAggregateMetric').val(specParts.metric); + var metricNameFound = selectValueFound('aggregateMetricSpecMetricName', specParts.metricName); + if (!metricNameFound) { + // Don't set the branch name because the metric name is invalid, + // and that determines the set of possible branches. + $('#aggregateMetricSpecValidationError').text("Loaded metric name '"+specParts.metricName+"' is invalid"); + $('#aggregateMetricSpecSaveLoading').hide(); + } else { + $('#aggregateMetricSpecMetricName').val(specParts.metricName); + // Don't need to hide the loading image because updateBranchList will. + var successCallback = function () { + var branchNameFound = selectValueFound('aggregateMetricSpecBranch', aggregateMetricSpec.branch); + if (!branchNameFound) { + $('#aggregateMetricSpecValidationError').text("Loaded branch '"+aggregateMetricSpec.branch+"' is invalid"); + } else { + $('#aggregateMetricSpecBranch').val(aggregateMetricSpec.branch); + } + }; + updateBranchList(successCallback); + } + } + + // Spec Details panel handlers + + /** Handler for the metric name select. */ + $('select#aggregateMetricSpecMetricName').change(function () { + updateSpec(); + updateBranchList(); + }); + + /** Handler for aggregate metric select. */ + $('select#aggregateMetricSpecAggregateMetric').change( function () { + updateSpec(); + }); + + /** Handler for aggregate metric param change. */ + $('input#aggregateMetricSpecParam').on('keyup change', function () { + updateSpec(); + }); + + // Spec Details panel button handlers + /** * Handler for Update button: * update the spec, @@ -389,19 +494,161 @@ $(document).ready(function () { saveAggregateMetricSpec(); }); - /** Handler for the metric name select. */ - $('select#aggregateMetricSpecMetricName').change(function () { - updateSpec(); - updateBranchList(); + /** Handler for Spec Details Cancel button, hide the details panel. */ + $('input#aggregateMetricSpecCancel').click(function () { + $('div#aggregateMetricSpecCreateEdit').hide(); + unhighlightActiveRow(); }); - /** Handler for aggregate metric select. */ - $('select#aggregateMetricSpecAggregateMetric').change( function () { - updateSpec(); + //:~ Alerted Users panel + + /** + * Add a row to the alertedUsers table for the passed in user. + * @param string userName + * @param string userId + */ + function addToAlertedUsersTable(userName, userId) { + + function createActionLink(qtip, actionClass, imgPath, label) { + var actionLink = ' '; + actionLink += ' '+label+''; + return actionLink; + } + + var row = '' + userName + ''; + row += createActionLink('Remove user from alerts', 'removeAlertedUser', '/public/images/icons/close.png', 'Remove alerts'); + row += ''; + $('#aggregateMetricSpecAlertedUsers tbody').append(row); + } + + // Alerted Users panel handlers + + /** + * Handler for Remove alerted user action, delete the user from being + * alerted. The handler is tied to a static parent as the links can be + * dynamically generated through creation of new alerts. + */ + $('#aggregateMetricSpecAlertedUsers').on('click', 'a.removeAlertedUser', function() { + var row = $(this).closest('tr'); + row.addClass('activeRow'); + var aggregateMetricSpecId = $('#aggregateMetricSpecEditId').val(); + $('#aggregateMetricSpecAlertsLoading').show(); + var userId = $(this).data('user_id'); + + var jsonMethod = 'midas.tracker.aggregatemetricspecnotifieduser.delete'; + var args = 'aggregateMetricSpecId=' + aggregateMetricSpecId + '&userId=' + userId; + callAjaxWebApi(jsonMethod, 'POST', args, function (retVal) { + // TODO better return value handling + row.remove(); + addClassesToTableRows('aggregateMetricSpecAlertedUsers'); + $('#aggregateMetricSpecAlertsLoading').hide(); + }); }); - /** Handler for aggregate metric param change. */ - $('input#aggregateMetricSpecParam').on('keyup change', function () { - updateSpec(); + // Live search for users + $.widget('custom.catcomplete', $.ui.autocomplete, { + _renderMenu: function (ul, items) { + 'use strict'; + var self = this, + currentCategory = '', + userIds = {}; + + $('#aggregateMetricSpecAlertedUsers .actionsList').children('a').each(function (ind, elem) { + var userId = $(elem).data('user_id'); + userIds[userId] = userId; + }); + + $.each(items, function (index, item) { + if (userIds[item.userid]) { + // Don't show a user in the list if they are already alerted. + return; + } + if (item.category != currentCategory) { + ul.append('
  • ' + item.category + '
  • '); + currentCategory = item.category; + } + self._renderItemData(ul, item); + }); + } }); + + var alertUserSearchCache = {}, + lastAlertUserShareXhr; + $('#addAlertUserSearch').catcomplete({ + minLength: 2, + delay: 10, + source: function (request, response) { + 'use strict'; + var term = request.term; + if (term in alertUserSearchCache) { + response(alertUserSearchCache[term]); + return; + } + $('#aggregateMetricSpecAlertsLoading').show(); + + lastAlertUserShareXhr = $.getJSON($('.webroot').val() + '/search/live?userSearch=true&allowEmail', + request, function (data, status, xhr) { + $('#aggregateMetricSpecAlertsLoading').hide(); + alertUserSearchCache[term] = data; + if (xhr === lastAlertUserShareXhr) { + response(data); + } + }); + }, // end source + select: function (event, ui) { + 'use strict'; + $('#aggregateMetricSpecAlertsLoading').show(); + var userId = ui.item.userid; + var userName = ui.item.value; + var aggregateMetricSpecId = $('#aggregateMetricSpecEditId').val(); + + var jsonMethod = 'midas.tracker.aggregatemetricspecnotifieduser.create'; + var args = 'aggregateMetricSpecId=' + aggregateMetricSpecId + '&userId=' + userId; + callAjaxWebApi(jsonMethod, 'POST', args, function (retVal) { + // TODO better return value handling + addToAlertedUsersTable(userName, userId); + addClassesToTableRows('aggregateMetricSpecAlertedUsers'); + $('#addAlertUserSearch').val('Start typing a name or email address...'); + $('#addAlertUserSearchValue').val('init'); + $('#aggregateMetricSpecAlertsLoading').hide(); + }); + } // end select + }); + + $('#addAlertUserSearch').focus(function () { + 'use strict'; + console.log('focus'); + console.log($('#addAlertUserSearchValue').val()); + if ($('#addAlertUserSearchValue').val() == 'init') { + $('#addAlertUserSearchValue').val($(this).val()); + $(this).val(''); + } + }).focusout(function () { + 'use strict'; + if ($(this).val() == '') { + $(this).val($('#addAlertUserSearchValue').val()); + $('#addAlertUserSearchValue').val('init'); + } + }); + + // Alerted Users panel button handlers + + /** Handler for Alerts Users Done button, hide the alerts panel. */ + $('input#aggregateMetricSpecUserAlertsDone').click(function () { + $('div#aggregateMetricUserAlerts').hide(); + unhighlightActiveRow(); + }); + + // Initialize the dialog now that it is loaded. + + // Parse json content + // jQuery 1.8 has weird bugs when using .html() here, use the old-style innerHTML here + var trackerJson = $.parseJSON($('div.trackerJsonContent')[0].innerHTML); + + // Create table rows. + for (var amsInd = 0; amsInd < trackerJson.aggregateMetricSpecs.length; amsInd++) { + addToSpecTable(trackerJson.aggregateMetricSpecs[amsInd]); + } + // Initialize the table row classess. + addClassesToTableRows('aggregateMetricSpecListTable'); }); diff --git a/modules/tracker/public/scss/producer/producer.aggregatemetric.scss b/modules/tracker/public/scss/producer/producer.aggregatemetric.scss index bcd94118e..0f81b350b 100644 --- a/modules/tracker/public/scss/producer/producer.aggregatemetric.scss +++ b/modules/tracker/public/scss/producer/producer.aggregatemetric.scss @@ -9,6 +9,15 @@ a { cursor: pointer; } } + + &.alertedUsersAction { + color: #56758b; + + &:hover { + background-color: #E5E5E5; + cursor: pointer; + } + } } div { @@ -20,6 +29,55 @@ div { margin-top: 10px; } + &.aggregateMetricSpecCreate { + border-top: 2px solid darkblue !important; + } + + &.aggregateMetricSpecEdit { + border-top: 2px solid darkblue !important; + } + + &#aggregateMetricUserAlerts { + border-top: 2px solid darkblue; + color: #555; + font-size: 13px; + margin-top: 15px; + margin-bottom: 10px; + padding-top: 10px; + text-align: center; + + div#aggregateMetricUserAlertsThreshold { + margin-top: 15px; + margin-bottom: 10px; + + table#aggregateMetricUserAlertsThresholdDefinition { + margin-top: 10px; + + td:first-of-type { + text-align: left; + } + } + } + + input#aggregateMetricUserAlertsSpec { + margin-top: 10px; + width: 100%; + } + + div#addAggregateMetricSpecAlertUser { + margin-top: 15px; + text-align: center; + } + + div.alertUserSearch { + margin-top: 10px; + + input { + width: 100%; + } + } + } + &.aggregateMetricSectionTitle { border-top: 1px solid #d7d7d7; color: #555; @@ -30,6 +88,7 @@ div { text-align: center; } + &.resultingMetricText { color: #555; font-size: 12px; @@ -37,6 +96,19 @@ div { text-align: center; } + &#aggregateMetricSpecUserAlertsSaveState { + margin-top: 20px; + + input { + background: #f6f9fe; + border: 1px solid #808080; + color: #2e6e9e; + cursor: pointer; + float: right; + margin-left: 10px; + } + } + &#aggregateMetricSpecSaveState { margin-top: 20px; @@ -55,6 +127,8 @@ div { } } + + } label { @@ -73,17 +147,53 @@ img { float: right; margin-right: 40px; } + + &#aggregateMetricSpecAlertsLoading { + float: left; + } } table { - &#aggreagteMetricSpecListTable { + &#aggregateMetricSpecAlertedUsers { table-layout: fixed; border: 2px solid #ddd; width: 100%; th { border-bottom: solid 1px black; + text-align: center; + + &:first-of-type { + width: 70%; + border-right: dotted 1px black; + } + } + + tr.even { + background-color: #f4f4f4; + } + + td.userName { + border-right: dotted 1px black; + text-align: left; + } + + tr.activeRow { + border: 2px solid darkblue; + background-color: #c0d1fe; + } + } + + + &#aggregateMetricSpecListTable { + table-layout: fixed; + border: 2px solid #ddd; + width: 100%; + + th { + border-bottom: solid 1px black; + text-align: center; &:first-of-type { width: 70%; @@ -95,13 +205,22 @@ table { border-right: dotted 1px black; } - tr.odd { + tr.even { background-color: #f4f4f4; } + tr.activeRow { + border: 2px solid darkblue; + background-color: #c0d1fe; + } + span.actionsList { display: flex; justify-content: space-around; + + a:first-of-type { + padding-left: 5px; + } } } } diff --git a/modules/tracker/views/producer/aggregatemetric.phtml b/modules/tracker/views/producer/aggregatemetric.phtml index 546571f78..8943411ab 100644 --- a/modules/tracker/views/producer/aggregatemetric.phtml +++ b/modules/tracker/views/producer/aggregatemetric.phtml @@ -18,6 +18,7 @@ limitations under the License. =========================================================================*/ + echo ''; echo ''; ?> @@ -26,38 +27,24 @@ echo '