GUACAMOLE-462: Merge add in-app player for session recordings.

This commit is contained in:
Virtually Nick
2022-03-08 19:06:27 -05:00
committed by GitHub
28 changed files with 1670 additions and 69 deletions

View File

@@ -1365,6 +1365,14 @@ Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTu
*/ */
var extraHeaders = extraTunnelHeaders || {}; var extraHeaders = extraTunnelHeaders || {};
/**
* The number of bytes in the file being downloaded, or null if this is not
* known.
*
* @type {number}
*/
this.size = null;
this.sendMessage = function sendMessage(elements) { this.sendMessage = function sendMessage(elements) {
// Do nothing // Do nothing
}; };
@@ -1411,6 +1419,9 @@ Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTu
} }
// Report overall size of stream in bytes, if known
tunnel.size = response.headers.get('Content-Length');
// Connection is open // Connection is open
tunnel.setState(Guacamole.Tunnel.State.OPEN); tunnel.setState(Guacamole.Tunnel.State.OPEN);

View File

@@ -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.

View File

@@ -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)

View File

@@ -180,6 +180,15 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
resolve : { updateCurrentToken: updateCurrentToken } 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 // Client view
.when('/client/:id', { .when('/client/:id', {
bodyClassName : 'client', bodyClassName : 'client',

View File

@@ -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}
*/
const 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];
};
}]);

View File

@@ -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) {
const 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.
*/
const 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;
}]);

View File

@@ -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() {
const 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}
*/
const 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}
*/
const 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;
}]);

View File

@@ -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() {
const 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;
}]);

View File

@@ -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'
]);

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -0,0 +1,41 @@
<!-- Actual playback display -->
<guac-player-display display="recording.getDisplay()"
ng-click="togglePlayback()"></guac-player-display>
<!-- Player controls -->
<div class="guac-player-controls" ng-show="recording">
<!-- Playback position slider -->
<input class="guac-player-seek" type="range" min="0" step="1"
ng-attr-max="{{ recording.getDuration() }}"
ng-change="beginSeekRequest()"
ng-model="playbackPosition"
ng-on-change="commitSeekRequest()">
<!-- Play button -->
<button class="guac-player-play"
ng-attr-title="{{ 'PLAYER.ACTION_PLAY' | translate }}"
ng-click="recording.play()"
ng-hide="recording.isPlaying()"><span class="play-icon"></span></button>
<!-- Pause button -->
<button class="guac-player-pause"
ng-attr-title="{{ 'PLAYER.ACTION_PAUSE' | translate }}"
ng-click="recording.pause()"
ng-show="recording.isPlaying()"><span class="pause-icon"></span></button>
<!-- Playback position and duration -->
<span class="guac-player-position">
{{ formatTime(playbackPosition) }} / {{ formatTime(recording.getDuration()) }}
</span>
</div>
<!-- Modal status indicator -->
<div class="guac-player-status" ng-show="operationMessage">
<guac-player-progress-indicator progress="operationProgress"></guac-player-progress-indicator>
<p translate="{{ operationMessage }}"></p>
<button class="guac-player-button guac-player-cancel"
ng-show="cancelOperation"
ng-click="cancelOperation()">{{ 'PLAYER.ACTION_CANCEL' | translate }}</button>
</div>

View File

@@ -0,0 +1,3 @@
<div class="guac-player-display" guac-resize="fitDisplay">
<div class="guac-player-display-container"></div>
</div>

View File

@@ -0,0 +1,12 @@
<div class="guac-player-progress-text">{{ percentage }}%</div>
<div class="guac-player-progress-bar-container" ng-class="{
'past-halfway' : progress > 0.5
}">
<div class="guac-player-progress-bar" ng-style="{
'-webkit-transform' : barTransform,
'-moz-transform' : barTransform,
'-ms-transform' : barTransform,
'-o-transform' : barTransform,
'transform' : barTransform
}"></div>
</div>

View File

@@ -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
const authenticationService = $injector.get('authenticationService');
const $routeParams = $injector.get('$routeParams');
/**
* The URL of the REST API resource exposing the requested session
* recording.
*
* @type {!string}
*/
const 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()
});
}]);

View File

@@ -82,11 +82,11 @@ angular.module('settings').directive('guacSettingsConnectionHistory', [function
* @type SortOrder * @type SortOrder
*/ */
$scope.order = new SortOrder([ $scope.order = new SortOrder([
'-startDate', '-entry.startDate',
'-duration', '-duration',
'username', 'entry.username',
'connectionName', 'entry.connectionName',
'remoteHost' 'entry.remoteHost'
]); ]);
// Get session date format // Get session date format
@@ -175,7 +175,7 @@ angular.module('settings').directive('guacSettingsConnectionHistory', [function
// Wrap all history entries for sake of display // Wrap all history entries for sake of display
$scope.historyEntryWrappers = []; $scope.historyEntryWrappers = [];
angular.forEach(historyEntries, function wrapHistoryEntry(historyEntry) { angular.forEach(historyEntries, function wrapHistoryEntry(historyEntry) {
$scope.historyEntryWrappers.push(new ConnectionHistoryEntryWrapper(historyEntry)); $scope.historyEntryWrappers.push(new ConnectionHistoryEntryWrapper($scope.dataSource, historyEntry));
}); });
}, requestService.DIE); }, requestService.DIE);
@@ -216,11 +216,11 @@ angular.module('settings').directive('guacSettingsConnectionHistory', [function
), ),
function pushRecord(historyEntryWrapper) { function pushRecord(historyEntryWrapper) {
records.push([ records.push([
historyEntryWrapper.username, historyEntryWrapper.entry.username,
$filter('date')(historyEntryWrapper.startDate, $scope.dateFormat), $filter('date')(historyEntryWrapper.entry.startDate, $scope.dateFormat),
historyEntryWrapper.duration / 1000, historyEntryWrapper.duration / 1000,
historyEntryWrapper.connectionName, historyEntryWrapper.entry.connectionName,
historyEntryWrapper.remoteHost historyEntryWrapper.entry.remoteHost
]); ]);
} }
); );

View File

@@ -26,6 +26,7 @@ angular.module('settings', [
'list', 'list',
'navigation', 'navigation',
'notification', 'notification',
'player',
'rest', 'rest',
'storage' 'storage'
]); ]);

View File

@@ -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;
}

View File

@@ -70,4 +70,20 @@
.settings.connectionHistory .history-list { .settings.connectionHistory .history-list {
width: 100%; width: 100%;
} }
.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;
}

View File

@@ -17,31 +17,42 @@
<table class="sorted history-list"> <table class="sorted history-list">
<thead> <thead>
<tr> <tr>
<th guac-sort-order="order" guac-sort-property="'username'"> <th guac-sort-order="order" guac-sort-property="'entry.username'">
{{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_USERNAME' | translate}} {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_USERNAME' | translate}}
</th> </th>
<th guac-sort-order="order" guac-sort-property="'startDate'"> <th guac-sort-order="order" guac-sort-property="'entry.startDate'">
{{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_STARTDATE' | translate}} {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_STARTDATE' | translate}}
</th> </th>
<th guac-sort-order="order" guac-sort-property="'duration'"> <th guac-sort-order="order" guac-sort-property="'duration'">
{{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_DURATION' | translate}} {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_DURATION' | translate}}
</th> </th>
<th guac-sort-order="order" guac-sort-property="'connectionName'"> <th guac-sort-order="order" guac-sort-property="'entry.connectionName'">
{{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_CONNECTION_NAME' | translate}} {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_CONNECTION_NAME' | translate}}
</th> </th>
<th guac-sort-order="order" guac-sort-property="'remoteHost'"> <th guac-sort-order="order" guac-sort-property="'entry.remoteHost'">
{{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_REMOTEHOST' | translate}} {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_REMOTEHOST' | translate}}
</th> </th>
<th>
{{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_LOGS' | translate}}
</th>
</tr> </tr>
</thead> </thead>
<tbody ng-class="{loading: !isLoaded()}"> <tbody ng-class="{loading: !isLoaded()}">
<tr ng-repeat="historyEntryWrapper in historyEntryWrapperPage" class="history"> <tr ng-repeat="historyEntryWrapper in historyEntryWrapperPage" class="history">
<td><guac-user-item username="historyEntryWrapper.username"></guac-user-item></td> <td><guac-user-item username="historyEntryWrapper.entry.username"></guac-user-item></td>
<td>{{historyEntryWrapper.startDate | date : dateFormat}}</td> <td>{{historyEntryWrapper.entry.startDate | date : dateFormat}}</td>
<td translate="{{historyEntryWrapper.readableDurationText}}" <td translate="{{historyEntryWrapper.readableDurationText}}"
translate-values="{VALUE: historyEntryWrapper.readableDuration.value, UNIT: historyEntryWrapper.readableDuration.unit}"></td> translate-values="{VALUE: historyEntryWrapper.readableDuration.value, UNIT: historyEntryWrapper.readableDuration.unit}"></td>
<td>{{historyEntryWrapper.connectionName}}</td> <td>{{historyEntryWrapper.entry.connectionName}}</td>
<td>{{historyEntryWrapper.remoteHost}}</td> <td>{{historyEntryWrapper.entry.remoteHost}}</td>
<td>
<a class="history-session-recording"
ng-show="historyEntryWrapper.sessionRecording"
ng-href="{{ historyEntryWrapper.sessionRecording.url }}"
ng-attr-title="{{ historyEntryWrapper.sessionRecording.description | resolve }}">
<span class="history-action-description">{{'SETTINGS_CONNECTION_HISTORY.ACTION_VIEW_RECORDING' | translate}}</span>
</a>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -0,0 +1,11 @@
<guac-viewport class="settings view connectionHistoryPlayer"
ng-class="{
'no-recording' : !selectedRecording,
'paused' : !playing,
'playing' : playing
}">
<!-- Player for selected recording -->
<guac-player src="tunnel"></guac-player>
</guac-viewport>

View File

@@ -24,7 +24,11 @@ angular.module('settings').factory('ConnectionHistoryEntryWrapper', ['$injector'
function defineConnectionHistoryEntryWrapper($injector) { function defineConnectionHistoryEntryWrapper($injector) {
// Required types // Required types
var ConnectionHistoryEntry = $injector.get('ConnectionHistoryEntry'); const ActivityLog = $injector.get('ActivityLog');
const ConnectionHistoryEntry = $injector.get('ConnectionHistoryEntry');
// Required services
const $translate = $injector.get('$translate');
/** /**
* Wrapper for ConnectionHistoryEntry which adds display-specific * Wrapper for ConnectionHistoryEntry which adds display-specific
@@ -34,55 +38,14 @@ angular.module('settings').factory('ConnectionHistoryEntryWrapper', ['$injector'
* @param {ConnectionHistoryEntry} historyEntry * @param {ConnectionHistoryEntry} historyEntry
* The ConnectionHistoryEntry that should be wrapped. * The ConnectionHistoryEntry that should be wrapped.
*/ */
var ConnectionHistoryEntryWrapper = function ConnectionHistoryEntryWrapper(historyEntry) { const ConnectionHistoryEntryWrapper = function ConnectionHistoryEntryWrapper(dataSource, historyEntry) {
/** /**
* The identifier of the connection associated with this history entry. * The wrapped ConnectionHistoryEntry.
* *
* @type String * @type ConnectionHistoryEntry
*/ */
this.connectionIdentifier = historyEntry.connectionIdentifier; this.entry = historyEntry;
/**
* The name of the connection associated with this history entry.
*
* @type String
*/
this.connectionName = historyEntry.connectionName;
/**
* The remote host associated with this history entry.
*
* @type String
*/
this.remoteHost = historyEntry.remoteHost;
/**
* The username of the user associated with this particular usage of
* the connection.
*
* @type String
*/
this.username = historyEntry.username;
/**
* The time that usage began, in seconds since 1970-01-01 00:00:00 UTC.
*
* @type Number
*/
this.startDate = historyEntry.startDate;
/**
* The time that usage ended, in seconds since 1970-01-01 00:00:00 UTC.
* The absence of an endDate does NOT necessarily indicate that the
* connection is still in use, particularly if the server was shutdown
* or restarted before the history entry could be updated. To determine
* whether a connection is still active, check the active property of
* this history entry.
*
* @type Number
*/
this.endDate = historyEntry.endDate;
/** /**
* The total amount of time the connection associated with the wrapped * The total amount of time the connection associated with the wrapped
@@ -90,7 +53,7 @@ angular.module('settings').factory('ConnectionHistoryEntryWrapper', ['$injector'
* *
* @type Number * @type Number
*/ */
this.duration = this.endDate - this.startDate; this.duration = historyEntry.endDate - historyEntry.startDate;
/** /**
* An object providing value and unit properties, denoting the duration * An object providing value and unit properties, denoting the duration
@@ -101,7 +64,7 @@ angular.module('settings').factory('ConnectionHistoryEntryWrapper', ['$injector'
this.readableDuration = null; this.readableDuration = null;
// Set the duration if the necessary information is present // Set the duration if the necessary information is present
if (this.endDate && this.startDate) if (historyEntry.endDate && historyEntry.startDate)
this.readableDuration = new ConnectionHistoryEntry.Duration(this.duration); this.readableDuration = new ConnectionHistoryEntry.Duration(this.duration);
/** /**
@@ -115,9 +78,70 @@ angular.module('settings').factory('ConnectionHistoryEntryWrapper', ['$injector'
this.readableDurationText = 'SETTINGS_CONNECTION_HISTORY.TEXT_HISTORY_DURATION'; this.readableDurationText = 'SETTINGS_CONNECTION_HISTORY.TEXT_HISTORY_DURATION';
// Inform user if end date is not known // Inform user if end date is not known
if (!this.endDate) if (!historyEntry.endDate)
this.readableDurationText = 'SETTINGS_CONNECTION_HISTORY.INFO_CONNECTION_DURATION_UNKNOWN'; 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.<string>}
*/
this.description = template.description;
}; };
return ConnectionHistoryEntryWrapper; return ConnectionHistoryEntryWrapper;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path style="stroke-width:.87949842;fill:#fff" d="M20 16h8v32h-8zm16 0h8v32h-8z"/></svg>

After

Width:  |  Height:  |  Size: 151 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path style="opacity:1;fill:#00e;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0" d="m88.625 45.491-16.88-29.237h33.76z" transform="matrix(0 .94788 .54726 0 15.105 -52.005)"/></svg>

After

Width:  |  Height:  |  Size: 297 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path style="opacity:1;fill:#fff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0" d="m88.625 45.491-16.88-29.237h33.76z" transform="matrix(0 .94788 .54726 0 15.105 -52.005)"/></svg>

After

Width:  |  Height:  |  Size: 297 B

View File

@@ -25,11 +25,14 @@
"ACTION_MANAGE_USER_GROUPS" : "Groups", "ACTION_MANAGE_USER_GROUPS" : "Groups",
"ACTION_NAVIGATE_BACK" : "Back", "ACTION_NAVIGATE_BACK" : "Back",
"ACTION_NAVIGATE_HOME" : "Home", "ACTION_NAVIGATE_HOME" : "Home",
"ACTION_PAUSE" : "Pause",
"ACTION_PLAY" : "Play",
"ACTION_SAVE" : "Save", "ACTION_SAVE" : "Save",
"ACTION_SEARCH" : "Search", "ACTION_SEARCH" : "Search",
"ACTION_SHARE" : "Share", "ACTION_SHARE" : "Share",
"ACTION_UPDATE_PASSWORD" : "Update Password", "ACTION_UPDATE_PASSWORD" : "Update Password",
"ACTION_VIEW_HISTORY" : "History", "ACTION_VIEW_HISTORY" : "History",
"ACTION_VIEW_RECORDING" : "View",
"DIALOG_HEADER_ERROR" : "Error", "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" : { "PROTOCOL_KUBERNETES" : {
"FIELD_HEADER_BACKSPACE" : "Backspace key sends:", "FIELD_HEADER_BACKSPACE" : "Backspace key sends:",
@@ -851,8 +865,9 @@
"SETTINGS_CONNECTION_HISTORY" : { "SETTINGS_CONNECTION_HISTORY" : {
"ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD",
"ACTION_SEARCH" : "@:APP.ACTION_SEARCH", "ACTION_SEARCH" : "@:APP.ACTION_SEARCH",
"ACTION_VIEW_RECORDING" : "@:APP.ACTION_VIEW_RECORDING",
"FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
@@ -867,6 +882,7 @@
"TABLE_HEADER_SESSION_CONNECTION_NAME" : "Connection name", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Connection name",
"TABLE_HEADER_SESSION_DURATION" : "Duration", "TABLE_HEADER_SESSION_DURATION" : "Duration",
"TABLE_HEADER_SESSION_LOGS" : "Logs",
"TABLE_HEADER_SESSION_REMOTEHOST" : "Remote host", "TABLE_HEADER_SESSION_REMOTEHOST" : "Remote host",
"TABLE_HEADER_SESSION_STARTDATE" : "Start time", "TABLE_HEADER_SESSION_STARTDATE" : "Start time",
"TABLE_HEADER_SESSION_USERNAME" : "Username", "TABLE_HEADER_SESSION_USERNAME" : "Username",