Adding the new React server info modal

master
WolverinDEV 2021-04-24 12:58:32 +02:00
parent 7ed13f5b6a
commit f933b5d2bf
20 changed files with 842 additions and 611 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
import {ServerConnectionInfoResult} from "tc-shared/tree/ServerDefinitions";
export interface ModalServerBandwidthEvents {
notify_connection_info: { info: ServerConnectionInfoResult },
}

View File

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

View File

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

View File

@ -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 => {

View File

@ -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({

View File

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

View File

@ -119,4 +119,10 @@ registerModal({
modalId: "modal-server-info",
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
});