From aa40c6a7e490a7907d7afe87e35ce49ea4e0d842 Mon Sep 17 00:00:00 2001
From: ionutgoldan
Date: Tue, 3 Apr 2018 16:15:27 +0300
Subject: [PATCH] Bug 1376829 - Add notes to alert summaries (#3345)
---
.../api/test_performance_alertsummary_api.py | 2 +
.../0006_add_alert_summary_notes.py | 20 +++
treeherder/perf/models.py | 2 +
.../webapp/api/performance_serializers.py | 3 +-
ui/css/perf.css | 4 +
ui/js/controllers/perf/alerts.js | 51 +++++-
ui/js/models/perf/alerts.js | 15 ++
ui/js/perf.js | 2 +
ui/partials/perf/alertsctrl.html | 63 ++++---
ui/partials/perf/editnotesctrl.html | 28 ++++
ui/vendor/ng-text-truncate.js | 157 ++++++++++++++++++
11 files changed, 321 insertions(+), 26 deletions(-)
create mode 100644 treeherder/perf/migrations/0006_add_alert_summary_notes.py
create mode 100644 ui/partials/perf/editnotesctrl.html
create mode 100644 ui/vendor/ng-text-truncate.js
diff --git a/tests/webapp/api/test_performance_alertsummary_api.py b/tests/webapp/api/test_performance_alertsummary_api.py
index a205c44ce..c7a2c975c 100644
--- a/tests/webapp/api/test_performance_alertsummary_api.py
+++ b/tests/webapp/api/test_performance_alertsummary_api.py
@@ -72,6 +72,7 @@ def test_alert_summaries_get(webapp, test_perf_alert_summary,
'alerts',
'bug_number',
'issue_tracker',
+ 'notes',
'framework',
'id',
'last_updated',
@@ -115,6 +116,7 @@ def test_alert_summaries_get_onhold(webapp, test_perf_alert_summary,
'alerts',
'bug_number',
'issue_tracker',
+ 'notes',
'framework',
'id',
'last_updated',
diff --git a/treeherder/perf/migrations/0006_add_alert_summary_notes.py b/treeherder/perf/migrations/0006_add_alert_summary_notes.py
new file mode 100644
index 000000000..17e2cb5f3
--- /dev/null
+++ b/treeherder/perf/migrations/0006_add_alert_summary_notes.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.10 on 2018-03-08 14:53
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('perf', '0005_permit_github_links'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='performancealertsummary',
+ name='notes',
+ field=models.TextField(null=True, blank=True),
+ ),
+ ]
diff --git a/treeherder/perf/models.py b/treeherder/perf/models.py
index d9c61d582..750c83f13 100644
--- a/treeherder/perf/models.py
+++ b/treeherder/perf/models.py
@@ -205,6 +205,8 @@ class PerformanceAlertSummary(models.Model):
manually_created = models.BooleanField(default=False)
+ notes = models.TextField(null=True, blank=True)
+
last_updated = models.DateTimeField(db_index=True)
UNTRIAGED = 0
diff --git a/treeherder/webapp/api/performance_serializers.py b/treeherder/webapp/api/performance_serializers.py
index 1169d87b3..62c6bc36f 100644
--- a/treeherder/webapp/api/performance_serializers.py
+++ b/treeherder/webapp/api/performance_serializers.py
@@ -120,7 +120,8 @@ class PerformanceAlertSummarySerializer(serializers.ModelSerializer):
model = PerformanceAlertSummary
fields = ['id', 'push_id', 'prev_push_id',
'last_updated', 'repository', 'framework', 'alerts',
- 'related_alerts', 'status', 'bug_number', 'issue_tracker']
+ 'related_alerts', 'status', 'bug_number',
+ 'issue_tracker', 'notes']
class PerformanceBugTemplateSerializer(serializers.ModelSerializer):
diff --git a/ui/css/perf.css b/ui/css/perf.css
index 48c6143e7..f5a1e98b4 100644
--- a/ui/css/perf.css
+++ b/ui/css/perf.css
@@ -410,6 +410,10 @@ span.compare-notsure {
border-bottom: 1px solid transparent;
}
+.notes-preview {
+ white-space: pre-wrap;
+}
+
.alert-summary-header-element {
flex: none;
padding: 8px;
diff --git a/ui/js/controllers/perf/alerts.js b/ui/js/controllers/perf/alerts.js
index 0087c6ad0..63f15d97c 100644
--- a/ui/js/controllers/perf/alerts.js
+++ b/ui/js/controllers/perf/alerts.js
@@ -1,5 +1,8 @@
+import angular from 'angular';
+
import perf from '../../perf';
import modifyAlertsCtrlTemplate from '../../../partials/perf/modifyalertsctrl.html';
+import editAlertSummaryNotesCtrlTemplate from '../../../partials/perf/editnotesctrl.html';
import { getApiUrl } from "../../../helpers/urlHelper";
perf.factory('PhBugs', [
@@ -67,7 +70,39 @@ perf.controller(
}
});
}]);
+perf.controller(
+ 'EditAlertSummaryNotesCtrl', ['$scope', '$uibModalInstance', 'alertSummary',
+ function ($scope, $uibModalInstance, alertSummary) {
+ $scope.title = "Edit notes";
+ $scope.placeholder = "Leave notes here...";
+ $scope.error = false;
+ $scope.alertSummaryCopy = angular.copy(alertSummary);
+ $scope.saveChanges = function () {
+ $scope.modifying = true;
+ $scope.alertSummaryCopy.saveNotes().then(function () {
+ _.merge(alertSummary, $scope.alertSummaryCopy);
+ $scope.modifying = false;
+ $scope.error = false;
+
+ $uibModalInstance.close();
+ }, function () {
+ $scope.error = true;
+ $scope.modifying = false;
+ }
+ );
+ };
+
+ $scope.cancel = function () {
+ $uibModalInstance.dismiss('cancel');
+ };
+
+ $scope.$on('modal.closing', function (event) {
+ if ($scope.modifying) {
+ event.preventDefault();
+ }
+ });
+ }]);
perf.controller(
'MarkDownstreamAlertsCtrl', ['$scope', '$uibModalInstance', '$q', 'alertSummary',
'allAlertSummaries', 'phAlertStatusMap',
@@ -168,6 +203,19 @@ perf.controller('AlertsCtrl', [
return Math.min(Math.abs(percent)*5, 100);
};
+ $scope.editAlertSummaryNotes = function (alertSummary) {
+ $uibModal.open({
+ template: editAlertSummaryNotesCtrlTemplate,
+ controller: 'EditAlertSummaryNotesCtrl',
+ size: 'md',
+ resolve: {
+ alertSummary: function () {
+ return alertSummary;
+ }
+ }
+ });
+ };
+
// can filter by alert statuses or just show everything
$scope.statuses = _.map(phAlertSummaryStatusMap);
$scope.statuses = $scope.statuses.concat({ id: -1, text: "all" });
@@ -217,7 +265,6 @@ perf.controller('AlertsCtrl', [
}
});
$scope.numFilteredAlertSummaries = $scope.alertSummaries.filter(summary => !summary.anyVisible).length;
-
}
// these methods handle the business logic of alert selection and
@@ -374,6 +421,8 @@ perf.controller('AlertsCtrl', [
_.defaults(resultSetToSummaryMap,
_.set({}, alertSummary.repository, {}));
+ alertSummary.originalNotes = alertSummary.notes;
+
[alertSummary.push_id, alertSummary.prev_push_id].forEach(
function (resultSetId) {
// skip nulls
diff --git a/ui/js/models/perf/alerts.js b/ui/js/models/perf/alerts.js
index 67c9689f3..13d4f1896 100644
--- a/ui/js/models/perf/alerts.js
+++ b/ui/js/models/perf/alerts.js
@@ -63,6 +63,10 @@ treeherder.factory('PhAlerts', [
Object.assign(this, alertSummaryData);
this._initializeAlerts(optionCollectionMap);
};
+ AlertSummary.prototype.modify = function (modification) {
+ return $http.put(getApiUrl(`/performance/alertsummary/${this.id}/`),
+ modification);
+ };
_.forEach(phAlertSummaryStatusMap, function (status) {
AlertSummary.prototype['is' + _.capitalize(status.text)] = function () {
return this.status === status.id;
@@ -217,6 +221,17 @@ treeherder.factory('PhAlerts', [
return _.find(phAlertSummaryStatusMap, { id: this.status }).text;
};
+ AlertSummary.prototype.saveNotes = function () {
+ const alertSummary = this;
+ return this.modify({ notes: this.notes }).then(() => {
+ alertSummary.originalNotes = alertSummary.notes;
+ alertSummary.notesChanged = false;
+ });
+ };
+ AlertSummary.prototype.editingNotes = function () {
+ this.notesChanged = (this.notes !== this.originalNotes);
+ };
+
function _getAlertSummary(id) {
// get a specific alert summary
// in order to cancel the http request, a canceller must be used
diff --git a/ui/js/perf.js b/ui/js/perf.js
index 5267cb912..242d36377 100644
--- a/ui/js/perf.js
+++ b/ui/js/perf.js
@@ -3,6 +3,7 @@ import angularClipboardModule from 'angular-clipboard';
import uiBootstrap from 'angular1-ui-bootstrap4';
import uiRouter from 'angular-ui-router';
+import ngTextTruncateModule from '../vendor/ng-text-truncate';
import treeherderModule from './treeherder';
export default angular.module('perf', [
@@ -10,4 +11,5 @@ export default angular.module('perf', [
uiBootstrap,
treeherderModule.name,
angularClipboardModule.name,
+ ngTextTruncateModule.name
]);
diff --git a/ui/partials/perf/alertsctrl.html b/ui/partials/perf/alertsctrl.html
index f3417d94b..b394cdaec 100644
--- a/ui/partials/perf/alertsctrl.html
+++ b/ui/partials/perf/alertsctrl.html
@@ -88,6 +88,12 @@
Unlink from bug
+
+
+ Add notes
+ Edit notes
+
+
Re-open
@@ -195,31 +201,40 @@
-
-
-
-
diff --git a/ui/partials/perf/editnotesctrl.html b/ui/partials/perf/editnotesctrl.html
new file mode 100644
index 000000000..1ec4a5433
--- /dev/null
+++ b/ui/partials/perf/editnotesctrl.html
@@ -0,0 +1,28 @@
+
diff --git a/ui/vendor/ng-text-truncate.js b/ui/vendor/ng-text-truncate.js
new file mode 100644
index 000000000..85ae4e11b
--- /dev/null
+++ b/ui/vendor/ng-text-truncate.js
@@ -0,0 +1,157 @@
+// vendored in to use more modern module syntax, based off of:
+// https://www.npmjs.com/package/ng-text-truncate-2
+
+(function (root, factory) {
+ /* istanbul ignore next */
+ if (typeof define === 'function' && define.amd) {
+ define(['angular'], factory);
+ } else if (typeof module === 'object' && module.exports) {
+ module.exports = factory(require('angular'));
+ } else {
+ root.ngTextTruncate = factory(root.angular);
+ }
+}(this, function (angular) {
+ 'use strict';
+
+
+ return angular.module( 'ngTextTruncate', [] )
+
+
+ .directive( "ngTextTruncate", [ "$compile", "ValidationServices", "CharBasedTruncation", "WordBasedTruncation",
+ function( $compile, ValidationServices, CharBasedTruncation, WordBasedTruncation ) {
+ return {
+ restrict: "A",
+ scope: {
+ text: "=ngTextTruncate",
+ charsThreshould: "@ngTtCharsThreshold",
+ wordsThreshould: "@ngTtWordsThreshold",
+ customMoreLabel: "@ngTtMoreLabel",
+ customLessLabel: "@ngTtLessLabel"
+ },
+ controller: ["$scope", "$element", "$attrs", function( $scope, $element, $attrs ) {
+ $scope.toggleShow = function() {
+ $scope.open = !$scope.open;
+ };
+
+ $scope.useToggling = $attrs.ngTtNoToggling === undefined;
+ }],
+ link: function( $scope, $element, $attrs ) {
+ $scope.open = false;
+
+ ValidationServices.failIfWrongThreshouldConfig( $scope.charsThreshould, $scope.wordsThreshould );
+
+ var CHARS_THRESHOLD = parseInt( $scope.charsThreshould );
+ var WORDS_THRESHOLD = parseInt( $scope.wordsThreshould );
+
+ $scope.$watch( "text", function() {
+ $element.empty();
+
+ if( CHARS_THRESHOLD ) {
+ if( $scope.text && CharBasedTruncation.truncationApplies( $scope, CHARS_THRESHOLD ) ) {
+ CharBasedTruncation.applyTruncation( CHARS_THRESHOLD, $scope, $element );
+
+ } else {
+ $element.append( $scope.text );
+ }
+
+ } else {
+
+ if( $scope.text && WordBasedTruncation.truncationApplies( $scope, WORDS_THRESHOLD ) ) {
+ WordBasedTruncation.applyTruncation( WORDS_THRESHOLD, $scope, $element );
+
+ } else {
+ $element.append( $scope.text );
+ }
+
+ }
+ } );
+ }
+ };
+ }] )
+
+
+
+ .factory( "ValidationServices", function() {
+ return {
+ failIfWrongThreshouldConfig: function( firstThreshould, secondThreshould ) {
+ if( (! firstThreshould && ! secondThreshould) || (firstThreshould && secondThreshould) ) {
+ throw "You must specify one, and only one, type of threshould (chars or words)";
+ }
+ }
+ };
+ })
+
+
+
+ .factory( "CharBasedTruncation", [ "$compile", function( $compile ) {
+ return {
+ truncationApplies: function( $scope, threshould ) {
+ return $scope.text.length > threshould;
+ },
+
+ applyTruncation: function( threshould, $scope, $element ) {
+ if( $scope.useToggling ) {
+ var el = angular.element( "" +
+ $scope.text.substr( 0, threshould ) +
+ "..." +
+ "" +
+ " " + ($scope.customMoreLabel ? $scope.customMoreLabel : "More") +
+ "" +
+ "" +
+ $scope.text.substring( threshould ) +
+ "" +
+ " " + ($scope.customLessLabel ? $scope.customLessLabel : "Less") +
+ "" +
+ "" +
+ "" );
+ $compile( el )( $scope );
+ $element.append( el );
+
+ } else {
+ $element.append( $scope.text.substr( 0, threshould ) + "..." );
+
+ }
+ }
+ };
+ }])
+
+
+
+ .factory( "WordBasedTruncation", [ "$compile", function( $compile ) {
+ return {
+ truncationApplies: function( $scope, threshould ) {
+ return $scope.text.split( " " ).length > threshould;
+ },
+
+ applyTruncation: function( threshould, $scope, $element ) {
+ var splitText = $scope.text.split( " " );
+ if( $scope.useToggling ) {
+ var el = angular.element( "" +
+ splitText.slice( 0, threshould ).join( " " ) + " " +
+ "..." +
+ "" +
+ " " + ($scope.customMoreLabel ? $scope.customMoreLabel : "More") +
+ "" +
+ "" +
+ splitText.slice( threshould, splitText.length ).join( " " ) +
+ "" +
+ " " + ($scope.customLessLabel ? $scope.customLessLabel : "Less") +
+ "" +
+ "" +
+ "" );
+ $compile( el )( $scope );
+ $element.append( el );
+
+ } else {
+ $element.append( splitText.slice( 0, threshould ).join( " " ) + "..." );
+ }
+ }
+ };
+ }]);
+}));