diff --git a/modules/tracker/controllers/ProducerController.php b/modules/tracker/controllers/ProducerController.php
index e330f3fd2..53e902b60 100644
--- a/modules/tracker/controllers/ProducerController.php
+++ b/modules/tracker/controllers/ProducerController.php
@@ -33,7 +33,7 @@ class Tracker_ProducerController extends Tracker_AppController
public $_models = array('Community');
/** @var array */
- public $_moduleModels = array('Producer', 'Trend');
+ public $_moduleModels = array('AggregateMetric', 'AggregateMetricSpec', 'Producer', 'Trend');
/**
* List all producers for a given community (in the tab). Requires read permission on community.
@@ -64,6 +64,7 @@ public function listAction()
$this->view->community = $communityDao;
$this->view->producers = $this->Tracker_Producer->getByCommunityId($communityId);
+ $this->view->isAdmin = $this->Community->policyCheck($communityDao, $this->userSession->Dao, MIDAS_POLICY_ADMIN);
}
/**
@@ -235,4 +236,48 @@ public function editsubmitAction()
$this->Tracker_Producer->save($producerDao);
echo JsonComponent::encode(array('status' => 'ok', 'message' => 'Changes saved', 'producer' => $producerDao));
}
+
+ /**
+ * Dialog for create/edit/view/deleting an aggregate metric for this producer.
+ *
+ * @param producerId Id of the producer. Admin permission on the associated community required.
+ * @throws Zend_Exception
+ */
+ public function aggregatemetricAction()
+ {
+ $this->disableLayout();
+
+ $producerId = $this->getParam('producerId');
+ /** @var Tracker_ProducerDao $producerDao */
+ $producerDao = $this->Tracker_Producer->load($producerId);
+ /** @var CommunityDao $communityDao */
+ $communityDao = $producerDao->getCommunity();
+
+ if ($communityDao === false || $this->Community->policyCheck($communityDao, $this->userSession->Dao, MIDAS_POLICY_ADMIN) === false
+ ) {
+ throw new Zend_Exception('The associated community does not exist or you do not Admin access to the community', 403);
+ }
+
+ $producerParams = array(
+ 'producer_id' => $producerDao->getProducerId(),
+ 'key_metric' => 1,
+ );
+ $distinctTrendNames = array();
+ /** @var array $producerTrends */
+ $producerTrends = $this->Tracker_Trend->getAllByParams($producerParams);
+ /** @var Tracker_TrendDao $trendDao */
+ foreach ($producerTrends as $trendDao) {
+ /** @var string $trendMetricName */
+ $trendMetricName = $trendDao->getMetricName();
+ if (!in_array($trendMetricName, $distinctTrendNames)) {
+ $distinctTrendNames[] = $trendMetricName;
+ }
+ }
+
+ /** @var array $aggregateMetricSpecs */
+ $aggregateMetricSpecs = $this->Tracker_AggregateMetricSpec->getAggregateMetricSpecsForProducer($producerDao);
+ $this->view->producer = $producerDao;
+ $this->view->aggregateMetricSpecs = $aggregateMetricSpecs;
+ $this->view->distinctTrendNames = $distinctTrendNames;
+ }
}
diff --git a/modules/tracker/public/css/producer/producer.aggregatemetric.css b/modules/tracker/public/css/producer/producer.aggregatemetric.css
new file mode 100644
index 000000000..dabf6b36e
--- /dev/null
+++ b/modules/tracker/public/css/producer/producer.aggregatemetric.css
@@ -0,0 +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}
diff --git a/modules/tracker/public/css/producer/producer.list.css b/modules/tracker/public/css/producer/producer.list.css
index a66b66d05..39f8ef5c7 100644
--- a/modules/tracker/public/css/producer/producer.list.css
+++ b/modules/tracker/public/css/producer/producer.list.css
@@ -1 +1 @@
-div.introText{margin-bottom:20px}div.producerList a.producerLink{font-size:18px;font-weight:bold;text-decoration:none}div.producerList a.producerLink:hover{text-decoration:underline}div.noProducers{font-style:italic}div.producerDescription{margin-top:6px}div.producerContainer{border-top:1px solid #d7d7d7;margin-top:20px;padding-top:8px}div.createProducerContainer{border-top:1px solid #d7d7d7;margin-top:20px;padding-top:10px}form.createProducer input[type=text]{width:300px}form.createProducer textarea{resize:none;width:298px}
+div.introText{margin-bottom:20px}div.producerList a.producerLink{font-size:18px;font-weight:bold;text-decoration:none}div.producerList a.producerLink:hover{text-decoration:underline}div.producerManageAggregateMetric{float:right;color:#56758b}div.producerManageAggregateMetric:hover{background-color:#e5e5e5;cursor:pointer}div.noProducers{font-style:italic}div.producerDescription{margin-top:6px}div.producerContainer{border-top:1px solid #d7d7d7;margin-top:20px;padding-top:8px}div.createProducerContainer{border-top:1px solid #d7d7d7;margin-top:20px;padding-top:10px}form.createProducer input[type=text]{width:300px}form.createProducer textarea{resize:none;width:298px}
diff --git a/modules/tracker/public/js/producer/producer.aggregateMetric.js b/modules/tracker/public/js/producer/producer.aggregateMetric.js
new file mode 100644
index 000000000..31eca41eb
--- /dev/null
+++ b/modules/tracker/public/js/producer/producer.aggregateMetric.js
@@ -0,0 +1,407 @@
+// Midas Server. Copyright Kitware SAS. Licensed under the Apache License 2.0.
+
+/* global ajaxWebApi */
+/* global json */
+
+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();
+
+ /**
+ * Interface to server side AggregateMetricSpec REST API.
+ * @param method The HTTP method {'POST'|'PUT'|'DELETE'|'GET'}
+ * @param aggregateMetricSpecId (Required for all except POST)
+ * @param args an object containing key value pairs for the AggregateMetricSpec object for PUT calls (Optional except for PUT)
+ * @param sCb success callback, passed the return value (Optional)
+ * @param eCb error callback, passed the return value (Optional)
+ * @param cCb complete callback, passed the return value (Optional)
+ */
+ function aggregatemetricspecRest(method, aggregateMetricSpecId, args, sCb, eCb, cCb) {
+ var url = json.global.webroot + '/rest/tracker/aggregatemetricspec';
+ if (aggregateMetricSpecId) {
+ url += '/' + aggregateMetricSpecId;
+ }
+ url += '?useSession=true';
+
+ var restCall = {
+ url: url,
+ type: method,
+ success: function (retVal) {
+ if (sCb) { sCb(retVal); }
+ },
+ error: function (retVal) {
+ if (eCb) {
+ eCb(retVal);
+ } else {
+ midas.createNotice(retVal.message, 3000, 'error');
+ }
+ },
+ complete: function (retVal) {
+ if (cCb) { cCb(retVal); }
+ },
+ log: $('
')
+ };
+ if (args) {
+ restCall.data = args;
+ }
+ $.ajax(restCall);
+ }
+
+ /**
+ * 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();
+ $('img#aggregateMetricSpecSaveLoading').show();
+ ajaxWebApi.ajax({
+ method: 'midas.tracker.branchesformetricname.list',
+ args: 'producerId=' + producerId + '&trendMetricName='+metricName,
+ success: function (retVal) {
+ $('#aggregateMetricSpecBranch').find('option').remove();
+ var branches = retVal.data;
+ $.each(branches, function (key, value) {
+ $('#aggregateMetricSpecBranch').append('');
+ });
+ $('img#aggregateMetricSpecSaveLoading').hide();
+ if (successCallback) { successCallback(); }
+ },
+ error: function (retVal) {
+ midas.createNotice(retVal.message, 3000, 'error');
+ },
+ complete: function () {},
+ log: $('')
+ });
+ }
+
+ /** 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;
+ }
+
+ /**
+ * Reset and display the spec details panel, defaults to create mode.
+ * @param {bool} true if edit mode, false (the default) for create mode
+ */
+ function showDetailsPanel(editMode) {
+ clearSpecInputs();
+ $('div#aggregateMetricSpecCreateEdit').show();
+ $('img#aggregateMetricSpecSaveLoading').hide();
+ $('div#aggregateMetricSpecSaveState input').prop('disabled', false);
+ if (editMode) {
+ $('.aggregateMetricSpecCreate').hide();
+ $('.aggregateMetricSpecEdit').show();
+ } else {
+ $('.aggregateMetricSpecCreate').show();
+ $('.aggregateMetricSpecEdit').hide();
+ }
+ }
+
+ /**
+ * Add a row to the aggregateMetricSpec table for the passed in aggregateMetricSpec.
+ * @param aggregateMetricSpec object with AggregateMetricSpecDao key value pairs
+ */
+ function addToSpecTable(aggregateMetricSpec) {
+ var row = '' + aggregateMetricSpec.name + ' | ';
+ row += '';
+ row += ' ';
+ row += ' Edit';
+ row += ' ';
+ row += ' Delete';
+ row += ' |
';
+ $('#aggreagteMetricSpecListTable tbody').append(row);
+ addClassesToSpecTableRows();
+ }
+
+ /**
+ * Update an existing aggregateMetricSpec in the spec table, with the properties
+ * of the passed in aggergateMetricSpec, currently only updates the name.
+ * @param aggregateMetricSpec object with AggregateMetricSpecDao key value pairs
+ */
+ function updateSpecInTable(aggregateMetricSpec) {
+ var amsId = aggregateMetricSpec.aggregate_metric_spec_id;
+ var name = aggregateMetricSpec.name;
+ // Get the row of this spec in the table.
+ var row = $("td").find("[data-aggregate_metric_spec_id='" + amsId + "']").first().closest('tr');
+ // Get the first cell of that row and output the spec's name.
+ $('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);
+ }
+ }
+
+ /**
+ * Handler for Delete action, delete an Aggregate Metric Spec on the server
+ * and remove it from the table. The handler is tied
+ * 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');
+ var sCb = function (data) {
+ row.remove();
+ $('#aggregateMetricSpecDeleteLoading').hide();
+ addClassesToSpecTableRows();
+ };
+ $('#aggregateMetricSpecDeleteLoading').show();
+ aggregatemetricspecRest('DELETE', aggregateMetricSpecId, null, sCb, null, null);
+ });
+
+ /** Handler for Add action, open the details panel in Create state. */
+ $('div#addAggregateMetricSpec').click(function () {
+ showDetailsPanel();
+ });
+
+ /**
+ * Handler for Edit action, open the details panel in Edit state after
+ * loading the details of the Aggregate Metric Spec. The handler is tied
+ * 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');
+ showDetailsPanel(true);
+ $('img#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();
+ });
+
+ /**
+ * Save an aggregateMetricSpec, either as a new Dao or update an existing one,
+ * depending on whether aggregateMetricSpecId is passed, will perform validation
+ * on input fields before saving, and will update or add the spec to the spec table.
+ * @param aggregateMetricSpecId if passed the id of an existing spec to update
+ */
+ function saveAggregateMetricSpec(aggregateMetricSpecId) {
+ $('div#aggregateMetricSpecSaveState input').prop('disabled', true);
+ $('#aggregateMetricSpecValidationError').text('');
+ var specValues = getSpecInputsValues();
+
+ // Validate the inputs.
+ // Order matters, so return after the first invalid field found.
+ var requiredFields = [
+ {'value': specValues.aggregateMetricSpec.name, 'name': 'Name'},
+ {'value': specValues.specInputs.metricName, 'name': 'Metric name'},
+ {'value': specValues.aggregateMetricSpec.branch, 'name': 'Branch'},
+ {'value': specValues.specInputs.aggregateMetric, 'name': 'Aggregate metric'},
+ {'value': specValues.specInputs.param, 'name': 'Param (percentile)'}
+ ];
+ for (var i = 0; i < requiredFields.length; i++) {
+ var value = requiredFields[i].value;
+ var name = requiredFields[i].name;
+ if (!value || value === '') {
+ $('#aggregateMetricSpecValidationError').text(name + ' is a required field');
+ $('div#aggregateMetricSpecSaveState input').prop('disabled', false);
+ return;
+ }
+ }
+ var param = specValues.specInputs.param;
+ if (!$.isNumeric(param) || param < 0 || param > 100) {
+ $('#aggregateMetricSpecValidationError').text('Param (percentile) must be >= 0 and <= 100');
+ $('div#aggregateMetricSpecSaveState input').prop('disabled', false);
+ return;
+ }
+ // Comparison and valid must be empty or not together.
+ var comparisonEmpty = (!specValues.aggregateMetricSpec.comparison || specValues.aggregateMetricSpec.comparison === '');
+ var valueEmpty = (!specValues.aggregateMetricSpec.value || specValues.aggregateMetricSpec.value === '');
+ if (comparisonEmpty !== valueEmpty) {
+ $('#aggregateMetricSpecValidationError').text('Comparison and value must be set together');
+ $('div#aggregateMetricSpecSaveState input').prop('disabled', false);
+ return;
+ } else if (!valueEmpty && !$.isNumeric(specValues.aggregateMetricSpec.value)) {
+ // The case where comparison and value are provided.
+ $('#aggregateMetricSpecValidationError').text('Value must be numeric');
+ $('div#aggregateMetricSpecSaveState input').prop('disabled', false);
+ return;
+ }
+
+ // Save the AMS on the server.
+ $('img#aggregateMetricSpecSaveLoading').show();
+ var successCallback = function (aggregateMetricSpec) {
+ if (aggregateMetricSpecId) {
+ updateSpecInTable(aggregateMetricSpec);
+ } else {
+ addToSpecTable(aggregateMetricSpec);
+ }
+ $('div#aggregateMetricSpecCreateEdit').hide();
+ }
+ var method = aggregateMetricSpecId ? 'PUT' : 'POST';
+ aggregateMetricSpecId = aggregateMetricSpecId ? aggregateMetricSpecId : null;
+ aggregatemetricspecRest(method, aggregateMetricSpecId, specValues.aggregateMetricSpec, successCallback);
+ }
+
+ /**
+ * Handler for Update button:
+ * update the spec,
+ * adjust its name in the spec listing table,
+ * hide the details panel.
+ */
+ $('input#aggregateMetricSpecUpdate').click(function () {
+ saveAggregateMetricSpec($('#aggregateMetricSpecEditId').val());
+ });
+
+ /**
+ * Handler for Create button:
+ * create the new spec,
+ * add it to the spec listing table,
+ * hide the details panel.
+ */
+ $('input#aggregateMetricSpecCreate').click(function () {
+ saveAggregateMetricSpec();
+ });
+
+ /** 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();
+ });
+});
diff --git a/modules/tracker/public/js/producer/producer.list.js b/modules/tracker/public/js/producer/producer.list.js
index ffdb54f98..29a425ab5 100644
--- a/modules/tracker/public/js/producer/producer.list.js
+++ b/modules/tracker/public/js/producer/producer.list.js
@@ -1,3 +1,13 @@
// Midas Server. Copyright Kitware SAS. Licensed under the Apache License 2.0.
-$(document).ready(function () {});
+var midas = midas || {};
+
+$(document).ready(function () {
+ 'use strict';
+
+ $('div.producerManageAggregateMetric').click(function () {
+ var producerId = $(event.target).data('producer_id');
+ midas.loadDialog('aggregateMetricProducerId' + producerId, '/tracker/producer/aggregatemetric?producerId=' + producerId);
+ midas.showDialog('Manage Aggregate Metric Specs', false);
+ });
+});
diff --git a/modules/tracker/public/scss/producer/producer.aggregatemetric.scss b/modules/tracker/public/scss/producer/producer.aggregatemetric.scss
new file mode 100644
index 000000000..bcd94118e
--- /dev/null
+++ b/modules/tracker/public/scss/producer/producer.aggregatemetric.scss
@@ -0,0 +1,107 @@
+// Midas Server. Copyright Kitware SAS. Licensed under the Apache License 2.0.
+
+a {
+ &.aggregateMetricSpecAction {
+ color: #56758b;
+
+ &:hover {
+ background-color: #E5E5E5;
+ cursor: pointer;
+ }
+ }
+}
+
+div {
+ addAggregateMetricSpec {
+ margin-top: 10px;
+ }
+
+ aggregateMetricSpecList {
+ margin-top: 10px;
+ }
+
+ &.aggregateMetricSectionTitle {
+ border-top: 1px solid #d7d7d7;
+ color: #555;
+ font-size: 13px;
+ margin-top: 15px;
+ margin-bottom: 10px;
+ padding-top: 10px;
+ text-align: center;
+ }
+
+ &.resultingMetricText {
+ color: #555;
+ font-size: 12px;
+ margin-top: 10px;
+ text-align: center;
+ }
+
+ aggregateMetricSpecSaveState {
+ margin-top: 20px;
+
+ input {
+ background: #f6f9fe;
+ border: 1px solid #808080;
+ color: #2e6e9e;
+ cursor: pointer;
+ float: right;
+ margin-left: 10px;
+ }
+
+ input:disabled {
+ color: #c0c0c0;
+ cursor: not-allowed;
+ }
+ }
+
+}
+
+label {
+ aggregateMetricSpecValidationError {
+ color: crimson;
+ }
+}
+
+img {
+ aggregateMetricSpecSaveLoading {
+ float: left;
+ margin-right: 15px;
+ }
+
+ aggregateMetricSpecDeleteLoading {
+ float: right;
+ margin-right: 40px;
+ }
+}
+
+table {
+
+ aggreagteMetricSpecListTable {
+ table-layout: fixed;
+ border: 2px solid #ddd;
+ width: 100%;
+
+ th {
+ border-bottom: solid 1px black;
+
+ &:first-of-type {
+ width: 70%;
+ border-right: dotted 1px black;
+ }
+ }
+
+ td.specName {
+ border-right: dotted 1px black;
+ }
+
+ tr.odd {
+ background-color: #f4f4f4;
+ }
+
+ span.actionsList {
+ display: flex;
+ justify-content: space-around;
+ }
+ }
+}
diff --git a/modules/tracker/public/scss/producer/producer.list.scss b/modules/tracker/public/scss/producer/producer.list.scss
index 21ab0fe0d..ffdea4c6d 100644
--- a/modules/tracker/public/scss/producer/producer.list.scss
+++ b/modules/tracker/public/scss/producer/producer.list.scss
@@ -15,6 +15,16 @@ div {
}
}
+ &.producerManageAggregateMetric {
+ float: right;
+ color: #56758b;
+
+ &:hover {
+ background-color: #E5E5E5;
+ cursor: pointer;
+ }
+ }
+
&.noProducers {
font-style: italic;
}
diff --git a/modules/tracker/views/producer/aggregatemetric.phtml b/modules/tracker/views/producer/aggregatemetric.phtml
new file mode 100644
index 000000000..546571f78
--- /dev/null
+++ b/modules/tracker/views/producer/aggregatemetric.phtml
@@ -0,0 +1,151 @@
+';
+echo '';
+?>
+
+
+
+ producer->getProducerId().'" />';
+ echo $this->producer->getDisplayName().' existing aggregate metric specs';
+ ?>
+
+
+
+
+ Name |
+ Actions |
+
+
+ aggregateMetricSpecs as $aggregateMetricSpecDao) {
+ echo '';
+ echo ''.$aggregateMetricSpecDao->getName().' | ';
+ echo ' ';
+ echo ' ';
+ echo ' Edit';
+ echo ' ';
+ echo ' Delete';
+ echo ' |
';
+ }
+ ?>
+
+
+
+
Add aggregate metric spec'; ?>
+

+
+
+
Create the aggregate metric spec
+
Edit aggregate metric spec
+
+
+
+
Define the aggregate metric
+
+
Define the notification threshold
+
+
+
+
+
diff --git a/modules/tracker/views/producer/list.phtml b/modules/tracker/views/producer/list.phtml
index cd6ef00f6..8ef6b904b 100644
--- a/modules/tracker/views/producer/list.phtml
+++ b/modules/tracker/views/producer/list.phtml
@@ -40,6 +40,9 @@
if ($producerDao->getDescription() != '') {
echo ''.$this->escape($producerDao->getDescription()).'
';
}
+ if ($this->isAdmin) {
+ echo '
Aggregate Metrics
';
+ }
echo '';
}
?>