mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +00:00
GUACAMOLE-462: Merge add in-app player for session recordings.
This commit is contained in:
@@ -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);
|
||||||
|
|
||||||
|
@@ -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.
|
@@ -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)
|
||||||
|
|
@@ -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',
|
||||||
|
@@ -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];
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}]);
|
388
guacamole/src/main/frontend/src/app/player/directives/player.js
Normal file
388
guacamole/src/main/frontend/src/app/player/directives/player.js
Normal 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;
|
||||||
|
|
||||||
|
}]);
|
@@ -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;
|
||||||
|
|
||||||
|
}]);
|
@@ -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;
|
||||||
|
|
||||||
|
}]);
|
52
guacamole/src/main/frontend/src/app/player/playerModule.js
Normal file
52
guacamole/src/main/frontend/src/app/player/playerModule.js
Normal 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'
|
||||||
|
]);
|
147
guacamole/src/main/frontend/src/app/player/styles/player.css
Normal file
147
guacamole/src/main/frontend/src/app/player/styles/player.css
Normal 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;
|
||||||
|
|
||||||
|
}
|
@@ -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;
|
||||||
|
}
|
@@ -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);
|
||||||
|
|
||||||
|
}
|
170
guacamole/src/main/frontend/src/app/player/styles/seek.css
Normal file
170
guacamole/src/main/frontend/src/app/player/styles/seek.css
Normal 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;
|
||||||
|
}
|
@@ -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>
|
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="guac-player-display" guac-resize="fitDisplay">
|
||||||
|
<div class="guac-player-display-container"></div>
|
||||||
|
</div>
|
@@ -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>
|
@@ -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()
|
||||||
|
});
|
||||||
|
|
||||||
|
}]);
|
@@ -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
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@@ -26,6 +26,7 @@ angular.module('settings', [
|
|||||||
'list',
|
'list',
|
||||||
'navigation',
|
'navigation',
|
||||||
'notification',
|
'notification',
|
||||||
|
'player',
|
||||||
'rest',
|
'rest',
|
||||||
'storage'
|
'storage'
|
||||||
]);
|
]);
|
||||||
|
@@ -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;
|
||||||
|
}
|
@@ -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;
|
||||||
|
}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
@@ -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;
|
||||||
|
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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",
|
||||||
|
Reference in New Issue
Block a user