diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnectionHistory.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnectionHistory.js index 9f5c80da0..216641d78 100644 --- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnectionHistory.js +++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnectionHistory.js @@ -39,8 +39,10 @@ angular.module('settings').directive('guacSettingsConnectionHistory', [function var SortOrder = $injector.get('SortOrder'); // Get required services + var $filter = $injector.get('$filter'); var $routeParams = $injector.get('$routeParams'); var $translate = $injector.get('$translate'); + var csvService = $injector.get('csvService'); var historyService = $injector.get('historyService'); /** @@ -178,6 +180,53 @@ angular.module('settings').directive('guacSettingsConnectionHistory', [function }; + /** + * Initiates a download of a CSV version of the displayed history + * search results. + */ + $scope.downloadCSV = function downloadCSV() { + + // Translate CSV header + $translate([ + 'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_USERNAME', + 'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_STARTDATE', + 'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_DURATION', + 'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_CONNECTION_NAME', + 'SETTINGS_CONNECTION_HISTORY.FILENAME_HISTORY_CSV' + ]).then(function headerTranslated(translations) { + + // Initialize records with translated header row + var records = [[ + translations['SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_USERNAME'], + translations['SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_STARTDATE'], + translations['SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_DURATION'], + translations['SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_CONNECTION_NAME'] + ]]; + + // Add rows for all history entries, using the same sort + // order as the displayed table + angular.forEach( + $filter('orderBy')( + $scope.historyEntryWrappers, + $scope.order.predicate + ), + function pushRecord(historyEntryWrapper) { + records.push([ + historyEntryWrapper.username, + $filter('date')(historyEntryWrapper.startDate, $scope.dateFormat), + historyEntryWrapper.duration / 1000, + historyEntryWrapper.connectionName + ]); + } + ); + + // Save the result + saveAs(csvService.toBlob(records), translations['SETTINGS_CONNECTION_HISTORY.FILENAME_HISTORY_CSV']); + + }); + + }; + // Initialize search results $scope.search(); diff --git a/guacamole/src/main/webapp/app/settings/services/csvService.js b/guacamole/src/main/webapp/app/settings/services/csvService.js new file mode 100644 index 000000000..e68cf15e4 --- /dev/null +++ b/guacamole/src/main/webapp/app/settings/services/csvService.js @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * A service for generating downloadable CSV links given arbitrary data. + */ +angular.module('settings').factory('csvService', [function csvService() { + + var service = {}; + + /** + * Encodes an arbitrary value for inclusion in a CSV file as an individual + * field. With the exception of null and undefined (which are both + * interpreted as equivalent to an empty string), all values are coerced to + * a string and, if non-numeric, included within double quotes. If the + * value itself includes double quotes, those quotes will be properly + * escaped. + * + * @param {*} field + * The arbitrary value to encode. + * + * @return {String} + * The provided value, coerced to a string and properly escaped for + * CSV. + */ + var encodeField = function encodeField(field) { + + // Coerce field to string + if (field === null || field === undefined) + field = ''; + else + field = '' + field; + + // Do not quote numeric fields + if (/^[0-9.]*$/.test(field)) + return field; + + // Enclose all other fields in quotes, escaping any quotes therein + return '"' + field.replace(/"/g, '""') + '"'; + + }; + + /** + * Encodes each of the provided values for inclusion in a CSV file as + * fields within the same record (in the manner specified by + * encodeField()), separated by commas. + * + * @param {*[]} fields + * An array of arbitrary values which make up the record. + * + * @return {String} + * A CSV record containing the each value in the given array. + */ + var encodeRecord = function encodeRecord(fields) { + return fields.map(encodeField).join(','); + }; + + /** + * Encodes an entire array of records as properly-formatted CSV, where each + * entry in the provided array is an array of arbitrary fields. + * + * @param {Array.<*[]>} records + * An array of all records making up the desired CSV. + * + * @return {String} + * An entire CSV containing each provided record, separated by CR+LF + * line terminators. + */ + var encodeCSV = function encodeCSV(records) { + return records.map(encodeRecord).join('\r\n'); + }; + + /** + * Creates a new Blob containing properly-formatted CSV generated from the + * given array of records, where each entry in the provided array is an + * array of arbitrary fields. + * + * @param {Array.<*[]>} records + * An array of all records making up the desired CSV. + * + * @returns {Blob} + * A new Blob containing each provided record in CSV format. + */ + service.toBlob = function toBlob(records) { + return new Blob([ encodeCSV(records) ], { type : 'text/csv' }); + }; + + return service; + +}]); diff --git a/guacamole/src/main/webapp/app/settings/styles/history.css b/guacamole/src/main/webapp/app/settings/styles/history.css index 174903e06..7b05e1ef2 100644 --- a/guacamole/src/main/webapp/app/settings/styles/history.css +++ b/guacamole/src/main/webapp/app/settings/styles/history.css @@ -46,9 +46,26 @@ } -.settings.connectionHistory .filter .search-button { +.settings.connectionHistory .filter .search-string { + -ms-flex: 1 1 auto; + -moz-box-flex: 1; + -webkit-box-flex: 1; + -webkit-flex: 1 1 auto; + flex: 1 1 auto; +} + +.settings.connectionHistory .filter .search-button, +.settings.connectionHistory .filter button { + + -ms-flex: 0 0 auto; + -moz-box-flex: 0; + -webkit-box-flex: 0; + -webkit-flex: 0 0 auto; + flex: 0 0 auto; + margin-top: 0; margin-bottom: 0; + } .settings.connectionHistory .history-list { diff --git a/guacamole/src/main/webapp/app/settings/templates/settingsConnectionHistory.html b/guacamole/src/main/webapp/app/settings/templates/settingsConnectionHistory.html index d0de13459..2963ba138 100644 --- a/guacamole/src/main/webapp/app/settings/templates/settingsConnectionHistory.html +++ b/guacamole/src/main/webapp/app/settings/templates/settingsConnectionHistory.html @@ -7,6 +7,7 @@
+
diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json index 4995f54c8..86090cd00 100644 --- a/guacamole/src/main/webapp/translations/en.json +++ b/guacamole/src/main/webapp/translations/en.json @@ -13,6 +13,7 @@ "ACTION_CONTINUE" : "Continue", "ACTION_DELETE" : "Delete", "ACTION_DELETE_SESSIONS" : "Kill Sessions", + "ACTION_DOWNLOAD" : "Download", "ACTION_LOGIN" : "Login", "ACTION_LOGOUT" : "Logout", "ACTION_MANAGE_CONNECTIONS" : "Connections", @@ -575,10 +576,13 @@ "SETTINGS_CONNECTION_HISTORY" : { - "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", + "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", + "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FILENAME_HISTORY_CSV" : "history.csv", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", "HELP_CONNECTION_HISTORY" : "History records for past connections are listed here and can be sorted by clicking the column headers. To search for specific records, enter a filter string and click \"Search\". Only records which match the provided filter string will be listed.",