Merge branch 'staging_environment' into get_permissions_fix
This commit is contained in:
commit
777089e8c4
65 changed files with 1144 additions and 1773 deletions
|
|
@ -63,7 +63,7 @@ import {
|
|||
} from "providers";
|
||||
import React, { SetStateAction, useCallback, useEffect, useState } from "react";
|
||||
// import { getThemeType } from "theme";
|
||||
import { getGrainUnit, stringToMaterialColour } from "utils";
|
||||
import { stringToMaterialColour } from "utils";
|
||||
//import InfiniteScroll from "react-infinite-scroller";
|
||||
import PageContainer from "./PageContainer";
|
||||
import ObjectTable from "objects/ObjectTable";
|
||||
|
|
@ -934,7 +934,7 @@ export default function Bins(props: Props) {
|
|||
return " bu";
|
||||
}
|
||||
|
||||
switch (getGrainUnit()) {
|
||||
switch (user.grainUnit()) {
|
||||
case pond.GrainUnit.GRAIN_UNIT_TONNE:
|
||||
return " mT";
|
||||
case pond.GrainUnit.GRAIN_UNIT_TON:
|
||||
|
|
@ -947,9 +947,9 @@ export default function Bins(props: Props) {
|
|||
const customQuantityDisplay = (customInventory: pond.CustomInventory, bushels: number) => {
|
||||
let amount = bushels
|
||||
if(customInventory.bushelsPerTonne > 1){
|
||||
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
|
||||
if(user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
|
||||
amount = bushels/customInventory.bushelsPerTonne
|
||||
}else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
|
||||
}else if (user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
|
||||
amount = bushels/(customInventory.bushelsPerTonne*0.907)
|
||||
}
|
||||
}
|
||||
|
|
@ -959,9 +959,9 @@ export default function Bins(props: Props) {
|
|||
const supportedGrainDisplay = (grain: pond.Grain, bushels: number) => {
|
||||
const describer = GrainDescriber(grain)
|
||||
let amount = bushels
|
||||
if(getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
|
||||
if(user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TONNE){
|
||||
amount = bushels/describer.bushelsPerTonne
|
||||
}else if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
|
||||
}else if (user.grainUnit() === pond.GrainUnit.GRAIN_UNIT_TON){
|
||||
amount = bushels/describer.bushelsPerTon
|
||||
}
|
||||
return Math.round(amount*100)/100
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import PageContainer from "./PageContainer";
|
|||
|
||||
export default function Contracts() {
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
const [{ as }] = useGlobalState()
|
||||
const [{ as, user }] = useGlobalState()
|
||||
const contractAPI = useContractAPI();
|
||||
const [contracts, setContracts] = useState<Contract[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -42,7 +42,7 @@ export default function Contracts() {
|
|||
let contractPermissions: Map<string, pond.Permission[]> = new Map();
|
||||
if (resp.data.contracts){
|
||||
resp.data.contracts.forEach(contract => {
|
||||
let c = Contract.create(contract);
|
||||
let c = Contract.create(contract, user);
|
||||
contracts.push(c);
|
||||
let p = pond.EvaluatePermissionsResponse.fromObject(
|
||||
resp.data.contractPermissions[c.key()]
|
||||
|
|
@ -56,7 +56,7 @@ export default function Contracts() {
|
|||
})
|
||||
.catch(err => {});
|
||||
}
|
||||
}, [contractAPI, year, as]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [contractAPI, year, as, user]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import Grid from '@mui/material/Grid2';
|
|||
import { Component, Device, Interaction, User } from "models";
|
||||
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
|
||||
import { useDeviceAPI, useGlobalState, useSnackbar } from "providers";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDeviceStatusStreams } from "hooks/useDeviceStatusStreams";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import PageContainer from "./PageContainer";
|
||||
import { useHTTP, useMobile } from "hooks";
|
||||
|
|
@ -23,6 +24,25 @@ import DeviceWizard from "device/DeviceWizard";
|
|||
import DeviceScannedComponents from "device/autoDetect/DeviceScannedComponents";
|
||||
import { useWebSocket } from "hooks/useWebSocket";
|
||||
|
||||
type TimestampedReading = { timestamp?: string };
|
||||
|
||||
function getTimestampMillis(timestamp?: string): number | undefined {
|
||||
if (!timestamp) return undefined;
|
||||
const parsed = Date.parse(timestamp);
|
||||
return Number.isNaN(parsed) ? undefined : parsed;
|
||||
}
|
||||
|
||||
function pickNewestReading<T extends TimestampedReading>(
|
||||
current: T | null | undefined,
|
||||
incoming: T | null | undefined
|
||||
): T | null | undefined {
|
||||
const currentTs = getTimestampMillis(current?.timestamp);
|
||||
const incomingTs = getTimestampMillis(incoming?.timestamp);
|
||||
if (incomingTs === undefined) return current ?? incoming;
|
||||
if (currentTs === undefined) return incoming ?? current;
|
||||
return incomingTs >= currentTs ? incoming : current;
|
||||
}
|
||||
|
||||
export interface DevicePageData {
|
||||
device: Device;
|
||||
components: Component[];
|
||||
|
|
@ -38,7 +58,7 @@ export default function DevicePage() {
|
|||
const isMobile = useMobile()
|
||||
const deviceID = useParams<{ deviceID: string }>()?.deviceID ?? "";
|
||||
const groupID = useParams<{ groupID: string }>()?.groupID ?? "";
|
||||
const { state } = useLocation();
|
||||
const { state, pathname: locationPathname } = useLocation();
|
||||
const [{ as, team, user }] = useGlobalState()
|
||||
// console.log(state)
|
||||
const [device, setDevice] = useState<Device>(state?.device ? Device.create(state.device) : Device.create())
|
||||
|
|
@ -68,6 +88,63 @@ export default function DevicePage() {
|
|||
|
||||
const { token } = useHTTP();
|
||||
|
||||
const liveContextKeys = useMemo(() => getContextKeys(), [locationPathname]);
|
||||
const liveContextTypes = useMemo(() => getContextTypes(), [locationPathname]);
|
||||
/** Matches getDevicePageData: omit ?as= when first context type is "team". */
|
||||
const liveAs =
|
||||
as && !(liveContextTypes.length > 0 && liveContextTypes[0] === "team")
|
||||
? as
|
||||
: undefined;
|
||||
|
||||
const liveStatusDeviceIds = useMemo(
|
||||
() => (deviceID ? [deviceID] : []),
|
||||
[deviceID]
|
||||
);
|
||||
|
||||
const handleLiveDeviceStatus = useCallback(
|
||||
(streamDeviceId: string, status: pond.DeviceStatus) => {
|
||||
setDevice((prev) => {
|
||||
if (prev.id().toString() !== streamDeviceId) return prev;
|
||||
const updated = Device.clone(prev);
|
||||
const incomingStatus = pond.DeviceStatus.fromObject(status);
|
||||
const currentStatus = prev.status ?? pond.DeviceStatus.create();
|
||||
|
||||
incomingStatus.plenum = pickNewestReading(currentStatus.plenum, incomingStatus.plenum);
|
||||
incomingStatus.sen5x = pickNewestReading(currentStatus.sen5x, incomingStatus.sen5x);
|
||||
incomingStatus.co = pickNewestReading(currentStatus.co, incomingStatus.co);
|
||||
incomingStatus.co2 = pickNewestReading(currentStatus.co2, incomingStatus.co2);
|
||||
incomingStatus.no2 = pickNewestReading(currentStatus.no2, incomingStatus.no2);
|
||||
incomingStatus.o2 = pickNewestReading(currentStatus.o2, incomingStatus.o2);
|
||||
incomingStatus.lel = pickNewestReading(currentStatus.lel, incomingStatus.lel);
|
||||
incomingStatus.h2s = pickNewestReading(currentStatus.h2s, incomingStatus.h2s);
|
||||
|
||||
const currentLastActive = getTimestampMillis(currentStatus.lastActive);
|
||||
const incomingLastActive = getTimestampMillis(incomingStatus.lastActive);
|
||||
if (
|
||||
currentLastActive !== undefined &&
|
||||
incomingLastActive !== undefined &&
|
||||
incomingLastActive < currentLastActive
|
||||
) {
|
||||
incomingStatus.lastActive = currentStatus.lastActive;
|
||||
}
|
||||
|
||||
updated.status = incomingStatus;
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useDeviceStatusStreams({
|
||||
deviceIds: liveStatusDeviceIds,
|
||||
onStatusUpdate: handleLiveDeviceStatus,
|
||||
token,
|
||||
keys: liveContextKeys,
|
||||
types: liveContextTypes,
|
||||
as: liveAs,
|
||||
enabled: Boolean(deviceID && token),
|
||||
});
|
||||
|
||||
const loadDevice = () => {
|
||||
if (loading) return
|
||||
if (state?.devicePageData) {
|
||||
|
|
@ -173,7 +250,7 @@ export default function DevicePage() {
|
|||
// When the backend receives a new reading and updates a component,
|
||||
// this fires and updates the component in state without a page refresh.
|
||||
useWebSocket<{ key: string; component: Component } | null>({
|
||||
path: `/live/devices/${deviceID}/components`,
|
||||
path: `/v1/live/devices/${deviceID}/components`,
|
||||
parse: (e) => {
|
||||
try {
|
||||
const raw = JSON.parse(e.data);
|
||||
|
|
@ -197,32 +274,9 @@ export default function DevicePage() {
|
|||
return updated;
|
||||
});
|
||||
},
|
||||
token,
|
||||
enabled: !!deviceID,
|
||||
});
|
||||
|
||||
// --- Real-time device updates (optional) ---
|
||||
// Updates device-level info (name, status, lastActive, etc.)
|
||||
useWebSocket<Device | null>({
|
||||
path: `/live/devices/${deviceID}`,
|
||||
parse: (e) => {
|
||||
try {
|
||||
const raw = JSON.parse(e.data);
|
||||
const dev = Device.any(raw);
|
||||
if (!dev?.settings) {
|
||||
console.warn("[ws] received device without settings:", raw);
|
||||
return null;
|
||||
}
|
||||
return dev;
|
||||
} catch (err) {
|
||||
console.warn("[ws] failed to parse device:", err);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
onMessage: (updatedDevice) => {
|
||||
if (!updatedDevice) return;
|
||||
setDevice(updatedDevice);
|
||||
},
|
||||
keys: liveContextKeys,
|
||||
types: liveContextTypes,
|
||||
as: liveAs,
|
||||
token,
|
||||
enabled: !!deviceID,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -264,10 +264,50 @@ export default function Devices() {
|
|||
)
|
||||
}, [])
|
||||
|
||||
/** Same context chain as deviceAPI.list / GetContext on the backend (path + group tab + ?as=). */
|
||||
const statusStreamKeys = useMemo(() => {
|
||||
if (tab !== "all") {
|
||||
const g = groups[parseInt(tab)]
|
||||
if (g) {
|
||||
const keys = [...getContextKeys()]
|
||||
keys.push(g.id().toString())
|
||||
return keys
|
||||
}
|
||||
}
|
||||
return getContextKeys()
|
||||
}, [tab, groups, location.pathname])
|
||||
|
||||
const statusStreamTypes = useMemo(() => {
|
||||
if (tab !== "all") {
|
||||
const g = groups[parseInt(tab)]
|
||||
if (g) {
|
||||
const types = [...getContextTypes()]
|
||||
types.push("group")
|
||||
return types
|
||||
}
|
||||
}
|
||||
return getContextTypes()
|
||||
}, [tab, groups, location.pathname])
|
||||
|
||||
/** Match Device page / getDevicePageData: omit ?as= when URL context already starts with team. */
|
||||
const statusStreamAs = useMemo(() => {
|
||||
const types =
|
||||
tab !== "all" && groups[parseInt(tab)]
|
||||
? [...getContextTypes(), "group"]
|
||||
: getContextTypes()
|
||||
if (as && !(types.length > 0 && types[0] === "team")) {
|
||||
return as
|
||||
}
|
||||
return undefined
|
||||
}, [as, tab, groups, location.pathname])
|
||||
|
||||
useDeviceStatusStreams({
|
||||
deviceIds,
|
||||
onStatusUpdate: handleStatusUpdate,
|
||||
token,
|
||||
keys: statusStreamKeys,
|
||||
types: statusStreamTypes,
|
||||
as: statusStreamAs,
|
||||
enabled: devices.length > 0 && !!token,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import TemperatureIcon from "component/TemperatureIcon";
|
|||
import FuelIcon from "products/CommonIcons/fuelIcon";
|
||||
import moment from "moment";
|
||||
import Warning from "@mui/icons-material/Warning";
|
||||
import { getTemperatureUnit } from "utils";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
|
|
@ -286,7 +285,7 @@ export default function Heater() {
|
|||
|
||||
const tempUnit = () => {
|
||||
let unit = "°C";
|
||||
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||
if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||
unit = "°F";
|
||||
}
|
||||
return unit;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue