Compare commits

...

34 Commits

Author SHA1 Message Date
Aakansha Doshi
98c16659c9 Add safe check so collab stats dont show up for host apps 2021-02-08 21:42:05 +05:30
Aakansha Doshi
5644063fc7 remove ts ignore 2021-02-07 21:27:18 +05:30
Aakansha Doshi
85b8050cc5 move getAverage to util 2021-02-07 20:59:09 +05:30
Aakansha Doshi
60d3bf1718 fix 2021-02-07 20:44:42 +05:30
Aakansha Doshi
b8e1b1f3ad rename 2021-02-07 20:43:46 +05:30
Lipis
57432b9779
Update src/utils.ts 2021-02-07 17:11:30 +02:00
Lipis
156073a407
Update src/networkStats.ts 2021-02-07 17:06:57 +02:00
Aakansha Doshi
f676a20332 rename 2021-02-07 20:34:54 +05:30
Aakansha Doshi
63af29d345 cleanup 2021-02-07 20:03:37 +05:30
Aakansha Doshi
69a1b74e05 simulate ping every 5 sec 2021-02-07 19:34:04 +05:30
Panayiotis Lipiridis
163dbd47d4 Merge branch 'aakansha-net-stats' of github.com:excalidraw/excalidraw into aakansha-net-stats
* 'aakansha-net-stats' of github.com:excalidraw/excalidraw:
  Update src/utils.ts
  Update Stats.scss
2021-02-07 15:52:01 +02:00
Panayiotis Lipiridis
08889adfa5 4 2021-02-07 15:51:56 +02:00
Panayiotis Lipiridis
aaf4943fa3 Average speed 2021-02-07 15:51:12 +02:00
Lipis
4fd18d1b3e
Update src/utils.ts
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-02-07 14:55:50 +02:00
Lipis
f8cf19cae9
Update Stats.scss 2021-02-07 14:47:39 +02:00
Panayiotis Lipiridis
6540c5460e Bits 2021-02-07 14:38:15 +02:00
Panayiotis Lipiridis
dc95cf3447 bytes 2021-02-07 14:34:33 +02:00
Panayiotis Lipiridis
1ca985aa4a Bytes 2021-02-07 14:33:26 +02:00
Aakansha Doshi
bc9c8f7ee2 clear timer before starting new one 2021-02-07 17:36:27 +05:30
Panayiotis Lipiridis
3eacd6f07b Speed 2021-02-07 13:54:57 +02:00
Panayiotis Lipiridis
afa929b2a2 Bigger image 2021-02-07 13:17:17 +02:00
Panayiotis Lipiridis
370c9a643f Source 2021-02-07 13:15:10 +02:00
Panayiotis Lipiridis
22c57c8f4a const 2021-02-07 12:40:27 +02:00
Panayiotis Lipiridis
eef9662195 Fix tests 2021-02-07 12:25:37 +02:00
Panayiotis Lipiridis
69a7b1f2b5 2021-02-07 12:16:17 +02:00
Panayiotis Lipiridis
3b11b1d9d3 minor fixes 2021-02-07 12:11:59 +02:00
Aakansha Doshi
b778035854 update interval to be 5sec 2021-02-07 15:10:27 +05:30
Aakansha Doshi
92c2a42edf use image from our server, this fixes the time mismatch issue i was facing earlier due to diff url/image 2021-02-07 15:04:24 +05:30
Aakansha Doshi
7cadc3de52 update image size to be ~3kb and check speed every 15seconds 2021-02-07 02:56:52 +05:30
Aakansha Doshi
336b7222de Merge remote-tracking branch 'origin/master' into aakansha-net-stats 2021-02-06 22:20:58 +05:30
Aakansha Doshi
4d0ebf5ac5 move collab stats before version 2021-02-06 22:19:04 +05:30
Aakansha Doshi
949e9ae03a fix specs 2021-02-06 20:20:48 +05:30
Aakansha Doshi
b5151bda5a fix 2021-02-06 20:08:30 +05:30
Aakansha Doshi
8157c84d11 feat: show network stats for collaboration 2021-02-06 19:50:35 +05:30
9 changed files with 346 additions and 1 deletions

View File

@ -73,6 +73,8 @@ export const getDefaultAppState = (): Omit<
zenModeEnabled: false, zenModeEnabled: false,
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } }, zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
viewModeEnabled: false, viewModeEnabled: false,
networkSpeed: 0,
networkPing: 0,
}; };
}; };
@ -153,6 +155,8 @@ const APP_STATE_STORAGE_CONF = (<
zenModeEnabled: { browser: true, export: false }, zenModeEnabled: { browser: true, export: false },
zoom: { browser: true, export: false }, zoom: { browser: true, export: false },
viewModeEnabled: { browser: false, export: false }, viewModeEnabled: { browser: false, export: false },
networkSpeed: { browser: false, export: false },
networkPing: { browser: false, export: false },
}); });
const _clearAppStateForStorage = <ExportType extends "export" | "browser">( const _clearAppStateForStorage = <ExportType extends "export" | "browser">(

View File

@ -51,6 +51,7 @@ import {
GRID_SIZE, GRID_SIZE,
LINE_CONFIRM_THRESHOLD, LINE_CONFIRM_THRESHOLD,
MIME_TYPES, MIME_TYPES,
NETWORK_TIMEOUT_MS,
POINTER_BUTTON, POINTER_BUTTON,
TAP_TWICE_TIMEOUT, TAP_TWICE_TIMEOUT,
TEXT_TO_CENTER_SNAP_THRESHOLD, TEXT_TO_CENTER_SNAP_THRESHOLD,
@ -183,6 +184,7 @@ import LayerUI from "./LayerUI";
import { Stats } from "./Stats"; import { Stats } from "./Stats";
import { Toast } from "./Toast"; import { Toast } from "./Toast";
import { actionToggleViewMode } from "../actions/actionToggleViewMode"; import { actionToggleViewMode } from "../actions/actionToggleViewMode";
import { getNetworkSpeed, getNetworkPing } from "../networkStats";
const { history } = createHistory(); const { history } = createHistory();
@ -205,6 +207,9 @@ const gesture: Gesture = {
initialScale: null, initialScale: null,
}; };
const shouldEnableNetworkStats = !!(
typeof process !== "undefined" && process.env?.REACT_APP_SOCKET_SERVER_URL
);
export type PointerDownState = Readonly<{ export type PointerDownState = Readonly<{
// The first position at which pointerDown happened // The first position at which pointerDown happened
origin: Readonly<{ x: number; y: number }>; origin: Readonly<{ x: number; y: number }>;
@ -289,6 +294,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
height: window.innerHeight, height: window.innerHeight,
}; };
private scene: Scene; private scene: Scene;
private networkSpeedIntervalId?: any;
private networkPingIntervalId?: any;
constructor(props: ExcalidrawProps) { constructor(props: ExcalidrawProps) {
super(props); super(props);
const defaultAppState = getDefaultAppState(); const defaultAppState = getDefaultAppState();
@ -469,6 +476,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
setAppState={this.setAppState} setAppState={this.setAppState}
elements={this.scene.getElements()} elements={this.scene.getElements()}
onClose={this.toggleStats} onClose={this.toggleStats}
isCollaborating={this.props.isCollaborating}
shouldEnableNetworkStats={shouldEnableNetworkStats}
/> />
)} )}
{this.state.toastMessage !== null && ( {this.state.toastMessage !== null && (
@ -748,6 +757,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.removeEventListeners(); this.removeEventListeners();
this.scene.destroy(); this.scene.destroy();
clearTimeout(touchTimeout); clearTimeout(touchTimeout);
clearTimeout(this.networkSpeedIntervalId);
touchTimeout = 0; touchTimeout = 0;
} }
@ -874,6 +884,34 @@ class App extends React.Component<ExcalidrawProps, AppState> {
gridSize: this.props.gridModeEnabled ? GRID_SIZE : null, gridSize: this.props.gridModeEnabled ? GRID_SIZE : null,
}); });
} }
if (
shouldEnableNetworkStats &&
(prevState.showStats !== this.state.showStats ||
prevProps.isCollaborating !== this.props.isCollaborating)
) {
const navigator: Navigator & {
connection?: {
addEventListener: Function;
removeEventListener: Function;
};
} = window.navigator;
if (this.state.showStats && this.props.isCollaborating) {
this.calculateNetStats();
navigator?.connection?.addEventListener(
"change",
this.calculateNetStats,
);
} else {
navigator?.connection?.removeEventListener(
"change",
this.calculateNetStats,
);
clearTimeout(this.networkSpeedIntervalId);
}
}
document document
.querySelector(".excalidraw") .querySelector(".excalidraw")
?.classList.toggle("Appearance_dark", this.state.appearance === "dark"); ?.classList.toggle("Appearance_dark", this.state.appearance === "dark");
@ -999,6 +1037,42 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
} }
private calculateNetStats = () => {
this.checkNetworkPing();
this.checkNetworkSpeed();
};
private checkNetworkPing = async () => {
if (!this.state.showStats || !this.props.isCollaborating) {
clearTimeout(this.networkPingIntervalId);
return;
}
const networkPing = await getNetworkPing();
this.setState({ networkPing });
if (this.networkPingIntervalId) {
clearTimeout(this.networkPingIntervalId);
}
this.networkPingIntervalId = setTimeout(
this.checkNetworkPing,
NETWORK_TIMEOUT_MS,
);
};
private checkNetworkSpeed = async () => {
if (!this.state.showStats || !this.props.isCollaborating) {
clearTimeout(this.networkSpeedIntervalId);
return;
}
const networkSpeed = await getNetworkSpeed();
this.setState({ networkSpeed });
if (this.networkSpeedIntervalId) {
clearTimeout(this.networkSpeedIntervalId);
}
this.networkSpeedIntervalId = setTimeout(
this.checkNetworkSpeed,
NETWORK_TIMEOUT_MS,
);
};
// Copy/paste // Copy/paste
private onCut = withBatchedUpdates((event: ClipboardEvent) => { private onCut = withBatchedUpdates((event: ClipboardEvent) => {

View File

@ -11,7 +11,13 @@ import { t } from "../i18n";
import useIsMobile from "../is-mobile"; import useIsMobile from "../is-mobile";
import { getTargetElements } from "../scene"; import { getTargetElements } from "../scene";
import { AppState } from "../types"; import { AppState } from "../types";
import { debounce, getVersion, nFormatter } from "../utils"; import {
debounce,
formatSpeedBits,
formatTime,
getVersion,
nFormatter,
} from "../utils";
import { close } from "./icons"; import { close } from "./icons";
import { Island } from "./Island"; import { Island } from "./Island";
import "./Stats.scss"; import "./Stats.scss";
@ -30,6 +36,8 @@ export const Stats = (props: {
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
onClose: () => void; onClose: () => void;
isCollaborating?: boolean;
shouldEnableNetworkStats: boolean;
}) => { }) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [storageSizes, setStorageSizes] = useState<StorageSizes>({ const [storageSizes, setStorageSizes] = useState<StorageSizes>({
@ -170,6 +178,37 @@ export const Stats = (props: {
</td> </td>
</tr> </tr>
)} )}
{props.shouldEnableNetworkStats && props.isCollaborating ? (
<>
<tr>
<th colSpan={2}>{t("stats.collaboration")}</th>
</tr>
<tr>
<td>{t("stats.collaborators")}</td>
<td>{props.appState.collaborators.size}</td>
</tr>
<tr>
<td>{t("stats.ping")}</td>
<td>
{props.appState.networkPing === 0
? "…"
: props.appState.networkPing > 0
? formatTime(props.appState.networkPing)
: t("stats.error")}
</td>
</tr>
<tr>
<td>{t("stats.speed")}</td>
<td>
{props.appState.networkSpeed === 0
? "…"
: props.appState.networkSpeed > 0
? formatSpeedBits(props.appState.networkSpeed)
: t("stats.error")}
</td>
</tr>
</>
) : null}
<tr> <tr>
<th colSpan={2}>{t("stats.version")}</th> <th colSpan={2}>{t("stats.version")}</th>
</tr> </tr>

View File

@ -8,6 +8,8 @@ export const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5;
export const ELEMENT_TRANSLATE_AMOUNT = 1; export const ELEMENT_TRANSLATE_AMOUNT = 1;
export const TEXT_TO_CENTER_SNAP_THRESHOLD = 30; export const TEXT_TO_CENTER_SNAP_THRESHOLD = 30;
export const SHIFT_LOCKING_ANGLE = Math.PI / 12; export const SHIFT_LOCKING_ANGLE = Math.PI / 12;
export const NETWORK_TIMEOUT_MS = 4000;
export const CURSOR_TYPE = { export const CURSOR_TYPE = {
TEXT: "text", TEXT: "text",
CROSSHAIR: "crosshair", CROSSHAIR: "crosshair",

View File

@ -227,11 +227,16 @@
}, },
"stats": { "stats": {
"angle": "Angle", "angle": "Angle",
"collaboration": "Collaboration",
"collaborators": "Collaborators",
"element": "Element", "element": "Element",
"elements": "Elements", "elements": "Elements",
"error": "Error",
"height": "Height", "height": "Height",
"ping": "Ping",
"scene": "Scene", "scene": "Scene",
"selected": "Selected", "selected": "Selected",
"speed": "Speed",
"storage": "Storage", "storage": "Storage",
"title": "Stats for nerds", "title": "Stats for nerds",
"total": "Total", "total": "Total",

64
src/networkStats.ts Normal file
View File

@ -0,0 +1,64 @@
import { getAverage } from "./utils";
const IMAGE_URL = `${process.env.REACT_APP_SOCKET_SERVER_URL}/test256.png`;
const IMAGE_SIZE_BITS = 141978 * 8;
const AVERAGE_MAX = 4;
const speedHistory: number[] = [];
const pushSpeed = (speed: number): void => {
speedHistory.push(speed);
if (speedHistory.length > AVERAGE_MAX) {
speedHistory.shift();
}
};
const getSpeedBits = (
imageSize: number,
startTime: number,
endTime: number,
): number => {
const duration = (endTime - startTime) / 1000;
if (duration > 0) {
return imageSize / duration;
}
return 0;
};
const processImage = (): Promise<number> => {
return new Promise((resolve) => {
const image = new Image();
let endTime: number;
image.onload = () => {
endTime = new Date().getTime();
const speed = getSpeedBits(IMAGE_SIZE_BITS, startTime, endTime);
pushSpeed(speed);
resolve(getAverage(speedHistory));
};
image.onerror = () => {
resolve(-1);
};
const startTime = new Date().getTime();
image.src = `${IMAGE_URL}?t=${startTime}`;
});
};
export const getNetworkSpeed = async (): Promise<number> => {
return await processImage();
};
export const getNetworkPing = async () => {
const startTime = new Date().getTime();
try {
await fetch(process.env.REACT_APP_SOCKET_SERVER_URL, {
mode: "no-cors",
method: "HEAD",
});
const endTime = new Date().getTime();
return endTime - startTime;
} catch (error) {
return -1;
}
};

View File

@ -40,6 +40,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -500,6 +502,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -966,6 +970,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -1741,6 +1747,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -1944,6 +1952,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -2401,6 +2411,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -2653,6 +2665,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -2816,6 +2830,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -3292,6 +3308,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -3599,6 +3617,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -3802,6 +3822,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -4045,6 +4067,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -4296,6 +4320,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -4678,6 +4704,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -4972,6 +5000,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -5278,6 +5308,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -5485,6 +5517,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -5648,6 +5682,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -6100,6 +6136,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -6417,6 +6455,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -8450,6 +8490,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -8811,6 +8853,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -9065,6 +9109,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -9317,6 +9363,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -9631,6 +9679,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -9794,6 +9844,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -9957,6 +10009,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -10120,6 +10174,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -10313,6 +10369,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -10506,6 +10564,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -10699,6 +10759,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -10892,6 +10954,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -11055,6 +11119,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -11218,6 +11284,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -11411,6 +11479,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -11574,6 +11644,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -11767,6 +11839,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -12482,6 +12556,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -12734,6 +12810,8 @@ Object {
"lastPointerDownWith": "touch", "lastPointerDownWith": "touch",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -12835,6 +12913,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -12934,6 +13014,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -13097,6 +13179,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -13404,6 +13488,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -13711,6 +13797,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -13874,6 +13962,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -14069,6 +14159,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -14317,6 +14409,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -14640,6 +14734,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -15478,6 +15574,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -15785,6 +15883,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -16092,6 +16192,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -16470,6 +16572,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -16636,6 +16740,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -16956,6 +17062,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -17194,6 +17302,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -17448,6 +17558,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -17774,6 +17886,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -17873,6 +17987,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -18036,6 +18152,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -18856,6 +18974,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -18955,6 +19075,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -19708,6 +19830,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -20112,6 +20236,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -20384,6 +20510,8 @@ Object {
"lastPointerDownWith": "touch", "lastPointerDownWith": "touch",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -20485,6 +20613,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -20982,6 +21112,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,
@ -21081,6 +21213,8 @@ Object {
"lastPointerDownWith": "mouse", "lastPointerDownWith": "mouse",
"multiElement": null, "multiElement": null,
"name": "Untitled-201933152653", "name": "Untitled-201933152653",
"networkPing": 0,
"networkSpeed": 0,
"offsetLeft": 0, "offsetLeft": 0,
"offsetTop": 0, "offsetTop": 0,
"openMenu": null, "openMenu": null,

View File

@ -88,6 +88,8 @@ export type AppState = {
appearance: "light" | "dark"; appearance: "light" | "dark";
gridSize: number | null; gridSize: number | null;
viewModeEnabled: boolean; viewModeEnabled: boolean;
networkSpeed: number;
networkPing: number;
/** top-most selected groups (i.e. does not include nested groups) */ /** top-most selected groups (i.e. does not include nested groups) */
selectedGroupIds: { [groupId: string]: boolean }; selectedGroupIds: { [groupId: string]: boolean };

View File

@ -363,6 +363,17 @@ export const nFormatter = (num: number, digits: number): string => {
); );
}; };
export const formatSpeedBits = (speed: number): string => {
// source: https://en.wikipedia.org/wiki/Data-rate_units#Conversion_table
const suffix = ["bps", "kbps", "Mbps", "Gbps"];
let index = 0;
while (speed > 1000) {
index++;
speed = speed / 1000;
}
return `${speed.toFixed(index > 1 ? 1 : 0)} ${suffix[index]}`;
};
export const getVersion = () => { export const getVersion = () => {
return ( return (
document.querySelector<HTMLMetaElement>('meta[name="version"]')?.content || document.querySelector<HTMLMetaElement>('meta[name="version"]')?.content ||
@ -370,6 +381,16 @@ export const getVersion = () => {
); );
}; };
export const formatTime = (mseconds: number): string => {
return mseconds < 1000
? `${mseconds} ms`
: `${(mseconds / 1000).toFixed(1)} s`;
};
export const getAverage = (arr: Array<number>): number => {
return arr.reduce((sum, currentVal) => sum + currentVal) / arr.length;
};
// Adapted from https://github.com/Modernizr/Modernizr/blob/master/feature-detects/emoji.js // Adapted from https://github.com/Modernizr/Modernizr/blob/master/feature-detects/emoji.js
export const supportsEmoji = () => { export const supportsEmoji = () => {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");