From 8b9585b14587e3ba9925efddfeba83aa095798d3 Mon Sep 17 00:00:00 2001 From: Rick Carlino Date: Thu, 5 Sep 2019 20:51:49 -0500 Subject: [PATCH] UI for quality of service meter. NEXT: Needs tests. --- frontend/__test_support__/fake_state/pings.ts | 9 +++ frontend/__tests__/app_test.tsx | 2 + frontend/app.tsx | 6 +- .../__tests__/connectivity_test.tsx | 2 + .../connectivity/__tests__/index_test.tsx | 2 + .../devices/connectivity/connectivity.tsx | 4 ++ frontend/devices/connectivity/index.tsx | 5 +- frontend/devices/connectivity/qos.ts | 14 +++-- frontend/devices/connectivity/qos_panel.tsx | 59 +++++++++++++++++++ frontend/devices/connectivity/qos_row.tsx | 34 ----------- frontend/nav/__tests__/nav_index_test.tsx | 2 + frontend/nav/index.tsx | 3 +- frontend/nav/interfaces.ts | 2 + 13 files changed, 102 insertions(+), 42 deletions(-) create mode 100644 frontend/__test_support__/fake_state/pings.ts create mode 100644 frontend/devices/connectivity/qos_panel.tsx delete mode 100644 frontend/devices/connectivity/qos_row.tsx diff --git a/frontend/__test_support__/fake_state/pings.ts b/frontend/__test_support__/fake_state/pings.ts new file mode 100644 index 000000000..24a61e2ea --- /dev/null +++ b/frontend/__test_support__/fake_state/pings.ts @@ -0,0 +1,9 @@ +import { PingDictionary } from "../../devices/connectivity/qos"; + +export function fakePings(): PingDictionary { + return { + "a": { kind: "timeout", start: 1, end: 4, id: "a" }, + "b": { kind: "pending", start: 1, end: 4, id: "b" }, + "c": { kind: "complete", start: 2, end: 3, id: "c" } + }; +} diff --git a/frontend/__tests__/app_test.tsx b/frontend/__tests__/app_test.tsx index 212c55666..930a2e070 100644 --- a/frontend/__tests__/app_test.tsx +++ b/frontend/__tests__/app_test.tsx @@ -20,6 +20,7 @@ import { import { ResourceName } from "farmbot"; import { fakeTimeSettings } from "../__test_support__/fake_time_settings"; import { error } from "../toast/toast"; +import { fakePings } from "../__test_support__/fake_state/pings"; const FULLY_LOADED: ResourceName[] = [ "Sequence", "Regimen", "FarmEvent", "Point", "Tool", "Device"]; @@ -42,6 +43,7 @@ const fakeProps = (): AppProps => { resources: buildResourceIndex().index, autoSync: false, alertCount: 0, + pings: fakePings() }; }; diff --git a/frontend/app.tsx b/frontend/app.tsx index 86f29454c..f6a73ba83 100644 --- a/frontend/app.tsx +++ b/frontend/app.tsx @@ -29,6 +29,7 @@ import { ResourceIndex } from "./resources/interfaces"; import { isBotOnline } from "./devices/must_be_online"; import { getStatus } from "./connectivity/reducer_support"; import { getAllAlerts } from "./messages/state_to_props"; +import { PingDictionary } from "./devices/connectivity/qos"; /** For the logger module */ init(); @@ -50,6 +51,7 @@ export interface AppProps { resources: ResourceIndex; autoSync: boolean; alertCount: number; + pings: PingDictionary; } export function mapStateToProps(props: Everything): AppProps { @@ -76,6 +78,7 @@ export function mapStateToProps(props: Everything): AppProps { resources: props.resources.index, autoSync: !!(fbosConfig && fbosConfig.auto_sync), alertCount: getAllAlerts(props.resources).length, + pings: props.bot.connectivity.pings }; } /** Time at which the app gives up and asks the user to refresh */ @@ -133,7 +136,8 @@ export class App extends React.Component { tour={this.props.tour} autoSync={this.props.autoSync} alertCount={this.props.alertCount} - device={getDeviceAccountSettings(this.props.resources)} />} + device={getDeviceAccountSettings(this.props.resources)} + pings={this.props.pings} />} {syncLoaded && this.props.children} {!(["controls", "account", "regimens"].includes(currentPage)) && ", () => { const statusRow = { @@ -26,6 +27,7 @@ describe("", () => { bot, rowData, flags, + pings: fakePings() }); it("sets hovered connection", () => { diff --git a/frontend/devices/connectivity/__tests__/index_test.tsx b/frontend/devices/connectivity/__tests__/index_test.tsx index 9d79b2adc..c1b6d5b26 100644 --- a/frontend/devices/connectivity/__tests__/index_test.tsx +++ b/frontend/devices/connectivity/__tests__/index_test.tsx @@ -7,6 +7,7 @@ import { SpecialStatus } from "farmbot"; import { bot } from "../../../__test_support__/fake_state/bot"; import { fakeDevice } from "../../../__test_support__/resource_index_builder"; import { resetConnectionInfo } from "../../actions"; +import { fakePings } from "../../../__test_support__/fake_state/pings"; describe("", () => { const fakeProps = (): ConnectivityPanel["props"] => ({ @@ -14,6 +15,7 @@ describe("", () => { dispatch: jest.fn(), deviceAccount: fakeDevice(), status: SpecialStatus.SAVED, + pings: fakePings() }); it("renders the default use case", () => { diff --git a/frontend/devices/connectivity/connectivity.tsx b/frontend/devices/connectivity/connectivity.tsx index ddeec2227..54acb72d3 100644 --- a/frontend/devices/connectivity/connectivity.tsx +++ b/frontend/devices/connectivity/connectivity.tsx @@ -8,11 +8,14 @@ import { ChipTemperatureDisplay, WiFiStrengthDisplay, VoltageDisplay } from "../components/fbos_settings/fbos_details"; import { t } from "../../i18next_wrapper"; +import { QosPanel } from "./qos_panel"; +import { PingDictionary } from "./qos"; export interface ConnectivityProps { bot: BotState; rowData: StatusRowProps[]; flags: DiagnosisProps; + pings: PingDictionary; } interface ConnectivityState { @@ -42,6 +45,7 @@ export class Connectivity + diff --git a/frontend/devices/connectivity/index.tsx b/frontend/devices/connectivity/index.tsx index deef8df85..2834ce6b6 100644 --- a/frontend/devices/connectivity/index.tsx +++ b/frontend/devices/connectivity/index.tsx @@ -8,12 +8,14 @@ import { Connectivity } from "./connectivity"; import { BotState } from "../interfaces"; import { connectivityData } from "./generate_data"; import { resetConnectionInfo } from "../actions"; +import { PingDictionary } from "./qos"; interface Props { status: SpecialStatus; dispatch: Function; bot: BotState; deviceAccount: TaggedDevice; + pings: PingDictionary; } export class ConnectivityPanel extends React.Component { @@ -39,7 +41,8 @@ export class ConnectivityPanel extends React.Component { + flags={this.data.flags} + pings={this.props.pings} /> ; } diff --git a/frontend/devices/connectivity/qos.ts b/frontend/devices/connectivity/qos.ts index a2e0f5804..462b0856b 100644 --- a/frontend/devices/connectivity/qos.ts +++ b/frontend/devices/connectivity/qos.ts @@ -94,13 +94,17 @@ const mapper = (p: Ping) => (p.kind === "complete") ? export const calculateLatency = (s: PingDictionary): LatencyReport => { - const latency: number[] = + let latency: number[] = betterCompact(getAll(s).map(mapper)); - + // Prevents "Infinity" from showing up in UI + // when the app is loading or the bot is 100% + // offline: + if (latency.length == 0) { latency = [0]; } + const average = Math.round(latency.reduce((a, b) => a + b, 0) / latency.length); return { - best: Math.min(...latency) || 0, - worst: Math.max(...latency) || 0, - average: latency.reduce((a, b) => a + b, 0) / latency.length, + best: Math.min(...latency), + worst: Math.max(...latency), + average, total: latency.length }; }; diff --git a/frontend/devices/connectivity/qos_panel.tsx b/frontend/devices/connectivity/qos_panel.tsx new file mode 100644 index 000000000..d52bc5dc6 --- /dev/null +++ b/frontend/devices/connectivity/qos_panel.tsx @@ -0,0 +1,59 @@ +import { + calculateLatency, + calculatePingLoss, + PingDictionary, +} from "./qos"; +import React from "react"; +import { t } from "../../i18next_wrapper"; + +interface Props { + pings: PingDictionary; +} + +interface KeyValProps { + k: string; + v: number | string; +} + +function Row({ k, v }: KeyValProps) { + return

+ {t(k)}: + {v} +

; + +} + +export class QosPanel extends React.Component { + get pingState(): PingDictionary { + return this.props.pings; + } + + get latencyReport() { + return calculateLatency(this.pingState); + } + + get qualityReport() { + return calculatePingLoss(this.pingState); + } + + render() { + const r = { ...this.latencyReport, ...this.qualityReport }; + const errorRate = ((r.complete) / r.total); + const percentage = (r.average).toFixed(1); + + return
+ +
+ + + + + + + + +
+
; + + } +} diff --git a/frontend/devices/connectivity/qos_row.tsx b/frontend/devices/connectivity/qos_row.tsx deleted file mode 100644 index de460dd91..000000000 --- a/frontend/devices/connectivity/qos_row.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { - calculateLatency, - calculatePingLoss, - PingDictionary, -} from "./qos"; -import React from "react"; - -interface Props { - pings: PingDictionary; -} - -export class QosRow extends React.Component { - get pingState(): PingDictionary { return this.props.pings; } - - render() { - const reportA = calculateLatency(this.pingState); - const reportB = calculatePingLoss(this.pingState); - const report = { ...reportA, ...reportB }; - const ber = ((report.complete || 0) / report.total) || 0; - return
-
-
    -
  • best: {report.best || 0}
  • -
  • worst: {report.worst || 0}
  • -
  • average: {(report.average || 0).toFixed(1)}
  • -
  • Pings OK: {report.complete}
  • -
  • Pings pending: {report.pending || 0}
  • -
  • Pings failed: {report.timeout || 0}
  • -
  • Total pings: {report.total || 0}
  • -
  • Percent OK: {(100 * ber).toFixed(1)}
  • -
-
; - } -} diff --git a/frontend/nav/__tests__/nav_index_test.tsx b/frontend/nav/__tests__/nav_index_test.tsx index f69312612..76763a2fa 100644 --- a/frontend/nav/__tests__/nav_index_test.tsx +++ b/frontend/nav/__tests__/nav_index_test.tsx @@ -11,6 +11,7 @@ import { NavBarProps } from "../interfaces"; import { fakeDevice } from "../../__test_support__/resource_index_builder"; import { maybeSetTimezone } from "../../devices/timezones/guess_timezone"; import { fakeTimeSettings } from "../../__test_support__/fake_time_settings"; +import { fakePings } from "../../__test_support__/fake_state/pings"; describe("NavBar", () => { const fakeProps = (): NavBarProps => ({ @@ -25,6 +26,7 @@ describe("NavBar", () => { device: fakeDevice(), autoSync: false, alertCount: 0, + pings: fakePings() }); it("has correct parent classname", () => { diff --git a/frontend/nav/index.tsx b/frontend/nav/index.tsx index 3a2da0e55..144267145 100644 --- a/frontend/nav/index.tsx +++ b/frontend/nav/index.tsx @@ -129,7 +129,8 @@ export class NavBar extends React.Component> { + flags={this.connectivityData.flags} + pings={this.props.pings} /> diff --git a/frontend/nav/interfaces.ts b/frontend/nav/interfaces.ts index a416b8125..4c15e3f14 100644 --- a/frontend/nav/interfaces.ts +++ b/frontend/nav/interfaces.ts @@ -2,6 +2,7 @@ import { BotState } from "../devices/interfaces"; import { TaggedUser, TaggedLog, TaggedDevice } from "farmbot"; import { GetWebAppConfigValue } from "../config_storage/actions"; import { TimeSettings } from "../interfaces"; +import { PingDictionary } from "../devices/connectivity/qos"; export interface SyncButtonProps { dispatch: Function; @@ -23,6 +24,7 @@ export interface NavBarProps { device: TaggedDevice; autoSync: boolean; alertCount: number; + pings: PingDictionary; } export interface NavBarState {