You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by oc...@apache.org on 2020/11/30 18:18:55 UTC

[trafficcontrol] branch 5.0.x updated: Adds a more powerful UI grid for changelogs and removes changelog entry for unqueuing servers updates/revals (#5329)

This is an automated email from the ASF dual-hosted git repository.

ocket8888 pushed a commit to branch 5.0.x
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/5.0.x by this push:
     new f5d305d  Adds a more powerful UI grid for changelogs and removes changelog entry for unqueuing servers updates/revals (#5329)
f5d305d is described below

commit f5d305d03c1f00c9ee759fd91231267a45bb0d3d
Author: Jeremy Mitchell <mi...@users.noreply.github.com>
AuthorDate: Fri Nov 27 11:14:10 2020 -0700

    Adds a more powerful UI grid for changelogs and removes changelog entry for unqueuing servers updates/revals (#5329)
    
    * swaps jquery datatable for ag-grid for change logs table
    
    * the number of change log days that TP fetches is now configurable
    
    * changes default to 7 days of change logs that TP fetches
    
    * removes changelog message created on server update/reval unqueue
    
    * add changelog entry
    
    * shows how many days of change logs are being displayed
    
    * allows user to specify type of input dialog
    
    * properties file should only be loaded once
    
    * allows the user to specify the number of days of change logs to retrieve.
    
    * enforces a min and max on number of days requested
    
    * updates CHANGELOG.md
    
    * updates per PR review
    
    (cherry picked from commit 063eb105d2a86b58e6543f29dbc3bb4c1ec0d9ff)
---
 CHANGELOG.md                                       |   3 +
 traffic_ops/traffic_ops_golang/server/update.go    |  14 --
 .../app/src/common/models/PropertiesModel.js       |   1 +
 .../modules/dialog/input/dialog.input.tpl.html     |   2 +-
 .../table/changeLogs/TableChangeLogsController.js  | 243 ++++++++++++++++++++-
 .../table/changeLogs/table.changeLogs.tpl.html     |  60 ++---
 .../src/modules/private/changeLogs/list/index.js   |   8 +-
 .../app/src/traffic_portal_properties.json         |   4 +
 8 files changed, 283 insertions(+), 52 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ede1bf2..28bfae9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -70,6 +70,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Traffic Ops: added validation for topology updates and server updates/deletions to ensure that topologies have at least one server per cachegroup in each CDN of any assigned delivery services
 - Traffic Ops: added validation for delivery service updates to ensure that topologies have at least one server per cachegroup in each CDN of any assigned delivery services
 - Traffic Ops: added a feature to get delivery services filtered by the `active` flag
+- Traffic Portal: upgraded change log UI table to use more powerful/performant ag-grid component
+- Traffic Portal: change log days are now configurable in traffic_portal_properties.json (default is 7 days) and can be overridden by the user in TP
 
 ### Fixed
 - Fixed #5188 - DSR (delivery service request) incorrectly marked as complete and error message not displaying when DSR fulfilled and DS update fails in Traffic Portal. [Related Github issue](https://github.com/apache/trafficcontrol/issues/5188)
@@ -162,6 +164,7 @@ will be returned indicating that overlap exists.
 - Changed deprecated AsyncHttpClient Java dependency to use new active mirror and updated to version 2.12.1.
 - Changed Traffic Portal to use the more performant and powerful ag-grid for the delivery service request (DSR) table.
 - Updated CDN in a Box to CentOS 8 and added `CENTOS_VERSION` Docker build arg so CDN in a Box can be built for CentOS 7, if desired
+- Traffic Ops: removed change log entry created during server update/revalidation unqueue
 
 ### Deprecated
 - Deprecated the non-nullable `DeliveryService` Go struct and other structs that use it. `DeliveryServiceNullable` structs should be used instead.
diff --git a/traffic_ops/traffic_ops_golang/server/update.go b/traffic_ops/traffic_ops_golang/server/update.go
index 21fcd63..a4e15c1 100644
--- a/traffic_ops/traffic_ops_golang/server/update.go
+++ b/traffic_ops/traffic_ops_golang/server/update.go
@@ -102,20 +102,6 @@ func UpdateHandler(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	err = api.CreateChangeLogBuildMsg(
-		api.ApiChange,
-		api.Updated,
-		inf.User,
-		inf.Tx.Tx,
-		"server-update-status",
-		hostName,
-		map[string]interface{}{"host_name": hostName},
-	)
-	if err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("writing changelog: "+err.Error()))
-		return
-	}
-
 	respMsg := "successfully set server '" + hostName + "'"
 	if hasUpdated {
 		respMsg += " updated=" + strconv.FormatBool(strToBool(updated))
diff --git a/traffic_portal/app/src/common/models/PropertiesModel.js b/traffic_portal/app/src/common/models/PropertiesModel.js
index 5720a18..b1326ca 100644
--- a/traffic_portal/app/src/common/models/PropertiesModel.js
+++ b/traffic_portal/app/src/common/models/PropertiesModel.js
@@ -21,6 +21,7 @@ var PropertiesModel = function() {
 	this.loaded = false;
 
 	this.setProperties = function(properties) {
+		if (this.loaded) return;
 		this.properties = properties;
 		this.loaded = true;
 	};
diff --git a/traffic_portal/app/src/common/modules/dialog/input/dialog.input.tpl.html b/traffic_portal/app/src/common/modules/dialog/input/dialog.input.tpl.html
index 35b5346..19bb68a 100644
--- a/traffic_portal/app/src/common/modules/dialog/input/dialog.input.tpl.html
+++ b/traffic_portal/app/src/common/modules/dialog/input/dialog.input.tpl.html
@@ -24,7 +24,7 @@ under the License.
 <div class="modal-body">
     <p ng-bind-html="params.message"></p>
     <form name="inputForm" novalidate>
-        <input type="text" class="form-control" ng-model="inputValue" ng-maxlength="256">
+        <input type="{{params.type ? params.type : 'text'}}" class="form-control" ng-model="inputValue" ng-maxlength="256">
     </form>
 </div>
 <div class="modal-footer">
diff --git a/traffic_portal/app/src/common/modules/table/changeLogs/TableChangeLogsController.js b/traffic_portal/app/src/common/modules/table/changeLogs/TableChangeLogsController.js
index d8281df..59900f7 100644
--- a/traffic_portal/app/src/common/modules/table/changeLogs/TableChangeLogsController.js
+++ b/traffic_portal/app/src/common/modules/table/changeLogs/TableChangeLogsController.js
@@ -17,25 +17,248 @@
  * under the License.
  */
 
-var TableChangeLogsController = function(changeLogs, $scope, $state, dateUtils) {
+var TableChangeLogsController = function(tableName, changeLogs, $scope, $state, $uibModal, dateUtils, propertiesModel, messageModel) {
 
-	$scope.changeLogs = changeLogs;
+	/**
+	 * Gets value to display a default tooltip.
+	 */
+	function defaultTooltip(params) {
+		return params.value;
+	}
 
-	$scope.getRelativeTime = dateUtils.getRelativeTime;
+	/**
+	 * Formats the contents of a 'lastUpdated' column cell as "relative to now".
+	 */
+	function dateCellFormatterRelative(params) {
+		return params.value ? dateUtils.getRelativeTime(params.value) : params.value;
+	}
 
-	$scope.refresh = function() {
-		$state.reload(); // reloads all the resolves for the view
+	function dateCellFormatter(params) {
+		return params.value.toUTCString();
+	}
+
+	let columns = [
+		{
+			headerName: "Occurred",
+			field: "lastUpdated",
+			hide: false,
+			filter: "agDateColumnFilter",
+			tooltip: dateCellFormatterRelative,
+			valueFormatter: dateCellFormatterRelative
+		},
+		{
+			headerName: "Created (UTC)",
+			field: "lastUpdated",
+			hide: false,
+			filter: "agDateColumnFilter",
+			tooltip: dateCellFormatter,
+			valueFormatter: dateCellFormatter
+		},
+		{
+			headerName: "User",
+			field: "user",
+			hide: false
+		},
+		{
+			headerName: "Level",
+			field: "level",
+			hide: true
+		},
+		{
+			headerName: "Message",
+			field: "message",
+			hide: false
+		}
+	];
+
+	$scope.days = (propertiesModel.properties.changeLogs) ? propertiesModel.properties.changeLogs.days : 7;
+
+	/** All of the change logs - lastUpdated fields converted to actual Date */
+	$scope.changeLogs = changeLogs.map(
+		function(x) {
+			x.lastUpdated = x.lastUpdated ? new Date(x.lastUpdated.replace("+00", "Z")) : x.lastUpdated;
+		});
+
+	$scope.quickSearch = '';
+
+	$scope.pageSize = 100;
+
+	/** Options, configuration, data and callbacks for the ag-grid table. */
+	$scope.gridOptions = {
+		columnDefs: columns,
+		enableCellTextSelection: true,
+		suppressMenuHide: true,
+		multiSortKey: 'ctrl',
+		alwaysShowVerticalScroll: true,
+		defaultColDef: {
+			filter: true,
+			sortable: true,
+			resizable: true,
+			tooltip: defaultTooltip
+		},
+		rowData: changeLogs,
+		pagination: true,
+		paginationPageSize: $scope.pageSize,
+		rowBuffer: 0,
+		onColumnResized: function(params) {
+			localStorage.setItem(tableName + "_table_columns", JSON.stringify($scope.gridOptions.columnApi.getColumnState()));
+		},
+		tooltipShowDelay: 500,
+		allowContextMenuWithControlKey: true,
+		preventDefaultOnContextMenu: true,
+		onColumnVisible: function(params) {
+			if (params.visible){
+				return;
+			}
+			for (let column of params.columns) {
+				if (column.filterActive) {
+					const filterModel = $scope.gridOptions.api.getFilterModel();
+					if (column.colId in filterModel) {
+						delete filterModel[column.colId];
+						$scope.gridOptions.api.setFilterModel(filterModel);
+					}
+				}
+			}
+		},
+		colResizeDefault: "shift"
+	};
+
+	/** Allows the user to change the number of days queried for change logs. */
+	$scope.changeDays = function() {
+		const params = {
+			title: 'Change Number of Days',
+			message: 'Enter the number of days of change logs you need access to (between 1 and 365).',
+			type: 'number'
+		};
+		const modalInstance = $uibModal.open({
+			templateUrl: 'common/modules/dialog/input/dialog.input.tpl.html',
+			controller: 'DialogInputController',
+			size: 'md',
+			resolve: {
+				params: function () {
+					return params;
+				}
+			}
+		});
+		modalInstance.result.then(function(days) {
+			let numOfDays = parseInt(days, 10);
+			if (numOfDays >= 1 && numOfDays <= 365) {
+				propertiesModel.properties.changeLogs.days = numOfDays;
+				$state.reload();
+			} else {
+				messageModel.setMessages([{level: 'error', text: 'Number of days must be between 1 and 365' }], false);
+			}
+		}, function () {
+			console.log('Cancelled');
+		});
+	};
+
+	/** Toggles the visibility of a column that has the ID provided as 'col'. */
+	$scope.toggleVisibility = function(col) {
+		const visible = $scope.gridOptions.columnApi.getColumn(col).isVisible();
+		$scope.gridOptions.columnApi.setColumnVisible(col, !visible);
+	};
+
+	/** Downloads the table as a CSV */
+	$scope.exportCSV = function() {
+		const params = {
+			allColumns: true,
+			fileName: "change_logs.csv",
+		};
+		$scope.gridOptions.api.exportDataAsCsv(params);
+	}
+
+	$scope.onQuickSearchChanged = function() {
+		$scope.gridOptions.api.setQuickFilter($scope.quickSearch);
+		localStorage.setItem(tableName + "_quick_search", $scope.quickSearch);
 	};
 
+	$scope.onPageSizeChanged = function() {
+		const value = Number($scope.pageSize);
+		$scope.gridOptions.api.paginationSetPageSize(value);
+		localStorage.setItem(tableName + "_page_size", value);
+	};
+
+	$scope.clearTableFilters = function() {
+		// clear the quick search
+		$scope.quickSearch = '';
+		$scope.onQuickSearchChanged();
+		// clear any column filters
+		$scope.gridOptions.api.setFilterModel(null);
+	};
+
+	/**** Initialization code, including loading user columns from localstorage ****/
 	angular.element(document).ready(function () {
-		$('#changeLogsTable').dataTable({
-			"aLengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]],
-			"iDisplayLength": 25,
-			"aaSorting": []
+		try {
+			// need to create the show/hide column checkboxes and bind to the current visibility
+			const colstates = JSON.parse(localStorage.getItem(tableName + "_table_columns"));
+			if (colstates) {
+				if (!$scope.gridOptions.columnApi.setColumnState(colstates)) {
+					console.error("Failed to load stored column state: one or more columns not found");
+				}
+			} else {
+				$scope.gridOptions.api.sizeColumnsToFit();
+			}
+		} catch (e) {
+			console.error("Failure to retrieve required column info from localStorage (key=" + tableName + "_table_columns):", e);
+		}
+
+		try {
+			const filterState = JSON.parse(localStorage.getItem(tableName + "_table_filters")) || {};
+			$scope.gridOptions.api.setFilterModel(filterState);
+		} catch (e) {
+			console.error("Failure to load stored filter state:", e);
+		}
+
+		$scope.gridOptions.api.addEventListener("filterChanged", function() {
+			localStorage.setItem(tableName + "_table_filters", JSON.stringify($scope.gridOptions.api.getFilterModel()));
+		});
+
+		try {
+			const sortState = JSON.parse(localStorage.getItem(tableName + "_table_sort"));
+			$scope.gridOptions.api.setSortModel(sortState);
+		} catch (e) {
+			console.error("Failure to load stored sort state:", e);
+		}
+
+		try {
+			$scope.quickSearch = localStorage.getItem(tableName + "_quick_search");
+			$scope.gridOptions.api.setQuickFilter($scope.quickSearch);
+		} catch (e) {
+			console.error("Failure to load stored quick search:", e);
+		}
+
+		try {
+			const ps = localStorage.getItem(tableName + "_page_size");
+			if (ps && ps > 0) {
+				$scope.pageSize = Number(ps);
+				$scope.gridOptions.api.paginationSetPageSize($scope.pageSize);
+			}
+		} catch (e) {
+			console.error("Failure to load stored page size:", e);
+		}
+
+		$scope.gridOptions.api.addEventListener("sortChanged", function() {
+			localStorage.setItem(tableName + "_table_sort", JSON.stringify($scope.gridOptions.api.getSortModel()));
+		});
+
+		$scope.gridOptions.api.addEventListener("columnMoved", function() {
+			localStorage.setItem(tableName + "_table_columns", JSON.stringify($scope.gridOptions.columnApi.getColumnState()));
+		});
+
+		$scope.gridOptions.api.addEventListener("columnVisible", function() {
+			$scope.gridOptions.api.sizeColumnsToFit();
+			try {
+				colStates = $scope.gridOptions.columnApi.getColumnState();
+				localStorage.setItem(tableName + "_table_columns", JSON.stringify(colStates));
+			} catch (e) {
+				console.error("Failed to store column defs to local storage:", e);
+			}
 		});
+
 	});
 
 };
 
-TableChangeLogsController.$inject = ['changeLogs', '$scope', '$state', 'dateUtils'];
+TableChangeLogsController.$inject = ['tableName', 'changeLogs', '$scope', '$state', '$uibModal', 'dateUtils', 'propertiesModel', 'messageModel'];
 module.exports = TableChangeLogsController;
diff --git a/traffic_portal/app/src/common/modules/table/changeLogs/table.changeLogs.tpl.html b/traffic_portal/app/src/common/modules/table/changeLogs/table.changeLogs.tpl.html
index 951801e..4c6f311 100644
--- a/traffic_portal/app/src/common/modules/table/changeLogs/table.changeLogs.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/changeLogs/table.changeLogs.tpl.html
@@ -20,35 +20,45 @@ under the License.
 <div class="x_panel">
     <div class="x_title">
         <ol class="breadcrumb pull-left">
-            <li class="active">Change Logs</li>
+            <li class="active">Change Logs <button type="button" class="btn btn-link" ng-click="changeDays()">[ last {{days}} days ]</button></li>
         </ol>
-        <div class="pull-right" role="group">
-            <button class="btn btn-default" title="Refresh" ng-click="refresh()"><i class="fa fa-refresh"></i></button>
+        <div class="pull-right">
+            <div class="form-inline" role="search">
+                <input id="quickSearch" name="quickSearch" type="search" class="form-control text-input" placeholder="Quick search..." ng-model="quickSearch" ng-change="onQuickSearchChanged()" aria-label="Search"/>
+                <div class="input-group text-input">
+                    <span class="input-group-addon">
+                        <label for="pageSize">Page size</label>
+                    </span>
+                    <input id="pageSize" name="pageSize" type="number" class="form-control" placeholder="100" ng-model="pageSize" ng-change="onPageSizeChanged()" />
+                </div>
+                <div id="toggleColumns" class="btn-group" role="group" title="Select Table Columns" uib-dropdown is-open="columnSettings.isopen">
+                    <button type="button" class="btn btn-default dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" aria-expanded="false">
+                        <i class="fa fa-columns"></i>&nbsp;
+                        <span class="caret"></span>
+                    </button>
+                    <menu ng-click="$event.stopPropagation()" class="column-settings dropdown-menu-right dropdown-menu" uib-dropdown-menu>
+                        <li role="menuitem" ng-repeat="c in gridOptions.columnApi.getAllColumns() | orderBy:'colDef.headerName'">
+                            <div class="checkbox">
+                                <label><input type="checkbox" ng-checked="c.isVisible()" ng-click="toggleVisibility(c.colId)">{{::c.colDef.headerName}}</label>
+                            </div>
+                        </li>
+                    </menu>
+                </div>
+                <div class="btn-group" role="group" uib-dropdown is-open="more.isopen">
+                    <button name="moreBtn" type="button" class="btn btn-default dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" aria-expanded="false">
+                        More&nbsp;
+                        <span class="caret"></span>
+                    </button>
+                    <ul class="dropdown-menu-right dropdown-menu" uib-dropdown-menu>
+                        <li role="menuitem"><button class="menu-item-button" type="button" ng-click="clearTableFilters()">Clear Table Filters</button></li>
+                        <li role="menuitem"><button class="menu-item-button" type="button" ng-click="exportCSV()">Export CSV</button></li>
+                    </ul>
+                </div>
+            </div>
         </div>
         <div class="clearfix"></div>
     </div>
     <div class="x_content">
-        <br>
-        <table id="changeLogsTable" class="table responsive-utilities jambo_table">
-            <thead>
-            <tr class="headings">
-                <th>Occurred</th>
-                <th>Timestamp (UTC)</th>
-                <th>User</th>
-                <th>Type</th>
-                <th>Message</th>
-            </tr>
-            </thead>
-            <tbody>
-            <tr ng-repeat="c in ::changeLogs">
-                <td data-search="^{{::getRelativeTime(c.lastUpdated)}}$" data-order="{{::c.lastUpdated}}">{{::getRelativeTime(c.lastUpdated)}}</td>
-                <td data-search="^{{::c.lastUpdated}}$">{{::c.lastUpdated}}</td>
-                <td data-search="^{{::c.user}}$">{{::c.user}}</td>
-                <td data-search="^{{::c.level}}$">{{::c.level}}</td>
-                <td data-search="^{{::c.message}}$">{{::c.message}}</td>
-            </tr>
-            </tbody>
-        </table>
+        <div style="height: 740px;" ag-grid="gridOptions" class="change-logs-table ag-theme-alpine"></div>
     </div>
 </div>
-
diff --git a/traffic_portal/app/src/modules/private/changeLogs/list/index.js b/traffic_portal/app/src/modules/private/changeLogs/list/index.js
index 7279939..f25f143 100644
--- a/traffic_portal/app/src/modules/private/changeLogs/list/index.js
+++ b/traffic_portal/app/src/modules/private/changeLogs/list/index.js
@@ -27,8 +27,12 @@ module.exports = angular.module('trafficPortal.private.changeLogs.list', [])
 						templateUrl: 'common/modules/table/changeLogs/table.changeLogs.tpl.html',
 						controller: 'TableChangeLogsController',
 						resolve: {
-							changeLogs: function(changeLogService) {
-								return changeLogService.getChangeLogs({ days: 3 });
+							tableName: function() {
+								return 'changeLogs';
+							},
+							changeLogs: function(changeLogService, propertiesModel) {
+								const days = (propertiesModel.properties.changeLogs) ? propertiesModel.properties.changeLogs.days : 7;
+								return changeLogService.getChangeLogs({ days: days });
 							}
 						}
 					}
diff --git a/traffic_portal/app/src/traffic_portal_properties.json b/traffic_portal/app/src/traffic_portal_properties.json
index 770ba91..3766f42 100644
--- a/traffic_portal/app/src/traffic_portal_properties.json
+++ b/traffic_portal/app/src/traffic_portal_properties.json
@@ -203,6 +203,10 @@
         "baseUrl": "https://trafficstats.domain.com/dashboard/script/traffic_ops_server.js?which="
       }
     },
+    "changeLogs": {
+      "_comment": "Change log settings",
+      "days": 7
+    },
     "customMenu": {
       "_comments": "These are custom items you want to add to the menu. 'items' is an array of hashes where each hash has 'name' (the menu item name), 'embed' (true|false to determine if content is embedded in TP or not), and 'url' (the url of the content)",
       "name": "Other",