From 4ab35caf50534b5645a6f563e33ec3dcdf2af469 Mon Sep 17 00:00:00 2001 From: Corentin SORIANO Date: Fri, 31 Jul 2026 16:53:50 +0200 Subject: [PATCH] GUACAMOLE-2312: Display the number of available connections in a balancing group. --- .../app/groupList/directives/guacGroupList.js | 54 ++++++++++- .../src/app/groupList/types/GroupListItem.js | 96 ++++++++++++++++++- .../app/home/templates/connectionGroup.html | 6 ++ .../main/frontend/src/translations/en.json | 1 + .../main/frontend/src/translations/fr.json | 1 + 5 files changed, 155 insertions(+), 3 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/groupList/directives/guacGroupList.js b/guacamole/src/main/frontend/src/app/groupList/directives/guacGroupList.js index 16ff9ddc0f..4c955a1fd2 100644 --- a/guacamole/src/main/frontend/src/app/groupList/directives/guacGroupList.js +++ b/guacamole/src/main/frontend/src/app/groupList/directives/guacGroupList.js @@ -93,6 +93,7 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() // Required services var activeConnectionService = $injector.get('activeConnectionService'); + var authenticationService = $injector.get('authenticationService'); var dataSourceService = $injector.get('dataSourceService'); var requestService = $injector.get('requestService'); @@ -109,6 +110,14 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() */ var connectionCount = {}; + /** + * Like connectionCount, but restricted to the connections owned + * by the currently-authenticated user. + * + * @type Object.> + */ + var userConnectionCount = {}; + /** * A list of all items which should appear at the root level. As * connections and connection groups from multiple data sources may @@ -137,6 +146,38 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() return connectionCount[dataSource][connection.identifier]; }; + /** + * Returns the number of connections by the current user through + * the given connection group, by summing across all descendants. + * + * @param {String} dataSource + * @param {ConnectionGroup} connectionGroup + * @returns {Number} + */ + var countUserActiveConnectionGroups = function countUserActiveConnectionGroups(dataSource, connectionGroup) { + let count = 0; + const userConnections = userConnectionCount[dataSource]; + + if (!userConnections) + return 0; + + // Count active connections through child connections + if (connectionGroup.childConnections) { + connectionGroup.childConnections.forEach(function(child) { + count += userConnections[child.identifier] ?? 0; + }); + } + + // Recursively count active connections through child connection groups + if (connectionGroup.childConnectionGroups) { + connectionGroup.childConnectionGroups.forEach(function(child) { + count += countUserActiveConnectionGroups(dataSource, child); + }); + } + + return count; + }; + /** * Returns whether a @link{GroupListItem} of the given type can be * displayed. If there is no template associated with the given @@ -184,7 +225,7 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() rootItem = GroupListItem.fromConnectionGroup(dataSource, connectionGroup, $scope.isVisible(GroupListItem.Type.CONNECTION), $scope.isVisible(GroupListItem.Type.SHARING_PROFILE), - countActiveConnections); + countActiveConnections, null, countUserActiveConnectionGroups); // If root group is to be shown, add it as a root item if ($scope.showRootGroup) @@ -206,8 +247,13 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() ) .then(function activeConnectionsRetrieved(activeConnectionMap) { + const currentUsername = authenticationService.getCurrentUsername(); + // Within each data source, count each active connection by identifier angular.forEach(activeConnectionMap, function addActiveConnections(activeConnections, dataSource) { + + userConnectionCount[dataSource] ??= {}; + angular.forEach(activeConnections, function addActiveConnection(activeConnection) { // If counter already exists, increment @@ -219,6 +265,12 @@ angular.module('groupList').directive('guacGroupList', [function guacGroupList() else connectionCount[dataSource][identifier] = 1; + // Track connections belonging to the current user + if (activeConnection.username === currentUsername) { + userConnectionCount[dataSource][identifier] ??= 0; + userConnectionCount[dataSource][identifier]++; + } + }); }); diff --git a/guacamole/src/main/frontend/src/app/groupList/types/GroupListItem.js b/guacamole/src/main/frontend/src/app/groupList/types/GroupListItem.js index 2255cb4c9f..41881ad82f 100644 --- a/guacamole/src/main/frontend/src/app/groupList/types/GroupListItem.js +++ b/guacamole/src/main/frontend/src/app/groupList/types/GroupListItem.js @@ -124,6 +124,17 @@ angular.module('groupList').factory('GroupListItem', ['$injector', function defi return null; }); + /** + * Returns the number of available connections for this connection + * group. + * + * @returns {Number} + * The number of available connections for this connection group. + */ + this.getAvailableConnections = template.getAvailableConnections || (function getAvailableConnections() { + return null; + }); + /** * Returns the unique string identifier that must be used when * connecting to a connection or connection group represented by this @@ -178,6 +189,18 @@ angular.module('groupList').factory('GroupListItem', ['$injector', function defi }; + /** + * Returns the unique maximum number of connections that can be active + * for this connection group, if known. If unknown, null may be returned. + * + * @returns {Number} + * The unique maximum number of connections that can be active for + * this connection group. + */ + this.getEffectiveMaxConnections = template.getEffectiveMaxConnections || (function getEffectiveMaxConnections() { + return null; + }); + /** * The connection, connection group, or sharing profile whose data is * exposed within this GroupListItem. If the type of this GroupListItem @@ -312,7 +335,8 @@ angular.module('groupList').factory('GroupListItem', ['$injector', function defi */ GroupListItem.fromConnectionGroup = function fromConnectionGroup(dataSource, connectionGroup, includeConnections, includeSharingProfiles, - countActiveConnections, countActiveConnectionGroups) { + countActiveConnections, countActiveConnectionGroups, + countUserActiveConnectionGroups) { var children = []; @@ -329,7 +353,8 @@ angular.module('groupList').factory('GroupListItem', ['$injector', function defi connectionGroup.childConnectionGroups.forEach(function addChildGroup(child) { children.push(GroupListItem.fromConnectionGroup(dataSource, child, includeConnections, includeSharingProfiles, - countActiveConnections, countActiveConnectionGroups)); + countActiveConnections, countActiveConnectionGroups, + countUserActiveConnectionGroups)); }); } @@ -360,6 +385,73 @@ angular.module('groupList').factory('GroupListItem', ['$injector', function defi }, + // Available slots considering both global and per-user limits + getAvailableConnections : function getAvailableConnections() { + // Get total active connections across all children + let totalActive = 0; + connectionGroup.childConnections.forEach(function addChildConnection(child) { + if (!countActiveConnections) + return; + + totalActive += countActiveConnections(dataSource, child) ?? 0; + }); + + // Get current user's active connections across all children + if (!countUserActiveConnectionGroups) + return totalActive; + const userActive = countUserActiveConnectionGroups(dataSource, connectionGroup) ?? 0; + + return this.getEffectiveMaxConnections() - Math.max(totalActive, userActive); + }, + + // Effective max slots from this user's perspective + getEffectiveMaxConnections : function getEffectiveMaxConnections() { + + /** + * Parses the given value as an integer, returning Infinity if the + * value is not a valid integer. + * + * @param {String} value + * The value to parse as an integer. + * + * @returns {Number} + * The parsed integer, or Infinity if the given value is not a + * valid integer. + */ + const parseIntOrInfinity = (value) => { + const parsed = Number.parseInt(value, 10); + return Number.isNaN(parsed) ? Infinity : parsed; + }; + + // Group limits + const attrs = connectionGroup.attributes ?? {}; + const maxGlobal = parseIntOrInfinity(attrs['max-connections']); + const maxPerUser = parseIntOrInfinity(attrs['max-connections-per-user']); + + // Per-connection limits are summed across all children + let maxPerConnection = Infinity; + connectionGroup.childConnections.forEach(function addChildConnection(child) { + const childAttrs = child.attributes ?? {}; + const childMaxGlobal = parseIntOrInfinity(childAttrs['max-connections']); + const childMaxPerUser = parseIntOrInfinity(childAttrs['max-connections-per-user']); + + // If neither limit is set, this child does not affect the effective max + if (childMaxGlobal === Infinity && childMaxPerUser === Infinity) + return; + + maxPerConnection = maxPerConnection === Infinity ? 0 : maxPerConnection; + maxPerConnection += Math.min(childMaxGlobal, childMaxPerUser); + }); + + const minValue = Math.min(maxGlobal, maxPerUser, maxPerConnection); + + // If the minimum value is Infinity, it means there are no effective + // limits, so return null + if (minValue === Infinity) + return null; + + return minValue; + }, // Wrapped item wrappedItem : connectionGroup diff --git a/guacamole/src/main/frontend/src/app/home/templates/connectionGroup.html b/guacamole/src/main/frontend/src/app/home/templates/connectionGroup.html index ba0a204ba1..fae8d75579 100644 --- a/guacamole/src/main/frontend/src/app/home/templates/connectionGroup.html +++ b/guacamole/src/main/frontend/src/app/home/templates/connectionGroup.html @@ -7,4 +7,10 @@ {{item.name}} + + + diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 27f24a9762..37169829ea 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -286,6 +286,7 @@ "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_AVAILABLE_CONNECTIONS" : "{AVAILABLE} of {TOTAL} {TOTAL, plural, one{connection} other{connections}} available", "INFO_NO_RECENT_CONNECTIONS" : "No recent connections.", diff --git a/guacamole/src/main/frontend/src/translations/fr.json b/guacamole/src/main/frontend/src/translations/fr.json index cd2dc821bf..1747cbbd3c 100644 --- a/guacamole/src/main/frontend/src/translations/fr.json +++ b/guacamole/src/main/frontend/src/translations/fr.json @@ -280,6 +280,7 @@ "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_AVAILABLE_CONNECTIONS" : "{AVAILABLE} {AVAILABLE, plural, one{connexion disponible} other{connexions disponibles}} sur {TOTAL}", "INFO_NO_RECENT_CONNECTIONS" : "Pas de connexion récente.",