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'; + ?> +
+ + + + + + + + aggregateMetricSpecs as $aggregateMetricSpecDao) { + echo ''; + echo ''; + echo ' '; + } + ?> + +
NameActions
'.$aggregateMetricSpecDao->getName().''; + echo ' '; + echo ' Edit'; + echo ' '; + echo ' Delete'; + echo '
+
+ Add aggregate metric spec'; ?> +
+