From 2f1d46aa8671da1fbe743c42da86f6ad7bb5cf88 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 1 Jun 2021 17:34:08 -0700 Subject: [PATCH] GUACAMOLE-724: Provide separate, client-specific notifications for each tiled client. --- .../client/controllers/clientController.js | 359 --------------- .../directives/guacClientNotification.js | 431 ++++++++++++++++++ .../src/app/client/styles/notification.css | 57 ++- .../templates/guacClientNotification.html | 5 + .../client/templates/guacTiledClients.html | 4 + 5 files changed, 495 insertions(+), 361 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js create mode 100644 guacamole/src/main/frontend/src/app/client/templates/guacClientNotification.html diff --git a/guacamole/src/main/frontend/src/app/client/controllers/clientController.js b/guacamole/src/main/frontend/src/app/client/controllers/clientController.js index c391b3b2f..c19aa594e 100644 --- a/guacamole/src/main/frontend/src/app/client/controllers/clientController.js +++ b/guacamole/src/main/frontend/src/app/client/controllers/clientController.js @@ -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.} 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; diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js b/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js new file mode 100644 index 000000000..9021b0b7b --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js @@ -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.} 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; + +}]); diff --git a/guacamole/src/main/frontend/src/app/client/styles/notification.css b/guacamole/src/main/frontend/src/app/client/styles/notification.css index 77a0a641a..5c8a8f989 100644 --- a/guacamole/src/main/frontend/src/app/client/styles/notification.css +++ b/guacamole/src/main/frontend/src/app/client/styles/notification.css @@ -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; } + diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacClientNotification.html b/guacamole/src/main/frontend/src/app/client/templates/guacClientNotification.html new file mode 100644 index 000000000..9948a2b1c --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/templates/guacClientNotification.html @@ -0,0 +1,5 @@ +
+ + + +
diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html b/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html index f1ceb64ac..ecae4efa3 100644 --- a/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html +++ b/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html @@ -8,6 +8,10 @@

{{ getClientTitle(client) }}

+ + + + \ No newline at end of file