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
var ConnectionGroup = $injector.get('ConnectionGroup');
var ManagedClient = $injector.get('ManagedClient');
var ManagedClientGroup = $injector.get('ManagedClientGroup');
var ManagedClientState = $injector.get('ManagedClientState');
var ManagedFilesystem = $injector.get('ManagedFilesystem');
var Protocol = $injector.get('Protocol');
@@ -152,37 +153,66 @@ 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).
* Each key is the ID of the connection used by that client.
*
* @type Object.<String, ManagedClient>
* @borrows ManagedClientGroup.getName
*/
$scope.otherClients = {};
$scope.getName = ManagedClientGroup.getName;
/**
* Reloads the contents of $scope.clients and $scope.otherClients to
* reflect the client IDs currently listed in the URL.
* Reloads the contents of $scope.clientGroup to reflect the client IDs
* currently listed in the URL.
*/
var updateAttachedClients = function updateAttachedClients() {
var ids = $routeParams.id.split(/[ +]/);
var previousClients = $scope.clientGroup ? $scope.clientGroup.clients.slice() : [];
detachCurrentGroup();
$scope.clients = [];
$scope.otherClients = angular.extend({}, guacClientManager.getManagedClients());
$scope.clientGroup = guacClientManager.getManagedClientGroup($routeParams.id);
$scope.clientGroup.attached = true;
// Separate active clients by whether they should be displayed within
// the current view
ids.forEach(function groupClients(id) {
$scope.clients.push(guacClientManager.getManagedClient(id));
delete $scope.otherClients[id];
});
// Ensure menu is closed if updated view is not a modification of the
// current view (has no clients in common). The menu should remain open
// only while the current view is being modified, not when navigating
// to an entirely different view.
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);
});
}
};
@@ -424,7 +454,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
$scope.menu.connectionParameters = ManagedClient.getArgumentModel($scope.client);
// 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;
});
@@ -606,15 +636,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
* otherwise.
*/
$scope.isConnectionUnstable = function isConnectionUnstable() {
var unstable = false;
angular.forEach($scope.clients, function checkStability(client) {
unstable |= client.clientState.tunnelUnstable;
});
return unstable;
return _.findIndex($scope.clientGroup.clients, client => client.clientState.tunnelUnstable) !== -1;
};
@@ -830,22 +852,7 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
// Clean up when view destroyed
$scope.$on('$destroy', function clientViewDestroyed() {
// 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);
}
detachCurrentGroup();
});
}]);

View File

@@ -29,6 +29,7 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua
var sessionStorageFactory = $injector.get('sessionStorageFactory');
// Required types
var ManagedClientGroup = $injector.get('ManagedClientGroup');
var ManagedClientState = $injector.get('ManagedClientState');
/**
@@ -49,12 +50,12 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua
scope: {
/**
* The ManagedClient instances associated with the active
* The ManagedClientGroup instances associated with the active
* connections to be displayed within this panel.
*
* @type ManagedClient[]|Object.<String, ManagedClient>
* @type ManagedClientGroup[]
*/
clients : '='
clientGroups : '='
},
templateUrl: 'app/client/templates/guacClientPanel.html',
@@ -75,71 +76,68 @@ angular.module('client').directive('guacClientPanel', ['$injector', function gua
$scope.panelHidden = panelHidden;
/**
* Returns whether this panel currently has any clients associated
* with it.
* Returns whether this panel currently has any client groups
* associated with it.
*
* @return {Boolean}
* true if at least one client is associated with this panel,
* false otherwise.
* true if at least one client group is associated with this
* panel, false otherwise.
*/
$scope.hasClients = function hasClients() {
return !!_.find($scope.clients, $scope.isManaged);
$scope.hasClientGroups = function hasClientGroups() {
return $scope.clientGroups && $scope.clientGroups.length;
};
/**
* Returns whether the status of the given client 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.
* @borrows ManagedClientGroup.getIdentifier
*/
$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
* The client to test.
* @param {ManagedClientGroup} clientGroup
* The client group to test.
*
* @returns {Boolean}
* true if the given client requires the user's attention,
* 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
switch (client.clientState.connectionState) {
case ManagedClientState.ConnectionState.CONNECTION_ERROR:
case ManagedClientState.ConnectionState.TUNNEL_ERROR:
case ManagedClientState.ConnectionState.DISCONNECTED:
return true;
}
// Test whether the client has encountered an error
switch (client.clientState.connectionState) {
case ManagedClientState.ConnectionState.CONNECTION_ERROR:
case ManagedClientState.ConnectionState.TUNNEL_ERROR:
case ManagedClientState.ConnectionState.DISCONNECTED:
return true;
}
return false;
return false;
}) !== -1;
};
/**
* Returns whether the given client is currently being managed by
* the guacClientManager service.
* Initiates an orderly disconnect of all clients within the given
* 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
* The client to test.
*
* @returns {Boolean}
* true if the given client is being managed by the
* guacClientManager service, false otherwise.
* @param {ManagedClientGroup} clientGroup
* The group of clients to disconnect.
*/
$scope.isManaged = function isManaged(client) {
return !!guacClientManager.getManagedClients()[client.id];
};
/**
* 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);
$scope.disconnect = function disconnect(clientGroup) {
guacClientManager.removeManagedClientGroup(ManagedClientGroup.getIdentifier(clientGroup));
};
/**

View File

@@ -32,50 +32,17 @@ angular.module('client').directive('guacTiledClients', [function guacTiledClient
directive.scope = {
/**
* The Guacamole clients that should be displayed in an evenly-tiled
* grid arrangement.
* The group of Guacamole clients that should be displayed in an
* evenly-tiled grid arrangement.
*
* @type ManagedClient[]
* @type ManagedClientGroup
*/
clients : '='
clientGroup : '='
};
directive.controller = ['$scope', '$injector', '$element',
function guacTiledListController($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());
};
function guacTiledClientsController($scope, $injector, $element) {
/**
* 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.
*/
$scope.assignFocus = function assignFocus(client) {
// Clear focus of all other clients
$scope.clientGroup.clients.forEach(client => {
client.clientProperties.focused = false;
});
client.clientProperties.focused = true;
};
/**
@@ -98,10 +72,10 @@ angular.module('client').directive('guacTiledClients', [function guacTiledClient
* otherwise.
*/
$scope.hasMultipleClients = function hasMultipleClients() {
return $scope.clients && $scope.clients.length > 1;
return $scope.clientGroup && $scope.clientGroup.clients.length > 1;
};
/**
/**
* Returns the CSS width that should be applied to each tile to
* achieve an even arrangement.
*
@@ -109,7 +83,7 @@ angular.module('client').directive('guacTiledClients', [function guacTiledClient
* The CSS width that should be applied to each tile.
*/
$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.
*/
$scope.getTileHeight = function getTileHeight() {
return Math.floor(100 / getRows()) + '%';
};
/**
* 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 || '...';
return Math.floor(100 / $scope.clientGroup.rows) + '%';
};
}];

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

@@ -24,7 +24,8 @@ angular.module('client').factory('guacClientManager', ['$injector',
function guacClientManager($injector) {
// Required types
var ManagedClient = $injector.get('ManagedClient');
var ManagedClient = $injector.get('ManagedClient');
var ManagedClientGroup = $injector.get('ManagedClientGroup');
// Required services
var $window = $injector.get('$window');
@@ -56,6 +57,53 @@ angular.module('client').factory('guacClientManager', ['$injector',
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
* 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}
* true if an existing client was removed, false otherwise.
*/
service.removeManagedClient = function replaceManagedClient(id) {
service.removeManagedClient = function removeManagedClient(id) {
var managedClients = storedManagedClients();
// Remove client if it exists
if (id in managedClients) {
// Pull client out of any containing groups
ungroupManagedClient(id);
// Disconnect and remove
managedClients[id].client.disconnect();
delete managedClients[id];
@@ -102,11 +153,31 @@ angular.module('client').factory('guacClientManager', ['$injector',
*/
service.replaceManagedClient = function replaceManagedClient(id) {
// Disconnect any existing client
service.removeManagedClient(id);
var managedClients = storedManagedClients();
var managedClientGroups = storedManagedClientGroups();
// Set new client
return storedManagedClients()[id] = ManagedClient.getInstance(id);
// Remove client if it exists
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();
// 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
if (!(id in managedClients))
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() {
@@ -146,8 +295,9 @@ angular.module('client').factory('guacClientManager', ['$injector',
for (var id in managedClients)
managedClients[id].client.disconnect();
// Clear managed clients
// Clear managed clients and client groups
storedManagedClients({});
storedManagedClientGroups([]);
};

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
<ul class="tiled-client-list" ng-class="{ 'multiple-clients' : hasMultipleClients() }">
<li class="client-tile"
ng-repeat="client in clients"
ng-repeat="client in clientGroup.clients"
ng-style="{ 'width' : getTileWidth(), 'height' : getTileHeight() }"
ng-class="{ 'focused' : client.clientProperties.focused }"
ng-click="assignFocus(client)">
<h3>{{ getClientTitle(client) }}</h3>
<h3>{{ client.title }}</h3>
<guac-client client="client"></guac-client>
<!-- 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
* client while it is active, allowing it to be detached and reattached
* from different client views.
* client while it is active, allowing it to be maintained in the
* background. One or more ManagedClients are grouped within
* ManagedClientGroups before being attached to the client view.
*
* @constructor
* @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() {
@@ -44,21 +45,12 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
controller: ['$scope', '$injector', function guacRecentConnectionsController($scope, $injector) {
// Required types
var ActiveConnection = $injector.get('ActiveConnection');
var ClientIdentifier = $injector.get('ClientIdentifier');
var RecentConnection = $injector.get('RecentConnection');
// Required services
var guacClientManager = $injector.get('guacClientManager');
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.
*
@@ -68,16 +60,12 @@ angular.module('home').directive('guacRecentConnections', [function guacRecentCo
/**
* 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}
* true if recent (or active) connections are present, false
* otherwise.
* true if recent connections are present, false otherwise.
*/
$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) {
// Clear connection arrays
$scope.activeConnections = [];
$scope.recentConnections = [];
// 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
guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) {
// 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];
$scope.recentConnections.push(new RecentConnection(object.name, historyEntry));

View File

@@ -3,23 +3,6 @@
<!-- Text displayed if no recent connections exist -->
<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 -->
<div ng-repeat="recentConnection in recentConnections" class="connection">
<a href="#/client/{{recentConnection.entry.id}}">

View File

@@ -24,11 +24,12 @@ angular.module('index').controller('indexController', ['$scope', '$injector',
function indexController($scope, $injector) {
// Required services
var $document = $injector.get('$document');
var $route = $injector.get('$route');
var $window = $injector.get('$window');
var clipboardService = $injector.get('clipboardService');
var guacNotification = $injector.get('guacNotification');
var $document = $injector.get('$document');
var $route = $injector.get('$route');
var $window = $injector.get('$window');
var clipboardService = $injector.get('clipboardService');
var guacNotification = $injector.get('guacNotification');
var guacClientManager = $injector.get('guacClientManager');
/**
* 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;
/**
* 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
* process.

View File

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

View File

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