Merge branch 'staging' into predeploy
commit
88f51b50b5
|
@ -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>;
|
||||
|
|
|
@ -41,6 +41,7 @@ describe("filterSensorReadings()", () => {
|
|||
location: undefined,
|
||||
showPreviousPeriod: false,
|
||||
deviation: 0,
|
||||
hovered: undefined,
|
||||
});
|
||||
|
||||
it("filters by date", () => {
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
});
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>;
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue