Merge pull request #159 from glyptodon/session-storage

GUAC-1161: Implement session-local storage.
This commit is contained in:
James Muehlner
2015-04-24 22:22:42 -07:00
7 changed files with 211 additions and 134 deletions

View File

@@ -30,18 +30,34 @@ angular.module('client').factory('guacClientManager', ['$injector',
var ManagedClient = $injector.get('ManagedClient');
// Required services
var $window = $injector.get('$window');
var $rootScope = $injector.get('$rootScope');
var $window = $injector.get('$window');
var sessionStorageFactory = $injector.get('sessionStorageFactory');
var service = {};
/**
* Map of all active managed clients. Each key is the ID of the connection
* used by that client.
* Getter/setter which retrieves or sets the map of all active managed
* clients. Each key is the ID of the connection used by that client.
*
* @type Object.<String, ManagedClient>
* @type Function
*/
service.managedClients = {};
var storedManagedClients = sessionStorageFactory.create({}, function destroyClientStorage() {
// Disconnect all clients when storage is destroyed
service.clear();
});
/**
* Returns a map of all active managed clients. Each key is the ID of the
* connection used by that client.
*
* @returns {Object.<String, ManagedClient>}
* A map of all active managed clients.
*/
service.getManagedClients = function getManagedClients() {
return storedManagedClients();
};
/**
* Removes the existing ManagedClient associated with the connection having
@@ -55,13 +71,15 @@ angular.module('client').factory('guacClientManager', ['$injector',
* true if an existing client was removed, false otherwise.
*/
service.removeManagedClient = function replaceManagedClient(id) {
var managedClients = storedManagedClients();
// Remove client if it exists
if (id in service.managedClients) {
if (id in managedClients) {
// Disconnect and remove
service.managedClients[id].client.disconnect();
delete service.managedClients[id];
managedClients[id].client.disconnect();
delete managedClients[id];
// A client was removed
return true;
@@ -96,7 +114,7 @@ angular.module('client').factory('guacClientManager', ['$injector',
service.removeManagedClient(id);
// Set new client
return service.managedClients[id] = ManagedClient.getInstance(id, connectionParameters);
return storedManagedClients()[id] = ManagedClient.getInstance(id, connectionParameters);
};
@@ -119,12 +137,14 @@ angular.module('client').factory('guacClientManager', ['$injector',
*/
service.getManagedClient = function getManagedClient(id, connectionParameters) {
var managedClients = storedManagedClients();
// Create new managed client if it doesn't already exist
if (!(id in service.managedClients))
service.managedClients[id] = ManagedClient.getInstance(id, connectionParameters);
if (!(id in managedClients))
managedClients[id] = ManagedClient.getInstance(id, connectionParameters);
// Return existing client
return service.managedClients[id];
return managedClients[id];
};
@@ -133,23 +153,20 @@ angular.module('client').factory('guacClientManager', ['$injector',
*/
service.clear = function clear() {
var managedClients = storedManagedClients();
// Disconnect each managed client
for (var id in service.managedClients)
service.managedClients[id].client.disconnect();
for (var id in managedClients)
managedClients[id].client.disconnect();
// Clear managed clients
service.managedClients = {};
storedManagedClients({});
};
// Disconnect all clients when window is unloaded
$window.addEventListener('unload', service.clear);
// Clear clients on logout
$rootScope.$on('guacLogout', function handleLogout() {
service.clear();
});
return service;
}]);

View File

@@ -137,11 +137,13 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
if (rootGroup)
addVisibleConnectionGroup(rootGroup);
var managedClients = guacClientManager.getManagedClients();
// Add all active connections
for (var id in guacClientManager.managedClients) {
for (var id in managedClients) {
// Get corresponding managed client
var client = guacClientManager.managedClients[id];
var client = managedClients[id];
// Add active connections for clients with associated visible objects
if (id in visibleObjects) {
@@ -157,7 +159,7 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) {
// Add recent connections for history entries with associated visible objects
if (historyEntry.id in visibleObjects && !(historyEntry.id in guacClientManager.managedClients)) {
if (historyEntry.id in visibleObjects && !(historyEntry.id in managedClients)) {
var object = visibleObjects[historyEntry.id];
$scope.recentConnections.push(new RecentConnection(object.name, historyEntry));

View File

@@ -23,4 +23,6 @@
/**
* The module for code used to display arbitrary notifications.
*/
angular.module('notification', []);
angular.module('notification', [
'storage'
]);

View File

@@ -23,34 +23,33 @@
/**
* Service for displaying notifications and modal status dialogs.
*/
angular.module('notification').factory('guacNotification', ['$rootScope',
function guacNotification($rootScope) {
angular.module('notification').factory('guacNotification', ['$injector',
function guacNotification($injector) {
// Required services
var $rootScope = $injector.get('$rootScope');
var sessionStorageFactory = $injector.get('sessionStorageFactory');
var service = {};
/**
* The current status notification, or false if no status is currently
* shown.
* Getter/setter which retrieves or sets the current status notification,
* which may simply be false if no status is currently shown.
*
* @type Function
*/
var storedStatus = sessionStorageFactory.create(false);
/**
* Retrieves the current status notification, which may simply be false if
* no status is currently shown.
*
* @type Notification|Boolean
*/
service.status = false;
service.getStatus = function getStatus() {
return storedStatus();
};
/**
* All currently-visible notifications.
*
* @type Notification[]
*/
service.notifications = [];
/**
* The ID of the most recently shown notification, or 0 if no notifications
* have yet been shown.
*
* @type Number
*/
var notificationUniqueID = 0;
/**
* Shows or hides the given notification as a modal status. If a status
* notification is currently shown, no further statuses will be shown
@@ -77,59 +76,10 @@ angular.module('notification').factory('guacNotification', ['$rootScope',
* guacNotification.showStatus(false);
*/
service.showStatus = function showStatus(status) {
if (!service.status || !status)
service.status = status;
if (!storedStatus() || !status)
storedStatus(status);
};
/**
* Adds a notification to the the list of notifications shown.
*
* @param {Notification|Object} notification
* The notification to add.
*
* @returns {Number}
* A unique ID for the notification that's just been added.
*
* @example
*
* var id = guacNotification.addNotification({
* 'title' : 'Download',
* 'text' : 'You have a file ready for download!',
* 'actions' : {
* 'name' : 'download',
* 'callback' : function () {
* // download the file and remove the notification here
* }
* }
* });
*/
service.addNotification = function addNotification(notification) {
var id = ++notificationUniqueID;
service.notifications.push({
notification : notification,
id : id
});
return id;
};
/**
* Remove a notification by unique ID.
*
* @param {Number} id
* The unique ID of the notification to remove. This ID is retrieved
* from the initial call to addNotification.
*/
service.removeNotification = function removeNotification(id) {
for (var i = 0; i < service.notifications.length; i++) {
if (service.notifications[i].id === id) {
service.notifications.splice(i, 1);
return;
}
}
};
// Hide status upon navigation
$rootScope.$on('$routeChangeSuccess', function() {
service.showStatus(false);

View File

@@ -0,0 +1,134 @@
/*
* 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.
*/
/**
* Factory for session-local storage. Creating session-local storage creates a
* getter/setter with semantics tied to the user's session. If a user is logged
* in, the storage is consistent. If the user logs out, the storage will not
* persist new values, and attempts to retrieve the existing value will result
* only in the default value.
*/
angular.module('storage').factory('sessionStorageFactory', ['$injector', function sessionStorageFactory($injector) {
// Required services
var $rootScope = $injector.get('$rootScope');
var authenticationService = $injector.get('authenticationService');
var service = {};
/**
* Creates session-local storage that uses the provided default value or
* getter to obtain new values as necessary. Beware that if the default is
* an object, the resulting getter provide deep copies for new values.
*
* @param {Function|*} [template]
* The default value for new users, or a getter which returns a newly-
* created default value.
*
* @param {Function} [destructor]
* Function which will be called just before the stored value is
* destroyed on logout, if a value is stored.
*
* @returns {Function}
* A getter/setter which returns or sets the current value of the new
* session-local storage. Newly-set values will only persist of the
* user is actually logged in.
*/
service.create = function create(template, destructor) {
/**
* Whether new values may be stored and retrieved.
*
* @type Boolean
*/
var enabled = !!authenticationService.getCurrentToken();
/**
* Getter which returns the default value for this storage.
*
* @type Function
*/
var getter;
// If getter provided, use that
if (typeof template === 'function')
getter = template;
// Otherwise, always create a deep copy
else
getter = function getCopy() {
return angular.copy(template);
};
/**
* The current value of this storage, or undefined if not yet set.
*/
var value = undefined;
// Reset value and allow storage when the user is logged in
$rootScope.$on('guacLogin', function userLoggedIn() {
enabled = true;
value = undefined;
});
// Reset value and disallow storage when the user is logged out
$rootScope.$on('guacLogout', function userLoggedOut() {
// Call destructor before storage is teared down
if (angular.isDefined(value) && destructor)
destructor(value);
// Destroy storage
enabled = false;
value = undefined;
});
// Return getter/setter for value
return function sessionLocalGetterSetter(newValue) {
// Only actually store/retrieve values if enabled
if (enabled) {
// Set value if provided
if (angular.isDefined(newValue))
value = newValue;
// Obtain new value if unset
if (!angular.isDefined(value))
value = getter();
// Return current value
return value;
}
// Otherwise, just pretend to store/retrieve
return angular.isDefined(newValue) ? newValue : getter();
};
};
return service;
}]);

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2013 Glyptodon LLC
* 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
@@ -20,30 +20,9 @@
* THE SOFTWARE.
*/
#notificationArea {
position: fixed;
right: 0.5em;
bottom: 0.5em;
max-width: 25%;
width: 2in;
}
#notificationArea .notification {
font-size: 0.7em;
text-align: center;
width: 100%;
overflow: hidden;
}
#notificationArea .notification .text {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#notificationArea .notification.error .text {
white-space: normal;
text-overflow: clip;
text-align: left;
}
/**
* Module which provides generic storage services.
*/
angular.module('storage', [
'auth'
]);

View File

@@ -38,22 +38,15 @@ THE SOFTWARE.
<div ng-if="!expectedCredentials">
<!-- Global status/error dialog -->
<div ng-class="{shown: guacNotification.status}" class="status-outer">
<div ng-class="{shown: guacNotification.getStatus()}" class="status-outer">
<div class="status-middle">
<guac-notification notification="guacNotification.status"></guac-notification>
<guac-notification notification="guacNotification.getStatus()"></guac-notification>
</div>
</div>
<div id="content" ng-view>
</div>
<!-- Notification area -->
<div id="notificationArea">
<div ng-repeat="wrapper in guacNotification.notifications">
<guac-notification notification="wrapper.notification"></guac-notification>
</div>
</div>
</div>
<!-- Login screen for logged-out users -->