GUACAMOLE-724: Abstract away groups of running clients within their own type.

This commit is contained in:
Michael Jumper
2021-06-16 01:57:24 -07:00
parent bfd3cbc204
commit aae80292cb
16 changed files with 603 additions and 240 deletions

View File

@@ -26,6 +26,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
// Required types // Required types
var ConnectionGroup = $injector.get('ConnectionGroup'); var ConnectionGroup = $injector.get('ConnectionGroup');
var ManagedClient = $injector.get('ManagedClient'); var ManagedClient = $injector.get('ManagedClient');
var ManagedClientGroup = $injector.get('ManagedClientGroup');
var ManagedClientState = $injector.get('ManagedClientState'); var ManagedClientState = $injector.get('ManagedClientState');
var ManagedFilesystem = $injector.get('ManagedFilesystem'); var ManagedFilesystem = $injector.get('ManagedFilesystem');
var Protocol = $injector.get('Protocol'); var Protocol = $injector.get('Protocol');
@@ -152,38 +153,67 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
}; };
/** /**
* The client which should be attached to the client UI. * The set of clients that should be attached to the client UI. This will
* be immediately initialized by a call to updateAttachedClients() below.
* *
* @type ManagedClient[] * @type ManagedClientGroup[]
*/ */
$scope.clients = []; $scope.clientGroup = null;
/** /**
* All active clients which are not any current client ($scope.clients). * @borrows ManagedClientGroup.getName
* Each key is the ID of the connection used by that client.
*
* @type Object.<String, ManagedClient>
*/ */
$scope.otherClients = {}; $scope.getName = ManagedClientGroup.getName;
/** /**
* Reloads the contents of $scope.clients and $scope.otherClients to * Reloads the contents of $scope.clientGroup to reflect the client IDs
* reflect the client IDs currently listed in the URL. * currently listed in the URL.
*/ */
var updateAttachedClients = function updateAttachedClients() { var updateAttachedClients = function updateAttachedClients() {
var ids = $routeParams.id.split(/[ +]/); var previousClients = $scope.clientGroup ? $scope.clientGroup.clients.slice() : [];
detachCurrentGroup();
$scope.clients = []; $scope.clientGroup = guacClientManager.getManagedClientGroup($routeParams.id);
$scope.otherClients = angular.extend({}, guacClientManager.getManagedClients()); $scope.clientGroup.attached = true;
// Separate active clients by whether they should be displayed within // Ensure menu is closed if updated view is not a modification of the
// the current view // current view (has no clients in common). The menu should remain open
ids.forEach(function groupClients(id) { // only while the current view is being modified, not when navigating
$scope.clients.push(guacClientManager.getManagedClient(id)); // to an entirely different view.
delete $scope.otherClients[id]; if (_.isEmpty(_.intersection(previousClients, $scope.clientGroup.clients)))
$scope.menu.shown = false;
};
/**
* Detaches the ManagedClientGroup currently attached to the client
* interface via $scope.clientGroup such that the interface can be safely
* cleaned up or another ManagedClientGroup can take its place.
*/
var detachCurrentGroup = function detachCurrentGroup() {
var managedClientGroup = $scope.clientGroup;
if (managedClientGroup) {
// Flag group as detached
managedClientGroup.attached = false;
// Remove all disconnected clients from management (the user has
// seen their status)
_.filter(managedClientGroup.clients, client => {
var connectionState = client.clientState.connectionState;
return connectionState === ManagedClientState.ConnectionState.DISCONNECTED
|| connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR
|| connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR;
}).forEach(client => {
guacClientManager.removeManagedClient(client.id);
}); });
}
}; };
// Init sets of clients based on current URL ... // Init sets of clients based on current URL ...
@@ -424,7 +454,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
$scope.menu.connectionParameters = ManagedClient.getArgumentModel($scope.client); $scope.menu.connectionParameters = ManagedClient.getArgumentModel($scope.client);
// Disable client keyboard if the menu is shown // Disable client keyboard if the menu is shown
angular.forEach($scope.clients, function updateKeyboardEnabled(client) { angular.forEach($scope.clientGroup.clients, function updateKeyboardEnabled(client) {
client.clientProperties.keyboardEnabled = !menuShown; client.clientProperties.keyboardEnabled = !menuShown;
}); });
@@ -606,15 +636,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
* otherwise. * otherwise.
*/ */
$scope.isConnectionUnstable = function isConnectionUnstable() { $scope.isConnectionUnstable = function isConnectionUnstable() {
return _.findIndex($scope.clientGroup.clients, client => client.clientState.tunnelUnstable) !== -1;
var unstable = false;
angular.forEach($scope.clients, function checkStability(client) {
unstable |= client.clientState.tunnelUnstable;
});
return unstable;
}; };
@@ -830,22 +852,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
// Clean up when view destroyed // Clean up when view destroyed
$scope.$on('$destroy', function clientViewDestroyed() { $scope.$on('$destroy', function clientViewDestroyed() {
detachCurrentGroup();
// Remove client from client manager if no longer connected
var managedClient = $scope.client;
if (managedClient) {
// Get current connection state
var connectionState = managedClient.clientState.connectionState;
// If disconnected, remove from management
if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED
|| connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR
|| connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR)
guacClientManager.removeManagedClient(managedClient.id);
}
}); });
}]); }]);

View File

@@ -29,6 +29,7 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua
var sessionStorageFactory = $injector.get('sessionStorageFactory'); var sessionStorageFactory = $injector.get('sessionStorageFactory');
// Required types // Required types
var ManagedClientGroup = $injector.get('ManagedClientGroup');
var ManagedClientState = $injector.get('ManagedClientState'); var ManagedClientState = $injector.get('ManagedClientState');
/** /**
@@ -49,12 +50,12 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua
scope: { scope: {
/** /**
* The ManagedClient instances associated with the active * The ManagedClientGroup instances associated with the active
* connections to be displayed within this panel. * connections to be displayed within this panel.
* *
* @type ManagedClient[]|Object.<String, ManagedClient> * @type ManagedClientGroup[]
*/ */
clients : '=' clientGroups : '='
}, },
templateUrl: 'app/client/templates/guacClientPanel.html', templateUrl: 'app/client/templates/guacClientPanel.html',
@@ -75,30 +76,42 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua
$scope.panelHidden = panelHidden; $scope.panelHidden = panelHidden;
/** /**
* Returns whether this panel currently has any clients associated * Returns whether this panel currently has any client groups
* with it. * associated with it.
* *
* @return {Boolean} * @return {Boolean}
* true if at least one client is associated with this panel, * true if at least one client group is associated with this
* false otherwise. * panel, false otherwise.
*/ */
$scope.hasClients = function hasClients() { $scope.hasClientGroups = function hasClientGroups() {
return !!_.find($scope.clients, $scope.isManaged); return $scope.clientGroups && $scope.clientGroups.length;
}; };
/** /**
* Returns whether the status of the given client has changed in a * @borrows ManagedClientGroup.getIdentifier
* way that requires the user's attention. This may be due to an */
* error, or due to a server-initiated disconnect. $scope.getIdentifier = ManagedClientGroup.getIdentifier;
/**
* @borrows ManagedClientGroup.getTitle
*/
$scope.getTitle = ManagedClientGroup.getTitle;
/**
* Returns whether the status of any client within the given client
* group has changed in a way that requires the user's attention.
* This may be due to an error, or due to a server-initiated
* disconnect.
* *
* @param {ManagedClient} client * @param {ManagedClientGroup} clientGroup
* The client to test. * The client group to test.
* *
* @returns {Boolean} * @returns {Boolean}
* true if the given client requires the user's attention, * true if the given client requires the user's attention,
* false otherwise. * false otherwise.
*/ */
$scope.hasStatusUpdate = function hasStatusUpdate(client) { $scope.hasStatusUpdate = function hasStatusUpdate(clientGroup) {
return _.findIndex(clientGroup.clients, (client) => {
// Test whether the client has encountered an error // Test whether the client has encountered an error
switch (client.clientState.connectionState) { switch (client.clientState.connectionState) {
@@ -110,36 +123,21 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua
return false; return false;
}) !== -1;
}; };
/** /**
* Returns whether the given client is currently being managed by * Initiates an orderly disconnect of all clients within the given
* the guacClientManager service. * group. The clients are removed from management such that
* attempting to connect to any of the same connections will result
* in new connections being established, rather than displaying a
* notification that the connection has ended.
* *
* @param {ManagedClient} client * @param {ManagedClientGroup} clientGroup
* The client to test. * The group of clients to disconnect.
*
* @returns {Boolean}
* true if the given client is being managed by the
* guacClientManager service, false otherwise.
*/ */
$scope.isManaged = function isManaged(client) { $scope.disconnect = function disconnect(clientGroup) {
return !!guacClientManager.getManagedClients()[client.id]; guacClientManager.removeManagedClientGroup(ManagedClientGroup.getIdentifier(clientGroup));
};
/**
* Initiates an orderly disconnect of the given client. The client
* is removed from management such that attempting to connect to
* the same connection will result in a new connection being
* established, rather than displaying a notification that the
* connection has ended.
*
* @param {type} client
* @returns {undefined}
*/
$scope.disconnect = function disconnect(client) {
client.client.disconnect();
guacClientManager.removeManagedClient(client.id);
}; };
/** /**

View File

@@ -32,50 +32,17 @@ angular.module('client').directive('guacTiledClients', [function guacTiledClient
directive.scope = { directive.scope = {
/** /**
* The Guacamole clients that should be displayed in an evenly-tiled * The group of Guacamole clients that should be displayed in an
* grid arrangement. * evenly-tiled grid arrangement.
* *
* @type ManagedClient[] * @type ManagedClientGroup
*/ */
clients : '=' clientGroup : '='
}; };
directive.controller = ['$scope', '$injector', '$element', directive.controller = ['$scope', '$injector', '$element',
function guacTiledListController($scope, $injector, $element) { function guacTiledClientsController($scope, $injector, $element) {
/**
* Returns the number of columns that should be used to evenly arrange
* all provided clients in a tiled grid.
*
* @returns {Number}
* The number of columns that should be used for the grid of
* clients.
*/
var getColumns = function getColumns() {
if (!$scope.clients || !$scope.clients.length)
return 0;
return Math.ceil(Math.sqrt($scope.clients.length));
};
/**
* Returns the number of rows that should be used to evenly arrange all
* provided clients in a tiled grid.
*
* @returns {Number}
* The number of rows that should be used for the grid of clients.
*/
var getRows = function getRows() {
if (!$scope.clients || !$scope.clients.length)
return 0;
return Math.ceil($scope.clients.length / getColumns());
};
/** /**
* Assigns keyboard focus to the given client, allowing that client to * Assigns keyboard focus to the given client, allowing that client to
@@ -86,7 +53,14 @@ angular.module('client').directive('guacTiledClients', [function guacTiledClient
* The client that should receive keyboard focus. * The client that should receive keyboard focus.
*/ */
$scope.assignFocus = function assignFocus(client) { $scope.assignFocus = function assignFocus(client) {
// Clear focus of all other clients
$scope.clientGroup.clients.forEach(client => {
client.clientProperties.focused = false;
});
client.clientProperties.focused = true; client.clientProperties.focused = true;
}; };
/** /**
@@ -98,7 +72,7 @@ angular.module('client').directive('guacTiledClients', [function guacTiledClient
* otherwise. * otherwise.
*/ */
$scope.hasMultipleClients = function hasMultipleClients() { $scope.hasMultipleClients = function hasMultipleClients() {
return $scope.clients && $scope.clients.length > 1; return $scope.clientGroup && $scope.clientGroup.clients.length > 1;
}; };
/** /**
@@ -109,7 +83,7 @@ angular.module('client').directive('guacTiledClients', [function guacTiledClient
* The CSS width that should be applied to each tile. * The CSS width that should be applied to each tile.
*/ */
$scope.getTileWidth = function getTileWidth() { $scope.getTileWidth = function getTileWidth() {
return Math.floor(100 / getColumns()) + '%'; return Math.floor(100 / $scope.clientGroup.columns) + '%';
}; };
/** /**
@@ -120,22 +94,7 @@ angular.module('client').directive('guacTiledClients', [function guacTiledClient
* The CSS height that should be applied to each tile. * The CSS height that should be applied to each tile.
*/ */
$scope.getTileHeight = function getTileHeight() { $scope.getTileHeight = function getTileHeight() {
return Math.floor(100 / getRows()) + '%'; return Math.floor(100 / $scope.clientGroup.rows) + '%';
};
/**
* Returns the display title of the given Guacamole client. If the
* title is not yet known, a placeholder title will be returned.
*
* @param {ManagedClient} client
* The client whose title should be retrieved.
*
* @returns {String}
* The title of the given client, or a placeholder title if the
* client's title is not yet known.
*/
$scope.getClientTitle = function getClientTitle(client) {
return client.title || '...';
}; };
}]; }];

View File

@@ -0,0 +1,73 @@
/*
* 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 directive for displaying a group of Guacamole clients as a non-interactive
* thumbnail of tiled client displays.
*/
angular.module('client').directive('guacTiledThumbnails', [function guacTiledThumbnails() {
var directive = {
restrict: 'E',
replace: true,
templateUrl: 'app/client/templates/guacTiledThumbnails.html'
};
directive.scope = {
/**
* The group of clients to display as a thumbnail of tiled client
* displays.
*
* @type ManagedClientGroup
*/
clientGroup : '='
};
directive.controller = ['$scope', '$injector', '$element',
function guacTiledThumbnailsController($scope, $injector, $element) {
/**
* Returns the CSS width that should be applied to each tile to
* achieve an even arrangement.
*
* @returns {String}
* The CSS width that should be applied to each tile.
*/
$scope.getTileWidth = function getTileWidth() {
return Math.floor(100 / $scope.clientGroup.columns) + '%';
};
/**
* Returns the CSS height that should be applied to each tile to
* achieve an even arrangement.
*
* @returns {String}
* The CSS height that should be applied to each tile.
*/
$scope.getTileHeight = function getTileHeight() {
return Math.floor(100 / $scope.clientGroup.rows) + '%';
};
}];
return directive;
}]);

View File

@@ -25,6 +25,7 @@ angular.module('client').factory('guacClientManager', ['$injector',
// Required types // Required types
var ManagedClient = $injector.get('ManagedClient'); var ManagedClient = $injector.get('ManagedClient');
var ManagedClientGroup = $injector.get('ManagedClientGroup');
// Required services // Required services
var $window = $injector.get('$window'); var $window = $injector.get('$window');
@@ -56,6 +57,53 @@ angular.module('client').factory('guacClientManager', ['$injector',
return storedManagedClients(); return storedManagedClients();
}; };
/**
* Getter/setter which retrieves or sets the array of all active managed
* client groups.
*
* @type Function
*/
var storedManagedClientGroups = sessionStorageFactory.create([], function destroyClientGroupStorage() {
// Disconnect all clients when storage is destroyed
service.clear();
});
/**
* Returns an array of all managed client groups.
*
* @returns {ManagedClientGroup[]>}
* An array of all active managed client groups.
*/
service.getManagedClientGroups = function getManagedClientGroups() {
return storedManagedClientGroups();
};
/**
* Removes the ManagedClient with the given ID from all
* ManagedClientGroups, automatically adjusting the tile size of the
* clients that remain in each group. All client groups that are empty
* after the client is removed will also be removed.
*
* @param {string} id
* The ID of the ManagedClient to remove.
*/
var ungroupManagedClient = function ungroupManagedClient(id) {
var managedClientGroups = storedManagedClientGroups();
// Remove client from all groups
managedClientGroups.forEach(group => {
_.remove(group.clients, client => (client.id === id));
ManagedClientGroup.recalculateTiles(group);
});
// Remove any groups that are now empty
_.remove(managedClientGroups, group => !group.clients.length);
};
/** /**
* Removes the existing ManagedClient associated with the connection having * Removes the existing ManagedClient associated with the connection having
* the given ID, if any. If no such a ManagedClient already exists, this * the given ID, if any. If no such a ManagedClient already exists, this
@@ -67,13 +115,16 @@ angular.module('client').factory('guacClientManager', ['$injector',
* @returns {Boolean} * @returns {Boolean}
* true if an existing client was removed, false otherwise. * true if an existing client was removed, false otherwise.
*/ */
service.removeManagedClient = function replaceManagedClient(id) { service.removeManagedClient = function removeManagedClient(id) {
var managedClients = storedManagedClients(); var managedClients = storedManagedClients();
// Remove client if it exists // Remove client if it exists
if (id in managedClients) { if (id in managedClients) {
// Pull client out of any containing groups
ungroupManagedClient(id);
// Disconnect and remove // Disconnect and remove
managedClients[id].client.disconnect(); managedClients[id].client.disconnect();
delete managedClients[id]; delete managedClients[id];
@@ -102,11 +153,31 @@ angular.module('client').factory('guacClientManager', ['$injector',
*/ */
service.replaceManagedClient = function replaceManagedClient(id) { service.replaceManagedClient = function replaceManagedClient(id) {
// Disconnect any existing client var managedClients = storedManagedClients();
service.removeManagedClient(id); var managedClientGroups = storedManagedClientGroups();
// Set new client // Remove client if it exists
return storedManagedClients()[id] = ManagedClient.getInstance(id); if (id in managedClients) {
var hadFocus = managedClients[id].clientProperties.focused;
managedClients[id].client.disconnect();
delete managedClients[id];
// Remove client from all groups
managedClientGroups.forEach(group => {
var index = _.findIndex(group.clients, client => (client.id === id));
if (index === -1)
return;
group.clients[index] = managedClients[id] = ManagedClient.getInstance(id);
managedClients[id].clientProperties.focused = hadFocus;
});
}
return managedClients[id];
}; };
@@ -126,6 +197,10 @@ angular.module('client').factory('guacClientManager', ['$injector',
var managedClients = storedManagedClients(); var managedClients = storedManagedClients();
// Ensure any existing client is removed from its containing group
// prior to being returned
ungroupManagedClient(id);
// Create new managed client if it doesn't already exist // Create new managed client if it doesn't already exist
if (!(id in managedClients)) if (!(id in managedClients))
managedClients[id] = ManagedClient.getInstance(id); managedClients[id] = ManagedClient.getInstance(id);
@@ -136,7 +211,81 @@ angular.module('client').factory('guacClientManager', ['$injector',
}; };
/** /**
* Disconnects and removes all currently-connected clients. * Returns the ManagedClientGroup having the given ID. If no such
* ManagedClientGroup exists, a new ManagedClientGroup is created by
* extracting the relevant connections from the ID.
*
* @param {String} id
* The ID of the ManagedClientGroup to retrieve or create.
*
* @returns {ManagedClientGroup}
* The ManagedClientGroup having the given ID.
*/
service.getManagedClientGroup = function getManagedClientGroup(id) {
var clients = [];
var clientIds = ManagedClientGroup.getClientIdentifiers(id);
// Separate active clients by whether they should be displayed within
// the current view
clientIds.forEach(function groupClients(id) {
clients.push(service.getManagedClient(id));
});
if (clients.length === 1) {
clients[0].clientProperties.focused = true;
}
var group = new ManagedClientGroup({
clients : clients
});
var managedClientGroups = storedManagedClientGroups();
managedClientGroups.push(group);
return group;
};
/**
* Removes the existing ManagedClientGroup having the given ID, if any,
* disconnecting and removing all ManagedClients associated with that
* group. If no such a ManagedClientGroup currently exists, this function
* has no effect.
*
* @param {String} id
* The ID of the ManagedClientGroup to remove.
*
* @returns {Boolean}
* true if a ManagedClientGroup was removed, false otherwise.
*/
service.removeManagedClientGroup = function removeManagedClientGroup(id) {
var managedClients = storedManagedClients();
var managedClientGroups = storedManagedClientGroups();
// Remove all matching groups (there SHOULD only be one)
var removed = _.remove(managedClientGroups, (group) => ManagedClientGroup.getIdentifier(group) === id);
// Disconnect all clients associated with the removed group(s)
removed.forEach((group) => {
group.clients.forEach((client) => {
var id = client.id;
if (managedClients[id]) {
managedClients[id].client.disconnect();
delete managedClients[id];
}
});
});
return !!removed.length;
};
/**
* Disconnects and removes all currently-connected clients and client
* groups.
*/ */
service.clear = function clear() { service.clear = function clear() {
@@ -146,8 +295,9 @@ angular.module('client').factory('guacClientManager', ['$injector',
for (var id in managedClients) for (var id in managedClients)
managedClients[id].client.disconnect(); managedClients[id].client.disconnect();
// Clear managed clients // Clear managed clients and client groups
storedManagedClients({}); storedManagedClients({});
storedManagedClientGroups([]);
}; };

View File

@@ -9,12 +9,7 @@
<div class="client-body" guac-touch-drag="clientDrag" guac-touch-pinch="clientPinch"> <div class="client-body" guac-touch-drag="clientDrag" guac-touch-pinch="clientPinch">
<!-- All connections in current display --> <!-- All connections in current display -->
<guac-tiled-clients clients="clients"></guac-tiled-clients> <guac-tiled-clients client-group="clientGroup"></guac-tiled-clients>
<!-- All other active connections -->
<div id="other-connections">
<guac-client-panel clients="otherClients"></guac-client-panel>
</div>
</div> </div>
@@ -52,9 +47,9 @@
<!-- Stationary header --> <!-- Stationary header -->
<div class="header"> <div class="header">
<h2 ng-hide="rootConnectionGroups">{{client.name}}</h2> <h2 ng-hide="rootConnectionGroups">{{ getName(clientGroup) }}</h2>
<h2 class="connection-select-menu" ng-show="rootConnectionGroups"> <h2 class="connection-select-menu" ng-show="rootConnectionGroups">
<guac-menu menu-title="client.name" interactive="true"> <guac-menu menu-title="getName(clientGroup)" interactive="true">
<div class="all-connections"> <div class="all-connections">
<guac-group-list-filter connection-groups="rootConnectionGroups" <guac-group-list-filter connection-groups="rootConnectionGroups"
filtered-connection-groups="filteredRootConnectionGroups" filtered-connection-groups="filteredRootConnectionGroups"

View File

@@ -1,29 +1,27 @@
<div class="client-panel" <div class="client-panel"
ng-class="{ 'has-clients': hasClients(), 'hidden' : panelHidden() }"> ng-class="{ 'has-clients': hasClientGroups(), 'hidden' : panelHidden() }">
<!-- Toggle panel visibility --> <!-- Toggle panel visibility -->
<div class="client-panel-handle" ng-click="togglePanel()"></div> <div class="client-panel-handle" ng-click="togglePanel()"></div>
<!-- List of connection thumbnails --> <!-- List of connection thumbnails -->
<ul class="client-panel-connection-list"> <ul class="client-panel-connection-list">
<li ng-repeat="client in clients | toArray | orderBy: [ '-value.lastUsed', 'value.title' ]" <li ng-repeat="clientGroup in clientGroups"
ng-class="{ 'needs-attention' : hasStatusUpdate(client.value) }" ng-if="!clientGroup.attached"
ng-show="isManaged(client.value)" ng-class="{ 'needs-attention' : hasStatusUpdate(clientGroup) }"
class="client-panel-connection"> class="client-panel-connection">
<!-- Close connection --> <!-- Close connection -->
<button class="close-other-connection" ng-click="disconnect(client.value)"> <button class="close-other-connection" ng-click="disconnect(clientGroup)">
<img ng-attr-alt="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}" <img ng-attr-alt="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
ng-attr-title="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}" ng-attr-title="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
src="images/x.png"> src="images/x.png">
</button> </button>
<!-- Thumbnail --> <!-- Thumbnail -->
<a href="#/client/{{client.value.id}}"> <a href="#/client/{{ getIdentifier(clientGroup) }}">
<div class="thumbnail"> <guac-tiled-thumbnails client-group="clientGroup"></guac-tiled-thumbnails>
<guac-thumbnail client="client.value"></guac-thumbnail> <div class="name">{{ getTitle(clientGroup) }}</div>
</div>
<div class="name">{{ client.value.title }}</div>
</a> </a>
</li> </li>

View File

@@ -1,12 +1,12 @@
<ul class="tiled-client-list" ng-class="{ 'multiple-clients' : hasMultipleClients() }"> <ul class="tiled-client-list" ng-class="{ 'multiple-clients' : hasMultipleClients() }">
<li class="client-tile" <li class="client-tile"
ng-repeat="client in clients" ng-repeat="client in clientGroup.clients"
ng-style="{ 'width' : getTileWidth(), 'height' : getTileHeight() }" ng-style="{ 'width' : getTileWidth(), 'height' : getTileHeight() }"
ng-class="{ 'focused' : client.clientProperties.focused }" ng-class="{ 'focused' : client.clientProperties.focused }"
ng-click="assignFocus(client)"> ng-click="assignFocus(client)">
<h3>{{ getClientTitle(client) }}</h3> <h3>{{ client.title }}</h3>
<guac-client client="client"></guac-client> <guac-client client="client"></guac-client>
<!-- Client-specific status/error dialog --> <!-- Client-specific status/error dialog -->

View File

@@ -0,0 +1,7 @@
<ul class="tiled-client-list">
<li class="client-tile"
ng-repeat="client in clientGroup.clients"
ng-style="{ 'width' : getTileWidth(), 'height' : getTileHeight() }">
<guac-thumbnail client="client"></guac-thumbnail>
</li>
</ul>

View File

@@ -62,8 +62,9 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
/** /**
* Object which serves as a surrogate interface, encapsulating a Guacamole * Object which serves as a surrogate interface, encapsulating a Guacamole
* client while it is active, allowing it to be detached and reattached * client while it is active, allowing it to be maintained in the
* from different client views. * background. One or more ManagedClients are grouped within
* ManagedClientGroups before being attached to the client view.
* *
* @constructor * @constructor
* @param {ManagedClient|Object} [template={}] * @param {ManagedClient|Object} [template={}]

View File

@@ -0,0 +1,209 @@
/*
* 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.
*/
/**
* Provides the ManagedClientGroup class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedClientGroup', [function defineManagedClientGroup() {
/**
* Object which serves as a grouping of ManagedClients. Each
* ManagedClientGroup may be attached, detached, and reattached dynamically
* from different client views, with its contents automatically displayed
* in a tiled arrangment if needed.
*
* @constructor
* @param {ManagedClientGroup|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedClientGroup.
*/
var ManagedClientGroup = function ManagedClientGroup(template) {
// Use empty object by default
template = template || {};
/**
* Whether this ManagedClientGroup is currently attached to the client
* interface (true) or is running in the background (false).
*
* @type {boolean}
* @default false
*/
this.attached = template.attached || false;
/**
* The clients that should be displayed within the client interface
* when this group is attached.
*
* @type {ManagedClient[]}
* @default []
*/
this.clients = template.clients || [];
/**
* The number of rows that should be used when arranging the clients
* within this group in a grid. By default, this value is automatically
* calculated from the number of clients.
*
* @type {number}
*/
this.rows = template.rows || ManagedClientGroup.getRows(this);
/**
* The number of columns that should be used when arranging the clients
* within this group in a grid. By default, this value is automatically
* calculated from the number of clients.
*
* @type {number}
*/
this.columns = template.columns || ManagedClientGroup.getColumns(this);
};
/**
* Updates the number of rows and columns stored within the given
* ManagedClientGroup such that the clients within the group are evenly
* distributed. This function should be called whenever the size of a
* group changes.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup that should be updated.
*/
ManagedClientGroup.recalculateTiles = function recalculateTiles(group) {
var recalculated = new ManagedClientGroup({
clients : group.clients
});
group.rows = recalculated.rows;
group.columns = recalculated.columns;
};
/**
* Returns the unique ID representing the given ManagedClientGroup. The ID
* of each ManagedClientGroup consists simply of the IDs of all its
* ManagedClients, separated by periods.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup to determine the ID of.
*
* @returns {string}
* The unique ID representing the given ManagedClientGroup.
*/
ManagedClientGroup.getIdentifier = function getIdentifier(group) {
return _.map(group.clients, client => client.id).join('.');
};
/**
* Returns an array of client identifiers for all clients contained within
* the given ManagedClientGroup. Order of the identifiers is preserved
* with respect to the order of the clients within the group.
*
* @param {ManagedClientGroup|string} group
* The ManagedClientGroup to retrieve the client identifiers from,
* or its ID.
*
* @returns {string[]}
* The client identifiers of all clients contained within the given
* ManagedClientGroup.
*/
ManagedClientGroup.getClientIdentifiers = function getClientIdentifiers(group) {
if (_.isString(group))
return group.split(/\./);
return group.clients.map(client => client.id);
};
/**
* Returns the number of columns that should be used to evenly arrange
* all provided clients in a tiled grid.
*
* @returns {Number}
* The number of columns that should be used for the grid of
* clients.
*/
ManagedClientGroup.getColumns = function getColumns(group) {
if (!group.clients.length)
return 0;
return Math.ceil(Math.sqrt(group.clients.length));
};
/**
* Returns the number of rows that should be used to evenly arrange all
* provided clients in a tiled grid.
*
* @returns {Number}
* The number of rows that should be used for the grid of clients.
*/
ManagedClientGroup.getRows = function getRows(group) {
if (!group.clients.length)
return 0;
return Math.ceil(group.clients.length / ManagedClientGroup.getColumns(group));
};
/**
* Returns the title which should be displayed as the page title if the
* given client group is attached to the interface.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup to determine the title of.
*
* @returns {string}
* The title of the given ManagedClientGroup.
*/
ManagedClientGroup.getTitle = function getTitle(group) {
// Use client-specific title if only one client
if (group.clients.length === 1)
return group.clients[0].title;
// With multiple clients, somehow combining multiple page titles would
// be confusing. Instead, use the combined names.
return ManagedClientGroup.getName(group);
};
/**
* Returns the combined names of all clients within the given
* ManagedClientGroup, as determined by the names of the associated
* connections or connection groups.
*
* @param {ManagedClientGroup} group
* The ManagedClientGroup to determine the name of.
*
* @returns {string}
* The combined names of all clients within the given
* ManagedClientGroup.
*/
ManagedClientGroup.getName = function getName(group) {
return _.filter(group.clients, (client => !!client.name)).map(client => client.name).join(', ') || '...';
};
return ManagedClientGroup;
}]);

View File

@@ -18,7 +18,8 @@
*/ */
/** /**
* A directive which displays the contents of a connection group. * A directive which displays the recently-accessed connections nested beneath
* each of the given connection groups.
*/ */
angular.module('home').directive('guacRecentConnections', [function guacRecentConnections() { angular.module('home').directive('guacRecentConnections', [function guacRecentConnections() {
@@ -44,21 +45,12 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
controller: ['$scope', '$injector', function guacRecentConnectionsController($scope, $injector) { controller: ['$scope', '$injector', function guacRecentConnectionsController($scope, $injector) {
// Required types // Required types
var ActiveConnection = $injector.get('ActiveConnection');
var ClientIdentifier = $injector.get('ClientIdentifier'); var ClientIdentifier = $injector.get('ClientIdentifier');
var RecentConnection = $injector.get('RecentConnection'); var RecentConnection = $injector.get('RecentConnection');
// Required services // Required services
var guacClientManager = $injector.get('guacClientManager');
var guacHistory = $injector.get('guacHistory'); var guacHistory = $injector.get('guacHistory');
/**
* Array of all known and visible active connections.
*
* @type ActiveConnection[]
*/
$scope.activeConnections = [];
/** /**
* Array of all known and visible recently-used connections. * Array of all known and visible recently-used connections.
* *
@@ -68,16 +60,12 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
/** /**
* Returns whether recent connections are available for display. * Returns whether recent connections are available for display.
* Note that, for the sake of this directive, recent connections
* include any currently-active connections, even if they are not
* yet in the history.
* *
* @returns {Boolean} * @returns {Boolean}
* true if recent (or active) connections are present, false * true if recent connections are present, false otherwise.
* otherwise.
*/ */
$scope.hasRecentConnections = function hasRecentConnections() { $scope.hasRecentConnections = function hasRecentConnections() {
return !!($scope.activeConnections.length || $scope.recentConnections.length); return !!$scope.recentConnections.length;
}; };
/** /**
@@ -149,7 +137,6 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
$scope.$watch("rootGroups", function setRootGroups(rootGroups) { $scope.$watch("rootGroups", function setRootGroups(rootGroups) {
// Clear connection arrays // Clear connection arrays
$scope.activeConnections = [];
$scope.recentConnections = []; $scope.recentConnections = [];
// Produce collection of visible objects // Produce collection of visible objects
@@ -160,29 +147,11 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
}); });
} }
var managedClients = guacClientManager.getManagedClients();
// Add all active connections
for (var id in managedClients) {
// Get corresponding managed client
var client = managedClients[id];
// Add active connections for clients with associated visible objects
if (id in visibleObjects) {
var object = visibleObjects[id];
$scope.activeConnections.push(new ActiveConnection(object.name, client));
}
}
// Add any recent connections that are visible // Add any recent connections that are visible
guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) { guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) {
// Add recent connections for history entries with associated visible objects // Add recent connections for history entries with associated visible objects
if (historyEntry.id in visibleObjects && !(historyEntry.id in managedClients)) { if (historyEntry.id in visibleObjects) {
var object = visibleObjects[historyEntry.id]; var object = visibleObjects[historyEntry.id];
$scope.recentConnections.push(new RecentConnection(object.name, historyEntry)); $scope.recentConnections.push(new RecentConnection(object.name, historyEntry));

View File

@@ -3,23 +3,6 @@
<!-- Text displayed if no recent connections exist --> <!-- Text displayed if no recent connections exist -->
<p class="placeholder" ng-hide="hasRecentConnections()">{{'HOME.INFO_NO_RECENT_CONNECTIONS' | translate}}</p> <p class="placeholder" ng-hide="hasRecentConnections()">{{'HOME.INFO_NO_RECENT_CONNECTIONS' | translate}}</p>
<!-- All active connections -->
<div ng-repeat="activeConnection in activeConnections" class="connection">
<a href="#/client/{{activeConnection.client.id}}">
<!-- Connection thumbnail -->
<div class="thumbnail">
<guac-thumbnail client="activeConnection.client"></guac-thumbnail>
</div>
<!-- Connection name -->
<div class="caption">
<span class="name">{{activeConnection.name}}</span>
</div>
</a>
</div>
<!-- All recent connections --> <!-- All recent connections -->
<div ng-repeat="recentConnection in recentConnections" class="connection"> <div ng-repeat="recentConnection in recentConnections" class="connection">
<a href="#/client/{{recentConnection.entry.id}}"> <a href="#/client/{{recentConnection.entry.id}}">

View File

@@ -29,6 +29,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
var $window = $injector.get('$window'); var $window = $injector.get('$window');
var clipboardService = $injector.get('clipboardService'); var clipboardService = $injector.get('clipboardService');
var guacNotification = $injector.get('guacNotification'); var guacNotification = $injector.get('guacNotification');
var guacClientManager = $injector.get('guacClientManager');
/** /**
* The error that prevents the current page from rendering at all. If no * The error that prevents the current page from rendering at all. If no
@@ -43,6 +44,14 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
*/ */
$scope.guacNotification = guacNotification; $scope.guacNotification = guacNotification;
/**
* All currently-active connections, grouped into their corresponding
* tiled views.
*
* @type ManagedClientGroup[]
*/
$scope.getManagedClientGroups = guacClientManager.getManagedClientGroups;
/** /**
* The message to display to the user as instructions for the login * The message to display to the user as instructions for the login
* process. * process.

View File

@@ -20,7 +20,7 @@
#other-connections .client-panel { #other-connections .client-panel {
display: none; display: none;
position: absolute; position: fixed;
right: 0; right: 0;
bottom: 0; bottom: 0;

View File

@@ -80,6 +80,11 @@
<div id="content" ng-view> <div id="content" ng-view>
</div> </div>
<!-- All active connections -->
<div id="other-connections">
<guac-client-panel client-groups="getManagedClientGroups()"></guac-client-panel>
</div>
</div> </div>
<!-- Polyfills --> <!-- Polyfills -->