component page graphs now render

This commit is contained in:
Carter 2025-02-24 11:46:16 -06:00
parent e5bc90e00e
commit c62bfbf916
10 changed files with 908 additions and 38 deletions

View file

@ -1,9 +1,8 @@
import { Box, Button, useTheme } from "@material-ui/core"; import { Box, Button, useTheme } from "@mui/material";
import MaterialChartTooltip from "charts/MaterialChartTooltip"; import MaterialChartTooltip from "charts/MaterialChartTooltip";
import moment from "moment"; import moment from "moment";
import { MeasurementDescriber } from "pbHelpers/MeasurementDescriber"; import { MeasurementDescriber } from "pbHelpers/MeasurementDescriber";
import { useGlobalState } from "providers"; import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { import {
Legend, Legend,
Line, Line,
@ -67,7 +66,6 @@ export default function MultiLineGraph(props: Props) {
const [refLeft, setRefLeft] = useState<number | undefined>(); const [refLeft, setRefLeft] = useState<number | undefined>();
const [refRight, setRefRight] = useState<number | undefined>(); const [refRight, setRefRight] = useState<number | undefined>();
const [xDomain, setXDomain] = useState<string[] | number[]>(["dataMin", "dataMax"]); const [xDomain, setXDomain] = useState<string[] | number[]>(["dataMin", "dataMax"]);
const [{ newStructure }] = useGlobalState();
useEffect(() => { useEffect(() => {
if (newXDomain) { if (newXDomain) {
@ -210,8 +208,7 @@ export default function MultiLineGraph(props: Props) {
{...props} {...props}
valueFormatter={value => { valueFormatter={value => {
return describer.convertWithUnits( return describer.convertWithUnits(
roundTo(parseFloat(String(value)), 2), roundTo(parseFloat(String(value)), 2)
newStructure
); );
}} }}
/> />

View file

@ -0,0 +1,94 @@
import { Chip, Theme, Tooltip } from "@mui/material";
import { Schedule } from "@mui/icons-material";
import moment from "moment";
import { useEffect, useState } from "react";
import { makeStyles } from "@mui/styles";
import { yellow } from "@mui/material/colors";
const useStyles = makeStyles((theme: Theme) => {
return ({
notReporting: {
color: theme.palette.text.secondary
},
reporting: {
color: yellow["900"]
}
})
});
interface Props {
lastMeasurement: string;
periodMs: number;
size?: "small" | "medium";
}
export function NextMeasurementChip(props: Props) {
const { lastMeasurement, periodMs, size } = props;
const classes = useStyles();
const [countdown, setCountdown] = useState(0);
useEffect(() => {
let timer = setInterval(() => {
if (countdown > 0) {
setCountdown(countdown - 1);
} else {
const now = moment();
let last = moment(lastMeasurement !== "" ? lastMeasurement : 0);
let diff = now.diff(last, "seconds");
const periodS = periodMs / 1000.0;
let rem = periodS - diff;
if (rem < 0) rem = 0;
setCountdown(rem);
}
}, 1000);
return () => clearInterval(timer);
}, [countdown, lastMeasurement, periodMs]);
const getTooltip = () => {
let tooltip = "Not reporting measurements";
if (periodMs > 0) {
if (periodMs < 60000) {
tooltip =
"Reports a measurement every " + moment.duration(periodMs, "ms").asSeconds() + " seconds";
} else {
tooltip =
"Reports a measurement every " +
moment
.duration(periodMs, "ms")
.humanize()
.replace("an ", "");
}
}
return tooltip;
};
const getLabel = () => {
let label = "Not reporting";
if (periodMs > 0) {
if (countdown < 5) {
label = "Measuring soon";
} else if (countdown <= 60) {
label = "Measuring in a minute";
} else {
label =
"Measuring " +
moment()
.utc()
.to(moment.utc().add(countdown, "s"));
}
}
return label;
};
return (
<Tooltip title={getTooltip()}>
<Chip
variant="outlined"
label={getLabel()}
icon={<Schedule className={periodMs > 0 ? classes.reporting : classes.notReporting} />}
size={size}
/>
</Tooltip>
);
}

View file

@ -4,16 +4,18 @@ import {
DialogContent, DialogContent,
Grid, Grid,
TextField, TextField,
Theme,
Typography Typography
} from "@material-ui/core"; } from "@mui/material";
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
import moment, { Moment } from "moment"; import moment, { Moment } from "moment";
import { DateRange as DateIcon } from "@material-ui/icons"; import { DateRange as DateIcon } from "@mui/icons-material";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import ResponsiveDialog from "common/ResponsiveDialog"; import ResponsiveDialog from "common/ResponsiveDialog";
import { DateRange, DateRangeDelimiter, StaticDateRangePicker } from "@material-ui/pickers"; // import { DateRange, DateRangeDelimiter, StaticDateRangePicker } from "@material-ui/pickers";
import { DateRangePreset, GetDefaultDateRange, SetDefaultPreset } from "./DateRange"; import { DateRangePreset, GetDefaultDateRange, SetDefaultPreset } from "./DateRange";
import { useThemeType } from "hooks"; import { useThemeType } from "hooks";
import { makeStyles } from "@mui/styles";
import dayjs from "dayjs";
interface Props { interface Props {
startDate: Moment; startDate: Moment;
@ -21,8 +23,11 @@ interface Props {
updateDateRange: (start: Moment, end: Moment, live: boolean) => void; updateDateRange: (start: Moment, end: Moment, live: boolean) => void;
} }
const useStyles = makeStyles((theme: Theme) => // Define a type similar to DateRange<Moment>
createStyles({ type DateRange<T> = [T | null, T | null];
const useStyles = makeStyles((_theme: Theme) => {
return ({
activeButtonLight: { activeButtonLight: {
border: "1px solid black" border: "1px solid black"
}, },
@ -30,7 +35,7 @@ const useStyles = makeStyles((theme: Theme) =>
border: "1px solid white" border: "1px solid white"
} }
}) })
); });
export default function TimeBar(props: Props) { export default function TimeBar(props: Props) {
const { updateDateRange, startDate, endDate } = props; const { updateDateRange, startDate, endDate } = props;
@ -83,7 +88,7 @@ export default function TimeBar(props: Props) {
onClose={() => setDateRangeDialog(false)} onClose={() => setDateRangeDialog(false)}
aria-labelledby="date-range-dialog"> aria-labelledby="date-range-dialog">
<DialogContent> <DialogContent>
<StaticDateRangePicker {/* <StaticDateRangePicker
disableFuture disableFuture
value={dateRange} value={dateRange}
onChange={date => setDateRange(date)} onChange={date => setDateRange(date)}
@ -94,7 +99,8 @@ export default function TimeBar(props: Props) {
<TextField {...endProps} /> <TextField {...endProps} />
</React.Fragment> </React.Fragment>
)} )}
/> /> */}
DATE PICKER GOES HERE
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setDateRangeDialog(false)} color="primary"> <Button onClick={() => setDateRangeDialog(false)} color="primary">

View file

@ -273,8 +273,6 @@ export default function ComponentSettings(props: Props) {
const hasAvailablePositions = (component: Component) => { const hasAvailablePositions = (component: Component) => {
// return true // TODO: actually fix this // return true // TODO: actually fix this
console.log(component.settings.addressType)
console.log(quack.AddressType.ADDRESS_TYPE_INCREMENTAL)
if (component.settings.addressType === quack.AddressType.ADDRESS_TYPE_INCREMENTAL) { if (component.settings.addressType === quack.AddressType.ADDRESS_TYPE_INCREMENTAL) {
return true; return true;
} }

View file

@ -1,20 +1,18 @@
import { import {
Chip, Chip,
Tooltip, Tooltip,
makeStyles,
createStyles,
Theme, Theme,
Avatar, Avatar,
useTheme useTheme,
} from "@material-ui/core"; } from "@mui/material";
import { purple } from "@material-ui/core/colors";
import DefaultIcon from "@material-ui/icons/Memory";
import { getDescription, GetComponentIcon } from "pbHelpers/ComponentType"; import { getDescription, GetComponentIcon } from "pbHelpers/ComponentType";
import React from "react";
import { Component } from "models"; import { Component } from "models";
import { purple } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import { Memory as DefaultIcon } from "@mui/icons-material"
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) => {
createStyles({ return ({
icon: { icon: {
color: purple["500"] color: purple["500"]
}, },
@ -23,7 +21,7 @@ const useStyles = makeStyles((theme: Theme) =>
width: theme.spacing(2.5) width: theme.spacing(2.5)
} }
}) })
); });
interface Props { interface Props {
component: Component; component: Component;
@ -36,7 +34,7 @@ export default function ComponentTypeChip(props: Props) {
const classes = useStyles(); const classes = useStyles();
const icon = () => { const icon = () => {
let componentIcon = GetComponentIcon(type, subtype, theme.palette.type); let componentIcon = GetComponentIcon(type, subtype, theme.palette.mode);
return componentIcon ? ( return componentIcon ? (
<Avatar <Avatar
variant="square" variant="square"

View file

@ -1,4 +1,10 @@
import { Box, Grid, Typography, makeStyles, createStyles, Theme } from "@material-ui/core"; import {
Box,
Grid,
Typography,
Theme
} from "@mui/material";
import { makeStyles } from "@mui/styles";
import { Component } from "models"; import { Component } from "models";
import moment from "moment"; import moment from "moment";
import { getMeasurementSummary, Summary } from "pbHelpers/ComponentType"; import { getMeasurementSummary, Summary } from "pbHelpers/ComponentType";
@ -16,8 +22,8 @@ interface Props {
omitTime?: boolean; omitTime?: boolean;
} }
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) => {
createStyles({ return ({
"@keyframes ripple": { "@keyframes ripple": {
from: { from: {
transform: "scale(.8)", transform: "scale(.8)",
@ -39,7 +45,7 @@ const useStyles = makeStyles((theme: Theme) =>
backgroundColor: "transparent" backgroundColor: "transparent"
} }
}) })
); });
//TODO: deprecated as unit measurements use a new measurement summary file //TODO: deprecated as unit measurements use a new measurement summary file
export default function MeasurementSummary(props: Props) { export default function MeasurementSummary(props: Props) {
@ -148,7 +154,7 @@ export default function MeasurementSummary(props: Props) {
const blacklist: Array<any> = []; const blacklist: Array<any> = [];
const filteredSummaries = summaries.filter((item: any) => !blacklist.includes(item.label)); const filteredSummaries = summaries.filter((item: any) => !blacklist.includes(item.label));
return ( return (
<Grid container direction="row" justify={centered ? "center" : "flex-start"} spacing={1}> <Grid container direction="row" justifyContent={centered ? "center" : "flex-start"} spacing={1}>
{filteredSummaries.map((summary: Summary, i: number) => { {filteredSummaries.map((summary: Summary, i: number) => {
let sumVal = summary.value; let sumVal = summary.value;
if (component.settings.type === quack.ComponentType.COMPONENT_TYPE_EDGE_TRIGGERED) { if (component.settings.type === quack.ComponentType.COMPONENT_TYPE_EDGE_TRIGGERED) {

View file

@ -0,0 +1,87 @@
import { Chip, Tooltip } from "@mui/material";
import { Link as LinkIcon } from "@mui/icons-material";
import React from "react";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { Component, Interaction } from "models";
// const styles = (theme: Theme) => ({
// accepted: {
// color: amber["300"]
// },
// pending: {
// color: theme.palette.text.secondary
// }
// });
interface Props {
interaction: Interaction;
component: Component;
components: Component[];
onClick: Function;
}
interface State {}
class InteractionChip extends React.Component<Props, State> {
label = () => {
const { interaction, component, components } = this.props;
let ourID = JSON.stringify(
quack.ComponentID.create({
type: component.settings.type,
addressType: component.settings.addressType,
address: component.settings.address
})
);
if (component.settings.address === 0) {
ourID = JSON.stringify(
quack.ComponentID.create({
type: component.settings.type,
addressType: component.settings.addressType
})
);
}
let sourceID = JSON.stringify((interaction.settings as pond.InteractionSettings).source);
let sinkID = JSON.stringify((interaction.settings as pond.InteractionSettings).sink);
for (let i = 0; i < components.length; i++) {
let c = components[i];
let otherID = JSON.stringify(
quack.ComponentID.create({
type: c.settings.type,
addressType: c.settings.addressType,
address: c.settings.address
})
);
if (c.settings.address === 0) {
otherID = JSON.stringify(
quack.ComponentID.create({
type: c.settings.type,
addressType: c.settings.addressType
})
);
}
if (ourID === otherID) {
continue;
}
if (sourceID === otherID || sinkID === otherID) {
return c.name();
}
}
return "Cloud";
};
render() {
const { /*classes,*/ interaction, onClick } = this.props;
const label = this.label();
const pending = !interaction.status.synced;
// const iconClass = pending ? classes.pending : classes.accepted;
const title = pending ? "Pending interaction with " + label : "Interacting with " + label;
return (
<Tooltip title={title}>
<Chip onClick={() => onClick()} label={label} icon={<LinkIcon /*className={iconClass}*/ />} />
</Tooltip>
);
}
}
export default InteractionChip;

View file

@ -13,6 +13,7 @@ import DevicePage from "pages/Device";
import GroupsPage from "pages/Groups"; import GroupsPage from "pages/Groups";
import GroupPage from "pages/Group"; import GroupPage from "pages/Group";
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
import DeviceComponent from "pages/DeviceComponent";
const DeviceHistory = lazy(() => import("pages/DeviceHistory")); const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
@ -72,6 +73,10 @@ export default function Router(props: Props) {
path="/:deviceID" // "/settings/basic" path="/:deviceID" // "/settings/basic"
element={<DevicePage />} element={<DevicePage />}
/> />
<Route
path="/:deviceID/components/:componentID" // "/settings/basic"
element={<DeviceComponent />}
/>
<Route <Route
path="/:deviceID/history" // "/settings/basic" path="/:deviceID/history" // "/settings/basic"
element={<DeviceHistory />} element={<DeviceHistory />}

View file

@ -73,7 +73,6 @@ export default function DevicePage() {
resp.data.components.forEach(comp => { resp.data.components.forEach(comp => {
newComps.push(Component.create(comp)) newComps.push(Component.create(comp))
}) })
console.log(resp.data.components)
let available = FindAvailablePositions( let available = FindAvailablePositions(
newComps, newComps,
device.settings.product device.settings.product
@ -139,10 +138,6 @@ export default function DevicePage() {
.finally(() => setLoading(false)); .finally(() => setLoading(false));
} }
useEffect(() => {
console.log(components)
}, [components])
useEffect(() => { useEffect(() => {
loadDevice() loadDevice()
}, [deviceID]) }, [deviceID])

View file

@ -0,0 +1,684 @@
import { Box, Grid2 as Grid, Skeleton, Theme, Tooltip } from "@mui/material";
import { makeStyles } from "@mui/styles";
import { NextMeasurementChip } from "common/NextMeasurementChip";
import ResponsiveTable from "common/ResponsiveTable";
// import { getTableIcons } from "common/ResponsiveTable";
import SmartBreadcrumb from "common/SmartBreadcrumb";
import StatusChip from "common/StatusChip";
import { GetDefaultDateRange } from "common/time/DateRange";
import ComponentActions from "component/ComponentActions";
import ComponentChart from "component/ComponentChart";
import ComponentTypeChip from "component/ComponentTypeChip";
// import MeasurementSummary from "component/MeasurementSummary";
// import UnitMeasurementSummary from "component/UnitMeasurementSummary";
import {
useComponentAPI,
useDeviceAPI,
useGroupAPI,
useInteractionsAPI,
usePrevious,
useSnackbar,
useUserAPI
} from "hooks";
import InteractionChip from "interactions/InteractionChip";
import InteractionSettings from "interactions/InteractionSettings";
// import MaterialTable from "material-table";
import { Component, Device, deviceScope, Group, Interaction } from "models";
import { convertedUnitMeasurement, MeasurementsFor, UnitMeasurement } from "models/UnitMeasurement";
import moment, { Moment } from "moment";
// import { MatchParams } from "navigation/Routes";
import PageContainer from "pages/PageContainer";
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
import {
DeviceAvailabilityMap,
FindAvailablePositions,
OffsetAvailabilityMap
} from "pbHelpers/DeviceAvailability";
import { getDefaultInteraction } from "pbHelpers/Interaction";
import { pond } from "protobuf-ts/pond";
import { useGlobalState, useTeamAPI } from "providers";
import { useCallback, useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";
// import { useRouteMatch } from "react-router";
import { or } from "utils/types";
const useStyles = makeStyles((theme: Theme) => {
return ({
toolbar: {
marginTop: theme.spacing(1),
paddingRight: theme.spacing(1),
paddingLeft: theme.spacing(1),
[theme.breakpoints.up("sm")]: {
paddingRight: theme.spacing(1),
paddingLeft: theme.spacing(2)
}
},
toolbarTitle: {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
},
contentContainer: {
padding: theme.spacing(1),
[theme.breakpoints.up("sm")]: {
paddingRight: theme.spacing(2)
},
[theme.breakpoints.up("lg")]: {
paddingRight: theme.spacing(3)
}
},
overviewContainer: {
position: "relative",
maxWidth: "100%",
overflowX: "auto",
marginBottom: theme.spacing(2)
},
verticalSpacing: {
marginBottom: theme.spacing(2)
},
tablePagination: {
overflow: "auto"
}
})
});
export default function DeviceComponent() {
const classes = useStyles();
// const match = useRouteMatch<MatchParams>();
const deviceAPI = useDeviceAPI();
const componentAPI = useComponentAPI();
const { error } = useSnackbar();
const groupAPI = useGroupAPI();
const interactionsAPI = useInteractionsAPI();
const userAPI = useUserAPI();
const teamAPI = useTeamAPI();
// const deviceID = parseInt(match.params.deviceID, 10);
// const componentID = match.params.componentID;
// const groupID = or(parseInt(match.params.groupID, 10), undefined);
const deviceID = +(useParams<{ deviceID: string }>()?.deviceID ?? "0");
const componentID = useParams<{ componentID: string }>()?.componentID ?? "";
const groupID = +(useParams<{ groupID: string }>()?.groupID ?? "");
const [isFirstRender, setIsFirstRender] = useState<boolean>(true);
const [loadingInitial, setLoadingInitial] = useState<boolean>(false);
const defaultDateRange = GetDefaultDateRange();
const [startDate, setStartDate] = useState<Moment>(defaultDateRange.start);
const prevStartDate = usePrevious(startDate);
const [endDate, setEndDate] = useState<Moment>(defaultDateRange.end);
const prevEndDate = usePrevious(endDate);
const live = useRef(defaultDateRange.live);
const [device, setDevice] = useState<Device>(Device.create());
const [group, setGroup] = useState<Group | undefined>(Group.create());
const [component, setComponent] = useState<Component>(Component.create());
const [components, setComponents] = useState<Component[]>([]);
const [sampling, setSampling] = useState(false);
const liveSamples = useRef<pond.Measurement[]>([]);
const [samples, setSamples] = useState<pond.Measurement[]>([]);
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
const [interactions, setInteractions] = useState<Interaction[]>([]);
const [selectedInteraction, setSelectedInteraction] = useState<Interaction>(
getDefaultInteraction()
);
const [pageSize, setPageSize] = useState<number>(5);
const [page, setPage] = useState(1)
const [order, setOrder] = useState("desc")
const [rows, setRows] = useState<convertedUnitMeasurement[]>([])
const [total, setTotal] = useState<number>(0)
const prevRowsPerPage = usePrevious(pageSize);
const [availablePositions, setAvailablePositions] = useState<DeviceAvailabilityMap>(new Map());
const [availableOffsets, setAvailableOffsets] = useState<OffsetAvailabilityMap>(new Map());
const tableRef: any = useRef();
let isInteractionSettingsOpen =
JSON.stringify(or(selectedInteraction, getDefaultInteraction())) !==
JSON.stringify(getDefaultInteraction());
const [{ user, as }] = useGlobalState();
const [unitMeasurements, setUnitMeasurements] = useState<UnitMeasurement[]>([]);
const [rm, setRM] = useState<pond.Measurement>(pond.Measurement.create());
const [recentUnitMeasurement, setRecentUnitMeasurement] = useState<UnitMeasurement[]>([]);
const [recentGoodUnitMeasurement, setRecentGoodUnitMeasurement] = useState<UnitMeasurement[]>([]);
const [{ showErrors }] = useGlobalState();
const sampleLimit = 200;
const [deviceComponentPrefs, setDeviceComponentPrefs] = useState<
pond.DeviceComponentPreferences
>();
console.log("device component")
/*const getContextKeys = useCallback(() => {
if (groupID === undefined || groupID < 1) return undefined;
return [groupID.toString(), deviceID.toString()];
}, [groupID, deviceID]);
const getContextTypes = useCallback(() => {
if (groupID === undefined || groupID < 1) return undefined;
return ["group", "device"];
}, [groupID]);*/
const setDefaultState = () => {
setDevice(Device.create());
setGroup(undefined);
setInteractions([]);
setSelectedInteraction(getDefaultInteraction());
setComponents([]);
setComponent(Component.create());
setPermissions([]);
setAvailablePositions(new Map());
setAvailableOffsets(new Map());
};
const loadUnitMeasurements = useCallback(() => {
setUnitMeasurements([]);
setSampling(true);
componentAPI
.sampleUnitMeasurements(
deviceID,
componentID,
startDate,
endDate,
sampleLimit,
false,
getContextKeys(),
getContextTypes(),
showErrors
)
.then(resp => {
if (resp.data.measurements) {
let newUnitMeasurements = resp.data.measurements.map((um: any) =>
UnitMeasurement.any(um, user)
);
setUnitMeasurements([...newUnitMeasurements]);
}
})
.finally(() => {
setSampling(false);
});
}, [componentAPI, deviceID, componentID, user, startDate, endDate, showErrors]);
useEffect(() => {
if (live.current && liveSamples) {
setSamples(liveSamples.current);
} else {
loadUnitMeasurements();
}
}, [loadUnitMeasurements]);
useEffect(() => {
if (component.key() !== "") {
interactionsAPI
.listInteractionsByComponent(deviceID, component.location())
.then((rInteractions: Interaction[]) => {
setInteractions(rInteractions);
})
.catch((err: any) => {
console.log(err);
});
}
}, [component, deviceID, interactionsAPI]);
const loadState = useCallback(() => {
if (user.settings.id === "") return;
setLoadingInitial(true);
deviceAPI
.get(deviceID, false, getContextKeys().slice(0, -1), getContextTypes().slice(0, -1))
.then((response: any) => {
const rDevice = Device.any(response.data);
setDevice(rDevice);
let componentsPromise = componentAPI.list(
deviceID,
false,
getContextKeys(),
getContextTypes(),
true
);
let userPromise = userAPI.getUser(user.settings.id, deviceScope(deviceID.toString()));
let prefPromise = deviceAPI.listDeviceComponentPreferences(deviceID);
Promise.all([componentsPromise, userPromise, prefPromise])
.then((responses: any) => {
const rawComponents: Array<any> = or(responses[0].data.components, []).sort(
(a: any, b: any) => {
if (or(a.settings.name, "").toLowerCase() < or(b.settings.name, "").toLowerCase()) {
return -1;
} else if (
or(a.settings.name, "").toLowerCase() > or(b.settings.name, "").toLowerCase()
) {
return 1;
} else {
return 0;
}
}
);
let rComponents: Component[] = [];
rawComponents.forEach((rawComponent: any) => {
rComponents.push(Component.any(rawComponent));
});
setComponents(rComponents);
let rComponent = rComponents.find(c => componentID === c.key());
setComponent(rComponent ? rComponent : Component.create());
if (rComponent?.status.lastMeasurement) {
setRM(rComponent.status.lastMeasurement);
}
if (rComponent?.lastMeasurement) {
let measurements = rComponent.lastMeasurement.map(um =>
UnitMeasurement.any(um, user)
);
setRecentUnitMeasurement(measurements);
}
if (rComponent?.status?.lastGoodMeasurement) {
let measurements = rComponent.status.lastGoodMeasurement.map(um =>
UnitMeasurement.any(um, user)
);
setRecentGoodUnitMeasurement(measurements);
}
setPermissions(responses[1].permissions);
let available = FindAvailablePositions(rComponents, rDevice.settings.product);
setAvailablePositions(available.GetAvailability());
setAvailableOffsets(available.offsetAvailability);
let allPrefs = responses[2].data.preferences;
Object.keys(allPrefs).forEach(compoundKey => {
let componentKey = compoundKey.split(":")[1]
? compoundKey.split(":")[1]
: compoundKey;
if (componentID === componentKey) {
setDeviceComponentPrefs(
pond.DeviceComponentPreferences.fromObject(allPrefs[compoundKey])
);
}
});
})
.catch(err => {
console.log(err);
setDefaultState();
})
.finally(() => {
setLoadingInitial(false);
if (groupID > 0) {
groupAPI
.getGroup(groupID)
.then((response: any) => {
setGroup(Group.any(response.data));
})
.catch((error: any) => {
setGroup(undefined);
});
}
if (as) {
teamAPI.getTeam(as, deviceScope(deviceID.toString())).then(resp => {
//if (resp.permissions.length > permissions.length) {
setPermissions(resp.permissions);
//}
});
}
});
})
.catch((err: any) => {
setDefaultState();
console.log(err);
});
}, [
componentAPI,
componentID,
deviceAPI,
deviceID,
groupAPI,
groupID,
userAPI,
as,
teamAPI,
user
]);
useEffect(() => {
if (isFirstRender) {
loadState();
setIsFirstRender(false);
} else if (prevStartDate !== startDate || prevEndDate !== endDate) {
if (tableRef && tableRef.current) {
tableRef.current.onChangePage(undefined, 0);
}
}
}, [
component,
endDate,
isFirstRender,
loadState,
prevEndDate,
prevRowsPerPage,
prevStartDate,
pageSize,
startDate,
tableRef
]);
const interactionChips = (
component: Component,
interactions: Interaction[],
components: Component[]
) => {
if (!component || !interactions || !components || interactions.length <= 0) {
return null;
}
let chips: any = [];
interactions.forEach((interaction: Interaction) => {
let rawInteraction = Interaction.clone(interaction);
let key =
JSON.stringify(rawInteraction.settings.source) +
JSON.stringify(rawInteraction.settings.sink) +
rawInteraction.settings.instance;
chips.push(
<Grid key={key}>
<InteractionChip
interaction={interaction}
component={component}
components={components}
onClick={() => selectInteraction(rawInteraction)}
/>
</Grid>
);
});
return chips;
};
const status = () => {
if (loadingInitial)
return (
<Box width={1} margin={1}>
<Skeleton variant="text" width="50%" />
</Box>
);
return (
<Grid
container
spacing={1}
flex-direction="row"
justifyContent="flex-start"
alignItems="center"
wrap="nowrap"
className={classes.overviewContainer}>
{!component.status.synced && (
<Grid >
<StatusChip key="status" status="pending" />
</Grid>
)}
<Grid >
{/* TODO: use the unit measurement timestamp */}
<NextMeasurementChip
lastMeasurement={or(rm, { timestamp: "" }).timestamp}
periodMs={component.settings.reportPeriodMs}
/>
</Grid>
<Grid >
<ComponentTypeChip component={component} />
</Grid>
{interactionChips(component, interactions, components)}
</Grid>
);
};
const selectInteraction = (newInteraction: Interaction) => {
setSelectedInteraction(newInteraction);
};
// This triggers table rerender when the show errors variable is flipped
useEffect(() => {
if (tableRef && tableRef.current) {
tableRef.current.onChangePage(undefined, 0);
}
}, [showErrors, tableRef]);
const load = () => {
componentAPI.listUnitMeasurements(
deviceID,
componentID,
startDate,
endDate,
pageSize,
page * pageSize,
order,
undefined,
getContextKeys(),
getContextTypes(),
showErrors
)
.then(response => {
let total: number = response.data.total ? response.data.total : 0;
let rows = UnitMeasurement.convertMeasurements(
response.data.measurements.map(um => UnitMeasurement.any(um, user))
);
// let maxPage = total / pageSize;
// if (page > maxPage) {
// tableRef.current.isOutsidePageNumbers();
// }
setRows(rows)
setTotal(total)
// resolve({
// data: rows,
// page: page,
// totalCount: total
// });
})
.catch((err: any) => {
// resolve({
// data: [],
// page: 0,
// totalCount: 0
// });
});
}
// const unitMeasurementsTable = () => {
// interface Row {
// timestamp: string;
// measurementsFor: MeasurementsFor[];
// }
// return (
// <Grid item xs={12} className={classes.verticalSpacing}>
// <MaterialTable
// key="unitMeasurements"
// tableRef={tableRef}
// // icons={getTableIcons()}
// columns={[
// {
// title: "Timestamp",
// type: "datetime",
// defaultSort: "desc",
// render: (row: Row) => {
// return (
// <Tooltip title={moment(row.timestamp).calendar()}>
// <span>{moment(row.timestamp).fromNow()}</span>
// </Tooltip>
// );
// }
// },
// {
// title: "Measurement",
// sorting: false,
// field: "measurement",
// render: (row: Row) => (
// <UnitMeasurementSummary
// component={component}
// reading={row}
// tableCell={true}
// excludedNodes={deviceComponentPrefs?.excludedNodes}
// />
// )
// }
// ]}
// options={{
// paging: true,
// pageSize: pageSize,
// pageSizeOptions: [5, 10, 20],
// padding: "default",
// showTitle: true,
// toolbar: true,
// sorting: !live.current,
// search: false,
// headerStyle: { position: "inherit", borderBottom: "1px solid rgba(81, 81, 81, 1)" }
// }}
// localization={{
// body: { emptyDataSourceMessage: "No measurements found" }
// }}
// data={query =>
// new Promise(resolve => {
// let pageSize = query.pageSize;
// let page = query.page;
// let orderBy =
// query.orderBy && query.orderBy.field ? query.orderBy.field.toString() : "timestamp";
// componentAPI
// .listUnitMeasurements(
// deviceID,
// componentID,
// startDate,
// endDate,
// pageSize,
// page * pageSize,
// orderBy,
// undefined,
// getContextKeys(),
// getContextTypes(),
// showErrors
// )
// .then(response => {
// let total: number = response.data.total ? response.data.total : 0;
// let rows = UnitMeasurement.convertMeasurements(
// response.data.measurements.map(um => UnitMeasurement.any(um, user))
// );
// let maxPage = total / pageSize;
// if (page > maxPage) {
// tableRef.current.isOutsidePageNumbers();
// }
// resolve({
// data: rows,
// page: page,
// totalCount: total
// });
// })
// .catch((err: any) => {
// resolve({
// data: [],
// page: 0,
// totalCount: 0
// });
// });
// })
// }
// />
// </Grid>
// );
// };
const handleRowsPerPageChange = (event: any) => {
const newRowsPerPage = parseInt(event.target.value, 10); // Get selected value
setPageSize(newRowsPerPage);
setPage(0)
}
const measurementsTable = () => {
return (
<Grid size={{ xs: 12 }} className={classes.verticalSpacing}>
<ResponsiveTable
rows={rows}
total={total}
page={page}
pageSize={pageSize}
setPage={setPage}
handleRowsPerPageChange={handleRowsPerPageChange}
/>
</Grid>
);
};
return (
<PageContainer>
<Grid
container
direction="row"
justifyContent="flex-start"
alignContent="center"
className={classes.toolbar}>
<Grid
size={{ xs: 8, sm: 9 }}
container
justifyContent="flex-start"
alignContent="center"
className={classes.toolbarTitle}>
<SmartBreadcrumb
deviceName={device.name()}
componentName={component.name()}
groupName={group ? group.name() : undefined}
loading={loadingInitial}
reload={loadState}
/>
</Grid>
<Grid size={{ xs: 4, sm: 3 }} container justifyContent="flex-end" alignContent="center">
<ComponentActions
device={device}
components={components}
component={component}
permissions={permissions}
availablePositions={availablePositions}
availableOffsets={availableOffsets}
refreshCallback={loadState}
initialStartDate={startDate}
initialEndDate={endDate}
deviceComponentPreferences={deviceComponentPrefs}
/>
</Grid>
</Grid>
<Grid
container
direction="column"
justifyContent="flex-start"
alignItems="center"
className={classes.contentContainer}>
<Grid container size={{ xs: 12 }} justifyContent="flex-end">
{status()}
</Grid>
<Grid container size={{ xs: 12 }}>
{
<Grid size={{ xs: 12 }} className={classes.verticalSpacing}>
<ComponentChart
deviceKey={deviceID.toString()}
componentKey={componentID}
component={component}
interactions={interactions}
sampling={sampling}
unitMeasurements={unitMeasurements}
sampleLimit={sampleLimit}
deviceComponentPreferences={deviceComponentPrefs}
updateDate={(newStartDate, newEndDate, newLive) => {
setStartDate(newStartDate);
setEndDate(newEndDate);
live.current = !!newLive;
}}
allowLive={true}
recentUnitMeasurement={
showErrors ? recentUnitMeasurement : recentGoodUnitMeasurement
}
/>
</Grid>
}
</Grid>
<Grid container size={{ xs: 12 }}>
{/* {newStructure ? unitMeasurementsTable() : measurementsTable()} */}
{measurementsTable()}
</Grid>
</Grid>
<InteractionSettings
device={device}
components={components.map((component: Component) => Component.clone(component))}
initialComponent={component}
initialInteraction={selectedInteraction}
mode="update"
isDialogOpen={isInteractionSettingsOpen}
closeDialogCallback={() => selectInteraction(getDefaultInteraction())}
refreshCallback={loadState}
canEdit={permissions.includes(pond.Permission.PERMISSION_WRITE)}
/>
</PageContainer>
);
}