UI for quality of service meter. NEXT: Needs tests.

pull/1418/head
Rick Carlino 2019-09-05 20:51:49 -05:00
parent 6e9ff55a38
commit 8b9585b145
13 changed files with 102 additions and 42 deletions

View File

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

View File

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

View File

@ -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<AppProps, {}> {
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)) &&
<ControlsPopup

View File

@ -4,6 +4,7 @@ import { Connectivity, ConnectivityProps } from "../connectivity";
import { bot } from "../../../__test_support__/fake_state/bot";
import { StatusRowProps } from "../connectivity_row";
import { fill } from "lodash";
import { fakePings } from "../../../__test_support__/fake_state/pings";
describe("<Connectivity />", () => {
const statusRow = {
@ -26,6 +27,7 @@ describe("<Connectivity />", () => {
bot,
rowData,
flags,
pings: fakePings()
});
it("sets hovered connection", () => {

View File

@ -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("<ConnectivityPanel/>", () => {
const fakeProps = (): ConnectivityPanel["props"] => ({
@ -14,6 +15,7 @@ describe("<ConnectivityPanel/>", () => {
dispatch: jest.fn(),
deviceAccount: fakeDevice(),
status: SpecialStatus.SAVED,
pings: fakePings()
});
it("renders the default use case", () => {

View File

@ -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
<WiFiStrengthDisplay wifiStrength={wifi_level} />
<VoltageDisplay throttled={throttled} />
</div>
<QosPanel pings={this.props.pings} />
</Col>
<Col md={12} lg={8}>
<ConnectivityRow from={t("from")} to={t("to")} header={true} />

View File

@ -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<Props, {}> {
@ -39,7 +41,8 @@ export class ConnectivityPanel extends React.Component<Props, {}> {
<Connectivity
bot={this.props.bot}
rowData={this.data.rowData}
flags={this.data.flags} />
flags={this.data.flags}
pings={this.props.pings} />
</WidgetBody>
</Widget>;
}

View File

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

View File

@ -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 <p>
<b>{t(k)}: </b>
<span>{v}</span>
</p>;
}
export class QosPanel extends React.Component<Props, {}> {
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 <div className="fbos-info">
<label>{t("Network Quality")}</label>
<div className="chip-temp-display">
<Row k="Sent" v={r.total} />
<Row k="Received" v={r.complete} />
<Row k="Lost" v={r.timeout} />
<Row k="Pending" v={r.pending} />
<Row k="Percent OK" v={(100 * errorRate).toFixed(1)} />
<Row k="Best Time (ms)" v={r.best} />
<Row k="Worst Time (ms)" v={r.worst} />
<Row k="Average Time (ms)" v={percentage} />
</div>
</div>;
}
}

View File

@ -1,34 +0,0 @@
import {
calculateLatency,
calculatePingLoss,
PingDictionary,
} from "./qos";
import React from "react";
interface Props {
pings: PingDictionary;
}
export class QosRow extends React.Component<Props, {}> {
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 <div>
<br />
<ul>
<li>best: {report.best || 0}</li>
<li>worst: {report.worst || 0}</li>
<li>average: {(report.average || 0).toFixed(1)}</li>
<li>Pings OK: {report.complete}</li>
<li>Pings pending: {report.pending || 0}</li>
<li>Pings failed: {report.timeout || 0}</li>
<li>Total pings: {report.total || 0}</li>
<li>Percent OK: {(100 * ber).toFixed(1)}</li>
</ul>
</div>;
}
}

View File

@ -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", () => {

View File

@ -129,7 +129,8 @@ export class NavBar extends React.Component<NavBarProps, Partial<NavBarState>> {
<Connectivity
bot={this.props.bot}
rowData={this.connectivityData.rowData}
flags={this.connectivityData.flags} />
flags={this.connectivityData.flags}
pings={this.props.pings} />
</Popover>
</div>
<RunTour currentTour={this.props.tour} />

View File

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