import { Box, Grid2 as Grid, Skeleton, Theme, Tooltip, Typography } from "@mui/material"; import { makeStyles } from "@mui/styles"; import { NextMeasurementChip } from "common/NextMeasurementChip"; import ResponsiveTable, { Column } from "common/ResponsiveTable"; // import { getTableIcons } from "common/ResponsiveTable"; import SmartBreadcrumb from "common/SmartBreadcrumb"; import PendingChangesChip from "common/PendingChangesChip"; import { GetDefaultDateRange } from "common/time/DateRange"; import ComponentActions from "component/ComponentActions"; import ComponentChart from "component/ComponentChart"; import ComponentTypeChip from "component/ComponentTypeChip"; import UnitMeasurementSummary from "component/UnitMeasurementSummary"; // import MeasurementSummary from "component/MeasurementSummary"; // import UnitMeasurementSummary from "component/UnitMeasurementSummary"; import { useComponentAPI, useDeviceAPI, useGroupAPI, useHTTP, useInteractionsAPI, usePendingChanges, usePrevious, useSnackbar, useUserAPI } from "hooks"; import { useWebSocket } from "hooks/useWebSocket"; import InteractionChip from "interactions/InteractionChip"; import InteractionSettings from "interactions/InteractionSettings"; import { cloneDeep } from "lodash"; // 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 } from "providers"; import { useTeamAPI } from "providers/pond/teamAPI"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useParams } from "react-router-dom"; // import { useRouteMatch } from "react-router"; import { isStaleStatusUpdate } from "utils/syncStatus"; 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(); 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(true); const [loadingInitial, setLoadingInitial] = useState(false); const defaultDateRange = GetDefaultDateRange(); const [startDate, setStartDate] = useState(defaultDateRange.start); const prevStartDate = usePrevious(startDate); const [endDate, setEndDate] = useState(defaultDateRange.end); const prevEndDate = usePrevious(endDate); const live = useRef(defaultDateRange.live); const [device, setDevice] = useState(Device.create()); const [group, setGroup] = useState(Group.create()); const [component, setComponent] = useState(Component.create()); const [components, setComponents] = useState([]); const [sampling, setSampling] = useState(false); const liveSamples = useRef([]); const [samples, setSamples] = useState([]); const [permissions, setPermissions] = useState([]); const [interactions, setInteractions] = useState([]); const [selectedInteraction, setSelectedInteraction] = useState( getDefaultInteraction() ); const [pageSize, setPageSize] = useState(5); const [page, setPage] = useState(0) const [order, setOrder] = useState("desc") const [rows, setRows] = useState([]) const [total, setTotal] = useState(0) const prevRowsPerPage = usePrevious(pageSize); const [availablePositions, setAvailablePositions] = useState(new Map()); const [availableOffsets, setAvailableOffsets] = useState(new Map()); const tableRef: any = useRef(); let isInteractionSettingsOpen = JSON.stringify(or(selectedInteraction, getDefaultInteraction())) !== JSON.stringify(getDefaultInteraction()); const [{ user, as, showErrors }] = useGlobalState(); const [unitMeasurements, setUnitMeasurements] = useState([]); const [rm, setRM] = useState(pond.Measurement.create()); const [recentUnitMeasurement, setRecentUnitMeasurement] = useState([]); const [recentGoodUnitMeasurement, setRecentGoodUnitMeasurement] = useState([]); const sampleLimit = 200; const [deviceComponentPrefs, setDeviceComponentPrefs] = useState< pond.DeviceComponentPreferences >(); const { token } = useHTTP(); const componentPending = usePendingChanges( component.status.synced, !loadingInitial && component.key() !== "" ); const liveContextKeys = useMemo(() => getContextKeys(), []); const liveContextTypes = useMemo(() => getContextTypes(), []); /** Matches the REST calls: omit ?as= when first context type is "team". */ const liveAs = as && !(liveContextTypes.length > 0 && liveContextTypes[0] === "team") ? as : undefined; // --- Real-time component updates --- // Streams component changes for this device, including status changes // when the device accepts pending component updates. useWebSocket<{ key: string; component: Component } | null>({ path: `/v1/live/devices/${deviceID}/components`, parse: (e) => { try { const raw = JSON.parse(e.data); const comp = Component.any(raw); if (!comp?.settings) { console.warn("[ws] received component without settings:", raw); return null; } return { key: comp.key(), component: comp }; } catch (err) { console.warn("[ws] failed to parse component:", err); return null; } }, onMessage: (data) => { if (!data || data.key !== componentID) return; setComponent((prev) => { if (prev.key() !== data.key) return prev; if (isStaleStatusUpdate(prev.status, data.component.status)) { return prev; } return data.component; }); }, keys: liveContextKeys, types: liveContextTypes, as: liveAs, token, enabled: !!deviceID && componentID !== "", }); 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, showErrors, as, ) .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, as]); useEffect(() => { if (live.current && liveSamples) { setSamples(liveSamples.current); } else { loadUnitMeasurements(); } }, [loadUnitMeasurements]); useEffect(() => { if (component.key() !== "") { interactionsAPI .listInteractionsByComponent(deviceID, component.location(), undefined, as) .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), as) .then((response: any) => { const rDevice = Device.any(response.data); setDevice(rDevice); let componentsPromise = componentAPI.list( deviceID, false, getContextKeys(), getContextTypes(), true, as ); let userPromise = userAPI.getUser(user.settings.id, deviceScope(deviceID.toString())); let prefPromise = deviceAPI.listDeviceComponentPreferences(deviceID, getContextKeys().slice(0, -1), getContextTypes().slice(0, -1), as); Promise.all([componentsPromise, userPromise, prefPromise]) .then((responses: any) => { const rawComponents: Array = 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?.status.measurement) { let measurements = rComponent.status.measurement.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( selectInteraction(rawInteraction)} /> ); }); return chips; }; const status = () => { if (loadingInitial) return ( ); return ( {(componentPending.pending || componentPending.accepted) && ( )} {/* TODO: use the unit measurement timestamp */} {interactionChips(component, interactions, components)} ); }; 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, showErrors, as ) .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 // }); }); } useEffect(() => { load() }, [page, pageSize, as]) const columns = (): Column[] => { return [ { title: "Timestamp", cellStyle: {width: "50%"}, render: row => { return ( {moment(row.timestamp).fromNow()} ) } }, { title: "Measurement", cellStyle: {width: "50%"}, render: row => { return ( ) } } ] } // const unitMeasurementsTable = () => { // interface Row { // timestamp: string; // measurementsFor: MeasurementsFor[]; // } // return ( // // { // return ( // // {moment(row.timestamp).fromNow()} // // ); // } // }, // { // title: "Measurement", // sorting: false, // field: "measurement", // render: (row: Row) => ( // // ) // } // ]} // 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 // }); // }); // }) // } // /> // // ); // }; const handleRowsPerPageChange = (event: any) => { const newRowsPerPage = parseInt(event.target.value, 10); // Get selected value setPageSize(newRowsPerPage); setPage(0) } const measurementsTable = () => { return ( { let currentRows = cloneDeep(rows) let newOffset = rows.length componentAPI.listUnitMeasurements( deviceID, componentID, startDate, endDate, pageSize, newOffset, order, undefined, getContextKeys(), getContextTypes(), showErrors, showErrors, as) .then(resp => { let newRows = UnitMeasurement.convertMeasurements( resp.data.measurements.map(um => UnitMeasurement.any(um, user)) ) setRows(currentRows.concat(newRows)) }).catch(err => { }) }} /> ); }; return ( {status()} { { setStartDate(newStartDate); setEndDate(newEndDate); live.current = !!newLive; }} allowLive={true} recentUnitMeasurement={ showErrors ? recentUnitMeasurement : recentGoodUnitMeasurement } /> } {/* {newStructure ? unitMeasurementsTable() : measurementsTable()} */} {measurementsTable()} Component.clone(component))} initialComponent={component} initialInteraction={selectedInteraction} mode="update" isDialogOpen={isInteractionSettingsOpen} closeDialogCallback={() => selectInteraction(getDefaultInteraction())} refreshCallback={loadState} canEdit={permissions.includes(pond.Permission.PERMISSION_WRITE)} /> ); }