GUAC-1193: Implement front end for connection history.

This commit is contained in:
James Muehlner
2015-10-13 23:38:55 -07:00
parent 03c1ac1876
commit fdbc68bb92
11 changed files with 357 additions and 41 deletions

View File

@@ -215,7 +215,7 @@ public class ConnectionRecordSearchTerm {
startCalendar.clear(); startCalendar.clear();
startCalendar.set( startCalendar.set(
Integer.parseInt(year), Integer.parseInt(year),
parseInt(month, 0), parseInt(month, 1) - 1,
parseInt(day, 1) parseInt(day, 1)
); );

View File

@@ -40,19 +40,20 @@
<select id="select" resultMap="ConnectionRecordResultMap"> <select id="select" resultMap="ConnectionRecordResultMap">
SELECT SELECT
connection_id, guacamole_connection.connection_id,
connection_name, guacamole_connection.connection_name,
guacamole_connection_history.user_id, guacamole_user.user_id,
username, guacamole_user.username,
start_date, guacamole_connection_history.start_date,
end_date guacamole_connection_history.end_date
FROM guacamole_connection_history FROM guacamole_connection_history
JOIN guacamole_connection ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
JOIN guacamole_user ON guacamole_connection_history.user_id = guacamole_user.user_id JOIN guacamole_user ON guacamole_connection_history.user_id = guacamole_user.user_id
WHERE WHERE
connection_id = #{identifier,jdbcType=VARCHAR} guacamole_connection.connection_id = #{identifier,jdbcType=VARCHAR}
ORDER BY ORDER BY
start_date DESC, guacamole_connection_history.start_date DESC,
end_date DESC guacamole_connection_history.end_date DESC
</select> </select>
@@ -61,7 +62,6 @@
INSERT INTO guacamole_connection_history ( INSERT INTO guacamole_connection_history (
connection_id, connection_id,
connection_name,
user_id, user_id,
start_date, start_date,
end_date end_date
@@ -108,8 +108,8 @@
<if test="term.startDate != null and term.endDate != null"> <if test="term.startDate != null and term.endDate != null">
OR ( OR (
(start_date BETWEEN #{term.startDate,jdbcType=DATE} AND #{term.endDate,jdbcType=DATE}) (start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP})
AND (end_date BETWEEN #{term.startDate,jdbcType=DATE} AND #{term.endDate,jdbcType=DATE}) AND (end_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP})
) )
</if> </if>
@@ -184,8 +184,8 @@
<if test="term.startDate != null and term.endDate != null"> <if test="term.startDate != null and term.endDate != null">
OR ( OR (
(start_date BETWEEN #{term.startDate,jdbcType=DATE} AND #{term.endDate,jdbcType=DATE}) (start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP})
AND (end_date BETWEEN #{term.startDate,jdbcType=DATE} AND #{term.endDate,jdbcType=DATE}) AND (end_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP})
) )
</if> </if>

View File

@@ -40,19 +40,20 @@
<select id="select" resultMap="ConnectionRecordResultMap"> <select id="select" resultMap="ConnectionRecordResultMap">
SELECT SELECT
connection_id, guacamole_connection.connection_id,
connection_name, guacamole_connection.connection_name,
guacamole_connection_history.user_id, guacamole_user.user_id,
username, guacamole_user.username,
start_date, guacamole_connection_history.start_date,
end_date guacamole_connection_history.end_date
FROM guacamole_connection_history FROM guacamole_connection_history
JOIN guacamole_connection ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
JOIN guacamole_user ON guacamole_connection_history.user_id = guacamole_user.user_id JOIN guacamole_user ON guacamole_connection_history.user_id = guacamole_user.user_id
WHERE WHERE
connection_id = #{identifier,jdbcType=INTEGER}::integer guacamole_connection.connection_id = #{identifier,jdbcType=INTEGER}::integer
ORDER BY ORDER BY
start_date DESC, guacamole_connection_history.start_date DESC,
end_date DESC guacamole_connection_history.end_date DESC
</select> </select>
@@ -61,7 +62,6 @@
INSERT INTO guacamole_connection_history ( INSERT INTO guacamole_connection_history (
connection_id, connection_id,
connection_name,
user_id, user_id,
start_date, start_date,
end_date end_date
@@ -108,8 +108,8 @@
<if test="term.startDate != null and term.endDate != null"> <if test="term.startDate != null and term.endDate != null">
OR ( OR (
(start_date BETWEEN #{term.startDate,jdbcType=DATE} AND #{term.endDate,jdbcType=DATE}) (start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP})
AND (end_date BETWEEN #{term.startDate,jdbcType=DATE} AND #{term.endDate,jdbcType=DATE}) AND (end_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP})
) )
</if> </if>
@@ -184,8 +184,8 @@
<if test="term.startDate != null and term.endDate != null"> <if test="term.startDate != null and term.endDate != null">
OR ( OR (
(start_date BETWEEN #{term.startDate,jdbcType=DATE} AND #{term.endDate,jdbcType=DATE}) (start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP})
AND (end_date BETWEEN #{term.startDate,jdbcType=DATE} AND #{term.endDate,jdbcType=DATE}) AND (end_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP})
) )
</if> </if>

View File

@@ -50,7 +50,7 @@ public class APIConnectionRecordSortPredicate {
* The name (not identifier) of the connection associated with the * The name (not identifier) of the connection associated with the
* connection record. * connection record.
*/ */
connection(ConnectionRecordSet.SortableProperty.CONNECTION_NAME), connectionName(ConnectionRecordSet.SortableProperty.CONNECTION_NAME),
/** /**
* The username (identifier) of the user associated with the connection * The username (identifier) of the user associated with the connection

View File

@@ -173,6 +173,7 @@ angular.module('navigation').factory('userPageService', ['$injector',
var canManageUsers = []; var canManageUsers = [];
var canManageConnections = []; var canManageConnections = [];
var canViewConnectionRecords = [];
var canManageSessions = []; var canManageSessions = [];
// Inspect the contents of each provided permission set // Inspect the contents of each provided permission set
@@ -234,12 +235,13 @@ angular.module('navigation').factory('userPageService', ['$injector',
) )
canManageConnections.push(dataSource); canManageConnections.push(dataSource);
// Determine whether the current user needs access to the session management UI // Determine whether the current user needs access to the session management UI or view connection history
if ( if (
// A user must be a system administrator to manage sessions // A user must be a system administrator to manage sessions
PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER)
) )
canManageSessions.push(dataSource); canManageSessions.push(dataSource);
canViewConnectionRecords.push(dataSource);
}); });
@@ -250,7 +252,18 @@ angular.module('navigation').factory('userPageService', ['$injector',
url : '/settings/sessions' url : '/settings/sessions'
})); }));
} }
// If user can manage connections, add links for connection management pages
angular.forEach(canViewConnectionRecords, function addConnectionHistoryLink(dataSource) {
pages.push(new PageDefinition({
name : [
'USER_MENU.ACTION_VIEW_HISTORY',
translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME'
],
url : '/settings/' + encodeURIComponent(dataSource) + '/history'
}));
});
// If user can manage users, add link to user management page // If user can manage users, add link to user management page
if (canManageUsers.length) { if (canManageUsers.length) {
pages.push(new PageDefinition({ pages.push(new PageDefinition({

View File

@@ -48,6 +48,13 @@ angular.module('rest').factory('ConnectionHistoryEntry', [function defineConnect
*/ */
this.connectionIdentifier = template.connectionIdentifier; this.connectionIdentifier = template.connectionIdentifier;
/**
* The name of the connection associated with this history entry.
*
* @type String
*/
this.connectionName = template.connectionName;
/** /**
* The time that usage began, in seconds since 1970-01-01 00:00:00 UTC. * The time that usage began, in seconds since 1970-01-01 00:00:00 UTC.
* *
@@ -107,7 +114,7 @@ angular.module('rest').factory('ConnectionHistoryEntry', [function defineConnect
* The name of the connection associated with the history entry (not * The name of the connection associated with the history entry (not
* the connection identifier). * the connection identifier).
*/ */
CONNECTION_NAME : 'connection', CONNECTION_NAME : 'connectionName',
/** /**
* The username of the user associated with the history entry (the user * The username of the user associated with the history entry (the user

View File

@@ -0,0 +1,142 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* A directive for viewing connection history records.
*/
angular.module('settings').directive('guacSettingsConnectionHistory', [function guacSettingsConnectionHistory() {
return {
// Element only
restrict: 'E',
replace: true,
scope: {
},
templateUrl: 'app/settings/templates/settingsConnectionHistory.html',
controller: ['$scope', '$injector', function settingsConnectionHistoryController($scope, $injector) {
// Get required types
var SortOrder = $injector.get('SortOrder');
// Get required services
var $routeParams = $injector.get('$routeParams');
var $translate = $injector.get('$translate');
var historyService = $injector.get('historyService');
/**
* The identifier of the currently-selected data source.
*
* @type String
*/
$scope.dataSource = $routeParams.dataSource;
/**
* The identifier of the currently-selected data source.
*
* @type String
*/
$scope.historyRecords = null;
/**
* The search terms to use when filtering the history records.
*
* @type String
*/
$scope.searchString = '';
/**
* The date format for use for start/end dates.
*
* @type String
*/
$scope.dateFormat = null;
/**
* SortOrder instance which stores the sort order of the history
* records.
*
* @type SortOrder
*/
$scope.order = new SortOrder([
'username',
'startDate',
'endDate',
'connectionName'
]);
// Get session date format
$translate('SETTINGS_CONNECTION_HISTORY.FORMAT_DATE')
.then(function dateFormatReceived(retrievedDateFormat) {
// Store received date format
$scope.dateFormat = retrievedDateFormat;
});
/**
* Returns true if the connection history records have been loaded,
* indicating that information needed to render the page is fully
* loaded.
*
* @returns {Boolean}
* true if the history records have been loaded, false
* otherwise.
*
*/
$scope.isLoaded = function isLoaded() {
return $scope.historyRecords !== null
&& $scope.dateFormat !== null;
};
/**
* Query the API for the connection record history, filtered by
* searchString, and ordered by order.
*/
$scope.search = function search() {
// Clear current results
$scope.historyRecords = null;
// Fetch history records
historyService.getConnectionHistory(
$scope.dataSource,
$scope.searchString.split(/\s+/),
$scope.order.predicate
)
.success(function historyRetrieved(historyRecords) {
// Store retrieved permissions
$scope.historyRecords = historyRecords;
});
};
// Initialize search results
$scope.search();
}]
};
}]);

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2014 Glyptodon LLC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
.settings.connectionHistory .filter {
/* IE10 */
display: -ms-flexbox;
-ms-flex-align: stretch;
-ms-flex-direction: row;
/* Ancient Mozilla */
display: -moz-box;
-moz-box-align: stretch;
-moz-box-orient: horizontal;
/* Ancient WebKit */
display: -webkit-box;
-webkit-box-align: stretch;
-webkit-box-orient: horizontal;
/* Old WebKit */
display: -webkit-flex;
-webkit-align-items: stretch;
-webkit-flex-direction: row;
/* W3C */
display: flex;
align-items: stretch;
flex-direction: row;
}
.settings.connectionHistory .filter .search-button {
margin-top: 0;
margin-bottom: 0;
}
.settings.connectionHistory .history-list {
width: 100%;
}

View File

@@ -33,9 +33,10 @@ THE SOFTWARE.
</div> </div>
<!-- Selected tab --> <!-- Selected tab -->
<guac-settings-users ng-if="activeTab === 'users'"></guac-settings-users> <guac-settings-users ng-if="activeTab === 'users'"></guac-settings-users>
<guac-settings-connections ng-if="activeTab === 'connections'"></guac-settings-connections> <guac-settings-connections ng-if="activeTab === 'connections'"></guac-settings-connections>
<guac-settings-sessions ng-if="activeTab === 'sessions'"></guac-settings-sessions> <guac-settings-connection-history ng-if="activeTab === 'history'"></guac-settings-connection-history>
<guac-settings-preferences ng-if="activeTab === 'preferences'"></guac-settings-preferences> <guac-settings-sessions ng-if="activeTab === 'sessions'"></guac-settings-sessions>
<guac-settings-preferences ng-if="activeTab === 'preferences'"></guac-settings-preferences>
</div> </div>

View File

@@ -0,0 +1,74 @@
<div class="settings section connectionHistory">
<!--
Copyright 2015 Glyptodon LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<!-- Connection history -->
<p>{{'SETTINGS_CONNECTION_HISTORY.HELP_CONNECTION_HISTORY' | translate}}</p>
<!-- Search controls -->
<div class="filter">
<input class="search-string" type="text" ng-model="searchString" />
<button class="search-button" ng-click="search()">{{'SETTINGS_CONNECTION_HISTORY.ACTION_SEARCH' | translate}}</button>
</div>
<!-- Search results -->
<div class="results" ng-class="{loading: !isLoaded()}">
<!-- List of current user sessions -->
<table class="sorted history-list">
<thead>
<tr>
<th guac-sort-order="order" guac-sort-property="'username'">
{{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_USERNAME' | translate}}
</th>
<th guac-sort-order="order" guac-sort-property="'startDate'">
{{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_STARTDATE' | translate}}
</th>
<th guac-sort-order="order" guac-sort-property="'endDate'">
{{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_ENDDATE' | translate}}
</th>
<th guac-sort-order="order" guac-sort-property="'connectionName'">
{{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_CONNECTION_NAME' | translate}}
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="historyRecord in historyRecordPage" class="history">
<td>{{historyRecord.username}}</td>
<td>{{historyRecord.startDate | date : dateFormat}}</td>
<td>{{historyRecord.endDate | date : dateFormat}}</td>
<td>{{historyRecord.connectionName}}</td>
</tr>
</tbody>
</table>
<!-- Text displayed if no history exists -->
<p class="placeholder" ng-hide="historyRecordPage.length">
{{'SETTINGS_CONNECTION_HISTORY.INFO_NO_HISTORY' | translate}}
</p>
<!-- Pager for history list -->
<guac-pager page="historyRecordPage" page-size="25"
items="historyRecords | orderBy : order.predicate"></guac-pager>
</div>
</div>

View File

@@ -20,7 +20,9 @@
"ACTION_NAVIGATE_BACK" : "Back", "ACTION_NAVIGATE_BACK" : "Back",
"ACTION_NAVIGATE_HOME" : "Home", "ACTION_NAVIGATE_HOME" : "Home",
"ACTION_SAVE" : "Save", "ACTION_SAVE" : "Save",
"ACTION_SEARCH" : "Search",
"ACTION_UPDATE_PASSWORD" : "Update Password", "ACTION_UPDATE_PASSWORD" : "Update Password",
"ACTION_VIEW_HISTORY" : "History",
"DIALOG_HEADER_ERROR" : "Error", "DIALOG_HEADER_ERROR" : "Error",
@@ -473,6 +475,23 @@
}, },
"SETTINGS_CONNECTION_HISTORY" : {
"ACTION_SEARCH" : "@:APP.ACTION_SEARCH",
"FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
"HELP_CONNECTION_HISTORY" : "All connection history records are listed here. If you wish to filter the connection records, you can enter a query.",
"INFO_NO_HISTORY" : "No matching connection history",
"TABLE_HEADER_SESSION_CONNECTION_NAME" : "Connection name",
"TABLE_HEADER_SESSION_ENDDATE" : "End Time",
"TABLE_HEADER_SESSION_STARTDATE" : "Start Time",
"TABLE_HEADER_SESSION_USERNAME" : "Username"
},
"SETTINGS_CONNECTIONS" : { "SETTINGS_CONNECTIONS" : {
"ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
@@ -561,10 +580,10 @@
"SECTION_HEADER_SESSIONS" : "Active Sessions", "SECTION_HEADER_SESSIONS" : "Active Sessions",
"TABLE_HEADER_SESSION_USERNAME" : "Username",
"TABLE_HEADER_SESSION_STARTDATE" : "Active since",
"TABLE_HEADER_SESSION_REMOTEHOST" : "Remote host",
"TABLE_HEADER_SESSION_CONNECTION_NAME" : "Connection name", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Connection name",
"TABLE_HEADER_SESSION_REMOTEHOST" : "Remote host",
"TABLE_HEADER_SESSION_STARTDATE" : "Active since",
"TABLE_HEADER_SESSION_USERNAME" : "Username",
"TEXT_CONFIRM_DELETE" : "Are you sure you want to kill all selected sessions? The users using these sessions will be immediately disconnected." "TEXT_CONFIRM_DELETE" : "Are you sure you want to kill all selected sessions? The users using these sessions will be immediately disconnected."
@@ -578,7 +597,8 @@
"ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS",
"ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS",
"ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS",
"ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME" "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME",
"ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY"
} }