diff --git a/guacamole/src/licenses/bundled/glyptodon-enterprise-player/LICENSE b/guacamole/src/licenses/bundled/glyptodon-enterprise-player/LICENSE new file mode 100644 index 000000000..69e97671e --- /dev/null +++ b/guacamole/src/licenses/bundled/glyptodon-enterprise-player/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2019 Glyptodon, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/guacamole/src/licenses/bundled/glyptodon-enterprise-player/README b/guacamole/src/licenses/bundled/glyptodon-enterprise-player/README new file mode 100644 index 000000000..c9cef3005 --- /dev/null +++ b/guacamole/src/licenses/bundled/glyptodon-enterprise-player/README @@ -0,0 +1,9 @@ +Session Recording Player for Glyptodon Enterprise +(https://github.com/glyptodon/glyptodon-enterprise-player) +---------------------------------------------------------- + + Version: 1.1.0-1 + From: 'Glyptodon, Inc.' (https://glyptodon.com/) + License(s): + MIT (bundled/glyptodon-enterprise-player/LICENSE) + diff --git a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js index 38ef98a5c..d1e78754c 100644 --- a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js @@ -180,6 +180,15 @@ angular.module('index').config(['$routeProvider', '$locationProvider', resolve : { updateCurrentToken: updateCurrentToken } }) + // Recording player + .when('/settings/:dataSource/recording/:identifier/:name', { + title : 'APP.NAME', + bodyClassName : 'settings', + templateUrl : 'app/settings/templates/settingsConnectionHistoryPlayer.html', + controller : 'connectionHistoryPlayerController', + resolve : { updateCurrentToken: updateCurrentToken } + }) + // Client view .when('/client/:id', { bodyClassName : 'client', diff --git a/guacamole/src/main/frontend/src/app/index/filters/resolveFilter.js b/guacamole/src/main/frontend/src/app/index/filters/resolveFilter.js new file mode 100644 index 000000000..ccf8ad749 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/index/filters/resolveFilter.js @@ -0,0 +1,53 @@ +/* + * 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. + */ + +/** + * Filter which accepts a promise as input, returning the resolved value of + * that promise if/when the promise is resolved. While the promise is not + * resolved, null is returned. + */ +angular.module('index').filter('resolve', [function resolveFilter() { + + /** + * The name of the property to use to store the resolved promise value. + * + * @type {!string} + */ + var RESOLVED_VALUE_KEY = '_guac_resolveFilter_resolvedValue'; + + return function resolveFilter(promise) { + + if (!promise) + return null; + + // Assign value to RESOLVED_VALUE_KEY automatically upon resolution of + // the received promise + if (!(RESOLVED_VALUE_KEY in promise)) { + promise[RESOLVED_VALUE_KEY] = null; + promise.then((value) => { + return promise[RESOLVED_VALUE_KEY] = value; + }); + } + + // Always return cached value + return promise[RESOLVED_VALUE_KEY]; + + }; + +}]); diff --git a/guacamole/src/main/frontend/src/app/player/directives/player.js b/guacamole/src/main/frontend/src/app/player/directives/player.js new file mode 100644 index 000000000..78e8adc5a --- /dev/null +++ b/guacamole/src/main/frontend/src/app/player/directives/player.js @@ -0,0 +1,388 @@ +/* + * 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. + */ + +/* + * NOTE: This session recording player implementation is based on the Session + * Recording Player for Glyptodon Enterprise which is available at + * https://github.com/glyptodon/glyptodon-enterprise-player under the + * following license: + * + * Copyright (C) 2019 Glyptodon, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Directive which plays back session recordings. This directive emits the + * following events based on state changes within the current recording: + * + * "guacPlayerLoading": + * A new recording has been selected and is now loading. + * + * "guacPlayerError": + * The current recording cannot be loaded or played due to an error. + * The recording may be unreadable (lack of permissions) or corrupt + * (protocol error). + * + * "guacPlayerProgress" + * Additional data has been loaded for the current recording and the + * recording's duration has changed. The new duration in milliseconds + * and the number of bytes loaded so far are passed to the event. + * + * "guacPlayerLoaded" + * The current recording has finished loading. + * + * "guacPlayerPlay" + * Playback of the current recording has started or has been resumed. + * + * "guacPlayerPause" + * Playback of the current recording has been paused. + * + * "guacPlayerSeek" + * The playback position of the current recording has changed. The new + * position within the recording is passed to the event as the number + * of milliseconds since the start of the recording. + */ +angular.module('player').directive('guacPlayer', ['$injector', function guacPlayer($injector) { + + var config = { + restrict : 'E', + templateUrl : 'app/player/templates/player.html' + }; + + config.scope = { + + /** + * A Blob containing the Guacamole session recording to load. + * + * @type {!Blob|Guacamole.Tunnel} + */ + src : '=' + + }; + + config.controller = ['$scope', '$element', '$injector', + function guacPlayerController($scope) { + + /** + * Guacamole.SessionRecording instance to be used to playback the + * session recording given via $scope.src. If the recording has not + * yet been loaded, this will be null. + * + * @type {Guacamole.SessionRecording} + */ + $scope.recording = null; + + /** + * The current playback position, in milliseconds. If a seek request is + * in progress, this will be the desired playback position of the + * pending request. + * + * @type {!number} + */ + $scope.playbackPosition = 0; + + /** + * The key of the translation string that describes the operation + * currently running in the background, or null if no such operation is + * running. + * + * @type {string} + */ + $scope.operationMessage = null; + + /** + * The current progress toward completion of the operation running in + * the background, where 0 represents no progress and 1 represents full + * completion. If no such operation is running, this value has no + * meaning. + * + * @type {!number} + */ + $scope.operationProgress = 0; + + /** + * The position within the recording of the current seek operation, in + * milliseconds. If a seek request is not in progress, this will be + * null. + * + * @type {number} + */ + $scope.seekPosition = null; + + /** + * Whether a seek request is currently in progress. A seek request is + * in progress if the user is attempting to change the current playback + * position (the user is manipulating the playback position slider). + * + * @type {boolean} + */ + var pendingSeekRequest = false; + + /** + * Whether playback should be resumed (play() should be invoked on the + * recording) once the current seek request is complete. This value + * only has meaning if a seek request is pending. + * + * @type {boolean} + */ + var resumeAfterSeekRequest = false; + + /** + * Formats the given number as a decimal string, adding leading zeroes + * such that the string contains at least two digits. The given number + * MUST NOT be negative. + * + * @param {!number} value + * The number to format. + * + * @returns {!string} + * The decimal string representation of the given value, padded + * with leading zeroes up to a minimum length of two digits. + */ + var zeroPad = function zeroPad(value) { + return value > 9 ? value : '0' + value; + }; + + /** + * Formats the given quantity of milliseconds as days, hours, minutes, + * and whole seconds, separated by colons (DD:HH:MM:SS). Hours are + * included only if the quantity is at least one hour, and days are + * included only if the quantity is at least one day. All included + * groups are zero-padded to two digits with the exception of the + * left-most group. + * + * @param {!number} value + * The time to format, in milliseconds. + * + * @returns {!string} + * The given quantity of milliseconds formatted as "DD:HH:MM:SS". + */ + $scope.formatTime = function formatTime(value) { + + // Round provided value down to whole seconds + value = Math.floor((value || 0) / 1000); + + // Separate seconds into logical groups of seconds, minutes, + // hours, etc. + var groups = [ 1, 24, 60, 60 ]; + for (var i = groups.length - 1; i >= 0; i--) { + var placeValue = groups[i]; + groups[i] = zeroPad(value % placeValue); + value = Math.floor(value / placeValue); + } + + // Format groups separated by colons, stripping leading zeroes and + // groups which are entirely zeroes, leaving at least minutes and + // seconds + var formatted = groups.join(':'); + return /^[0:]*([0-9]{1,2}(?::[0-9]{2})+)$/.exec(formatted)[1]; + + }; + + /** + * Pauses playback and decouples the position slider from current + * playback position, allowing the user to manipulate the slider + * without interference. Playback state will be resumed following a + * call to commitSeekRequest(). + */ + $scope.beginSeekRequest = function beginSeekRequest() { + + // If a recording is present, pause and save state if we haven't + // already done so + if ($scope.recording && !pendingSeekRequest) { + resumeAfterSeekRequest = $scope.recording.isPlaying(); + $scope.recording.pause(); + } + + // Flag seek request as in progress + pendingSeekRequest = true; + + }; + + /** + * Restores the playback state at the time beginSeekRequest() was + * called and resumes coupling between the playback position slider and + * actual playback position. + */ + $scope.commitSeekRequest = function commitSeekRequest() { + + // If a recording is present and there is an active seek request, + // restore the playback state at the time that request began and + // begin seeking to the requested position + if ($scope.recording && pendingSeekRequest) { + + $scope.seekPosition = null; + $scope.operationMessage = 'PLAYER.INFO_SEEK_IN_PROGRESS'; + $scope.operationProgress = 0; + + // Cancel seek when requested, updating playback position if + // that position changed + $scope.cancelOperation = function abortSeek() { + $scope.recording.cancel(); + $scope.playbackPosition = $scope.seekPosition || $scope.playbackPosition; + }; + + resumeAfterSeekRequest && $scope.recording.play(); + $scope.recording.seek($scope.playbackPosition, function seekComplete() { + $scope.operationMessage = null; + $scope.$evalAsync(); + }); + + } + + // Flag seek request as completed + pendingSeekRequest = false; + + }; + + /** + * Toggles the current playback state. If playback is currently paused, + * playback is resumed. If playback is currently active, playback is + * paused. If no recording has been loaded, this function has no + * effect. + */ + $scope.togglePlayback = function togglePlayback() { + if ($scope.recording) { + if ($scope.recording.isPlaying()) + $scope.recording.pause(); + else + $scope.recording.play(); + } + }; + + // Automatically load the requested session recording + $scope.$watch('src', function srcChanged(src) { + + // Reset position and seek state + pendingSeekRequest = false; + $scope.playbackPosition = 0; + + // Stop loading the current recording, if any + if ($scope.recording) { + $scope.recording.pause(); + $scope.recording.abort(); + } + + // If no recording is provided, reset to empty + if (!src) + $scope.recording = null; + + // Otherwise, begin loading the provided recording + else { + + $scope.recording = new Guacamole.SessionRecording(src); + + // Begin downloading the recording + $scope.recording.connect(); + + // Notify listeners when the recording is completely loaded + $scope.recording.onload = function recordingLoaded() { + $scope.operationMessage = null; + $scope.$emit('guacPlayerLoaded'); + $scope.$evalAsync(); + }; + + // Notify listeners if an error occurs + $scope.recording.onerror = function recordingFailed(message) { + $scope.operationMessage = null; + $scope.$emit('guacPlayerError', message); + $scope.$evalAsync(); + }; + + // Notify listeners when additional recording data has been + // loaded + $scope.recording.onprogress = function recordingLoadProgressed(duration, current) { + $scope.operationProgress = src.size ? current / src.size : 0; + $scope.$emit('guacPlayerProgress', duration, current); + $scope.$evalAsync(); + }; + + // Notify listeners when playback has started/resumed + $scope.recording.onplay = function playbackStarted() { + $scope.$emit('guacPlayerPlay'); + $scope.$evalAsync(); + }; + + // Notify listeners when playback has paused + $scope.recording.onpause = function playbackPaused() { + $scope.$emit('guacPlayerPause'); + $scope.$evalAsync(); + }; + + // Notify listeners when current position within the recording + // has changed + $scope.recording.onseek = function positionChanged(position, current, total) { + + // Update current playback position while playing + if ($scope.recording.isPlaying()) + $scope.playbackPosition = position; + + // Update seek progress while seeking + else { + $scope.seekPosition = position; + $scope.operationProgress = current / total; + } + + $scope.$emit('guacPlayerSeek', position); + $scope.$evalAsync(); + + }; + + $scope.operationMessage = 'PLAYER.INFO_LOADING_RECORDING'; + $scope.operationProgress = 0; + + $scope.cancelOperation = function abortLoad() { + $scope.recording.abort(); + $scope.operationMessage = null; + }; + + $scope.$emit('guacPlayerLoading'); + + } + + }); + + // Clean up resources when player is destroyed + $scope.$on('$destroy', function playerDestroyed() { + $scope.recording.pause(); + $scope.recording.abort(); + }); + + }]; + + return config; + +}]); diff --git a/guacamole/src/main/frontend/src/app/player/directives/playerDisplay.js b/guacamole/src/main/frontend/src/app/player/directives/playerDisplay.js new file mode 100644 index 000000000..20eab7cb8 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/player/directives/playerDisplay.js @@ -0,0 +1,138 @@ +/* + * 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. + */ + +/* + * NOTE: This session recording player implementation is based on the Session + * Recording Player for Glyptodon Enterprise which is available at + * https://github.com/glyptodon/glyptodon-enterprise-player under the + * following license: + * + * Copyright (C) 2019 Glyptodon, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Directive which contains a given Guacamole.Display, automatically scaling + * the display to fit available space. + */ +angular.module('player').directive('guacPlayerDisplay', [function guacPlayerDisplay() { + + var config = { + restrict : 'E', + templateUrl : 'app/player/templates/playerDisplay.html' + }; + + config.scope = { + + /** + * The Guacamole.Display instance which should be displayed within the + * directive. + * + * @type {Guacamole.Display} + */ + display : '=' + + }; + + config.controller = ['$scope', '$element', function guacPlayerDisplayController($scope, $element) { + + /** + * The root element of this instance of the guacPlayerDisplay + * directive. + * + * @type {Element} + */ + var element = $element.find('.guac-player-display')[0]; + + /** + * The element which serves as a container for the root element of the + * Guacamole.Display assigned to $scope.display. + * + * @type {HTMLDivElement} + */ + var container = $element.find('.guac-player-display-container')[0]; + + /** + * Rescales the Guacamole.Display currently assigned to $scope.display + * such that it exactly fits within this directive's available space. + * If no display is currently assigned or the assigned display is not + * at least 1x1 pixels in size, this function has no effect. + */ + $scope.fitDisplay = function fitDisplay() { + + // Ignore if no display is yet present + if (!$scope.display) + return; + + var displayWidth = $scope.display.getWidth(); + var displayHeight = $scope.display.getHeight(); + + // Ignore if the provided display is not at least 1x1 pixels + if (!displayWidth || !displayHeight) + return; + + // Fit display within available space + $scope.display.scale(Math.min(element.offsetWidth / displayWidth, + element.offsetHeight / displayHeight)); + + }; + + // Automatically add/remove the Guacamole.Display as $scope.display is + // updated + $scope.$watch('display', function displayChanged(display, oldDisplay) { + + // Clear out old display, if any + if (oldDisplay) { + container.innerHTML = ''; + oldDisplay.onresize = null; + } + + // If a new display is provided, add it to the container, keeping + // its scale in sync with changes to available space and display + // size + if (display) { + container.appendChild(display.getElement()); + display.onresize = $scope.fitDisplay; + $scope.fitDisplay(); + } + + }); + + }]; + + return config; + +}]); diff --git a/guacamole/src/main/frontend/src/app/player/directives/progressIndicator.js b/guacamole/src/main/frontend/src/app/player/directives/progressIndicator.js new file mode 100644 index 000000000..1d20fc065 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/player/directives/progressIndicator.js @@ -0,0 +1,101 @@ +/* + * 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. + */ + +/* + * NOTE: This session recording player implementation is based on the Session + * Recording Player for Glyptodon Enterprise which is available at + * https://github.com/glyptodon/glyptodon-enterprise-player under the + * following license: + * + * Copyright (C) 2019 Glyptodon, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Directive which displays an indicator showing the current progress of an + * arbitrary operation. + */ +angular.module('player').directive('guacPlayerProgressIndicator', [function guacPlayerProgressIndicator() { + + var config = { + restrict : 'E', + templateUrl : 'app/player/templates/progressIndicator.html' + }; + + config.scope = { + + /** + * A value between 0 and 1 inclusive which indicates current progress, + * where 0 represents no progress and 1 represents finished. + * + * @type {Number} + */ + progress : '=' + + }; + + config.controller = ['$scope', function guacPlayerProgressIndicatorController($scope) { + + /** + * The current progress of the operation as a percentage. This value is + * automatically updated as $scope.progress changes. + * + * @type {Number} + */ + $scope.percentage = 0; + + /** + * The CSS transform which should be applied to the bar portion of the + * progress indicator. This value is automatically updated as + * $scope.progress changes. + * + * @type {String} + */ + $scope.barTransform = null; + + // Keep percentage and bar transform up-to-date with changes to + // progress value + $scope.$watch('progress', function progressChanged(progress) { + progress = progress || 0; + $scope.percentage = Math.floor(progress * 100); + $scope.barTransform = 'rotate(' + (360 * progress - 45) + 'deg)'; + }); + + }]; + + return config; + +}]); diff --git a/guacamole/src/main/frontend/src/app/player/playerModule.js b/guacamole/src/main/frontend/src/app/player/playerModule.js new file mode 100644 index 000000000..c3bf3ea4a --- /dev/null +++ b/guacamole/src/main/frontend/src/app/player/playerModule.js @@ -0,0 +1,52 @@ +/* + * 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. + */ + +/* + * NOTE: This session recording player implementation is based on the Session + * Recording Player for Glyptodon Enterprise which is available at + * https://github.com/glyptodon/glyptodon-enterprise-player under the + * following license: + * + * Copyright (C) 2019 Glyptodon, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * Module providing in-browser playback of session recordings. + */ +angular.module('player', [ + 'element' +]); diff --git a/guacamole/src/main/frontend/src/app/player/styles/player.css b/guacamole/src/main/frontend/src/app/player/styles/player.css new file mode 100644 index 000000000..36a783eca --- /dev/null +++ b/guacamole/src/main/frontend/src/app/player/styles/player.css @@ -0,0 +1,147 @@ +/* + * 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. + */ + +/* + * NOTE: This session recording player implementation is based on the Session + * Recording Player for Glyptodon Enterprise which is available at + * https://github.com/glyptodon/glyptodon-enterprise-player under the + * following license: + * + * Copyright (C) 2019 Glyptodon, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +guac-player { + display: inline-block; + position: relative; +} + +guac-player .guac-player-display { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +guac-player .guac-player-controls { + + position: absolute; + left: 0; + bottom: 0; + width: 100%; + +} + +.guac-player-controls .guac-player-seek { + display: block; + width: 100%; +} + +.guac-player-controls .guac-player-play, +.guac-player-controls .guac-player-pause { + color: white; + background: transparent; + border: none; + width: 2em; + height: 2em; + min-width: 0; + padding: 0; + margin: 0; +} + +.guac-player-controls .guac-player-play:hover, +.guac-player-controls .guac-player-pause:hover { + background: rgba(255, 255, 255, 0.5); +} + +.guac-player-controls .pause-icon, +.guac-player-controls .play-icon { + display: inline-block; + width: 2em; + height: 2em; + background-size: contain; + background-position: center; + background-repeat: no-repeat; + vertical-align: middle; +} + +.guac-player-controls .play-icon { + background-image: url('images/action-icons/guac-play.svg'); +} + +.guac-player-controls .pause-icon { + background-image: url('images/action-icons/guac-pause.svg'); +} + +guac-player .guac-player-status { + + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + + background: rgba(0, 0, 0, 0.5); + z-index: 1; + + display: -webkit-box; + display: -webkit-flex; + display: -moz-box; + display: -ms-flexbox; + display: flex; + + -webkit-box-align: center; + -webkit-align-items: center; + -moz-box-align: center; + -ms-flex-align: center; + align-items: center; + + -webkit-box-pack: center; + -webkit-justify-content: center; + -moz-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -moz-box-orient: vertical; + -moz-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + +} diff --git a/guacamole/src/main/frontend/src/app/player/styles/playerDisplay.css b/guacamole/src/main/frontend/src/app/player/styles/playerDisplay.css new file mode 100644 index 000000000..c17cd4d65 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/player/styles/playerDisplay.css @@ -0,0 +1,77 @@ +/* + * 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. + */ + +/* + * NOTE: This session recording player implementation is based on the Session + * Recording Player for Glyptodon Enterprise which is available at + * https://github.com/glyptodon/glyptodon-enterprise-player under the + * following license: + * + * Copyright (C) 2019 Glyptodon, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +.guac-player-display { + + position: relative; + + display: -webkit-box; + display: -webkit-flex; + display: -moz-box; + display: -ms-flexbox; + display: flex; + + -webkit-box-align: center; + -webkit-align-items: center; + -moz-box-align: center; + -ms-flex-align: center; + align-items: center; + + -webkit-box-pack: center; + -webkit-justify-content: center; + -moz-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + +} + +.guac-player-display .guac-player-display-container { + -webkit-box-flex: 0; + -webkit-flex: 0 0 auto; + -moz-box-flex: 0; + -ms-flex: 0 0 auto; + flex: 0 0 auto; +} diff --git a/guacamole/src/main/frontend/src/app/player/styles/progressIndicator.css b/guacamole/src/main/frontend/src/app/player/styles/progressIndicator.css new file mode 100644 index 000000000..7d8c35e5e --- /dev/null +++ b/guacamole/src/main/frontend/src/app/player/styles/progressIndicator.css @@ -0,0 +1,125 @@ +/* + * 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. + */ + +/* + * NOTE: This session recording player implementation is based on the Session + * Recording Player for Glyptodon Enterprise which is available at + * https://github.com/glyptodon/glyptodon-enterprise-player under the + * following license: + * + * Copyright (C) 2019 Glyptodon, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +guac-player-progress-indicator { + + width: 128px; + height: 128px; + + position: relative; + + display: -webkit-box; + display: -webkit-flex; + display: -moz-box; + display: -ms-flexbox; + display: flex; + + -webkit-box-align: center; + -webkit-align-items: center; + -moz-box-align: center; + -ms-flex-align: center; + align-items: center; + + -webkit-box-pack: center; + -webkit-justify-content: center; + -moz-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + +} + +guac-player-progress-indicator .guac-player-progress-text { + font-size: 2em; + font-weight: bold; +} + +guac-player-progress-indicator .guac-player-progress-bar-container { + + position: absolute; + right: 0; + top: 0; + width: 50%; + height: 100%; + overflow: hidden; + +} + +guac-player-progress-indicator .guac-player-progress-bar-container.past-halfway { + overflow: visible; +} + +guac-player-progress-indicator .guac-player-progress-bar-container.past-halfway::before, +guac-player-progress-indicator .guac-player-progress-bar { + + position: absolute; + left: -64px; + top: 0; + width: 128px; + height: 128px; + + -webkit-border-radius: 128px; + -moz-border-radius: 128px; + border-radius: 128px; + + border: 12px solid #5AF; + border-bottom-color: transparent; + border-right-color: transparent; + +} + +guac-player-progress-indicator .guac-player-progress-bar-container.past-halfway::before { + + content: ' '; + display: block; + box-sizing: border-box; + + -webkit-transform: rotate(135deg); + -moz-transform: rotate(135deg); + -ms-transform: rotate(135deg); + -o-transform: rotate(135deg); + transform: rotate(135deg); + +} diff --git a/guacamole/src/main/frontend/src/app/player/styles/seek.css b/guacamole/src/main/frontend/src/app/player/styles/seek.css new file mode 100644 index 000000000..4b41f3a0a --- /dev/null +++ b/guacamole/src/main/frontend/src/app/player/styles/seek.css @@ -0,0 +1,170 @@ +/* + * 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. + */ + +/* + * NOTE: This session recording player implementation is based on the Session + * Recording Player for Glyptodon Enterprise which is available at + * https://github.com/glyptodon/glyptodon-enterprise-player under the + * following license: + * + * Copyright (C) 2019 Glyptodon, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/* + * General (not browser-specific) + */ + +input[type="range"] { + background: transparent; + width: 100%; + margin: 0; +} + +input[type="range"]:focus { + outline: none; +} + +/* + * WebKit + */ + +input[type="range"] { + -webkit-appearance: none; +} + +input[type="range"]::-webkit-slider-runnable-track { + + border: none; + border-radius: 0; + background: #5AF; + + width: 100%; + height: 0.5em; + + cursor: pointer; + +} + +input[type="range"]::-webkit-slider-thumb { + + border: none; + border-radius: 0; + background: white; + + width: 3px; + height: 0.5em; + + -webkit-appearance: none; + cursor: pointer; + +} + +input[type="range"]:focus::-webkit-slider-runnable-track { + background: #5AF; +} + +/* + * Firefox + */ + +input[type="range"]::-moz-range-track { + + border: none; + border-radius: 0; + background: #5AF; + + width: 100%; + height: 0.5em; + + cursor: pointer; + +} + +input[type="range"]::-moz-range-thumb { + + border: none; + border-radius: 0; + background: white; + + width: 3px; + height: 0.5em; + + cursor: pointer; + +} + +/* + * Internet Explorer + */ + +input[type="range"]::-ms-track { + + width: 100%; + height: 0.5em; + margin: 0; + + border: none; + border-radius: 0; + background: transparent; + color: transparent; + + cursor: pointer; + +} + +input[type="range"]::-ms-thumb { + + border: none; + border-radius: 0; + background: white; + + width: 3px; + height: 0.5em; + margin: 0; + + cursor: pointer; + +} + +input[type="range"]::-ms-fill-lower, +input[type="range"]::-ms-fill-upper, +input[type="range"]:focus::-ms-fill-lower, +input[type="range"]:focus::-ms-fill-upper { + border: none; + border-radius: 0; + background: #5AF; +} diff --git a/guacamole/src/main/frontend/src/app/player/templates/player.html b/guacamole/src/main/frontend/src/app/player/templates/player.html new file mode 100644 index 000000000..91594b40e --- /dev/null +++ b/guacamole/src/main/frontend/src/app/player/templates/player.html @@ -0,0 +1,41 @@ + + + + +
+ + + + + + + + + + + + + {{ formatTime(playbackPosition) }} / {{ formatTime(recording.getDuration()) }} + + +
+ + +
+ +

+ +
diff --git a/guacamole/src/main/frontend/src/app/player/templates/playerDisplay.html b/guacamole/src/main/frontend/src/app/player/templates/playerDisplay.html new file mode 100644 index 000000000..606afafcb --- /dev/null +++ b/guacamole/src/main/frontend/src/app/player/templates/playerDisplay.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/guacamole/src/main/frontend/src/app/player/templates/progressIndicator.html b/guacamole/src/main/frontend/src/app/player/templates/progressIndicator.html new file mode 100644 index 000000000..38c976893 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/player/templates/progressIndicator.html @@ -0,0 +1,12 @@ +
{{ percentage }}%
+
+
+
diff --git a/guacamole/src/main/frontend/src/app/settings/controllers/connectionHistoryPlayerController.js b/guacamole/src/main/frontend/src/app/settings/controllers/connectionHistoryPlayerController.js new file mode 100644 index 000000000..7adb64cff --- /dev/null +++ b/guacamole/src/main/frontend/src/app/settings/controllers/connectionHistoryPlayerController.js @@ -0,0 +1,50 @@ +/* + * 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. + */ + +/** + * The controller for the session recording player page. + */ +angular.module('manage').controller('connectionHistoryPlayerController', ['$scope', '$injector', + function connectionHistoryPlayerController($scope, $injector) { + + // Required services + var authenticationService = $injector.get('authenticationService'); + var $routeParams = $injector.get('$routeParams'); + + /** + * The URL of the REST API resource exposing the requested session + * recording. + * + * @type {!string} + */ + var recordingURL = 'api/session/data/' + encodeURIComponent($routeParams.dataSource) + + '/history/connections/' + encodeURIComponent($routeParams.identifier) + + '/logs/' + encodeURIComponent($routeParams.name); + + /** + * The tunnel which should be used to download the Guacamole session + * recording. + * + * @type Guacamole.Tunnel + */ + $scope.tunnel = new Guacamole.StaticHTTPTunnel(recordingURL, false, { + 'Guacamole-Token' : authenticationService.getCurrentToken() + }); + +}]); diff --git a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsConnectionHistory.js b/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsConnectionHistory.js index 42cf10c1b..58ffca938 100644 --- a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsConnectionHistory.js +++ b/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsConnectionHistory.js @@ -175,7 +175,7 @@ angular.module('settings').directive('guacSettingsConnectionHistory', [function // Wrap all history entries for sake of display $scope.historyEntryWrappers = []; angular.forEach(historyEntries, function wrapHistoryEntry(historyEntry) { - $scope.historyEntryWrappers.push(new ConnectionHistoryEntryWrapper(historyEntry)); + $scope.historyEntryWrappers.push(new ConnectionHistoryEntryWrapper($scope.dataSource, historyEntry)); }); }, requestService.DIE); diff --git a/guacamole/src/main/frontend/src/app/settings/settingsModule.js b/guacamole/src/main/frontend/src/app/settings/settingsModule.js index 62ad1c813..4eeb517af 100644 --- a/guacamole/src/main/frontend/src/app/settings/settingsModule.js +++ b/guacamole/src/main/frontend/src/app/settings/settingsModule.js @@ -26,6 +26,7 @@ angular.module('settings', [ 'list', 'navigation', 'notification', + 'player', 'rest', 'storage' ]); diff --git a/guacamole/src/main/frontend/src/app/settings/styles/history-player.css b/guacamole/src/main/frontend/src/app/settings/styles/history-player.css new file mode 100644 index 000000000..03d562a53 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/settings/styles/history-player.css @@ -0,0 +1,114 @@ +/* + * 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. + */ + +/* + * NOTE: This session recording player implementation is based on the Session + * Recording Player for Glyptodon Enterprise which is available at + * https://github.com/glyptodon/glyptodon-enterprise-player under the + * following license: + * + * Copyright (C) 2019 Glyptodon, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +.settings.connectionHistoryPlayer { + + background: black; + color: white; + + position: absolute; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + padding: 0; + margin: 0; + +} + +.settings.connectionHistoryPlayer guac-player { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.settings.connectionHistoryPlayer .guac-player-help-no-recording, +.settings.connectionHistoryPlayer .guac-player-help-recording-error { + margin: 8px; + max-width: 480px; +} + +.settings.connectionHistoryPlayer .guac-player-button { + + display: inline-block; + + border: 2px solid white; + border-radius: 0; + background: black; + color: white; + font-weight: bold; + + padding: 0.5em 1em; + margin: 8px; + +} + +.settings.connectionHistoryPlayer .guac-player-controls { + padding: 0.25em; +} + +.settings.connectionHistoryPlayer .guac-player-controls { + background: rgba(0, 0, 0, 0.5); +} + +.settings.connectionHistoryPlayer.playing .guac-player-controls { + opacity: 0; + -webkit-transition: opacity 0.25s linear 0.25s; + -moz-transition: opacity 0.25s linear 0.25s; + -o-transition: opacity 0.25s linear 0.25s; + transition: opacity 0.25s linear 0.25s; +} + +.settings.connectionHistoryPlayer.paused .guac-player-controls, +.settings.connectionHistoryPlayer.playing:hover .guac-player-controls { + opacity: 1; + -webkit-transition-delay: 0s; + -moz-transition-delay: 0s; + -o-transition-delay: 0s; + transition-delay: 0s; +} diff --git a/guacamole/src/main/frontend/src/app/settings/styles/history.css b/guacamole/src/main/frontend/src/app/settings/styles/history.css index 7b05e1ef2..d5c9dac00 100644 --- a/guacamole/src/main/frontend/src/app/settings/styles/history.css +++ b/guacamole/src/main/frontend/src/app/settings/styles/history.css @@ -70,4 +70,20 @@ .settings.connectionHistory .history-list { width: 100%; -} \ No newline at end of file +} + +.settings.connectionHistory a.history-session-recording { + color: #0000ee; +} + +.settings.connectionHistory a.history-session-recording::after { + display: inline-block; + content: ' '; + width: 1em; + height: 1em; + background-size: contain; + background-position: center; + background-repeat: no-repeat; + background-image: url('images/action-icons/guac-play-link.svg'); + vertical-align: middle; +} diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnectionHistory.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsConnectionHistory.html index 41e0f65eb..00c5c2e30 100644 --- a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnectionHistory.html +++ b/guacamole/src/main/frontend/src/app/settings/templates/settingsConnectionHistory.html @@ -32,6 +32,9 @@ {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_REMOTEHOST' | translate}} + + {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_LOGS' | translate}} + @@ -42,6 +45,14 @@ translate-values="{VALUE: historyEntryWrapper.readableDuration.value, UNIT: historyEntryWrapper.readableDuration.unit}"> {{historyEntryWrapper.entry.connectionName}} {{historyEntryWrapper.entry.remoteHost}} + + + {{'SETTINGS_CONNECTION_HISTORY.ACTION_VIEW_RECORDING' | translate}} + + diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnectionHistoryPlayer.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsConnectionHistoryPlayer.html new file mode 100644 index 000000000..41d6aa873 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/settings/templates/settingsConnectionHistoryPlayer.html @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/settings/types/ConnectionHistoryEntryWrapper.js b/guacamole/src/main/frontend/src/app/settings/types/ConnectionHistoryEntryWrapper.js index b9a3c5279..9015ea0be 100644 --- a/guacamole/src/main/frontend/src/app/settings/types/ConnectionHistoryEntryWrapper.js +++ b/guacamole/src/main/frontend/src/app/settings/types/ConnectionHistoryEntryWrapper.js @@ -24,8 +24,12 @@ angular.module('settings').factory('ConnectionHistoryEntryWrapper', ['$injector' function defineConnectionHistoryEntryWrapper($injector) { // Required types + var ActivityLog = $injector.get('ActivityLog'); var ConnectionHistoryEntry = $injector.get('ConnectionHistoryEntry'); + // Required services + var $translate = $injector.get('$translate'); + /** * Wrapper for ConnectionHistoryEntry which adds display-specific * properties, such as a duration. @@ -34,7 +38,7 @@ angular.module('settings').factory('ConnectionHistoryEntryWrapper', ['$injector' * @param {ConnectionHistoryEntry} historyEntry * The ConnectionHistoryEntry that should be wrapped. */ - var ConnectionHistoryEntryWrapper = function ConnectionHistoryEntryWrapper(historyEntry) { + var ConnectionHistoryEntryWrapper = function ConnectionHistoryEntryWrapper(dataSource, historyEntry) { /** * The wrapped ConnectionHistoryEntry. @@ -77,6 +81,67 @@ angular.module('settings').factory('ConnectionHistoryEntryWrapper', ['$injector' if (!historyEntry.endDate) this.readableDurationText = 'SETTINGS_CONNECTION_HISTORY.INFO_CONNECTION_DURATION_UNKNOWN'; + /** + * The graphical session recording associated with this history entry, + * if any. If no session recordings are associated with the entry, this + * will be null. If there are multiple session recordings, this will be + * the first such recording. + * + * @type {ConnectionHistoryEntryWrapper.Log} + */ + this.sessionRecording = (function getSessionRecording() { + + var identifier = historyEntry.identifier; + if (!identifier) + return null; + + var name = _.findKey(historyEntry.logs, log => log.type === ActivityLog.Type.GUACAMOLE_SESSION_RECORDING); + if (!name) + return null; + + var log = historyEntry.logs[name]; + return new ConnectionHistoryEntryWrapper.Log({ + + url : '#/settings/' + encodeURIComponent(dataSource) + + '/recording/' + encodeURIComponent(identifier) + + '/' + encodeURIComponent(name), + + description : $translate(log.description.key, log.description.variables) + + }); + + })(); + + }; + + /** + * Representation of the ActivityLog of a ConnectionHistoryEntry which adds + * display-specific properties, such as a URL for viewing the log. + * + * @param {ConnectionHistoryEntryWrapper.Log|Object} [template={}] + * The object whose properties should be copied within the new + * ConnectionHistoryEntryWrapper.Log. + */ + ConnectionHistoryEntryWrapper.Log = function Log(template) { + + // Use empty object by default + template = template || {}; + + /** + * The relative URL for a session recording player that loads the + * session recording represented by this log. + * + * @type {!string} + */ + this.url = template.url; + + /** + * A promise that resolves with a human-readable description of the log. + * + * @type {!Promise.} + */ + this.description = template.description; + }; return ConnectionHistoryEntryWrapper; diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-pause.svg b/guacamole/src/main/frontend/src/images/action-icons/guac-pause.svg new file mode 100644 index 000000000..2fbb899ee --- /dev/null +++ b/guacamole/src/main/frontend/src/images/action-icons/guac-pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-play-link.svg b/guacamole/src/main/frontend/src/images/action-icons/guac-play-link.svg new file mode 100644 index 000000000..5c5ced87f --- /dev/null +++ b/guacamole/src/main/frontend/src/images/action-icons/guac-play-link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-play.svg b/guacamole/src/main/frontend/src/images/action-icons/guac-play.svg new file mode 100644 index 000000000..8aa6ccba2 --- /dev/null +++ b/guacamole/src/main/frontend/src/images/action-icons/guac-play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index d0c2f6511..6896d0a93 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -25,11 +25,14 @@ "ACTION_MANAGE_USER_GROUPS" : "Groups", "ACTION_NAVIGATE_BACK" : "Back", "ACTION_NAVIGATE_HOME" : "Home", + "ACTION_PAUSE" : "Pause", + "ACTION_PLAY" : "Play", "ACTION_SAVE" : "Save", "ACTION_SEARCH" : "Search", "ACTION_SHARE" : "Share", "ACTION_UPDATE_PASSWORD" : "Update Password", "ACTION_VIEW_HISTORY" : "History", + "ACTION_VIEW_RECORDING" : "View", "DIALOG_HEADER_ERROR" : "Error", @@ -386,6 +389,17 @@ }, + "PLAYER" : { + + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_PAUSE" : "@:APP.ACTION_PAUSE", + "ACTION_PLAY" : "@:APP.ACTION_PLAY", + + "INFO_LOADING_RECORDING" : "Your recording is now being loaded. Please wait...", + "INFO_SEEK_IN_PROGRESS" : "Seeking to the requested position. Please wait..." + + }, + "PROTOCOL_KUBERNETES" : { "FIELD_HEADER_BACKSPACE" : "Backspace key sends:", @@ -851,8 +865,9 @@ "SETTINGS_CONNECTION_HISTORY" : { - "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", - "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", + "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", + "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", + "ACTION_VIEW_RECORDING" : "@:APP.ACTION_VIEW_RECORDING", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", @@ -867,6 +882,7 @@ "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Connection name", "TABLE_HEADER_SESSION_DURATION" : "Duration", + "TABLE_HEADER_SESSION_LOGS" : "Logs", "TABLE_HEADER_SESSION_REMOTEHOST" : "Remote host", "TABLE_HEADER_SESSION_STARTDATE" : "Start time", "TABLE_HEADER_SESSION_USERNAME" : "Username",