GUACAMOLE-724: Provide separate, client-specific notifications for each tiled client.

This commit is contained in:
Michael Jumper
2021-06-01 17:34:08 -07:00
parent fbdb692444
commit 2f1d46aa86
5 changed files with 495 additions and 361 deletions

View File

@@ -32,13 +32,11 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
var ScrollState = $injector.get('ScrollState');
// Required services
var $location = $injector.get('$location');
var authenticationService = $injector.get('authenticationService');
var connectionGroupService = $injector.get('connectionGroupService');
var clipboardService = $injector.get('clipboardService');
var dataSourceService = $injector.get('dataSourceService');
var guacClientManager = $injector.get('guacClientManager');
var guacNotification = $injector.get('guacNotification');
var iconService = $injector.get('iconService');
var preferenceService = $injector.get('preferenceService');
var requestService = $injector.get('requestService');
@@ -93,129 +91,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
*/
var DEL_KEY = 0xFFFF;
/**
* All client error codes handled and passed off for translation. Any error
* code not present in this list will be represented by the "DEFAULT"
* translation.
*/
var CLIENT_ERRORS = {
0x0201: true,
0x0202: true,
0x0203: true,
0x0207: true,
0x0208: true,
0x0209: true,
0x020A: true,
0x020B: true,
0x0301: true,
0x0303: true,
0x0308: true,
0x031D: true
};
/**
* All error codes for which automatic reconnection is appropriate when a
* client error occurs.
*/
var CLIENT_AUTO_RECONNECT = {
0x0200: true,
0x0202: true,
0x0203: true,
0x0207: true,
0x0208: true,
0x0301: true,
0x0308: true
};
/**
* All tunnel error codes handled and passed off for translation. Any error
* code not present in this list will be represented by the "DEFAULT"
* translation.
*/
var TUNNEL_ERRORS = {
0x0201: true,
0x0202: true,
0x0203: true,
0x0204: true,
0x0205: true,
0x0207: true,
0x0208: true,
0x0301: true,
0x0303: true,
0x0308: true,
0x031D: true
};
/**
* All error codes for which automatic reconnection is appropriate when a
* tunnel error occurs.
*/
var TUNNEL_AUTO_RECONNECT = {
0x0200: true,
0x0202: true,
0x0203: true,
0x0207: true,
0x0208: true,
0x0308: true
};
/**
* Action which logs out from Guacamole entirely.
*/
var LOGOUT_ACTION = {
name : "CLIENT.ACTION_LOGOUT",
className : "logout button",
callback : function logoutCallback() {
authenticationService.logout()
['catch'](requestService.IGNORE);
}
};
/**
* Action which returns the user to the home screen. If the home page has
* not yet been determined, this will be null.
*/
var NAVIGATE_HOME_ACTION = null;
// Assign home page action once user's home page has been determined
userPageService.getHomePage()
.then(function homePageRetrieved(homePage) {
// Define home action only if different from current location
if ($location.path() !== homePage.url) {
NAVIGATE_HOME_ACTION = {
name : "CLIENT.ACTION_NAVIGATE_HOME",
className : "home button",
callback : function navigateHomeCallback() {
$location.url(homePage.url);
}
};
}
}, requestService.WARN);
/**
* Action which replaces the current client with a newly-connected client.
*/
var RECONNECT_ACTION = {
name : "CLIENT.ACTION_RECONNECT",
className : "reconnect button",
callback : function reconnectCallback() {
$scope.client = guacClientManager.replaceManagedClient($routeParams.id);
guacNotification.showStatus(false);
}
};
/**
* The reconnect countdown to display if an error or status warrants an
* automatic, timed reconnect.
*/
var RECONNECT_COUNTDOWN = {
text: "CLIENT.TEXT_RECONNECT_COUNTDOWN",
callback: RECONNECT_ACTION.callback,
remaining: 15
};
/**
* Menu-specific properties.
*/
@@ -716,31 +591,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
$scope.page.title = title;
});
/**
* Displays a notification at the end of a Guacamole connection, whether
* that connection is ending normally or due to an error. As the end of
* a Guacamole connection may be due to changes in authentication status,
* this will also implicitly peform a re-authentication attempt to check
* for such changes, possibly resulting in auth-related events like
* guacInvalidCredentials.
*
* @param {Notification|Boolean|Object} status
* The status notification to show, as would be accepted by
* guacNotification.showStatus().
*/
var notifyConnectionClosed = function notifyConnectionClosed(status) {
// Re-authenticate to verify auth status at end of connection
authenticationService.updateCurrentToken($location.search())
['catch'](requestService.IGNORE)
// Show the requested status once the authentication check has finished
['finally'](function authenticationCheckComplete() {
guacNotification.showStatus(status);
});
};
/**
* Returns whether the current connection has been flagged as unstable due
* to an apparent network disruption.
@@ -761,215 +611,6 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
};
/**
* Notifies the user that the connection state has changed.
*
* @param {String} connectionState
* The current connection state, as defined by
* ManagedClientState.ConnectionState.
*/
var notifyConnectionState = function notifyConnectionState(connectionState) {
// Hide any existing status
guacNotification.showStatus(false);
// Do not display status if status not known
if (!connectionState)
return;
// Build array of available actions
var actions;
if (NAVIGATE_HOME_ACTION)
actions = [ NAVIGATE_HOME_ACTION, RECONNECT_ACTION, LOGOUT_ACTION ];
else
actions = [ RECONNECT_ACTION, LOGOUT_ACTION ];
// Get any associated status code
var status = $scope.client.clientState.statusCode;
// Connecting
if (connectionState === ManagedClientState.ConnectionState.CONNECTING
|| connectionState === ManagedClientState.ConnectionState.WAITING) {
guacNotification.showStatus({
title: "CLIENT.DIALOG_HEADER_CONNECTING",
text: {
key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
}
});
}
// Client error
else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) {
// Determine translation name of error
var errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
// Determine whether the reconnect countdown applies
var countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
// Show error status
notifyConnectionClosed({
className : "error",
title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
text : {
key : "CLIENT.ERROR_CLIENT_" + errorName
},
countdown : countdown,
actions : actions
});
}
// Tunnel error
else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) {
// Determine translation name of error
var errorName = (status in TUNNEL_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
// Determine whether the reconnect countdown applies
var countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
// Show error status
notifyConnectionClosed({
className : "error",
title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
text : {
key : "CLIENT.ERROR_TUNNEL_" + errorName
},
countdown : countdown,
actions : actions
});
}
// Disconnected
else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) {
notifyConnectionClosed({
title : "CLIENT.DIALOG_HEADER_DISCONNECTED",
text : {
key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
},
actions : actions
});
}
// Hide status and sync local clipboard once connected
else if (connectionState === ManagedClientState.ConnectionState.CONNECTED) {
// Sync with local clipboard
clipboardService.getLocalClipboard().then(function clipboardRead(data) {
$scope.$broadcast('guacClipboard', data);
}, angular.noop);
// Hide status notification
guacNotification.showStatus(false);
}
// Hide status for all other states
else
guacNotification.showStatus(false);
};
/**
* Prompts the user to enter additional connection parameters. If the
* protocol and associated parameters of the underlying connection are not
* yet known, this function has no effect and should be re-invoked once
* the parameters are known.
*
* @param {Object.<String, String>} requiredParameters
* The set of all parameters requested by the server via "required"
* instructions, where each object key is the name of a requested
* parameter and each value is the current value entered by the user.
*/
var notifyParametersRequired = function notifyParametersRequired(requiredParameters) {
/**
* Action which submits the current set of parameter values, requesting
* that the connection continue.
*/
var SUBMIT_PARAMETERS = {
name : "CLIENT.ACTION_CONTINUE",
className : "button",
callback : function submitParameters() {
if ($scope.client) {
var params = $scope.client.requiredParameters;
$scope.client.requiredParameters = null;
ManagedClient.sendArguments($scope.client, params);
}
}
};
/**
* Action which cancels submission of additional parameters and
* disconnects from the current connection.
*/
var CANCEL_PARAMETER_SUBMISSION = {
name : "CLIENT.ACTION_CANCEL",
className : "button",
callback : function cancelSubmission() {
$scope.client.requiredParameters = null;
$scope.disconnect();
}
};
// Attempt to prompt for parameters only if the parameters that apply
// to the underlying connection are known
if (!$scope.client.protocol || !$scope.client.forms)
return;
// Hide any existing status
guacNotification.showStatus(false);
// Prompt for parameters
guacNotification.showStatus({
formNamespace : Protocol.getNamespace($scope.client.protocol),
forms : $scope.client.forms,
formModel : requiredParameters,
formSubmitCallback : SUBMIT_PARAMETERS.callback,
actions : [ SUBMIT_PARAMETERS, CANCEL_PARAMETER_SUBMISSION ]
});
};
/**
* Returns whether the given connection state allows for submission of
* connection parameters via "argv" instructions.
*
* @param {String} connectionState
* The connection state to test, as defined by
* ManagedClientState.ConnectionState.
*
* @returns {boolean}
* true if the given connection state allows submission of connection
* parameters via "argv" instructions, false otherwise.
*/
var canSubmitParameters = function canSubmitParameters(connectionState) {
return (connectionState === ManagedClientState.ConnectionState.WAITING ||
connectionState === ManagedClientState.ConnectionState.CONNECTED);
};
// Show status dialog when connection status changes
$scope.$watchGroup([
'client.clientState.connectionState',
'client.requiredParameters',
'client.protocol',
'client.forms'
], function clientStateChanged(newValues) {
var connectionState = newValues[0];
var requiredParameters = newValues[1];
// Prompt for parameters only if parameters can actually be submitted
if (requiredParameters && canSubmitParameters(connectionState))
notifyParametersRequired(requiredParameters);
// Otherwise, just show general connection state
else
notifyConnectionState(connectionState);
});
$scope.zoomIn = function zoomIn() {
$scope.menu.autoFit = false;

View File

@@ -0,0 +1,431 @@
/*
* 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 non-global notification describing the status
* of a specific Guacamole client, including prompts for any information
* necessary to continue the connection.
*/
angular.module('client').directive('guacClientNotification', [function guacClientNotification() {
var directive = {
restrict: 'E',
replace: true,
templateUrl: 'app/client/templates/guacClientNotification.html'
};
directive.scope = {
/**
* The client whose status should be displayed.
*
* @type ManagedClient
*/
client : '='
};
directive.controller = ['$scope', '$injector', '$element',
function guacClientNotificationController($scope, $injector, $element) {
// Required types
var ManagedClient = $injector.get('ManagedClient');
var ManagedClientState = $injector.get('ManagedClientState');
var Protocol = $injector.get('Protocol');
// Required services
var $location = $injector.get('$location');
var authenticationService = $injector.get('authenticationService');
var guacClientManager = $injector.get('guacClientManager');
var requestService = $injector.get('requestService');
var userPageService = $injector.get('userPageService');
/**
* A Notification object describing the client status to display as a
* dialog or prompt, as would be accepted by guacNotification.showStatus(),
* or false if no status should be shown.
*
* @type {Notification|Object|Boolean}
*/
$scope.status = false;
/**
* All client error codes handled and passed off for translation. Any error
* code not present in this list will be represented by the "DEFAULT"
* translation.
*/
var CLIENT_ERRORS = {
0x0201: true,
0x0202: true,
0x0203: true,
0x0207: true,
0x0208: true,
0x0209: true,
0x020A: true,
0x020B: true,
0x0301: true,
0x0303: true,
0x0308: true,
0x031D: true
};
/**
* All error codes for which automatic reconnection is appropriate when a
* client error occurs.
*/
var CLIENT_AUTO_RECONNECT = {
0x0200: true,
0x0202: true,
0x0203: true,
0x0207: true,
0x0208: true,
0x0301: true,
0x0308: true
};
/**
* All tunnel error codes handled and passed off for translation. Any error
* code not present in this list will be represented by the "DEFAULT"
* translation.
*/
var TUNNEL_ERRORS = {
0x0201: true,
0x0202: true,
0x0203: true,
0x0204: true,
0x0205: true,
0x0207: true,
0x0208: true,
0x0301: true,
0x0303: true,
0x0308: true,
0x031D: true
};
/**
* All error codes for which automatic reconnection is appropriate when a
* tunnel error occurs.
*/
var TUNNEL_AUTO_RECONNECT = {
0x0200: true,
0x0202: true,
0x0203: true,
0x0207: true,
0x0208: true,
0x0308: true
};
/**
* Action which logs out from Guacamole entirely.
*/
var LOGOUT_ACTION = {
name : "CLIENT.ACTION_LOGOUT",
className : "logout button",
callback : function logoutCallback() {
authenticationService.logout()
['catch'](requestService.IGNORE);
}
};
/**
* Action which returns the user to the home screen. If the home page has
* not yet been determined, this will be null.
*/
var NAVIGATE_HOME_ACTION = null;
// Assign home page action once user's home page has been determined
userPageService.getHomePage()
.then(function homePageRetrieved(homePage) {
// Define home action only if different from current location
if ($location.path() !== homePage.url) {
NAVIGATE_HOME_ACTION = {
name : "CLIENT.ACTION_NAVIGATE_HOME",
className : "home button",
callback : function navigateHomeCallback() {
$location.url(homePage.url);
}
};
}
}, requestService.WARN);
/**
* Action which replaces the current client with a newly-connected client.
*/
var RECONNECT_ACTION = {
name : "CLIENT.ACTION_RECONNECT",
className : "reconnect button",
callback : function reconnectCallback() {
$scope.client = guacClientManager.replaceManagedClient($scope.client.id);
$scope.status = false;
}
};
/**
* The reconnect countdown to display if an error or status warrants an
* automatic, timed reconnect.
*/
var RECONNECT_COUNTDOWN = {
text: "CLIENT.TEXT_RECONNECT_COUNTDOWN",
callback: RECONNECT_ACTION.callback,
remaining: 15
};
/**
* Displays a notification at the end of a Guacamole connection, whether
* that connection is ending normally or due to an error. As the end of
* a Guacamole connection may be due to changes in authentication status,
* this will also implicitly peform a re-authentication attempt to check
* for such changes, possibly resulting in auth-related events like
* guacInvalidCredentials.
*
* @param {Notification|Boolean|Object} status
* The status notification to show, as would be accepted by
* guacNotification.showStatus().
*/
var notifyConnectionClosed = function notifyConnectionClosed(status) {
// Re-authenticate to verify auth status at end of connection
authenticationService.updateCurrentToken($location.search())
['catch'](requestService.IGNORE)
// Show the requested status once the authentication check has finished
['finally'](function authenticationCheckComplete() {
$scope.status = status;
});
};
/**
* Notifies the user that the connection state has changed.
*
* @param {String} connectionState
* The current connection state, as defined by
* ManagedClientState.ConnectionState.
*/
var notifyConnectionState = function notifyConnectionState(connectionState) {
// Hide any existing status
$scope.status = false;
// Do not display status if status not known
if (!connectionState)
return;
// Build array of available actions
var actions;
if (NAVIGATE_HOME_ACTION)
actions = [ NAVIGATE_HOME_ACTION, RECONNECT_ACTION, LOGOUT_ACTION ];
else
actions = [ RECONNECT_ACTION, LOGOUT_ACTION ];
// Get any associated status code
var status = $scope.client.clientState.statusCode;
// Connecting
if (connectionState === ManagedClientState.ConnectionState.CONNECTING
|| connectionState === ManagedClientState.ConnectionState.WAITING) {
$scope.status = {
title: "CLIENT.DIALOG_HEADER_CONNECTING",
text: {
key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
}
};
}
// Client error
else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) {
// Determine translation name of error
var errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
// Determine whether the reconnect countdown applies
var countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
// Show error status
notifyConnectionClosed({
className : "error",
title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
text : {
key : "CLIENT.ERROR_CLIENT_" + errorName
},
countdown : countdown,
actions : actions
});
}
// Tunnel error
else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) {
// Determine translation name of error
var errorName = (status in TUNNEL_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
// Determine whether the reconnect countdown applies
var countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null;
// Show error status
notifyConnectionClosed({
className : "error",
title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR",
text : {
key : "CLIENT.ERROR_TUNNEL_" + errorName
},
countdown : countdown,
actions : actions
});
}
// Disconnected
else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) {
notifyConnectionClosed({
title : "CLIENT.DIALOG_HEADER_DISCONNECTED",
text : {
key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase()
},
actions : actions
});
}
// Hide status and sync local clipboard once connected
else if (connectionState === ManagedClientState.ConnectionState.CONNECTED) {
// TODO: Move clipboard sync elsewhere
// Sync with local clipboard
/*
clipboardService.getLocalClipboard().then(function clipboardRead(data) {
$scope.$broadcast('guacClipboard', data);
}, angular.noop);
*/
// Hide status notification
$scope.status = false;
}
// Hide status for all other states
else
$scope.status = false;
};
/**
* Prompts the user to enter additional connection parameters. If the
* protocol and associated parameters of the underlying connection are not
* yet known, this function has no effect and should be re-invoked once
* the parameters are known.
*
* @param {Object.<String, String>} requiredParameters
* The set of all parameters requested by the server via "required"
* instructions, where each object key is the name of a requested
* parameter and each value is the current value entered by the user.
*/
var notifyParametersRequired = function notifyParametersRequired(requiredParameters) {
/**
* Action which submits the current set of parameter values, requesting
* that the connection continue.
*/
var SUBMIT_PARAMETERS = {
name : "CLIENT.ACTION_CONTINUE",
className : "button",
callback : function submitParameters() {
if ($scope.client) {
var params = $scope.client.requiredParameters;
$scope.client.requiredParameters = null;
ManagedClient.sendArguments($scope.client, params);
}
}
};
/**
* Action which cancels submission of additional parameters and
* disconnects from the current connection.
*/
var CANCEL_PARAMETER_SUBMISSION = {
name : "CLIENT.ACTION_CANCEL",
className : "button",
callback : function cancelSubmission() {
$scope.client.requiredParameters = null;
$scope.disconnect();
}
};
// Attempt to prompt for parameters only if the parameters that apply
// to the underlying connection are known
if (!$scope.client.protocol || !$scope.client.forms)
return;
// Prompt for parameters
$scope.status = {
formNamespace : Protocol.getNamespace($scope.client.protocol),
forms : $scope.client.forms,
formModel : requiredParameters,
formSubmitCallback : SUBMIT_PARAMETERS.callback,
actions : [ SUBMIT_PARAMETERS, CANCEL_PARAMETER_SUBMISSION ]
};
};
/**
* Returns whether the given connection state allows for submission of
* connection parameters via "argv" instructions.
*
* @param {String} connectionState
* The connection state to test, as defined by
* ManagedClientState.ConnectionState.
*
* @returns {boolean}
* true if the given connection state allows submission of connection
* parameters via "argv" instructions, false otherwise.
*/
var canSubmitParameters = function canSubmitParameters(connectionState) {
return (connectionState === ManagedClientState.ConnectionState.WAITING ||
connectionState === ManagedClientState.ConnectionState.CONNECTED);
};
// Show status dialog when connection status changes
$scope.$watchGroup([
'client.clientState.connectionState',
'client.requiredParameters',
'client.protocol',
'client.forms'
], function clientStateChanged(newValues) {
var connectionState = newValues[0];
var requiredParameters = newValues[1];
// Prompt for parameters only if parameters can actually be submitted
if (requiredParameters && canSubmitParameters(connectionState))
notifyParametersRequired(requiredParameters);
// Otherwise, just show general connection state
else
notifyConnectionState(connectionState);
});
}];
return directive;
}]);

View File

@@ -17,7 +17,60 @@
* under the License.
*/
.client .notification .parameters h3,
.client .notification .parameters .password-field .toggle-password {
.client-status-modal {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: none;
background: rgba(0, 0, 0, 0.5);
}
.client-status-modal.shown {
display: block;
}
.client-status-modal guac-modal {
position: absolute;
}
.client-status-modal .notification {
background: rgba(80, 80, 80, 0.4);
color: white;
width: 100%;
max-width: 100%;
padding: 1em;
text-align: center;
border: none;
}
.client-status-modal .notification .title-bar {
display: none
}
.client-status-modal .notification .button {
background: transparent;
border: 2px solid white;
box-shadow: none;
text-shadow: none;
font-weight: normal;
}
.client-status-modal .notification .button:hover {
text-decoration: underline;
background: rgba(255, 255, 255, 0.25);
}
.client-status-modal .notification .button:active {
background: rgba(255, 255, 255, 0.5);
}
.client-status-modal .notification .parameters h3,
.client-status-modal .notification .parameters .password-field .toggle-password {
display: none;
}

View File

@@ -0,0 +1,5 @@
<div class="client-status-modal" ng-class="{ shown: status }">
<guac-modal>
<guac-notification notification="status"></guac-notification>
</guac-modal>
</div>

View File

@@ -8,6 +8,10 @@
<h3>{{ getClientTitle(client) }}</h3>
<guac-client client="client"></guac-client>
<!-- Client-specific status/error dialog -->
<guac-client-notification client="client"></guac-client-notification>
</li>
</ul>