@robotical/sensors-dashboard
Version:
A dashboard tool for depicting Marty data by Robotical
396 lines (395 loc) • 21 kB
JavaScript
;
require("core-js/modules/es.symbol.description.js");
require("core-js/modules/esnext.iterator.for-each.js");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
require("core-js/modules/web.dom-collections.iterator.js");
require("core-js/modules/es.regexp.exec.js");
require("core-js/modules/es.string.replace.js");
require("core-js/modules/es.number.to-fixed.js");
require("core-js/modules/esnext.iterator.constructor.js");
require("core-js/modules/esnext.iterator.filter.js");
require("core-js/modules/es.promise.js");
require("core-js/modules/esnext.iterator.map.js");
require("core-js/modules/web.url.js");
require("core-js/modules/web.url-search-params.js");
require("core-js/modules/es.array.reduce.js");
require("core-js/modules/esnext.iterator.reduce.js");
require("core-js/modules/esnext.iterator.some.js");
var _react = require("react");
var _MartyInterface = require("../../app-bridge/MartyInterface");
var _getAddonsList = _interopRequireDefault(require("../../utils/get-addons-list"));
var _decider = require("../../utils/start-end-rules/decider");
var _startEndOptions = require("../../utils/start-end-rules/start-end-options");
var _AddonsList = _interopRequireDefault(require("../AddonsList"));
var _Graph = _interopRequireDefault(require("../Graph"));
var _GraphControls = _interopRequireDefault(require("../GraphControls"));
var _stylesModule = _interopRequireDefault(require("./styles.module.css"));
var _exportCsv = require("../../utils/export-csv");
var _fa = require("react-icons/fa");
var _Tooltip = _interopRequireDefault(require("@mui/material/Tooltip"));
var _raft = require("@robotical/webapp-types/dist-types/src/types/raft");
var _CogInterface = _interopRequireDefault(require("../../app-bridge/CogInterface"));
var _MockMartyInterface = _interopRequireDefault(require("../../app-bridge/mocks/MockMartyInterface"));
var _MockCogInterface = _interopRequireDefault(require("../../app-bridge/mocks/MockCogInterface"));
var _MockApplicationManager = require("../../mock/MockApplicationManager");
var _addonSelection = require("../../utils/addon-selection");
var _MicroBitInterface = _interopRequireDefault(require("../../app-bridge/MicroBitInterface"));
var _MicroBitWebBluetooth = require("../../microbit/MicroBitWebBluetooth");
var _jsxRuntime = require("react/jsx-runtime");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
const hasAddonProvider = raftInterface => typeof raftInterface.getAvailableAddons === "function";
function GraphArea(_ref) {
let {
graphId,
removeGraph,
mainRef,
raft,
deviceName
} = _ref;
const [addons, setAddons] = (0, _react.useState)([]);
const [, setRefreshGraphArea] = (0, _react.useState)(0);
const [refreshAddons, setRefreshAddons] = (0, _react.useState)(0);
const [refreshAddonsList, setRefreshAddonsList] = (0, _react.useState)(0);
const [autoScrollEnabled, setAutoScrollEnabled] = (0, _react.useState)(false);
const graphData = (0, _react.useRef)({});
const maxDataXValue = (0, _react.useRef)(0);
const isTracking = (0, _react.useRef)(false);
const hasSessionStarted = (0, _react.useRef)(false);
const hasStartRuleMet = (0, _react.useRef)(false);
const hasEndRuleMet = (0, _react.useRef)(false);
const startSelectedOption = (0, _react.useRef)(undefined);
const endSelectedOption = (0, _react.useRef)(undefined);
const startOptions = (0, _react.useRef)([]);
const endOptions = (0, _react.useRef)([]);
const startDisplayingTime = (0, _react.useRef)(null);
const [raftInterface, setRaftInterface] = (0, _react.useState)(null);
const chartPanelRef = (0, _react.useRef)(null);
const latestAddons = (0, _react.useRef)([]);
const reactId = (0, _react.useId)();
const statusId = "graph-status-".concat(reactId.replace(/:/g, ""));
const csvHintId = "graph-csv-hint-".concat(reactId.replace(/:/g, ""));
(0, _react.useEffect)(() => {
let interfaceInstance = null;
if ((0, _MicroBitWebBluetooth.isMicroBitDevice)(raft)) {
interfaceInstance = new _MicroBitInterface.default(raft);
} else if ((0, _MockApplicationManager.isMockRaft)(raft)) {
if (raft.type === _raft.RaftTypeE.COG) {
const mockCog = new _MockCogInterface.default();
mockCog.start();
interfaceInstance = mockCog;
} else {
const mockMarty = new _MockMartyInterface.default();
mockMarty.start();
interfaceInstance = mockMarty;
}
} else if (raft.type === _raft.RaftTypeE.COG) {
interfaceInstance = new _CogInterface.default(raft);
} else if (raft.type === _raft.RaftTypeE.MARTY) {
interfaceInstance = new _MartyInterface.MartyInterface(raft);
}
setRaftInterface(interfaceInstance);
return () => {
var _interfaceInstance;
(_interfaceInstance = interfaceInstance) === null || _interfaceInstance === void 0 ? void 0 : _interfaceInstance.unsubscribeFromPublishedData();
};
}, [raft]);
const cleanupStartEndRules = (0, _react.useCallback)(() => {
var _startSelectedOption$, _startSelectedOption$2, _endSelectedOption$cu, _endSelectedOption$cu2;
(_startSelectedOption$ = startSelectedOption.current) === null || _startSelectedOption$ === void 0 ? void 0 : (_startSelectedOption$2 = _startSelectedOption$[4]) === null || _startSelectedOption$2 === void 0 ? void 0 : _startSelectedOption$2.call(_startSelectedOption$); // cleanup rule
(_endSelectedOption$cu = endSelectedOption.current) === null || _endSelectedOption$cu === void 0 ? void 0 : (_endSelectedOption$cu2 = _endSelectedOption$cu[4]) === null || _endSelectedOption$cu2 === void 0 ? void 0 : _endSelectedOption$cu2.call(_endSelectedOption$cu); // cleanup rule
}, []);
const onSelectAddon = (0, _react.useCallback)(() => {
if (!hasSelectedSignal(latestAddons.current)) {
isTracking.current = false;
}
setRefreshAddons(oldVal => oldVal + 1);
}, []);
const addSelectedListener = (0, _react.useCallback)(addonsArr => {
for (const addon of addonsArr) {
for (const addonInput of addon.addonInputs) {
addonInput.setSelectedListener(onSelectAddon);
}
}
}, [onSelectAddon]);
const onAddonChange = (0, _react.useCallback)(evt => {
/* for the following if's, order matters */
if (!isTracking.current) return;
if (!startSelectedOption.current || !endSelectedOption.current) return;
// decide whether or not to start based on start/end rules
if (!hasStartRuleMet.current && !(0, _decider.isRuleMet)(evt, startSelectedOption.current)) return;
hasStartRuleMet.current = true;
if (hasEndRuleMet.current || (0, _decider.isRuleMet)(evt, endSelectedOption.current)) {
hasEndRuleMet.current = true;
isTracking.current = false;
setRefreshGraphArea(old => old + 1);
return;
}
if (!startDisplayingTime.current) {
startDisplayingTime.current = new Date().getTime();
}
maxDataXValue.current = getMaxDataXValue(graphData.current);
const traceId = "".concat(evt.whoAmI, "=>").concat(evt.addonInput);
const newTimestamp = (new Date().getTime() - startDisplayingTime.current) / 1000;
if (graphData.current.hasOwnProperty(traceId)) {
graphData.current[traceId].x.push(+newTimestamp.toFixed(2));
graphData.current[traceId].y.push(evt.value);
} else {
graphData.current[traceId] = {
x: [+newTimestamp.toFixed(2)],
y: [evt.value]
};
}
setRefreshGraphArea(old => old + 1);
}, []);
const addAddonsListeners = (0, _react.useCallback)(addns => {
for (const addon of addns) {
for (const addonInput of addon.addonInputs) {
if (addonInput.selected) {
raftInterface === null || raftInterface === void 0 ? void 0 : raftInterface.addEventListener("on".concat(addon.whoAmI + "=>" + addonInput.name, "Change"), graphId, onAddonChange);
} else {
raftInterface === null || raftInterface === void 0 ? void 0 : raftInterface.removeEventListener("on".concat(addon.whoAmI + "=>" + addonInput.name, "Change"), graphId, onAddonChange);
}
}
}
}, [raftInterface, graphId, onAddonChange]);
const removeAddonsListeners = (0, _react.useCallback)(addns => {
for (const addon of addns) {
for (const addonInput of addon.addonInputs) {
raftInterface === null || raftInterface === void 0 ? void 0 : raftInterface.removeEventListener("on".concat(addon.whoAmI + "=>" + addonInput.name, "Change"), graphId, onAddonChange);
}
}
}, [raftInterface, graphId, onAddonChange]);
const onAddonsChange = (0, _react.useCallback)(() => {
setRefreshAddons(oldV => oldV + 1);
setRefreshAddonsList(oldV => oldV + 1);
}, []);
(0, _react.useEffect)(() => {
raftInterface === null || raftInterface === void 0 ? void 0 : raftInterface.addEventListener("onAddonsChange", "", onAddonsChange);
return () => {
raftInterface === null || raftInterface === void 0 ? void 0 : raftInterface.removeEventListener("onAddonsChange", "", onAddonsChange);
};
}, [raftInterface, onAddonsChange]);
(0, _react.useEffect)(() => {
if (!raftInterface) return;
const normalisedAddons = hasAddonProvider(raftInterface) ? raftInterface.getAvailableAddons() : (0, _getAddonsList.default)(raft);
const addonsWithSelections = (0, _addonSelection.preserveAddonSelections)(normalisedAddons, latestAddons.current);
addSelectedListener(addonsWithSelections);
latestAddons.current = addonsWithSelections;
setAddons(addonsWithSelections);
}, [addSelectedListener, refreshAddonsList, raftInterface, raft]);
(0, _react.useEffect)(() => {
removeAddonsListeners(addons);
addAddonsListeners(addons);
const startOpts = (0, _startEndOptions.getRuleOptions)(addons, "start");
startOptions.current = startOpts;
startSelectedOption.current = startOpts.filter(opt => opt[1] === "default")[0];
const endOpts = (0, _startEndOptions.getRuleOptions)(addons, "end");
endOptions.current = endOpts;
endSelectedOption.current = endOpts.filter(opt => opt[1] === "default")[0];
setRefreshGraphArea(oldV => oldV + 1);
return () => {
removeAddonsListeners(addons);
cleanupStartEndRules();
};
}, [addAddonsListeners, addons, cleanupStartEndRules, refreshAddons, removeAddonsListeners]);
const stopHandler = (0, _react.useCallback)(() => {
isTracking.current = false;
hasSessionStarted.current = false;
for (const traceKey in graphData.current) {
delete graphData.current[traceKey];
}
maxDataXValue.current = 0;
hasStartRuleMet.current = false;
cleanupStartEndRules();
hasEndRuleMet.current = false;
startDisplayingTime.current = null;
setRefreshGraphArea(old => old + 1);
}, [cleanupStartEndRules]);
const setIsTracking = (0, _react.useCallback)(value => {
if (value && (!hasSelectedSignal(addons) || hasEndRuleMet.current)) return;
if (value) {
hasSessionStarted.current = true;
}
isTracking.current = value;
setRefreshGraphArea(old => old + 1);
}, [addons]);
const startHandler = (0, _react.useCallback)(() => {
setIsTracking(true);
}, [setIsTracking]);
const pauseHandler = (0, _react.useCallback)(() => {
setIsTracking(false);
}, [setIsTracking]);
const autoScrollHandler = (0, _react.useCallback)(() => {
setAutoScrollEnabled(oldValue => !oldValue);
}, []);
const onRuleOptionChange = (0, _react.useCallback)((selectedOption, rule) => {
if (isTracking.current) return; // changing rule won't affect graph if it's currently traking
cleanupStartEndRules();
if (rule === "start") {
startSelectedOption.current = selectedOption;
} else {
endSelectedOption.current = selectedOption;
}
setRefreshGraphArea(old => old + 1);
}, [cleanupStartEndRules]);
const exportCsvHandler = (0, _react.useCallback)(async () => {
const preparedCsvData = await (0, _exportCsv.prepareCSVData)(graphData.current);
const titles = (0, _exportCsv.prepareTitles)(graphData.current, "Time (s)");
const csvContent = [titles, ...preparedCsvData].map(row => row.map(value => "\"".concat(String(value).replace(/"/g, '""'), "\"")).join(",")).join("\n");
const blob = new Blob(["\uFEFF", csvContent], {
type: "text/csv;charset=utf-8"
});
const url = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = url;
downloadLink.download = (0, _exportCsv.getCSVTitle)(graphData.current);
document.body.appendChild(downloadLink);
downloadLink.click();
downloadLink.remove();
window.setTimeout(() => window.URL.revokeObjectURL(url), 0);
}, []);
const currentStartSelectedOption = startSelectedOption.current;
const currentEndSelectedOption = endSelectedOption.current;
const currentStartOptions = startOptions.current;
const currentEndOptions = endOptions.current;
const selectedSignalCount = getSelectedSignalCount(addons);
const hasData = hasGraphData(graphData.current);
const recordingState = selectedSignalCount === 0 ? "choose-signals" : isTracking.current ? "recording" : hasSessionStarted.current ? "paused" : "ready";
const statusLabel = {
"choose-signals": "Choose signals",
ready: "Ready",
recording: "Recording",
paused: "Paused"
}[recordingState];
const statusBadgeClassName = [_stylesModule.default.statusBadge, recordingState === "recording" ? _stylesModule.default.statusLive : recordingState === "ready" ? _stylesModule.default.statusReady : recordingState === "paused" ? _stylesModule.default.statusPaused : _stylesModule.default.statusChoose].join(" ");
const emptyChartMessage = recordingState === "choose-signals" ? "Choose at least one signal to begin." : recordingState === "ready" ? "Ready when you are. Press Record to start plotting data." : recordingState === "recording" ? "Waiting for the first sensor reading…" : "No readings have been captured yet. Resume when you’re ready.";
const graphControlsProps = {
onClickPlay: startHandler,
onClickPause: pauseHandler,
onClickStop: stopHandler,
onAutoScrollToggle: autoScrollHandler,
autoScrollEnabled,
onStartOptionChange: onRuleOptionChange,
onEndOptionChange: onRuleOptionChange,
startSelectedOption: currentStartSelectedOption,
endSelectedOption: currentEndSelectedOption,
startOptions: currentStartOptions,
endOptions: currentEndOptions,
recordingState,
hasData,
endConditionReached: hasEndRuleMet.current
};
return /*#__PURE__*/(0, _jsxRuntime.jsxs)("section", {
className: _stylesModule.default.graphCard,
"aria-label": "Sensor graph for ".concat(deviceName),
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("header", {
className: _stylesModule.default.graphHeader,
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
className: _stylesModule.default.graphHeading,
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("p", {
className: _stylesModule.default.graphEyebrow,
children: "Live data"
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)("h2", {
className: _stylesModule.default.graphTitle,
children: ["Sensor graph \u2014 ", deviceName]
})]
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
className: _stylesModule.default.graphHeaderActions,
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
className: statusBadgeClassName,
id: statusId,
children: statusLabel
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_Tooltip.default, {
title: hasData ? "Export recorded data to CSV" : "Record some data before exporting a CSV",
children: /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
className: _stylesModule.default.iconButtonWrapper,
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)("button", {
type: "button",
onClick: exportCsvHandler,
className: _stylesModule.default.iconButton,
disabled: !hasData,
"aria-describedby": !hasData ? csvHintId : undefined,
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_fa.FaFileCsv, {
"aria-hidden": "true"
}), /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
children: "CSV"
})]
})
})
}), !hasData && /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
className: _stylesModule.default.visuallyHidden,
id: csvHintId,
children: "CSV export becomes available after sensor data has been recorded."
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_Tooltip.default, {
title: "Close graph",
children: /*#__PURE__*/(0, _jsxRuntime.jsx)("button", {
type: "button",
className: _stylesModule.default.iconButton,
onClick: () => removeGraph(graphId),
"aria-label": "Close graph",
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_fa.FaTimes, {
"aria-hidden": "true"
})
})
})]
})]
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
className: _stylesModule.default.graphBody,
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
className: _stylesModule.default.chartPanel,
ref: chartPanelRef,
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_GraphControls.default, _objectSpread(_objectSpread({}, graphControlsProps), {}, {
mode: "primary"
})), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
className: _stylesModule.default.chartFrame,
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_Graph.default, {
mainRef: mainRef,
data: graphData.current,
maxDataXValue: maxDataXValue.current,
autoScrollEnabled: autoScrollEnabled,
containerRef: chartPanelRef
}), !hasData && /*#__PURE__*/(0, _jsxRuntime.jsx)("p", {
className: _stylesModule.default.emptyChartMessage,
children: emptyChartMessage
})]
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_GraphControls.default, _objectSpread(_objectSpread({}, graphControlsProps), {}, {
mode: "advanced"
}))]
}), /*#__PURE__*/(0, _jsxRuntime.jsxs)("aside", {
className: _stylesModule.default.sidebar,
"aria-labelledby": "".concat(statusId, "-signals"),
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
className: _stylesModule.default.sidebarHeader,
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("h3", {
id: "".concat(statusId, "-signals"),
children: "Select signals"
}), /*#__PURE__*/(0, _jsxRuntime.jsx)("p", {
children: "Choose the data streams you want to visualise in this graph."
})]
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_AddonsList.default, {
addons: addons
})]
})]
})]
});
}
const getMaxDataXValue = data => {
let mxDataXValue = 1;
for (const traceKey in data) {
const currLen = data[traceKey].x.length;
if (data[traceKey].x[currLen - 1] > mxDataXValue) mxDataXValue = data[traceKey].x[currLen - 1];
}
return mxDataXValue;
};
const getSelectedSignalCount = addons => addons.reduce((count, addon) => count + addon.addonInputs.reduce((addonCount, input) => addonCount + (input.selected ? 1 : 0), 0), 0);
const hasSelectedSignal = addons => getSelectedSignalCount(addons) > 0;
const hasGraphData = data => Object.values(data).some(trace => trace.x.length > 0);
var _default = exports.default = GraphArea;