Merge branch 'staging' into predeploy

pull/931/head
Rick Carlino 2018-07-25 09:01:19 -05:00 committed by GitHub
commit 88f51b50b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 585 additions and 150 deletions

View File

@ -5,75 +5,79 @@ import { Sensors } from "./sensors";
import { Row, Page, Col } from "../ui/index";
import { mapStateToProps } from "./state_to_props";
import { WebcamPanel } from "./webcam";
import { Props, MoveProps } from "./interfaces";
import { Props } from "./interfaces";
import { Move } from "./move";
import { BooleanSetting } from "../session_keys";
import { Feature } from "../devices/interfaces";
import { SensorReadings } from "./sensor_readings/sensor_readings";
/** Controls page. */
@connect(mapStateToProps)
export class Controls extends React.Component<Props, {}> {
get arduinoBusy() {
return !!this.props.bot.hardware.informational_settings.busy;
}
move = () => <Move
bot={this.props.bot}
user={this.props.user}
dispatch={this.props.dispatch}
arduinoBusy={this.arduinoBusy}
botToMqttStatus={this.props.botToMqttStatus}
firmwareSettings={this.props.firmwareSettings}
getWebAppConfigVal={this.props.getWebAppConfigVal} />
peripherals = () => <Peripherals
bot={this.props.bot}
peripherals={this.props.peripherals}
dispatch={this.props.dispatch}
disabled={this.arduinoBusy} />
webcams = () => <WebcamPanel
feeds={this.props.feeds}
dispatch={this.props.dispatch} />
sensors = () => this.props.shouldDisplay(Feature.sensors)
? <Sensors
bot={this.props.bot}
sensors={this.props.sensors}
dispatch={this.props.dispatch}
disabled={this.arduinoBusy} />
: <div id="hidden-sensors-widget" />
sensorReadings = () => this.props.sensorReadings.length > 0
? <SensorReadings
sensorReadings={this.props.sensorReadings}
sensors={this.props.sensors}
timeOffset={this.props.timeOffset} />
: <div id="hidden-sensor-history-widget" />
render() {
const arduinoBusy = !!this
.props
.bot
.hardware
.informational_settings
.busy;
const moveProps: MoveProps = {
bot: this.props.bot,
user: this.props.user,
dispatch: this.props.dispatch,
arduinoBusy,
botToMqttStatus: this.props.botToMqttStatus,
firmwareSettings: this.props.firmwareSettings,
getWebAppConfigVal: this.props.getWebAppConfigVal,
};
const showWebcamWidget = !this.props.getWebAppConfigVal(BooleanSetting.hide_webcam_widget);
const showWebcamWidget =
!this.props.getWebAppConfigVal(BooleanSetting.hide_webcam_widget);
return <Page className="controls">
{showWebcamWidget
?
<Row>
<Col xs={12} sm={6} md={5} mdOffset={1}>
<Move {...moveProps} />
<Peripherals
bot={this.props.bot}
peripherals={this.props.peripherals}
dispatch={this.props.dispatch}
disabled={arduinoBusy} />
<this.move />
<this.peripherals />
</Col>
<Col xs={12} sm={6}>
<WebcamPanel feeds={this.props.feeds} dispatch={this.props.dispatch} />
{this.props.shouldDisplay(Feature.sensors) &&
<Sensors
bot={this.props.bot}
sensors={this.props.sensors}
dispatch={this.props.dispatch}
disabled={arduinoBusy} />}
<this.webcams />
<this.sensors />
<this.sensorReadings />
</Col>
</Row>
:
<Row>
<Col xs={12} sm={6} md={5} mdOffset={1}>
<Move {...moveProps} />
<this.move />
</Col>
<Col xs={12} sm={5}>
<Peripherals
bot={this.props.bot}
peripherals={this.props.peripherals}
dispatch={this.props.dispatch}
disabled={arduinoBusy} />
{this.props.shouldDisplay(Feature.sensors) &&
<Sensors
bot={this.props.bot}
sensors={this.props.sensors}
dispatch={this.props.dispatch}
disabled={arduinoBusy} />}
{this.props.sensorReadings.length > 0 &&
<SensorReadings
sensorReadings={this.props.sensorReadings}
sensors={this.props.sensors}
timeOffset={this.props.timeOffset} />}
<this.peripherals />
<this.sensors />
<this.sensorReadings />
</Col>
</Row>}
</Page>;

View File

@ -41,6 +41,7 @@ describe("filterSensorReadings()", () => {
location: undefined,
showPreviousPeriod: false,
deviation: 0,
hovered: undefined,
});
it("filters by date", () => {

View File

@ -0,0 +1,90 @@
import * as React from "react";
import { mount } from "enzyme";
import { SensorReadingsPlot, calcTimeParams } from "../graph";
import { SensorReadingPlotProps } from "../interfaces";
import {
fakeSensorReading
} from "../../../__test_support__/fake_state/resources";
describe("<SensorReadingPlot />", () => {
function fakeProps(sr = fakeSensorReading()): SensorReadingPlotProps {
return {
readingsForPeriod: () => [sr],
endDate: 1515715140,
timeOffset: 0,
hover: jest.fn(),
hovered: undefined,
showPreviousPeriod: false,
timePeriod: 3600 * 24,
};
}
it("renders", () => {
const wrapper = mount(<SensorReadingsPlot {...fakeProps()} />);
const txt = wrapper.text().toLowerCase();
["analog", "digital", "pm", "500"]
.map(string => expect(txt).toContain(string));
});
it("renders years", () => {
const p = fakeProps();
p.timePeriod = 3600 * 24 * 365;
const wrapper = mount(<SensorReadingsPlot {...p} />);
expect(wrapper.text()).toContain("2017");
});
it("renders digital reading", () => {
const sr = fakeSensorReading();
sr.body.mode = 0;
sr.body.value = 1;
const p = fakeProps(sr);
const wrapper = mount(<SensorReadingsPlot {...p} />);
expect(wrapper.find("circle").first().props().cy).toEqual(77);
});
it("renders analog reading", () => {
const sr = fakeSensorReading();
sr.body.mode = 1;
sr.body.value = 1023;
const p = fakeProps(sr);
const wrapper = mount(<SensorReadingsPlot {...p} />);
expect(wrapper.find("circle").first().props().cy).toEqual(77);
});
it("hovers point", () => {
const sr = fakeSensorReading();
const p = fakeProps(sr);
const wrapper = mount(<SensorReadingsPlot {...p} />);
wrapper.find("circle").first().simulate("mouseEnter");
expect(p.hover).toHaveBeenCalledWith(sr.uuid);
});
it("unhovers point", () => {
const p = fakeProps();
const wrapper = mount(<SensorReadingsPlot {...p} />);
wrapper.find("circle").first().simulate("mouseLeave");
expect(p.hover).toHaveBeenCalledWith(undefined);
});
it("hovers point", () => {
const sr = fakeSensorReading();
sr.body.value = 555;
const p = fakeProps(sr);
p.hovered = sr.uuid;
const wrapper = mount(<SensorReadingsPlot {...p} />);
expect(wrapper.text()).toContain("555");
});
});
describe("calcTimeParams()", () => {
it("returns correct parameters", () => {
expect(calcTimeParams(3600 * 24))
.toEqual({ timeScale: 40, timeStep: 3600 });
expect(calcTimeParams(3600 * 24 * 7))
.toEqual({ timeScale: 40 * 7, timeStep: 3600 * 24 });
expect(calcTimeParams(3600 * 24 * 30))
.toEqual({ timeScale: 40 * 30, timeStep: 3600 * 24 });
expect(calcTimeParams(3600 * 24 * 365))
.toEqual({ timeScale: 40 * 30 * 12, timeStep: 3600 * 24 * 30 });
});
});

View File

@ -1,6 +1,7 @@
import * as React from "react";
import { mount, shallow } from "enzyme";
import { LocationSelection, LocationSelectionProps, LocationDisplay } from "../location_selection";
import { LocationSelection, LocationDisplay } from "../location_selection";
import { LocationSelectionProps } from "../interfaces";
describe("<LocationSelection />", () => {
function fakeProps(): LocationSelectionProps {

View File

@ -51,8 +51,8 @@ describe("<SensorReadings />", () => {
const expected = 1515715140;
const p = fakeProps();
const wrapper = mount<SensorReadings>(<SensorReadings {...p} />);
expect(wrapper.instance().state.endDate)
.toEqual(moment(p.sensorReadings[0].body.created_at).unix());
expect(wrapper.instance().state.endDate).toEqual(
moment(p.sensorReadings[0].body.created_at).startOf("day").unix());
wrapper.instance().setEndDate(expected);
expect(wrapper.instance().state.endDate).toEqual(expected);
});
@ -72,4 +72,23 @@ describe("<SensorReadings />", () => {
wrapper.instance().setDeviation(expected);
expect(wrapper.instance().state.deviation).toEqual(expected);
});
it("sets hover", () => {
const expected = "fake UUID";
const wrapper = mount<SensorReadings>(<SensorReadings {...fakeProps()} />);
expect(wrapper.instance().state.hovered).toEqual(undefined);
wrapper.instance().hover(expected);
expect(wrapper.instance().state.hovered).toEqual(expected);
});
it("clears filters", () => {
const s = fakeSensor();
const p = fakeProps();
p.sensors = [s];
const wrapper = mount<SensorReadings>(<SensorReadings {...p} />);
wrapper.setState({ location: { x: 1, y: 2, z: 3 }, sensor: s });
wrapper.instance().clearFilters();
expect(wrapper.instance().state.location).toEqual(undefined);
expect(wrapper.instance().state.sensor).toEqual(undefined);
});
});

View File

@ -1,7 +1,8 @@
import * as React from "react";
import { mount, shallow } from "enzyme";
import { SensorSelection, SensorSelectionProps } from "../sensor_selection";
import { SensorSelection } from "../sensor_selection";
import { fakeSensor } from "../../../__test_support__/fake_state/resources";
import { SensorSelectionProps } from "../interfaces";
describe("<SensorSelection />", () => {
function fakeProps(): SensorSelectionProps {

View File

@ -7,11 +7,13 @@ import {
} from "../../../__test_support__/fake_state/resources";
describe("<SensorReadingsTable />", () => {
function fakeProps(): SensorReadingsTableProps {
function fakeProps(sr = fakeSensorReading()): SensorReadingsTableProps {
return {
readingsForPeriod: () => [fakeSensorReading()],
readingsForPeriod: () => [sr],
sensors: [fakeSensor()],
timeOffset: 0,
hover: jest.fn(),
hovered: undefined,
};
}
@ -31,4 +33,27 @@ describe("<SensorReadingsTable />", () => {
const wrapper = mount(<SensorReadingsTable {...p} />);
expect(wrapper.text().toLowerCase()).toContain("analog");
});
it("hovers row", () => {
const sr = fakeSensorReading();
const p = fakeProps(sr);
const wrapper = mount(<SensorReadingsTable {...p} />);
wrapper.find("tr").last().simulate("mouseEnter");
expect(p.hover).toHaveBeenCalledWith(sr.uuid);
});
it("unhovers row", () => {
const p = fakeProps();
const wrapper = mount(<SensorReadingsTable {...p} />);
wrapper.find("tr").last().simulate("mouseLeave");
expect(p.hover).toHaveBeenCalledWith(undefined);
});
it("selects row", () => {
const sr = fakeSensorReading();
const p = fakeProps(sr);
p.hovered = sr.uuid;
const wrapper = mount(<SensorReadingsTable {...p} />);
expect(wrapper.find("tr").last().hasClass("selected")).toEqual(true);
});
});

View File

@ -2,9 +2,10 @@ import * as React from "react";
import { mount, shallow } from "enzyme";
import { TimePeriodSelection, getEndDate, DateDisplay } from "../time_period_selection";
import { fakeSensorReading } from "../../../__test_support__/fake_state/resources";
import { TimePeriodSelectionProps, DateDisplayProps } from "../interfaces";
describe("<TimePeriodSelection />", () => {
function fakeProps() {
function fakeProps(): TimePeriodSelectionProps {
return {
timePeriod: 3600 * 24,
endDate: 1515715140,
@ -47,7 +48,7 @@ describe("<TimePeriodSelection />", () => {
describe("getEndDate()", () => {
it("returns recent reading date", () => {
expect(getEndDate([fakeSensorReading()])).toEqual(1515702038);
expect(getEndDate([fakeSensorReading()])).toEqual(expect.any(Number));
});
it("returns current date", () => {
@ -56,7 +57,7 @@ describe("getEndDate()", () => {
});
describe("<DateDisplay />", () => {
function fakeProps() {
function fakeProps(): DateDisplayProps {
return {
timePeriod: 3600 * 24 * 7,
endDate: 1515715140,

View File

@ -4,6 +4,16 @@ import { every, isNumber } from "lodash";
import { Xyz } from "../../devices/interfaces";
import * as moment from "moment";
/** One day in seconds. */
const oneDay = 3600 * 24;
/** Calculate a sensor reading filtered time period end time in seconds. */
export const calcEndOfPeriod = (
timePeriod: number,
endDate: number,
period: "current" | "previous"
) => endDate + oneDay
- timePeriod * (period === "current" ? 0 : 1);
/** Filter sensor readings using sensor history widget state. */
export const filterSensorReadings =
(sensorReadings: TaggedSensorReading[],
@ -16,10 +26,10 @@ export const filterSensorReadings =
// Don't return sensor readings from the previous period if not desired.
if (period === "previous" && !showPreviousPeriod) { return []; }
/** Time period begin. */
const begin = endDate - (period === "current" ? 1 : 2) * timePeriod;
/** Time period end. */
const end = period === "current" ? endDate : endDate - timePeriod;
const end = calcEndOfPeriod(timePeriod, endDate, period);
/** Time period begin. */
const begin = end - timePeriod;
return sensorReadings
// Filter by date

View File

@ -0,0 +1,190 @@
import * as React from "react";
import * as moment from "moment";
import { range, clamp } from "lodash";
import { t } from "i18next";
import { SensorReadingPlotProps } from "./interfaces";
import { calcEndOfPeriod } from "./filter_readings";
/** For SensorReadings plot. */
export const calcTimeParams = (timePeriod: number): {
timeStep: number, timeScale: number
} => {
if (timePeriod > 3600 * 24 * 32) { // year
return { timeStep: 3600 * 24 * 30, timeScale: 40 * 30 * 12 };
}
if (timePeriod > 3600 * 24 * 8) { // month
return { timeStep: 3600 * 24, timeScale: 40 * 30 };
}
if (timePeriod > 3600 * 25) { // week
return { timeStep: 3600 * 24, timeScale: 40 * 7 };
}
// day
return { timeStep: 3600, timeScale: 40 };
};
interface PlotProps {
timePeriod: number;
timeStep: number;
timeScale: number;
timeMax: moment.Moment;
yZero: number;
yMax: number;
xMax: number;
showPreviousPeriod: boolean;
}
/** Plot axes and labels. */
const Axes =
({ yZero, yMax, xMax }: { yZero: number, yMax: number, xMax: number }) =>
<g id="axes">
<line id="y-axis" strokeWidth="5px"
x1={0} y1={yZero} x2={0} y2={yMax} />
<text id="y-axis-label" textAnchor="middle" fontWeight="normal"
x={-250} y={yZero - 500}>
{t("analog")}
</text>
{[0, 1].map(v =>
<text key={"digital_value_y_axis_label_" + v}
textAnchor="start" alignmentBaseline="middle"
x={xMax + 50} y={yZero - v * 1023}>
{v}
</text>)}
<text id="y-axis-label" textAnchor="start" fontWeight="normal"
x={xMax + 50} y={yZero - 500}>
{t("digital")}
</text>
<line id="x-axis" strokeWidth="5px"
x1={0} y1={yZero} x2={xMax} y2={yZero} />
</g>;
/** y-axis (SensorReading value) */
const HorizontalGridlines = (props: PlotProps) =>
<g id="horizontal-gridlines">
{range(0, 1100, 100).map(y => {
const id = "horizontal_gridline_" + y;
return <g id={id} key={id}>
<text textAnchor="end" alignmentBaseline="middle"
x={-50} y={props.yZero - y}>
{y}
</text>
<line stroke="gray"
x1={0} y1={props.yZero - y} x2={props.xMax} y2={props.yZero - y} />
</g>;
})}
</g>;
/** x-axis (time) labels */
const createTimeLabel =
(x: number, timePeriod: number, timeStep: number, timeMax: moment.Moment) =>
(period: "current" | "previous"): string => {
const calcFormat = () => {
if (timePeriod > 3600 * 24 * 32) { return "MMM D YYYY"; }
if (timeStep > 3600) { return "MMM D"; }
return "h:mm A";
};
return timeMax.clone()
.subtract(timePeriod * (period === "current" ? 1 : 2) - x,
"seconds")
.format(calcFormat());
};
/** x-axis (time) */
const VerticalGridlines = (props: PlotProps) =>
<g id="vertical-gridlines">
{range(props.timeStep, props.timePeriod + 1, props.timeStep).map(x => {
const id = "vertical_gridline_" + x;
/** label & major gridline every 3 hours/days/months and every week day */
const major = (x / props.timeStep)
% (props.timePeriod == 3600 * 24 * 7 ? 1 : 3) == 0;
const createLabel =
createTimeLabel(x, props.timePeriod, props.timeStep, props.timeMax);
return <g id={id} key={id}>
{major &&
<text textAnchor="middle"
x={x / props.timeScale}
y={props.yZero + 100}>
{createLabel("current")}
</text>}
{major && props.showPreviousPeriod &&
<text textAnchor="middle" stroke={"gray"}
x={x / props.timeScale}
y={props.yZero + 200}>
{createLabel("previous")}
</text>}
<line stroke={major ? "black" : "gray"}
x1={x / props.timeScale}
y1={props.yZero}
x2={x / props.timeScale}
y2={props.yMax} />
</g>;
})}
</g>;
/** SensorReadings (current and maybe previous time periods) */
const DataPoints = ({ plotProps, parentProps }: {
plotProps: PlotProps, parentProps: SensorReadingPlotProps
}) =>
<g id="sensor-readings">
{["current", "previous"].map((period: "current" | "previous") =>
<g id={period} key={period}>
{parentProps.readingsForPeriod(period).map(r => {
const created_at =
moment(r.body.created_at).utcOffset(parentProps.timeOffset);
const unixMax = calcEndOfPeriod(plotProps.timePeriod,
plotProps.timeMax.unix(), period);
/** calculated using scaled plot distance from x-axis end */
const cx = plotProps.xMax
- (unixMax - created_at.unix()) / plotProps.timeScale;
const cy = plotProps.yZero - clamp(r.body.value
* (r.body.mode == 0 && r.body.value <= 1 ? 1023 : 1), 0, 1023);
const color = period === "current" ? "black" : "gray";
const selected = parentProps.hovered === r.uuid;
return <g id={r.uuid} key={r.uuid}>
<circle fill={color} stroke={color}
onMouseEnter={() => parentProps.hover(r.uuid)}
onMouseLeave={() => parentProps.hover(undefined)}
r={selected ? 25 : 15}
cx={cx}
cy={cy} />
{selected &&
<text
x={cx + 30}
y={cy - 10}>
{r.body.value}
</text>}
</g>;
})}
</g>)}
</g>;
export const SensorReadingsPlot = (props: SensorReadingPlotProps) => {
const { timePeriod, endDate, timeOffset, showPreviousPeriod } = props;
const timeVBMax = 2800;
const yZero = 1100;
const { timeStep, timeScale } = calcTimeParams(props.timePeriod);
const plotProps: PlotProps = {
timePeriod,
timeStep,
timeScale,
timeMax: moment.unix(endDate).startOf("hour").utcOffset(timeOffset),
yZero,
yMax: yZero - 1023,
xMax: timeVBMax - 640,
showPreviousPeriod,
};
return <svg
className="sensor-readings-plot"
width="100%"
height="100%"
viewBox={`-350 -100 ${timeVBMax} ${plotProps.yZero + 400}`}>
<Axes yZero={plotProps.yZero} yMax={plotProps.yMax} xMax={plotProps.xMax} />
<VerticalGridlines {...plotProps} />
<HorizontalGridlines {...plotProps} />
<DataPoints plotProps={plotProps} parentProps={props} />
</svg>;
};

View File

@ -9,15 +9,82 @@ export interface SensorReadingsProps {
export interface SensorReadingsState {
sensor: TaggedSensor | undefined;
/** seconds */
timePeriod: number;
/** seconds */
endDate: number;
/** location filter setting */
location: AxisInputBoxGroupState | undefined;
/** Show the previous time period in addition to the current time period. */
showPreviousPeriod: boolean;
/** mm */
deviation: number;
/** TaggedSensorReading UUID */
hovered: string | undefined;
}
export interface SensorReadingsTableProps {
readingsForPeriod: (period: "current" | "previous") => TaggedSensorReading[];
sensors: TaggedSensor[];
timeOffset: number;
/** TaggedSensorReading UUID */
hovered: string | undefined;
hover: (hovered: string | undefined) => void;
}
export interface TableRowProps {
sensorReading: TaggedSensorReading;
sensorName: string;
timeOffset: number;
period: "previous" | "current";
/** TaggedSensorReading UUID */
hovered: string | undefined;
hover: (hovered: string | undefined) => void;
}
export interface SensorSelectionProps {
selectedSensor: TaggedSensor | undefined;
sensors: TaggedSensor[];
setSensor: (sensor: TaggedSensor) => void;
}
export interface LocationSelectionProps {
location: AxisInputBoxGroupState | undefined;
/** mm */
deviation: number;
setDeviation: (deviation: number) => void;
setLocation: (location: AxisInputBoxGroupState | undefined) => void;
}
export interface TimePeriodSelectionProps {
/** seconds */
timePeriod: number;
/** seconds */
endDate: number;
showPreviousPeriod: boolean;
setEndDate: (date: number) => void;
setPeriod: (period: number) => void;
togglePrevious: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
export interface DateDisplayProps {
/** seconds */
endDate: number;
timeOffset: number;
/** seconds */
timePeriod: number;
showPreviousPeriod: boolean;
}
export interface SensorReadingPlotProps {
readingsForPeriod: (period: "current" | "previous") => TaggedSensorReading[];
/** seconds */
endDate: number;
timeOffset: number;
/** TaggedSensorReading UUID */
hovered: string | undefined;
hover: (hovered: string | undefined) => void;
showPreviousPeriod: boolean;
/** seconds */
timePeriod: number;
}

View File

@ -5,13 +5,7 @@ import { AxisInputBoxGroupState } from "../interfaces";
import { Xyz } from "../../devices/interfaces";
import { AxisInputBox } from "../axis_input_box";
import { isNumber } from "lodash";
export interface LocationSelectionProps {
location: AxisInputBoxGroupState | undefined;
deviation: number;
setDeviation: (deviation: number) => void;
setLocation: (location: AxisInputBoxGroupState | undefined) => void;
}
import { LocationSelectionProps } from "./interfaces";
/** Select a location filter for sensor readings. */
export const LocationSelection =

View File

@ -12,6 +12,7 @@ import { ToolTips } from "../../constants";
import { t } from "i18next";
import { TaggedSensor } from "../../resources/tagged_resources";
import { AxisInputBoxGroupState } from "../interfaces";
import { SensorReadingsPlot } from "./graph";
export class SensorReadings
extends React.Component<SensorReadingsProps, SensorReadingsState> {
@ -22,6 +23,7 @@ export class SensorReadings
location: undefined,
showPreviousPeriod: false,
deviation: 0,
hovered: undefined,
};
/** Toggle display of previous time period. */
@ -33,6 +35,15 @@ export class SensorReadings
setLocation = (location: AxisInputBoxGroupState | undefined) =>
this.setState({ location });
setDeviation = (deviation: number) => this.setState({ deviation });
hover = (hovered: string | undefined) => this.setState({ hovered });
clearFilters = () => this.setState({
sensor: undefined,
timePeriod: 3600 * 24,
endDate: getEndDate(this.props.sensorReadings),
location: undefined,
showPreviousPeriod: false,
deviation: 0,
});
render() {
/** Return filtered sensor readings for the specified period.
@ -43,7 +54,11 @@ export class SensorReadings
return <Widget className="sensor-history-widget">
<WidgetHeader
title={t("Sensor History")}
helpText={ToolTips.SENSOR_HISTORY} />
helpText={ToolTips.SENSOR_HISTORY}>
<button className="fb-button gray" onClick={this.clearFilters}>
{t("clear filters")}
</button>
</WidgetHeader>
<WidgetBody>
<SensorSelection
selectedSensor={this.state.sensor}
@ -62,10 +77,20 @@ export class SensorReadings
setLocation={this.setLocation}
setDeviation={this.setDeviation} />
<hr />
<SensorReadingsPlot
readingsForPeriod={readingsForPeriod}
endDate={this.state.endDate}
timeOffset={this.props.timeOffset}
hover={this.hover}
hovered={this.state.hovered}
showPreviousPeriod={this.state.showPreviousPeriod}
timePeriod={this.state.timePeriod} />
<SensorReadingsTable
readingsForPeriod={readingsForPeriod}
sensors={this.props.sensors}
timeOffset={this.props.timeOffset} />
timeOffset={this.props.timeOffset}
hover={this.hover}
hovered={this.state.hovered} />
</WidgetBody>
<WidgetFooter>
<div className="sensor-history-footer">

View File

@ -2,15 +2,10 @@ import * as React from "react";
import { FBSelect, DropDownItem } from "../../ui";
import { t } from "i18next";
import { TaggedSensor } from "../../resources/tagged_resources";
import { SensorSelectionProps } from "./interfaces";
const ALL_CHOICE: DropDownItem = { label: t("All"), value: "" };
export interface SensorSelectionProps {
selectedSensor: TaggedSensor | undefined;
sensors: TaggedSensor[];
setSensor: (sensor: TaggedSensor) => void;
}
/** Select a sensor by which to filter sensor readings. */
export const SensorSelection = ({
selectedSensor, sensors, setSensor }: SensorSelectionProps) => {
@ -24,6 +19,7 @@ export const SensorSelection = ({
return <div>
<label>{t("Sensor")}</label>
<FBSelect
key={selectedSensor ? selectedSensor.uuid : "all_sensors"}
selectedItem={selectedSensor
? sensorDDIByUuidLookup[selectedSensor.uuid]
: ALL_CHOICE}

View File

@ -1,10 +1,9 @@
import * as React from "react";
import { SensorReadingsTableProps } from "./interfaces";
import { SensorReadingsTableProps, TableRowProps } from "./interfaces";
import { t } from "i18next";
import { xyzTableEntry } from "../../logs/components/logs_table";
import { formatLogTime } from "../../logs";
import * as moment from "moment";
import { TaggedSensorReading } from "../../resources/tagged_resources";
enum TableColWidth {
sensor = 125,
@ -49,17 +48,16 @@ const TableHeader = () =>
</table>;
/** Sensor reading. */
const TableRow = (props: {
sensorReading: TaggedSensorReading,
sensorName: string,
timeOffset: number,
period: "previous" | "current"
}) => {
const { sensorReading, timeOffset, period, sensorName } = props;
const TableRow = (props: TableRowProps) => {
const {
sensorReading, timeOffset, period, sensorName, hover, hovered
} = props;
const { uuid, body } = sensorReading;
const { value, x, y, z, created_at, mode } = body;
const color = period === "previous" ? "gray" : "";
return <tr key={uuid} style={{ color }}>
return <tr key={uuid}
className={`${period} ${hovered === uuid ? "selected" : ""}`}
onMouseEnter={() => hover(uuid)}
onMouseLeave={() => hover(undefined)}>
<td style={{ width: `${TableColWidth.sensor}px` }}>
{sensorName}
</td>
@ -100,7 +98,9 @@ export class SensorReadingsTable
sensorName={sensorName}
sensorReading={sensorReading}
timeOffset={this.props.timeOffset}
period={period} />;
period={period}
hover={this.props.hover}
hovered={this.props.hovered} />;
});
})}
</tbody>

View File

@ -3,6 +3,8 @@ import { FBSelect, Row, Col, BlurableInput } from "../../ui";
import { t } from "i18next";
import * as moment from "moment";
import { TaggedSensorReading } from "../../resources/tagged_resources";
import { TimePeriodSelectionProps, DateDisplayProps } from "./interfaces";
import { cloneDeep } from "lodash";
/** Look up time period label by seconds. */
const timePeriodLookup = {
@ -18,79 +20,72 @@ const timePeriodList = Object.entries(timePeriodLookup)
const blurableInputDateFormat = "YYYY-MM-DD";
const today = moment().startOf("day").unix();
/** Return default time period end date sensor readings widget state. */
export const getEndDate = (sensorReadings: TaggedSensorReading[]) =>
sensorReadings.length > 0
? moment(sensorReadings.reverse()[0].body.created_at).unix()
: moment().unix();
? moment(cloneDeep(sensorReadings).reverse()[0]
.body.created_at).startOf("day").unix()
: today;
/** Specify a time period by end date and duration. */
export const TimePeriodSelection =
({ timePeriod, endDate, showPreviousPeriod,
setEndDate, setPeriod, togglePrevious }: {
timePeriod: number,
endDate: number,
showPreviousPeriod: boolean,
setEndDate: (date: number) => void,
setPeriod: (period: number) => void,
togglePrevious: (event: React.ChangeEvent<HTMLInputElement>) => void
}) =>
<div>
<label>{t("Time period")}</label>
<FBSelect
selectedItem={
{ label: timePeriodLookup[timePeriod], value: timePeriod }}
onChange={ddi => setPeriod(parseInt("" + ddi.value))}
list={timePeriodList} />
<Row>
<Col xs={6}>
<label>{t("Period End Date")}</label>
<i className="fa fa-clock-o"
style={{ marginLeft: "1rem" }}
onClick={() => setEndDate(moment().unix())} />
<BlurableInput
type="date"
value={moment.unix(endDate).format(blurableInputDateFormat)}
onCommit={e => setEndDate(moment(e.currentTarget.value,
blurableInputDateFormat).unix())} />
</Col>
<Col xs={6}>
<label>{t("Show Previous Period")}</label>
<div className="fb-checkbox large">
<input type="checkbox"
checked={showPreviousPeriod}
onChange={togglePrevious} />
</div>
</Col>
</Row>
</div>;
export const TimePeriodSelection = (props: TimePeriodSelectionProps) => {
const { timePeriod, endDate, showPreviousPeriod,
setEndDate, setPeriod, togglePrevious } = props;
return <div>
<label>{t("Time period")}</label>
<FBSelect
key={timePeriod}
selectedItem={
{ label: timePeriodLookup[timePeriod], value: timePeriod }}
onChange={ddi => setPeriod(parseInt("" + ddi.value))}
list={timePeriodList} />
<Row>
<Col xs={6}>
<label>{t("Period End Date")}</label>
<i className="fa fa-clock-o"
style={{ marginLeft: "1rem" }}
onClick={() => setEndDate(today)} />
<BlurableInput
type="date"
value={moment.unix(endDate).format(blurableInputDateFormat)}
onCommit={e => setEndDate(moment(e.currentTarget.value,
blurableInputDateFormat).unix())} />
</Col>
<Col xs={6}>
<label>{t("Show Previous Period")}</label>
<div className="fb-checkbox large">
<input type="checkbox"
checked={showPreviousPeriod}
onChange={togglePrevious} />
</div>
</Col>
</Row>
</div>;
};
/** Format date for widget footer display. */
const formatDate = (unix: number, offset: number) =>
moment.unix(unix).utcOffset(offset).format("MMMM D");
/** Display sensor reading date filter settings. */
export const DateDisplay =
({ endDate, timeOffset, timePeriod, showPreviousPeriod }: {
endDate: number,
timeOffset: number,
timePeriod: number,
showPreviousPeriod: boolean,
}) => {
const dateRange = (end: number) => {
const begin = formatDate(end - timePeriod, timeOffset);
return timePeriod > 60 * 60 * 24
? `${begin}${formatDate(end, timeOffset)}`
: formatDate(end, timeOffset);
};
return <div className="date">
<label>{t("Date")}:</label>
<span>
{dateRange(endDate)}
</span>
{showPreviousPeriod &&
<span style={{ color: "gray" }}>
{" (" + dateRange(endDate - timePeriod) + ")"}
</span>}
</div>;
export const DateDisplay = (props: DateDisplayProps) => {
const { endDate, timeOffset, timePeriod, showPreviousPeriod } = props;
const dateRange = (end: number) => {
const begin = formatDate(end - timePeriod, timeOffset);
return timePeriod > 60 * 60 * 24
? `${begin}${formatDate(end, timeOffset)}`
: formatDate(end, timeOffset);
};
return <div className="date">
<label>{t("Date")}:</label>
<span>
{dateRange(endDate)}
</span>
{showPreviousPeriod &&
<span style={{ color: "gray" }}>
{" (" + dateRange(endDate - timePeriod) + ")"}
</span>}
</div>;
};

View File

@ -327,11 +327,27 @@ a {
}
}
.sensor-readings-plot {
max-height: 300px;
stroke: $black;
font-size: 60px;
font-weight: 100;
}
.sensor-history-table {
font-size: 1.2rem;
th, td {
width: 1%;
}
tr {
color: $black;
&.previous {
color: $medium_gray;
}
&.selected {
background: $gray;
}
}
.sensor-history-table-contents {
display: block;
max-height: 20rem;