Adding the new React server info modal
parent
7ed13f5b6a
commit
f933b5d2bf
|
@ -1,4 +1,9 @@
|
|||
# Changelog:
|
||||
* **24.04.21**
|
||||
- Removed the old server info modal and using the new React based and popoutable modal
|
||||
- Using the new React modal for the server info dialog. The modal now also has improved permission and error visualisation
|
||||
- Improved tooltip handling
|
||||
|
||||
* **19.04.21**
|
||||
- Fixed a bug that the client video box is shown as active even though the client does not stream any video
|
||||
- Fixed a bug that the video fullscreen windows pops open when a client leaves/joins the channel
|
||||
|
|
|
@ -20,7 +20,6 @@ import "./static/modal-keyselect.scss"
|
|||
import "./static/modal-query.scss"
|
||||
import "./static/modal-server.scss"
|
||||
import "./static/modal-musicmanage.scss"
|
||||
import "./static/modal-serverinfobandwidth.scss"
|
||||
import "./static/modal-settings.scss"
|
||||
import "./static/overlay-image-preview.scss"
|
||||
import "./static/color-variables.scss"
|
||||
|
|
|
@ -52,4 +52,11 @@ html:root {
|
|||
|
||||
--serverinfo-key: #557edc;
|
||||
--serverinfo-value: #d6d6d7;
|
||||
|
||||
/* Server bandwidth */
|
||||
--serverinfo-bandwidth-upload: #0a5eaa;
|
||||
--serverinfo-bandwidth-download: #9f2712;
|
||||
|
||||
--serverinfo-title: #e3e3e4;
|
||||
--serverinfo-statistics-title: #244c78;
|
||||
}
|
|
@ -1,222 +0,0 @@
|
|||
@import "mixin";
|
||||
|
||||
html:root {
|
||||
--serverinfo-bandwidth-upload: #0a5eaa;
|
||||
--serverinfo-bandwidth-download: #9f2712;
|
||||
|
||||
--serverinfo-title: #e3e3e4;
|
||||
--serverinfo-statistics-title: #244c78;
|
||||
}
|
||||
|
||||
:global {
|
||||
.modal-body.modal-server-info-bandwidth {
|
||||
padding: 0!important;
|
||||
width: 55em;
|
||||
|
||||
display: flex!important;
|
||||
flex-direction: column!important;
|
||||
justify-content: flex-start!important;
|
||||
|
||||
background-color: #2f2f35;
|
||||
|
||||
.container-tooltip {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
position: relative;
|
||||
width: 1.6em;
|
||||
margin-left: .5em;
|
||||
font-size: .9em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
|
||||
align-self: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.top {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
margin: 1em;
|
||||
padding: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
height: 12em;
|
||||
max-height: 12em;
|
||||
|
||||
.container-image {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
max-width: 18em;
|
||||
max-height: 11em; /* minus one padding */
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
margin-right: 2em;
|
||||
@include transition(.25s ease-in-out);
|
||||
}
|
||||
|
||||
.container-stats {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 25em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
|
||||
.statistic {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
> a {
|
||||
font-size: 1.25em;
|
||||
color: var(--serverinfo-title);
|
||||
line-height: normal;
|
||||
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.values {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
> a {
|
||||
font-size: 1.2em;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.upload {
|
||||
color: var(--serverinfo-bandwidth-upload);
|
||||
}
|
||||
|
||||
.download {
|
||||
color: var(--serverinfo-bandwidth-download);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
margin: 1em;
|
||||
padding: .5em;
|
||||
|
||||
border-radius: .2em;
|
||||
border: 1px solid #1f2122;
|
||||
|
||||
background-color: #28292b;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
//height: 15em;
|
||||
//max-height: 10em;
|
||||
|
||||
.statistic {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
.title {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
color: var(--serverinfo-statistics-title);
|
||||
font-size: 1.25em;
|
||||
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.body {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
height: 7em;
|
||||
|
||||
.container-canvas {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-width: 6em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.values {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
width: 8em;
|
||||
text-align: right;
|
||||
|
||||
.upload {
|
||||
color: var(--serverinfo-bandwidth-upload);
|
||||
}
|
||||
|
||||
.download {
|
||||
color: var(--serverinfo-bandwidth-download);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 50em) {
|
||||
:global {
|
||||
.modal-body.modal-server-info {
|
||||
.container-image {
|
||||
margin: 0!important;
|
||||
max-width: 0!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2454,200 +2454,5 @@
|
|||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script class="jsrender-template" id="tmpl_server_info" type="text/html">
|
||||
<div> <!-- required for the renderer -->
|
||||
<div class="container-top hostbanner">
|
||||
</div>
|
||||
<div class="container-body">
|
||||
<div class="group">
|
||||
<div class="container-image">
|
||||
<img src="img/serveredit_1.png">
|
||||
</div>
|
||||
<div class="container-properties">
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Server name" /}}</a>
|
||||
<div class="value server-name">
|
||||
error: name
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Server region" /}}</a>
|
||||
<div class="value server-region">
|
||||
error: region
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Slots" /}}</a>
|
||||
<div class="value server-slots">
|
||||
error: slots
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a class="key">{{tr "First run" /}}</a>
|
||||
<div class="value server-first-run">
|
||||
error: first run
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Uptime" /}}</a>
|
||||
<div class="value server-uptime">
|
||||
error: uptime
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group reverse">
|
||||
<div class="container-image">
|
||||
<img src="img/serveredit_2.png">
|
||||
</div>
|
||||
<div class="container-properties">
|
||||
<div class="row">
|
||||
<a class="key">{{tr "IP Address" /}}</a>
|
||||
<div class="value server-ip">
|
||||
error: ip
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Version" /}}</a>
|
||||
<div class="value server-version">
|
||||
<div class="container-tooltip">
|
||||
<img src="img/icon_tooltip.svg"/>
|
||||
<div class="tooltip">
|
||||
<a></a>
|
||||
</div>
|
||||
</div>
|
||||
<a></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Platform" /}}</a>
|
||||
<div class="value server-platform">
|
||||
error: platform
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-network">
|
||||
<div class="container-button">
|
||||
<button class="btn btn-purple button-show-bandwidth">{{tr "Show Bandwidth" /}}</button>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Average ping" /}}</a>
|
||||
<div class="value server-ping">
|
||||
error: average ping
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Average packet loss" /}}</a>
|
||||
<div class="value server-packet-loss">
|
||||
error: average packet loss
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="container-image">
|
||||
<img src="img/serveredit_3.png">
|
||||
</div>
|
||||
<div class="container-properties">
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Global unique ID" /}}</a>
|
||||
<div class="value server-unique-id">
|
||||
error: unique id
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Current Channels" /}}</a>
|
||||
<div class="value server-channel-count">
|
||||
error: channel count
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Voice data encryption" /}}</a>
|
||||
<div class="value server-voice-encryption">
|
||||
error: voice encryption
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Minimal security level" /}}</a>
|
||||
<div class="value server-min-security-level">
|
||||
error: security level
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a class="key">{{tr "Complains until ban" /}}</a>
|
||||
<div class="value server-complains">
|
||||
error: complains
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-buttons">
|
||||
<button class="btn btn-success button-update">{{tr "Refresh" /}}</button>
|
||||
<button class="btn btn-danger button-close">{{tr "Close" /}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<script class="jsrender-template" id="tmpl_server_info_bandwidth" type="text/html">
|
||||
<div> <!-- required for the renderer -->
|
||||
<div class="top">
|
||||
<div class="container-image">
|
||||
<img src="img/serveredit_3.png"> <!-- TODO Get the right image! -->
|
||||
</div>
|
||||
<div class="container-stats">
|
||||
<div class="statistic statistic-packets">
|
||||
<a>{{tr "Transmitted packets" /}}</a>
|
||||
<div class="values">
|
||||
<a class="upload" title="{{tr 'Upload bandwidth' /}}">error: upload</a>
|
||||
<a class="download" title="{{tr 'Download bandwidth' /}}">error: download</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="statistic statistic-bytes">
|
||||
<a>{{tr "Transmitted bytes" /}}</a>
|
||||
<div class="values">
|
||||
<a class="upload" title="{{tr 'Upload bandwidth' /}}">error: upload</a>
|
||||
<a class="download" title="{{tr 'Download bandwidth' /}}">error: download</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="statistic statistic-ft-bytes">
|
||||
<a>{{tr "Transferred file transfer bytes" /}}</a>
|
||||
<div class="values">
|
||||
<a class="upload" title="{{tr 'Upload bandwidth' /}}">error: upload</a>
|
||||
<a class="download" title="{{tr 'Download bandwidth' /}}">error: download</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="statistic statistic-bandwidth">
|
||||
<a class="title">{{tr "Current bandwidth" /}}</a>
|
||||
<div class="body">
|
||||
<div class="container-canvas">
|
||||
<canvas></canvas>
|
||||
</div>
|
||||
<div class="values">
|
||||
<a class="upload" title="{{tr 'Upload bandwidth' /}}">N Bytes/s</a>
|
||||
<a class="download" title="{{tr 'Download bandwidth' /}}">N Bytes/s</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="statistic statistic-ft-bandwidth">
|
||||
<a class="title">{{tr "Current file transfer bandwidth" /}}</a>
|
||||
<div class="body">
|
||||
<div class="container-canvas">
|
||||
<canvas></canvas>
|
||||
</div>
|
||||
<div class="values">
|
||||
<a class="upload" title="{{tr 'Upload bandwidth' /}}">N Bytes/s</a>
|
||||
<a class="download" title="{{tr 'Download bandwidth' /}}">N Bytes/s</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -295,7 +295,7 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
|||
}
|
||||
|
||||
requestConnectionInfo() : Promise<ServerConnectionInfoResult> {
|
||||
if(this.requestInfoPromise && Date.now() - 1000 < this.requestInfoPromiseTimestamp) {
|
||||
if(this.requestInfoPromise && Date.now() - 900 < this.requestInfoPromiseTimestamp) {
|
||||
return this.requestInfoPromise;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {LogCategory, logDebug} from "tc-shared/log";
|
||||
import {CallOnce} from "tc-shared/proto";
|
||||
|
||||
export type Entry = {
|
||||
timestamp: number;
|
||||
|
@ -10,21 +11,21 @@ export type Entry = {
|
|||
}
|
||||
|
||||
export type Style = {
|
||||
background_color: string;
|
||||
backgroundColor: string;
|
||||
|
||||
separator_color: string;
|
||||
separator_count: number;
|
||||
separator_width: number;
|
||||
separatorColor: string;
|
||||
separatorCount: number;
|
||||
separatorWidth: number;
|
||||
|
||||
upload: {
|
||||
fill: string;
|
||||
stroke: string;
|
||||
strike_width: number;
|
||||
strokeWidth: number;
|
||||
},
|
||||
download: {
|
||||
fill: string;
|
||||
stroke: string;
|
||||
strike_width: number;
|
||||
strokeWidth: number;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,58 +42,61 @@ export type TimeSpan = {
|
|||
}
|
||||
}
|
||||
|
||||
/* Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves
|
||||
*
|
||||
* Assuming A was the last point in the line plotted and B is the new point,
|
||||
* we draw a curve with control points P and Q as below.
|
||||
*
|
||||
* A---P
|
||||
* |
|
||||
* |
|
||||
* |
|
||||
* Q---B
|
||||
*
|
||||
* Importantly, A and P are at the same y coordinate, as are B and Q. This is
|
||||
* so adjacent curves appear to flow as one.
|
||||
*/
|
||||
export class Graph {
|
||||
private static _loops: (() => any)[] = [];
|
||||
private static animateCallbacks: (() => any)[] = [];
|
||||
private static registerAnimateCallback(callback: () => void) {
|
||||
this.animateCallbacks.push(callback);
|
||||
if(this.animateCallbacks.length === 1) {
|
||||
const animateLoop = () => {
|
||||
Graph.animateCallbacks.forEach(l => l());
|
||||
if(Graph.animateCallbacks.length > 0) {
|
||||
requestAnimationFrame(animateLoop);
|
||||
} else {
|
||||
logDebug(LogCategory.GENERAL, tr("NetGraph static terminate"));
|
||||
}
|
||||
};
|
||||
animateLoop();
|
||||
}
|
||||
}
|
||||
|
||||
private static removerAnimateCallback(callback: () => void) {
|
||||
this.animateCallbacks.remove(callback);
|
||||
}
|
||||
|
||||
readonly canvas: HTMLCanvasElement;
|
||||
public style: Style = {
|
||||
background_color: "#28292b",
|
||||
//background_color: "red",
|
||||
backgroundColor: "#28292b",
|
||||
|
||||
separator_color: "#283036",
|
||||
//separator_color: 'blue',
|
||||
separator_count: 10,
|
||||
separator_width: 1,
|
||||
separatorColor: "#283036",
|
||||
separatorCount: 10,
|
||||
separatorWidth: 1,
|
||||
|
||||
|
||||
upload: {
|
||||
fill: "#2d3f4d",
|
||||
stroke: "#336e9f",
|
||||
strike_width: 2,
|
||||
strokeWidth: 2,
|
||||
},
|
||||
|
||||
download: {
|
||||
fill: "#532c26",
|
||||
stroke: "#a9321c",
|
||||
strike_width: 2,
|
||||
strokeWidth: 2,
|
||||
}
|
||||
};
|
||||
|
||||
private _canvas_context: CanvasRenderingContext2D;
|
||||
private _entries: Entry[] = [];
|
||||
private _entry_max = {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private canvasContext: CanvasRenderingContext2D;
|
||||
|
||||
private entries: Entry[] = [];
|
||||
private entriesMax = {
|
||||
upload: 1,
|
||||
download: 1,
|
||||
};
|
||||
private _max_space = 1.12;
|
||||
private _max_gap = 5;
|
||||
private _animate_loop;
|
||||
private maxSpace = 1.12;
|
||||
private maxGap = 5;
|
||||
private animateLoop;
|
||||
|
||||
_time_span: TimeSpan = {
|
||||
timeSpan: TimeSpan = {
|
||||
origin: {
|
||||
begin: 0,
|
||||
end: 1,
|
||||
|
@ -105,138 +109,157 @@ export class Graph {
|
|||
}
|
||||
};
|
||||
|
||||
private _detailed_shown = false;
|
||||
callback_detailed_info: (upload: number, download: number, timestamp: number, event: MouseEvent) => any;
|
||||
callback_detailed_hide: () => any;
|
||||
private detailShown = false;
|
||||
callbackDetailedInfo: (upload: number, download: number, timestamp: number, event: MouseEvent) => any;
|
||||
callbackDetailedHide: () => any;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
this.canvas = canvas;
|
||||
this._animate_loop = () => this.draw();
|
||||
this.recalculate_cache(); /* initialize cache */
|
||||
constructor() {
|
||||
this.animateLoop = () => this.draw();
|
||||
this.recalculateCache(); /* initialize cache */
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this._canvas_context = this.canvas.getContext("2d");
|
||||
@CallOnce
|
||||
initialize() { }
|
||||
|
||||
Graph._loops.push(this._animate_loop);
|
||||
if(Graph._loops.length == 1) {
|
||||
const static_loop = () => {
|
||||
Graph._loops.forEach(l => l());
|
||||
if(Graph._loops.length > 0) {
|
||||
requestAnimationFrame(static_loop);
|
||||
} else {
|
||||
logDebug(LogCategory.GENERAL, tr("NetGraph static terminate"));
|
||||
}
|
||||
};
|
||||
static_loop();
|
||||
@CallOnce
|
||||
finalize() {
|
||||
this.initializeCanvas(undefined);
|
||||
}
|
||||
|
||||
initializeCanvas(canvas: HTMLCanvasElement | undefined) {
|
||||
if(this.canvas) {
|
||||
this.canvas.onmousemove = undefined;
|
||||
this.canvas.onmouseleave = undefined;
|
||||
|
||||
this.canvas = undefined;
|
||||
this.canvasContext = undefined;
|
||||
Graph.removerAnimateCallback(this.animateLoop);
|
||||
}
|
||||
|
||||
this.canvas.onmousemove = this.on_mouse_move.bind(this);
|
||||
this.canvas.onmouseleave = this.on_mouse_leave.bind(this);
|
||||
if(!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.canvas = canvas;
|
||||
this.canvasContext = this.canvas.getContext("2d");
|
||||
|
||||
Graph.registerAnimateCallback(this.animateLoop);
|
||||
this.canvas.onmousemove = this.onMouseMove.bind(this);
|
||||
this.canvas.onmouseleave = this.onMouseLeave.bind(this);
|
||||
}
|
||||
|
||||
terminate() {
|
||||
Graph._loops.remove(this._animate_loop);
|
||||
}
|
||||
maxGapSize(value?: number) : number { return typeof(value) === "number" ? (this.maxGap = value) : this.maxGap; }
|
||||
|
||||
max_gap_size(value?: number) : number { return typeof(value) === "number" ? (this._max_gap = value) : this._max_gap; }
|
||||
|
||||
private recalculate_cache(time_span?: boolean) {
|
||||
this._entries = this._entries.sort((a, b) => a.timestamp - b.timestamp);
|
||||
this._entry_max = {
|
||||
private recalculateCache(timespan?: boolean) {
|
||||
this.entries = this.entries.sort((a, b) => a.timestamp - b.timestamp);
|
||||
this.entriesMax = {
|
||||
download: 1,
|
||||
upload: 1
|
||||
};
|
||||
if(time_span) {
|
||||
this._time_span = {
|
||||
if(timespan) {
|
||||
this.timeSpan = {
|
||||
origin: {
|
||||
begin: 0,
|
||||
end: 0,
|
||||
time: 0
|
||||
},
|
||||
target: {
|
||||
begin: this._entries.length > 0 ? this._entries[0].timestamp : 0,
|
||||
end: this._entries.length > 0 ? this._entries.last().timestamp : 0,
|
||||
begin: this.entries.length > 0 ? this.entries[0].timestamp : 0,
|
||||
end: this.entries.length > 0 ? this.entries.last().timestamp : 0,
|
||||
time: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for(const entry of this._entries) {
|
||||
if(typeof(entry.upload) === "number")
|
||||
this._entry_max.upload = Math.max(this._entry_max.upload, entry.upload);
|
||||
for(const entry of this.entries) {
|
||||
if(typeof(entry.upload) === "number") {
|
||||
this.entriesMax.upload = Math.max(this.entriesMax.upload, entry.upload);
|
||||
}
|
||||
|
||||
if(typeof(entry.download) === "number")
|
||||
this._entry_max.download = Math.max(this._entry_max.download, entry.download);
|
||||
if(typeof(entry.download) === "number") {
|
||||
this.entriesMax.download = Math.max(this.entriesMax.download, entry.download);
|
||||
}
|
||||
}
|
||||
|
||||
this._entry_max.upload *= this._max_space;
|
||||
this._entry_max.download *= this._max_space;
|
||||
this.entriesMax.upload *= this.maxSpace;
|
||||
this.entriesMax.download *= this.maxSpace;
|
||||
}
|
||||
|
||||
insert_entry(entry: Entry) {
|
||||
if(this._entries.length > 0 && entry.timestamp < this._entries.last().timestamp)
|
||||
entryCount() : number {
|
||||
return this.entries.length;
|
||||
}
|
||||
|
||||
pushEntry(entry: Entry) {
|
||||
if(this.entries.length > 0 && entry.timestamp < this.entries.last().timestamp) {
|
||||
throw "invalid timestamp";
|
||||
}
|
||||
|
||||
this._entries.push(entry);
|
||||
this.entries.push(entry);
|
||||
|
||||
if(typeof(entry.upload) === "number")
|
||||
this._entry_max.upload = Math.max(this._entry_max.upload, entry.upload * this._max_space);
|
||||
if(typeof entry.upload === "number") {
|
||||
this.entriesMax.upload = Math.max(this.entriesMax.upload, entry.upload * this.maxSpace);
|
||||
}
|
||||
|
||||
if(typeof(entry.download) === "number")
|
||||
this._entry_max.download = Math.max(this._entry_max.download, entry.download * this._max_space);
|
||||
if(typeof entry.download === "number") {
|
||||
this.entriesMax.download = Math.max(this.entriesMax.download, entry.download * this.maxSpace);
|
||||
}
|
||||
}
|
||||
|
||||
insert_entries(entries: Entry[]) {
|
||||
this._entries.push(...entries);
|
||||
this.recalculate_cache();
|
||||
insertEntries(entries: Entry[]) {
|
||||
this.entries.push(...entries);
|
||||
this.recalculateCache();
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.canvas.style.height = "100%";
|
||||
this.canvas.style.width = "100%";
|
||||
const cstyle = getComputedStyle(this.canvas);
|
||||
|
||||
/* TODO: Do this within the next animate loop. We don't have to do this right here! */
|
||||
const cstyle = getComputedStyle(this.canvas);
|
||||
this.canvas.width = parseInt(cstyle.width);
|
||||
this.canvas.height = parseInt(cstyle.height);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
const time = this.calculate_time_span();
|
||||
const time = this.calculateTimespan();
|
||||
|
||||
let index = 0;
|
||||
for(;index < this._entries.length; index++) {
|
||||
if(this._entries[index].timestamp < time.begin)
|
||||
for(;index < this.entries.length; index++) {
|
||||
if(this.entries[index].timestamp < time.begin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(index == 0)
|
||||
if(index == 0) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/* keep the last entry as a reference point to the left */
|
||||
if(index > 1) {
|
||||
this._entries.splice(0, index - 1);
|
||||
this.recalculate_cache();
|
||||
this.entries.splice(0, index - 1);
|
||||
this.recalculateCache();
|
||||
}
|
||||
}
|
||||
|
||||
calculate_time_span() : { begin: number; end: number } {
|
||||
calculateTimespan() : { begin: number; end: number } {
|
||||
const time = Date.now();
|
||||
if(time >= this._time_span.target.time)
|
||||
return this._time_span.target;
|
||||
if(time >= this.timeSpan.target.time) {
|
||||
return this.timeSpan.target;
|
||||
}
|
||||
|
||||
if(time <= this._time_span.origin.time)
|
||||
return this._time_span.origin;
|
||||
if(time <= this.timeSpan.origin.time) {
|
||||
return this.timeSpan.origin;
|
||||
}
|
||||
|
||||
const ob = this._time_span.origin.begin;
|
||||
const oe = this._time_span.origin.end;
|
||||
const ot = this._time_span.origin.time;
|
||||
const ob = this.timeSpan.origin.begin;
|
||||
const oe = this.timeSpan.origin.end;
|
||||
const ot = this.timeSpan.origin.time;
|
||||
|
||||
const tb = this._time_span.target.begin;
|
||||
const te = this._time_span.target.end;
|
||||
const tt = this._time_span.target.time;
|
||||
const tb = this.timeSpan.target.begin;
|
||||
const te = this.timeSpan.target.end;
|
||||
const tt = this.timeSpan.target.time;
|
||||
|
||||
const offset = (time - ot) / (tt - ot);
|
||||
return {
|
||||
|
@ -245,8 +268,8 @@ export class Graph {
|
|||
};
|
||||
}
|
||||
|
||||
draw() {
|
||||
let ctx = this._canvas_context;
|
||||
private draw() {
|
||||
let ctx = this.canvasContext;
|
||||
|
||||
const height = this.canvas.height;
|
||||
const width = this.canvas.width;
|
||||
|
@ -257,21 +280,21 @@ export class Graph {
|
|||
ctx.filter = "";
|
||||
ctx.lineCap = "square";
|
||||
|
||||
ctx.fillStyle = this.style.background_color;
|
||||
ctx.fillStyle = this.style.backgroundColor;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
/* first of all print the separators */
|
||||
{
|
||||
const sw = this.style.separator_width;
|
||||
const swh = this.style.separator_width / 2;
|
||||
const sw = this.style.separatorWidth;
|
||||
const swh = this.style.separatorWidth / 2;
|
||||
|
||||
ctx.lineWidth = sw;
|
||||
ctx.strokeStyle = this.style.separator_color;
|
||||
ctx.strokeStyle = this.style.separatorColor;
|
||||
|
||||
ctx.beginPath();
|
||||
/* horizontal */
|
||||
{
|
||||
const dw = width / this.style.separator_count;
|
||||
const dw = width / this.style.separatorCount;
|
||||
let dx = dw / 2;
|
||||
while(dx < width) {
|
||||
ctx.moveTo(Math.floor(dx - swh) + .5, .5);
|
||||
|
@ -297,14 +320,14 @@ export class Graph {
|
|||
|
||||
/* draw the lines */
|
||||
{
|
||||
const t = this.calculate_time_span();
|
||||
const t = this.calculateTimespan();
|
||||
const tb = t.begin; /* time begin */
|
||||
const dt = t.end - t.begin; /* delta time */
|
||||
const dtw = width / dt; /* delta time width */
|
||||
|
||||
const draw_graph = (type: "upload" | "download", direction: number, max: number) => {
|
||||
const drawGraph = (type: "upload" | "download", direction: number, max: number) => {
|
||||
const hy = Math.floor(height / 2); /* half y */
|
||||
const by = hy - direction * this.style[type].strike_width; /* the "base" line */
|
||||
const by = hy - direction * this.style[type].strokeWidth; /* the "base" line */
|
||||
|
||||
const marked_points: ({x: number, y: number})[] = [];
|
||||
|
||||
|
@ -314,12 +337,13 @@ export class Graph {
|
|||
let x, y, lx = 0, ly = by; /* last x, last y */
|
||||
|
||||
const floor = a => a; //Math.floor;
|
||||
for(const entry of this._entries) {
|
||||
for(const entry of this.entries) {
|
||||
x = floor((entry.timestamp - tb) * dtw);
|
||||
if(typeof entry[type] === "number")
|
||||
y = floor(hy - direction * Math.max(hy * (entry[type] / max), this.style[type].strike_width));
|
||||
else
|
||||
y = hy - direction * this.style[type].strike_width;
|
||||
if(typeof entry[type] === "number") {
|
||||
y = floor(hy - direction * Math.max(hy * (entry[type] / max), this.style[type].strokeWidth));
|
||||
} else {
|
||||
y = hy - direction * this.style[type].strokeWidth;
|
||||
}
|
||||
|
||||
if(entry.timestamp < tb) {
|
||||
lx = x;
|
||||
|
@ -328,7 +352,7 @@ export class Graph {
|
|||
continue;
|
||||
}
|
||||
|
||||
if(x - lx > this._max_gap && this._max_gap > 0) {
|
||||
if(x - lx > this.maxGap && this.maxGap > 0) {
|
||||
ctx.lineTo(lx, by);
|
||||
ctx.lineTo(x, by);
|
||||
ctx.lineTo(x, y);
|
||||
|
@ -347,7 +371,7 @@ export class Graph {
|
|||
}
|
||||
|
||||
ctx.strokeStyle = this.style[type].stroke;
|
||||
ctx.lineWidth = this.style[type].strike_width;
|
||||
ctx.lineWidth = this.style[type].strokeWidth;
|
||||
ctx.lineJoin = "miter";
|
||||
ctx.stroke();
|
||||
|
||||
|
@ -375,29 +399,30 @@ export class Graph {
|
|||
}
|
||||
};
|
||||
|
||||
const shared_max = Math.max(this._entry_max.upload, this._entry_max.download);
|
||||
draw_graph("upload", 1, shared_max);
|
||||
draw_graph("download", -1, shared_max);
|
||||
const shared_max = Math.max(this.entriesMax.upload, this.entriesMax.download);
|
||||
drawGraph("upload", 1, shared_max);
|
||||
drawGraph("download", -1, shared_max);
|
||||
}
|
||||
}
|
||||
|
||||
private on_mouse_move(event: MouseEvent) {
|
||||
private onMouseMove(event: MouseEvent) {
|
||||
const offset = event.offsetX;
|
||||
const max_offset = this.canvas.width;
|
||||
|
||||
if(offset < 0) return;
|
||||
if(offset > max_offset) return;
|
||||
|
||||
const time_span = this.calculate_time_span();
|
||||
const time_span = this.calculateTimespan();
|
||||
const time = time_span.begin + (time_span.end - time_span.begin) * (offset / max_offset);
|
||||
let index = 0;
|
||||
for(;index < this._entries.length; index++) {
|
||||
if(this._entries[index].timestamp > time)
|
||||
for(;index < this.entries.length; index++) {
|
||||
if(this.entries[index].timestamp > time) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const entry_before = this._entries[index - 1]; /* In JS negative array access is allowed and returns undefined */
|
||||
const entry_next = this._entries[index]; /* In JS negative array access is allowed and returns undefined */
|
||||
const entry_before = this.entries[index - 1]; /* In JS negative array access is allowed and returns undefined */
|
||||
const entry_next = this.entries[index]; /* In JS negative array access is allowed and returns undefined */
|
||||
let entry: Entry;
|
||||
if(!entry_before || !entry_next) {
|
||||
entry = entry_before || entry_next;
|
||||
|
@ -411,24 +436,28 @@ export class Graph {
|
|||
}
|
||||
|
||||
if(!entry) {
|
||||
this.on_mouse_leave(event);
|
||||
this.onMouseLeave(event);
|
||||
} else {
|
||||
this._entries.forEach(e => e.highlight = false);
|
||||
this._detailed_shown = true;
|
||||
this.entries.forEach(e => e.highlight = false);
|
||||
this.detailShown = true;
|
||||
entry.highlight = true;
|
||||
|
||||
if(this.callback_detailed_info)
|
||||
this.callback_detailed_info(entry.upload, entry.download, entry.timestamp, event);
|
||||
if(this.callbackDetailedInfo) {
|
||||
this.callbackDetailedInfo(entry.upload, entry.download, entry.timestamp, event);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private on_mouse_leave(event: MouseEvent) {
|
||||
if(!this._detailed_shown) return;
|
||||
this._detailed_shown = false;
|
||||
private onMouseLeave(_event: MouseEvent) {
|
||||
if(!this.detailShown) {
|
||||
return;
|
||||
}
|
||||
this.detailShown = false;
|
||||
|
||||
this._entries.forEach(e => e.highlight = false);
|
||||
if(this.callback_detailed_hide)
|
||||
this.callback_detailed_hide();
|
||||
this.entries.forEach(e => e.highlight = false);
|
||||
if(this.callbackDetailedHide) {
|
||||
this.callbackDetailedHide();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -155,27 +155,54 @@ export function parseMessageWithArguments(pattern: string, argumentCount: number
|
|||
}
|
||||
|
||||
export namespace network {
|
||||
export const KB = 1024;
|
||||
export const MB = 1024 * KB;
|
||||
export const GB = 1024 * MB;
|
||||
export const TB = 1024 * GB;
|
||||
/* https://en.wikipedia.org/wiki/Kilobyte */
|
||||
export const KiB = 1024;
|
||||
export const MiB = 1024 * KiB;
|
||||
export const GiB = 1024 * MiB;
|
||||
export const TiB = 1024 * GiB;
|
||||
|
||||
export function byteSizeToString(value: number) {
|
||||
export const kB = 1000;
|
||||
export const MB = 1000 * kB;
|
||||
export const GB = 1000 * MB;
|
||||
export const TB = 1000 * GB;
|
||||
|
||||
export function binarySizeToString(value: number) {
|
||||
let v: number, unit;
|
||||
if(value > 5 * TiB) {
|
||||
unit = " TiB";
|
||||
v = value / TiB;
|
||||
} else if(value > 5 * GiB) {
|
||||
unit = " GiB";
|
||||
v = value / GiB;
|
||||
} else if(value > 5 * MiB) {
|
||||
unit = " MiB";
|
||||
v = value / MiB;
|
||||
} else if(value > 5 * KiB) {
|
||||
unit = " KiB";
|
||||
v = value / KiB;
|
||||
} else {
|
||||
return value + " B";
|
||||
}
|
||||
|
||||
return v.toFixed(2) + unit;
|
||||
}
|
||||
|
||||
export function decimalSizeToString(value: number) {
|
||||
let v: number, unit;
|
||||
if(value > 5 * TB) {
|
||||
unit = "tb";
|
||||
unit = " TB";
|
||||
v = value / TB;
|
||||
} else if(value > 5 * GB) {
|
||||
unit = "gb";
|
||||
unit = " GB";
|
||||
v = value / GB;
|
||||
} else if(value > 5 * MB) {
|
||||
unit = "mb";
|
||||
unit = " MB";
|
||||
v = value / MB;
|
||||
} else if(value > 5 * KB) {
|
||||
unit = "kb";
|
||||
v = value / KB;
|
||||
} else if(value > 5 * kB) {
|
||||
unit = " kB";
|
||||
v = value / kB;
|
||||
} else {
|
||||
return value + "b";
|
||||
return value + " B";
|
||||
}
|
||||
|
||||
return v.toFixed(2) + unit;
|
||||
|
@ -195,18 +222,18 @@ export namespace network {
|
|||
let points = value.toFixed(0).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
|
||||
|
||||
let v, unit;
|
||||
if(value > 2 * TB) {
|
||||
if(value > 2 * TiB) {
|
||||
unit = "TB";
|
||||
v = value / TB;
|
||||
} else if(value > GB) {
|
||||
v = value / TiB;
|
||||
} else if(value > GiB) {
|
||||
unit = "GB";
|
||||
v = value / GB;
|
||||
} else if(value > MB) {
|
||||
v = value / GiB;
|
||||
} else if(value > MiB) {
|
||||
unit = "MB";
|
||||
v = value / MB;
|
||||
} else if(value > KB) {
|
||||
v = value / MiB;
|
||||
} else if(value > KiB) {
|
||||
unit = "KB";
|
||||
v = value / KB;
|
||||
v = value / KiB;
|
||||
} else {
|
||||
unit = "";
|
||||
v = value;
|
||||
|
|
|
@ -140,11 +140,11 @@ const ComponentStatusRenderer = React.memo((props: { component: ConnectionCompon
|
|||
<React.Fragment key={"healthy"}>
|
||||
<div className={cssStyle.row}>
|
||||
<div className={cssStyle.key}><Translatable>Incoming:</Translatable></div>
|
||||
<div className={cssStyle.value}>{network.byteSizeToString(status.bytesReceived)}</div>
|
||||
<div className={cssStyle.value}>{network.binarySizeToString(status.bytesReceived)}</div>
|
||||
</div>
|
||||
<div className={cssStyle.row}>
|
||||
<div className={cssStyle.key}><Translatable>Outgoing:</Translatable></div>
|
||||
<div className={cssStyle.value}>{network.byteSizeToString(status.bytesSend)}</div>
|
||||
<div className={cssStyle.value}>{network.binarySizeToString(status.bytesSend)}</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
@ -83,24 +83,25 @@ function initialize_graph(modal: Modal, tag: JQuery, callbacks: ServerBandwidthI
|
|||
};
|
||||
show_info(undefined, undefined);
|
||||
|
||||
const graph = new Graph(canvas);
|
||||
graph.insert_entry({ timestamp: Date.now(), upload: undefined, download: undefined});
|
||||
const graph = new Graph();
|
||||
graph.initializeCanvas(canvas);
|
||||
graph.pushEntry({ timestamp: Date.now(), upload: undefined, download: undefined});
|
||||
callbacks.push((status, values) => {
|
||||
last_info = {status: status, info: values};
|
||||
|
||||
if(!values) {
|
||||
graph.insert_entry({ timestamp: Date.now(), upload: undefined, download: undefined});
|
||||
graph.pushEntry({ timestamp: Date.now(), upload: undefined, download: undefined});
|
||||
} else {
|
||||
graph.insert_entry({
|
||||
graph.pushEntry({
|
||||
timestamp: Date.now(),
|
||||
download: values[fields.download], //values.connection_bandwidth_received_last_second_total,
|
||||
upload: values[fields.uplaod], //values.connection_bandwidth_sent_last_second_total
|
||||
});
|
||||
}
|
||||
|
||||
/* set set that we want to show the entry within one second */
|
||||
graph._time_span.origin = Object.assign(graph.calculate_time_span(), { time: Date.now() });
|
||||
graph._time_span.target = {
|
||||
/* set that we want to show the entry within one second */
|
||||
graph.timeSpan.origin = Object.assign(graph.calculateTimespan(), { time: Date.now() });
|
||||
graph.timeSpan.target = {
|
||||
begin: Date.now() - 120 * 1000,
|
||||
end: Date.now(),
|
||||
time: Date.now() + 200
|
||||
|
@ -113,20 +114,20 @@ function initialize_graph(modal: Modal, tag: JQuery, callbacks: ServerBandwidthI
|
|||
}
|
||||
});
|
||||
|
||||
graph.max_gap_size(0);
|
||||
graph.maxGapSize(0);
|
||||
graph.initialize();
|
||||
|
||||
graph.callback_detailed_hide = () => {
|
||||
graph.callbackDetailedHide = () => {
|
||||
custom_info = false;
|
||||
show_info(undefined, undefined);
|
||||
};
|
||||
|
||||
graph.callback_detailed_info = (upload, download, timestamp, event) => {
|
||||
graph.callbackDetailedInfo = (upload, download, timestamp, event) => {
|
||||
custom_info = true;
|
||||
show_info(upload, download);
|
||||
};
|
||||
|
||||
modal.close_listener.push(() => graph.terminate());
|
||||
modal.close_listener.push(() => graph.finalize());
|
||||
modal.open_listener.push(() => graph.resize());
|
||||
|
||||
tag.addClass("window-resize-listener").on('resize', event => graph.resize());
|
||||
|
|
|
@ -18,7 +18,7 @@ import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
|||
import {joinClassList, useDependentState} from "tc-shared/ui/react-elements/Helper";
|
||||
|
||||
import kDefaultAvatarUrl from "../../../../img/style/avatar.png";
|
||||
import byteSizeToString = network.byteSizeToString;
|
||||
import byteSizeToString = network.binarySizeToString;
|
||||
|
||||
const ServerUniqueIdContext = React.createContext<string>(undefined);
|
||||
const EventContext = React.createContext<Registry<ModalAvatarUploadEvents>>(undefined);
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
||||
import {Registry} from "tc-events";
|
||||
import {CallOnce, ignorePromise} from "tc-shared/proto";
|
||||
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||
import {ModalServerBandwidthEvents} from "tc-shared/ui/modal/server-bandwidth/Definitions";
|
||||
|
||||
class Controller {
|
||||
readonly handler: ConnectionHandler;
|
||||
readonly events: Registry<ModalServerBandwidthEvents>;
|
||||
|
||||
private connectionInfoInterval: number;
|
||||
|
||||
constructor(handler: ConnectionHandler) {
|
||||
this.handler = handler;
|
||||
|
||||
this.events = new Registry<ModalServerBandwidthEvents>();
|
||||
}
|
||||
|
||||
@CallOnce
|
||||
initialize() {
|
||||
this.refreshConnectionInfo();
|
||||
this.connectionInfoInterval = setInterval(() => this.refreshConnectionInfo(), 1000);
|
||||
}
|
||||
|
||||
@CallOnce
|
||||
destroy() {
|
||||
clearInterval(this.connectionInfoInterval);
|
||||
this.connectionInfoInterval = 0;
|
||||
|
||||
this.events.destroy();
|
||||
}
|
||||
|
||||
private refreshConnectionInfo() {
|
||||
const server = this.handler.channelTree.server;
|
||||
server.requestConnectionInfo().then(info => this.events.fire_react("notify_connection_info", { info: info }));
|
||||
}
|
||||
}
|
||||
|
||||
export function spawnServerBandwidth(handler: ConnectionHandler) {
|
||||
const controller = new Controller(handler);
|
||||
controller.initialize();
|
||||
|
||||
const modal = spawnModal("modal-server-bandwidth", [
|
||||
controller.events.generateIpcDescription(),
|
||||
], {
|
||||
popoutable: true
|
||||
});
|
||||
|
||||
modal.getEvents().on("destroy", () => controller.destroy());
|
||||
modal.getEvents().on("destroy", handler.events().on("notify_connection_state_changed", event => {
|
||||
if(event.newState !== ConnectionState.CONNECTED) {
|
||||
modal.destroy();
|
||||
}
|
||||
}));
|
||||
ignorePromise(modal.show());
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import {ServerConnectionInfoResult} from "tc-shared/tree/ServerDefinitions";
|
||||
|
||||
export interface ModalServerBandwidthEvents {
|
||||
notify_connection_info: { info: ServerConnectionInfoResult },
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
@import "../../../../css/static/mixin";
|
||||
@import "../../../../css/static/properties";
|
||||
|
||||
.container {
|
||||
width: 55em;
|
||||
min-width: 30em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
background-color: #2f2f35;
|
||||
user-select: none;
|
||||
|
||||
&.windowed {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.top {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
margin: 1em;
|
||||
padding: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
height: 12em;
|
||||
max-height: 12em;
|
||||
|
||||
.image {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
max-width: 18em;
|
||||
max-height: 11em; /* minus one padding */
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
margin-right: 2em;
|
||||
@include transition(.25s ease-in-out);
|
||||
}
|
||||
|
||||
.stats {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 25em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
|
||||
.statistic {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
.title {
|
||||
font-size: 1.25em;
|
||||
color: var(--serverinfo-title);
|
||||
line-height: normal;
|
||||
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.values {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
.value {
|
||||
font-size: 1.2em;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.upload {
|
||||
color: var(--serverinfo-bandwidth-upload);
|
||||
}
|
||||
|
||||
.download {
|
||||
color: var(--serverinfo-bandwidth-download);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
margin: 1em;
|
||||
padding: .5em;
|
||||
|
||||
border-radius: .2em;
|
||||
border: 1px solid #1f2122;
|
||||
|
||||
background-color: #28292b;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
//height: 15em;
|
||||
//max-height: 10em;
|
||||
|
||||
.statistic {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
.title {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
color: var(--serverinfo-statistics-title);
|
||||
font-size: 1.25em;
|
||||
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.body {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
height: 7em;
|
||||
|
||||
.canvas {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-width: 6em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.values {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
width: 8em;
|
||||
text-align: right;
|
||||
|
||||
.upload {
|
||||
color: var(--serverinfo-bandwidth-upload);
|
||||
}
|
||||
|
||||
.download {
|
||||
color: var(--serverinfo-bandwidth-download);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 43em) {
|
||||
.top .image {
|
||||
margin: 0!important;
|
||||
max-width: 0!important;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,310 @@
|
|||
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
import React, {useContext, useEffect, useRef, useState} from "react";
|
||||
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {IpcRegistryDescription, Registry} from "tc-events";
|
||||
import {ModalServerBandwidthEvents} from "tc-shared/ui/modal/server-bandwidth/Definitions";
|
||||
import ImageTop from "./serveredit_3.png";
|
||||
import {ServerConnectionInfo, ServerConnectionInfoResult} from "tc-shared/tree/ServerDefinitions";
|
||||
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
|
||||
import {network} from "tc-shared/ui/frames/chat";
|
||||
import binarySizeToString = network.binarySizeToString;
|
||||
import {Graph} from "tc-shared/ui/elements/NetGraph";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import {Tooltip} from "tc-shared/ui/react-elements/Tooltip";
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
/*
|
||||
<script class="jsrender-template" id="tmpl_server_info_bandwidth" type="text/html">
|
||||
<div> <!-- required for the renderer -->
|
||||
<div class="bottom">
|
||||
<div class="statistic statistic-bandwidth">
|
||||
<a class="title">{{tr "Current bandwidth" /}}</a>
|
||||
<div class="body">
|
||||
<div class="container-canvas">
|
||||
<canvas></canvas>
|
||||
</div>
|
||||
<div class="values">
|
||||
<a class="upload" title="{{tr 'Upload bandwidth' /}}">N Bytes/s</a>
|
||||
<a class="download" title="{{tr 'Download bandwidth' /}}">N Bytes/s</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="statistic statistic-ft-bandwidth">
|
||||
<a class="title">{{tr "Current file transfer bandwidth" /}}</a>
|
||||
<div class="body">
|
||||
<div class="container-canvas">
|
||||
<canvas></canvas>
|
||||
</div>
|
||||
<div class="values">
|
||||
<a class="upload" title="{{tr 'Upload bandwidth' /}}">N Bytes/s</a>
|
||||
<a class="download" title="{{tr 'Download bandwidth' /}}">N Bytes/s</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
*/
|
||||
|
||||
const EventsContext = React.createContext<Registry<ModalServerBandwidthEvents>>(undefined);
|
||||
const ConnectionInfoContext = React.createContext<ServerConnectionInfoResult | { status: "loading" }>({ status: "loading" });
|
||||
|
||||
const TopContainerStatistic = React.memo((props: {
|
||||
children: [ React.ReactNode, (info: ServerConnectionInfo) => React.ReactNode, (info: ServerConnectionInfo) => React.ReactNode ]
|
||||
}) => (
|
||||
<div className={cssStyle.statistic}>
|
||||
<div className={cssStyle.title}>
|
||||
{props.children[0]}
|
||||
</div>
|
||||
<div className={cssStyle.values}>
|
||||
<div className={cssStyle.value + " " + cssStyle.upload} title={useTr("Upload bandwidth")}>
|
||||
<CurrentConnectionInfoVariable>
|
||||
{props.children[1]}
|
||||
</CurrentConnectionInfoVariable>
|
||||
</div>
|
||||
<div className={cssStyle.value + " " + cssStyle.download} title={useTr("Download bandwidth")}>
|
||||
<CurrentConnectionInfoVariable>
|
||||
{props.children[2]}
|
||||
</CurrentConnectionInfoVariable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
|
||||
const CurrentConnectionInfoProvider = React.memo((props: { children }) => {
|
||||
const events = useContext(EventsContext);
|
||||
const [ info, setInfo ] = useState<ServerConnectionInfoResult>(undefined);
|
||||
|
||||
events.reactUse("notify_connection_info", event => setInfo(event.info));
|
||||
|
||||
return (
|
||||
<ConnectionInfoContext.Provider value={info ? info : { status: "loading" }}>
|
||||
{props.children}
|
||||
</ConnectionInfoContext.Provider>
|
||||
)
|
||||
});
|
||||
|
||||
/* We're caching this so the tooltip can work properly */
|
||||
const NoPermissionRenderer = React.memo((props: { failedPermission: string }) => (
|
||||
<Tooltip
|
||||
tooltip={() => (
|
||||
<VariadicTranslatable text={"Failed on permission:\n{}"}>
|
||||
{props.failedPermission}
|
||||
</VariadicTranslatable>
|
||||
)}
|
||||
spawnHover={true}
|
||||
key={"no-permissions"}
|
||||
>
|
||||
<Translatable>No Permission</Translatable>
|
||||
</Tooltip>
|
||||
));
|
||||
|
||||
const CurrentConnectionInfoVariable = React.memo((props: {
|
||||
children: (info: ServerConnectionInfo) => React.ReactNode
|
||||
}) => {
|
||||
const info = useContext(ConnectionInfoContext);
|
||||
switch (info.status) {
|
||||
case "loading":
|
||||
return <Translatable key={"loading"}>loading</Translatable>;
|
||||
|
||||
case "error":
|
||||
return <React.Fragment key={"error"}>error: {info.message}</React.Fragment>;
|
||||
|
||||
case "no-permission":
|
||||
return <NoPermissionRenderer key={"no-permission"} failedPermission={info.failedPermission} />
|
||||
|
||||
case "success":
|
||||
return <React.Fragment key={"success"}>{props.children(info.result)}</React.Fragment>;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const CurrentBandwidthGraph = React.memo((props: {
|
||||
upload: keyof ServerConnectionInfo,
|
||||
download: keyof ServerConnectionInfo,
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const events = useContext(EventsContext);
|
||||
const graph = useRef(new Graph());
|
||||
const refCanvas = useRef<HTMLCanvasElement>();
|
||||
|
||||
const [ customInfo, setCustomInfo ] = useState<[number, number]>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const graphInstance = graph.current;
|
||||
graphInstance.maxGapSize(0);
|
||||
graphInstance.initialize();
|
||||
graphInstance.initializeCanvas(refCanvas.current);
|
||||
graphInstance.resize();
|
||||
|
||||
graphInstance.callbackDetailedInfo = (upload, download) => setCustomInfo([upload, download]);
|
||||
graphInstance.callbackDetailedHide = () => setCustomInfo(undefined);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => graphInstance.resize());
|
||||
resizeObserver.observe(refCanvas.current);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
|
||||
graphInstance.callbackDetailedInfo = undefined;
|
||||
graphInstance.callbackDetailedHide = undefined;
|
||||
graph.current.finalize();
|
||||
}
|
||||
}, []);
|
||||
|
||||
events.reactUse("notify_connection_info", event => {
|
||||
const graphInstance = graph.current;
|
||||
if(!graphInstance) {
|
||||
/* should never happen */
|
||||
return;
|
||||
}
|
||||
|
||||
if(graphInstance.entryCount() === 0) {
|
||||
graphInstance.pushEntry({
|
||||
timestamp: Date.now() - 400,
|
||||
|
||||
download: undefined,
|
||||
upload: undefined
|
||||
});
|
||||
}
|
||||
|
||||
if(event.info.status === "success") {
|
||||
graphInstance.pushEntry({
|
||||
timestamp: Date.now(),
|
||||
|
||||
download: event.info.result[props.download],
|
||||
upload: event.info.result[props.upload],
|
||||
});
|
||||
} else {
|
||||
graphInstance.pushEntry({
|
||||
timestamp: Date.now(),
|
||||
|
||||
download: undefined,
|
||||
upload: undefined
|
||||
});
|
||||
}
|
||||
|
||||
/* fade in the new data point within a second */
|
||||
graphInstance.timeSpan.origin = Object.assign(graphInstance.calculateTimespan(), { time: Date.now() });
|
||||
graphInstance.timeSpan.target = {
|
||||
begin: Date.now() - 120 * 1000,
|
||||
end: Date.now(),
|
||||
time: Date.now() + 200
|
||||
};
|
||||
|
||||
graphInstance.cleanup();
|
||||
}, undefined, []);
|
||||
|
||||
let uploadValue, downloadValue;
|
||||
if(customInfo) {
|
||||
if(typeof customInfo[0] === "number") {
|
||||
uploadValue = binarySizeToString(customInfo[0]) + "/s";
|
||||
} else {
|
||||
uploadValue = tr("Unknown");
|
||||
}
|
||||
|
||||
if(typeof customInfo[1] === "number") {
|
||||
downloadValue = binarySizeToString(customInfo[1]) + "/s";
|
||||
} else {
|
||||
downloadValue = tr("Unknown");
|
||||
}
|
||||
} else {
|
||||
uploadValue = <CurrentConnectionInfoVariable key={"general"}>{info => binarySizeToString(info[props.upload]) + "/s"}</CurrentConnectionInfoVariable>;
|
||||
downloadValue = <CurrentConnectionInfoVariable key={"general"}>{info => binarySizeToString(info[props.download]) + "/s"}</CurrentConnectionInfoVariable>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cssStyle.statistic}>
|
||||
<div className={cssStyle.title}>
|
||||
{props.children}
|
||||
</div>
|
||||
<div className={cssStyle.body}>
|
||||
<div className={cssStyle.canvas}>
|
||||
<canvas ref={refCanvas} />
|
||||
</div>
|
||||
|
||||
<div className={cssStyle.values} key={"general"}>
|
||||
<div className={cssStyle.upload} title={useTr("Upload bandwidth")}>
|
||||
{uploadValue}
|
||||
</div>
|
||||
<div className={cssStyle.download} title={useTr("Download bandwidth")}>
|
||||
{downloadValue}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
class Modal extends AbstractModal {
|
||||
private readonly events: Registry<ModalServerBandwidthEvents>;
|
||||
|
||||
constructor(events: IpcRegistryDescription<ModalServerBandwidthEvents>) {
|
||||
super();
|
||||
|
||||
this.events = Registry.fromIpcDescription(events);
|
||||
}
|
||||
|
||||
protected onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
this.events.destroy();
|
||||
}
|
||||
|
||||
renderBody(): React.ReactElement {
|
||||
return (
|
||||
<EventsContext.Provider value={this.events}>
|
||||
<CurrentConnectionInfoProvider>
|
||||
<div className={joinClassList(cssStyle.container, this.properties.windowed && cssStyle.windowed)}>
|
||||
<div className={cssStyle.top}>
|
||||
<div className={cssStyle.image}>
|
||||
<img draggable={false} alt={""} src={ImageTop} />
|
||||
</div>
|
||||
<div className={cssStyle.stats}>
|
||||
<TopContainerStatistic>
|
||||
<Translatable>Transmitted packets</Translatable>
|
||||
{info => binarySizeToString(info.connection_packets_sent_total)}
|
||||
{info => binarySizeToString(info.connection_packets_received_total)}
|
||||
</TopContainerStatistic>
|
||||
<TopContainerStatistic>
|
||||
<Translatable>Transmitted bytes</Translatable>
|
||||
{info => binarySizeToString(info.connection_bytes_sent_total)}
|
||||
{info => binarySizeToString(info.connection_bytes_received_total)}
|
||||
</TopContainerStatistic>
|
||||
<TopContainerStatistic>
|
||||
<Translatable>Transferred file transfer bytes</Translatable>
|
||||
{info => binarySizeToString(info.connection_filetransfer_bytes_received_total)}
|
||||
{info => binarySizeToString(info.connection_filetransfer_bytes_sent_total)}
|
||||
</TopContainerStatistic>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cssStyle.bottom}>
|
||||
<CurrentBandwidthGraph
|
||||
upload={"connection_bandwidth_sent_last_second_total"}
|
||||
download={"connection_bandwidth_received_last_second_total"}
|
||||
>
|
||||
<Translatable>Current Bandwidth</Translatable>
|
||||
</CurrentBandwidthGraph>
|
||||
{/* TODO: connection_filetransfer_bandwidth_* is per minute and not per second */}
|
||||
<CurrentBandwidthGraph
|
||||
upload={"connection_filetransfer_bandwidth_sent"}
|
||||
download={"connection_filetransfer_bandwidth_received"}
|
||||
>
|
||||
<Translatable>Current File Transfer bandwidth</Translatable>
|
||||
</CurrentBandwidthGraph>
|
||||
</div>
|
||||
</div>
|
||||
</CurrentConnectionInfoProvider>
|
||||
</EventsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
renderTitle(): string | React.ReactElement {
|
||||
return <Translatable>Server bandwidth usage</Translatable>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Modal;
|
Binary file not shown.
After Width: | Height: | Size: 294 KiB |
|
@ -7,6 +7,7 @@ import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
|||
import {ServerConnectionInfoResult, ServerProperties} from "tc-shared/tree/Server";
|
||||
import {LogCategory, logWarn} from "tc-shared/log";
|
||||
import {openServerInfoBandwidth} from "tc-shared/ui/modal/ModalServerInfoBandwidth";
|
||||
import {spawnServerBandwidth} from "tc-shared/ui/modal/server-bandwidth/Controller";
|
||||
|
||||
const kPropertyUpdateMatrix: {[T in keyof ServerProperties]?: [keyof ModalServerInfoVariables]} = {
|
||||
"virtualserver_name": [ "name" ],
|
||||
|
@ -176,9 +177,7 @@ export function spawnServerInfoNew(handler: ConnectionHandler) {
|
|||
});
|
||||
|
||||
controller.events.on("action_close", () => modal.destroy());
|
||||
controller.events.on("action_show_bandwidth", () => {
|
||||
openServerInfoBandwidth(handler.channelTree.server);
|
||||
});
|
||||
controller.events.on("action_show_bandwidth", () => spawnServerBandwidth(handler));
|
||||
|
||||
modal.getEvents().on("destroy", () => controller.destroy());
|
||||
modal.getEvents().on("destroy", handler.events().on("notify_connection_state_changed", event => {
|
||||
|
|
|
@ -75,9 +75,14 @@ export interface TooltipState {
|
|||
export interface TooltipProperties {
|
||||
tooltip: () => ReactNode | ReactNode[] | string;
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Enable the tooltip already when the span is hovered
|
||||
*/
|
||||
spawnHover?: boolean,
|
||||
}
|
||||
|
||||
export class Tooltip extends React.Component<TooltipProperties, TooltipState> {
|
||||
export class Tooltip extends React.PureComponent<TooltipProperties, TooltipState> {
|
||||
readonly tooltipId = guid();
|
||||
private refContainer = React.createRef<HTMLSpanElement>();
|
||||
private currentContainer: HTMLElement;
|
||||
|
@ -114,36 +119,42 @@ export class Tooltip extends React.Component<TooltipProperties, TooltipState> {
|
|||
|
||||
componentDidUpdate(prevProps: Readonly<TooltipProperties>, prevState: Readonly<TooltipState>, snapshot?: any): void {
|
||||
if(this.state.forceShow || this.state.hovered) {
|
||||
globalTooltipRef.current?.updateTooltip(this);
|
||||
globalTooltipRef.current?.setState({
|
||||
pageY: this.state.pageY,
|
||||
pageX: this.state.pageX,
|
||||
tooltipId: this.tooltipId
|
||||
});
|
||||
globalTooltipRef.current?.updateTooltip(this);
|
||||
} else if(prevState.forceShow || prevState.hovered) {
|
||||
globalTooltipRef.current?.unmountTooltip(this);
|
||||
}
|
||||
}
|
||||
|
||||
private onMouseEnter(event: React.MouseEvent) {
|
||||
/* check if may only the span has been hovered, should not be the case! */
|
||||
if(event.target === this.refContainer.current) {
|
||||
return;
|
||||
if(typeof this.props.spawnHover !== "boolean" || !this.props.spawnHover) {
|
||||
/* check if may only the span has been hovered, should not be the case! */
|
||||
if(event.target === this.refContainer.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
let container = event.target as HTMLElement;
|
||||
while(container.parentElement !== this.refContainer.current) {
|
||||
container = container.parentElement;
|
||||
}
|
||||
this.currentContainer = container;
|
||||
} else {
|
||||
this.currentContainer = this.refContainer.current;
|
||||
}
|
||||
|
||||
this.setState({ hovered: true });
|
||||
|
||||
let container = event.target as HTMLElement;
|
||||
while(container.parentElement !== this.refContainer.current)
|
||||
container = container.parentElement;
|
||||
this.currentContainer = container;
|
||||
|
||||
this.updatePosition();
|
||||
}
|
||||
|
||||
updatePosition() {
|
||||
const container = this.currentContainer || this.refContainer.current?.children.item(0) || this.refContainer.current;
|
||||
if(!container) return;
|
||||
if(!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
this.setState({
|
||||
|
|
|
@ -23,6 +23,7 @@ import {ModalAvatarUploadEvents, ModalAvatarUploadVariables} from "tc-shared/ui/
|
|||
import {ModalInputProcessorEvents, ModalInputProcessorVariables} from "tc-shared/ui/modal/input-processor/Definitios";
|
||||
import {ModalServerInfoEvents, ModalServerInfoVariables} from "tc-shared/ui/modal/server-info/Definitions";
|
||||
import {ModalAboutVariables} from "tc-shared/ui/modal/about/Definitions";
|
||||
import {ModalServerBandwidthEvents} from "tc-shared/ui/modal/server-bandwidth/Definitions";
|
||||
|
||||
export type ModalType = "error" | "warning" | "info" | "none";
|
||||
export type ModalRenderType = "page" | "dialog";
|
||||
|
@ -225,5 +226,8 @@ export interface ModalConstructorArguments {
|
|||
"modal-server-info": [
|
||||
/* events */ IpcRegistryDescription<ModalServerInfoEvents>,
|
||||
/* variables */ IpcVariableDescriptor<ModalServerInfoVariables>
|
||||
],
|
||||
"modal-server-bandwidth": [
|
||||
/* events */ IpcRegistryDescription<ModalServerBandwidthEvents>
|
||||
]
|
||||
}
|
|
@ -120,3 +120,9 @@ registerModal({
|
|||
classLoader: async () => await import("tc-shared/ui/modal/server-info/Renderer"),
|
||||
popoutSupported: true
|
||||
});
|
||||
|
||||
registerModal({
|
||||
modalId: "modal-server-bandwidth",
|
||||
classLoader: async () => await import("tc-shared/ui/modal/server-bandwidth/Renderer"),
|
||||
popoutSupported: true
|
||||
});
|
Loading…
Reference in New Issue