Merge branch 'airflow' into i2c_detect

This commit is contained in:
csawatzky 2025-07-28 15:02:14 -06:00
commit 1d9eaeee3a
32 changed files with 783 additions and 204 deletions

56
package-lock.json generated
View file

@ -49,6 +49,7 @@
"react-color": "^2.19.3",
"react-day-picker": "^9.6.5",
"react-dom": "^18.3.1",
"react-emoji-render": "^2.0.1",
"react-error-boundary": "^5.0.0",
"react-full-screen": "^1.1.1",
"react-horizontal-scrolling-menu": "^7.1.1",
@ -9188,6 +9189,15 @@
"node": ">=12"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@ -9971,6 +9981,18 @@
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
},
"node_modules/lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@ -11107,6 +11129,29 @@
"react": "^18.3.1"
}
},
"node_modules/react-emoji-render": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/react-emoji-render/-/react-emoji-render-2.0.1.tgz",
"integrity": "sha512-SKtsdwgEf2BFNiE9y4UBFZBWjkRcyWmhMprVly52+J77/zxThcfaQ3sCA0+2LtGJIRMpm4DGWSvyLg72fd1rXQ==",
"license": "MIT",
"dependencies": {
"classnames": "^2.2.5",
"emoji-regex": "^8.0.0",
"lodash.flatten": "^4.4.0",
"prop-types": "^15.5.8",
"string-replace-to-array": "^1.0.1"
},
"peerDependencies": {
"react": ">=0.14.0",
"react-dom": ">=0.14.0"
}
},
"node_modules/react-emoji-render/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/react-error-boundary": {
"version": "5.0.0",
"integrity": "sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ==",
@ -12408,6 +12453,17 @@
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-replace-to-array": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string-replace-to-array/-/string-replace-to-array-1.0.3.tgz",
"integrity": "sha512-QIdKvmfXbdFvblXAAz6IIjR7A+C6SU6m2A+e7fE/0EYDC5yfeWNMJQ193fPsW7nG+9q52dv/UjnVrDaNVZXpmQ==",
"license": "MIT",
"dependencies": {
"invariant": "^2.2.1",
"lodash.flatten": "^4.2.0",
"lodash.isstring": "^4.0.1"
}
},
"node_modules/string.prototype.matchall": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",

View file

@ -61,6 +61,7 @@
"react-color": "^2.19.3",
"react-day-picker": "^9.6.5",
"react-dom": "^18.3.1",
"react-emoji-render": "^2.0.1",
"react-error-boundary": "^5.0.0",
"react-full-screen": "^1.1.1",
"react-horizontal-scrolling-menu": "^7.1.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -19,7 +19,7 @@ if ('serviceWorker' in navigator) {
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
// <StrictMode>
<App />
</StrictMode>,
// </StrictMode>,
)

View file

@ -49,7 +49,7 @@ export default function BinsList(props: Props) {
//const scrollRef = useRef<any>(null);
const goToBin = (i: number) => {
let path = "/bins/" + bins[i].key();
let path = bins[i].key();
navigate(path, { state: {bin: bins[i]} });
};
@ -123,7 +123,9 @@ export default function BinsList(props: Props) {
}}
key={i}
className={classes.gridListTile}
onClick={() => goToBin(i)}>
onClick={() => {
!duplicate && goToBin(i)
}}>
<BinCardV2
bin={b}
duplicateBin={duplicateBin}
@ -136,7 +138,9 @@ export default function BinsList(props: Props) {
key={i}
style={{ width: "184px" }}
className={classes.gridListTile}
onClick={() => goToBin(i)}>
onClick={() => {
!duplicate && goToBin(i)
}}>
<BinCardV2
bin={b}
duplicateBin={duplicateBin}

View file

@ -13,7 +13,7 @@ import { Bin } from "models";
import moment, { Moment } from "moment";
import { pond, quack } from "protobuf-ts/pond";
import { useBinAPI, useGlobalState } from "providers";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { Legend } from "recharts";
import { getGrainUnit } from "utils";
@ -31,24 +31,13 @@ export default function BinLevelOverTime(props: Props) {
const { bin, fertilizerBin, colour, startDate, endDate, customHeight } = props;
const binAPI = useBinAPI();
const [{as}] = useGlobalState();
const [showCable, setShowCable] = useState(false);
const [manualData, setManualData] = useState<BarData[]>([]);
const [cableData, setCableData] = useState<BarData[]>([]);
const [data, setData] = useState<BarData[]>([]);
const [modeAreas, setModeAreas] = useState<RefArea[]>([]);
// const defaultDateRange = GetDefaultDateRange();
// const [startDate, setStartDate] = useState<Moment>(defaultDateRange.start);
// const [endDate, setEndDate] = useState<Moment>(defaultDateRange.end);
const [dataLoading, setDataLoading] = useState(false);
const [cableDataLoading, setCableDataLoading] = useState(false);
const [capacity, setCapacity] = useState<number | undefined>();
// const updateDateRange = (newStartDate: any, newEndDate: any) => {
// let range = GetDefaultDateRange();
// range.start = newStartDate;
// range.end = newEndDate;
// setStartDate(newStartDate);
// setEndDate(newEndDate);
// };
const [modeData, setModeData] = useState<BarData[]>([]);
const [histDataLoading, setHistDataLoading] = useState(false);
const getFill = (mode: pond.BinMode) => {
let fill = "";
@ -72,8 +61,8 @@ export default function BinLevelOverTime(props: Props) {
return fill;
};
useEffect(() => {
if (dataLoading || bin.key() === "") return;
//loads the data from the bin history table
const loadHistoryData = useCallback(() => {
setDataLoading(true);
let cap = bin.settings.specs?.bushelCapacity;
if (fertilizerBin && cap) {
@ -146,48 +135,125 @@ export default function BinLevelOverTime(props: Props) {
fill: getFill(bin.settings.mode)
});
}
setManualData(data);
setData(data);
setModeAreas(modeAreas);
})
.finally(() => {
setDataLoading(false);
});
}, [binAPI, bin, startDate, endDate, fertilizerBin]); // eslint-disable-line react-hooks/exhaustive-deps
},[startDate, endDate, bin, fertilizerBin])
useEffect(() => {
if (cableDataLoading || bin.key() === "") return;
setCableDataLoading(true);
//loads the data from the measuremnts table as a unit measurement
const loadMeasurementData = useCallback(() => {
setDataLoading(true);
let cap = bin.settings.specs?.bushelCapacity;
if (fertilizerBin && cap) {
cap = cap * 35.239;
}
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && cap) {
cap = cap / bin.bushelsPerTonne();
}
setCapacity(cap);
let autoBarData: BarData[] = [];
//load the measurment data to build the bushel bars
binAPI
.listBinMeasurements(bin.key(), startDate, endDate, 500, 0, "asc", undefined, undefined, undefined, undefined, as)
.listBinMeasurements(bin.key(), startDate, endDate, 500, 0, "asc")
.then(resp => {
let lastBushels = -1;
resp.data.measurements.forEach(objMeasurement => {
//set the bushel cable measurements
let m = pond.UnitMeasurementsForObject.fromObject(objMeasurement);
if (m.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_BUSHEL_CABLE) {
let cableBarData: BarData[] = [];
let lastBushels = -1;
//val will be an array of only a single number since this is not a measurement that involves nodes
m.values.forEach((val, i) => {
if (val.values[0] && m.timestamps[i]) {
if (lastBushels !== val.values[0]) {
cableBarData.push({
value: val.values[0],
timestamp: moment(m.timestamps[i]).valueOf()
});
lastBushels = val.values[0];
}
//val will be an array of only a single number since this is not a measurement that involves nodes
m.values.forEach((val, i) => {
if (val.values[0] && m.timestamps[i]) {
if (lastBushels !== val.values[0]) {
autoBarData.push({
value: val.values[0],
timestamp: moment(m.timestamps[i]).valueOf(),
});
lastBushels = val.values[0];
}
});
setCableData(cableBarData);
}
}
});
});
if (autoBarData.length === 0) {
let bushels = bin.settings.inventory?.grainBushels ?? 0;
let currentTime = moment().valueOf();
autoBarData.push({
value: fertilizerBin
? Math.round(bushels * 35.239)
: getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT
? Math.round((bushels / bin.bushelsPerTonne()) * 100) / 100
: bushels,
timestamp: currentTime
});
}
setData(autoBarData);
})
.catch(err => {})
.finally(() => {
setCableDataLoading(false);
setDataLoading(false);
});
}, [binAPI, bin, startDate, endDate, fertilizerBin, as]); // eslint-disable-line react-hooks/exhaustive-deps
},[startDate, endDate, bin, fertilizerBin])
const modeDataFromHistory = useCallback(()=>{
if (histDataLoading) return;
setHistDataLoading(true)
binAPI
.listHistoryBetween(bin.key(), 500, startDate.toISOString(), endDate.toISOString())
.then(resp => {
//loop through the history finding all the times the bin mode changed
let currentMode: pond.BinMode | undefined = undefined
let modeData: BarData[] = []
resp.data.history.forEach(hist => {
let histBin = Bin.create()
histBin.settings = hist.settings ?? pond.BinSettings.create()
if(hist.settings && currentMode !== hist.settings.mode){
currentMode = hist.settings.mode
modeData.push({
timestamp: moment(hist.timestamp).valueOf(),
value: 1,
fill: getFill(currentMode)
})
}
})
setModeData(modeData)
})
.catch(err => {
})
.finally(()=>{
setHistDataLoading(false)
})
},[bin, startDate, endDate])
useEffect(() => {
//dont call a load function if loading is already in progress
if (dataLoading || bin.key() === "") return;
//check what is controlling the bin inventory
let control = bin.inventoryControl()
//for automatic lidar and cables get the data from measurements as an object measurement
if(control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR ||
control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC
){
//load the measurement data for the bar graph
loadMeasurementData()
//load the data for the storage mode changes
modeDataFromHistory()
//for manual and hybrid (lidar or in the future, cables) get the data from bin_history
}else if(control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_MANUAL ||
control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR ||
control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_UNKNOWN
){
loadHistoryData()
}
},[bin, loadMeasurementData, loadHistoryData])
const legend = () => {
return (
@ -203,15 +269,16 @@ export default function BinLevelOverTime(props: Props) {
);
};
const manualChart = () => {
const inventoryChart = () => {
return (
<BarGraph
customHeight={customHeight}
data={manualData}
data={data}
refAreas={modeAreas}
barColour={colour}
yMax={capacity}
graphLegend={legend()}
yMin={0}
graphLegend={modeAreas.length > 0 ? legend() : undefined}
labelColour="white"
labels
useGradient
@ -219,62 +286,28 @@ export default function BinLevelOverTime(props: Props) {
);
};
const cableChart = () => {
if (cableData.length === 0) {
return (
<Box height={customHeight} paddingTop={"25%"}>
<Typography style={{ textAlign: "center", fontSize: 25, fontWeight: 650 }}>
No cable estimates for selected time frame
</Typography>
</Box>
);
}
const autoBinModeChart = () => {
return (
<BarGraph
customHeight={customHeight}
data={cableData}
//refAreas={modeAreas}
barColour={colour}
yMax={capacity}
yMin={0}
data={modeData}
customHeight={100}
hideYAxis
graphLegend={legend()}
labelColour="white"
labels
useGradient
yMax={1}
yMin={0}
/>
);
};
)
}
return (
<Card raised style={{ padding: 5 }}>
<Grid
container
direction="row"
alignContent="center"
alignItems="center"
justifyContent="space-between">
<Grid>
<Typography style={{ fontSize: 25, fontWeight: 650, marginLeft: 20 }}>
Grain Levels Over Time
</Typography>
</Grid>
<Grid>
<FormControlLabel
control={
<Switch
checked={showCable}
onChange={(_, checked) => {
setShowCable(checked);
}}
color="primary"
/>
}
label="Cable Estimates"
/>
</Grid>
</Grid>
{dataLoading ? <CircularProgress /> : showCable ? cableChart() : manualChart()}
<Typography style={{ fontSize: 25, fontWeight: 650, marginLeft: 20 }}>
Grain Levels Over Time
</Typography>
{dataLoading ? <CircularProgress /> : inventoryChart()}
{/* when using auto will need to make a new chart just for the bin modes since the auto inventory comes from another place */}
{(bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR ||
bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC) && autoBinModeChart()}
</Card>
);
}

View file

@ -15,6 +15,7 @@ import {
export interface BarData {
timestamp: number;
value: number;
fill?: string;
}
export interface RefArea {
@ -36,8 +37,9 @@ export interface AxisLabel {
interface Props {
data: BarData[];
barColour: string;
barColour?: string;
refAreas?: RefArea[];
hideYAxis?: boolean;
yMin?: number;
yMax?: number;
graphLegend?: JSX.Element;
@ -60,6 +62,7 @@ export default function BarGraph(props: Props) {
data,
barColour,
refAreas,
hideYAxis,
yMin,
yMax,
graphLegend,
@ -103,6 +106,7 @@ export default function BarGraph(props: Props) {
{graphLegend}
<YAxis
domain={[yMin ?? "dataMin", yMax ?? "auto"]}
hide={hideYAxis}
interval="preserveStartEnd"
scale={"linear"}
label={yLabel}

View file

@ -1,4 +1,4 @@
import Joyride, { Step, CallBackProps, STATUS, TooltipRenderProps, Locale } from "react-joyride";
import Joyride, { Step, CallBackProps, STATUS, TooltipRenderProps, Locale, ACTIONS, EVENTS } from "react-joyride";
import {
Button,
Card,
@ -10,12 +10,13 @@ import {
Tooltip,
IconButton,
Theme,
useTheme
useTheme,
} from "@mui/material";
import { KeyboardArrowLeft, KeyboardArrowRight, Close as CloseIcon } from "@mui/icons-material";
import classNames from "classnames";
import { grey } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import { CSSProperties } from "react";
const useStyles = makeStyles((theme: Theme) => {
return ({
@ -46,6 +47,10 @@ const useStyles = makeStyles((theme: Theme) => {
})
});
export interface TourStep extends Step {
onNext?: () => void; // Define onNext as an optional function
}
const StepContainer = (props: TooltipRenderProps, numSteps: number, skip: () => void) => {
const {
continuous,
@ -114,18 +119,30 @@ const StepContainer = (props: TooltipRenderProps, numSteps: number, skip: () =>
interface Props {
run: boolean;
steps: Step[];
steps: TourStep[];
endTourCallback: () => void;
spotlightStyle?: CSSProperties,
setStepIndex?: React.Dispatch<React.SetStateAction<number>>,
}
export default function Tour(props: Props) {
const { run, steps, endTourCallback } = props;
const { run, steps, endTourCallback, setStepIndex, spotlightStyle } = props;
const theme = useTheme();
const handleTourCallback = (data: CallBackProps) => {
const { status } = data;
const { action, index, status, type } = data;
const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED];
if (setStepIndex) setStepIndex(index)
if (action === ACTIONS.NEXT && type === EVENTS.STEP_AFTER) {
// Safely access the step and call onNext if it exists
const currentStep = steps[index];
if (currentStep.onNext) {
currentStep.onNext();
}
}
if (finishedStatuses.includes(status)) {
endTourCallback();
}
@ -150,6 +167,9 @@ export default function Tour(props: Props) {
primaryColor: theme.palette.primary.main,
textColor: theme.palette.text.primary,
zIndex: theme.zIndex.modal
},
spotlight: {
...spotlightStyle
}
}}
/>

View file

@ -50,7 +50,10 @@ export default function SyncDevice(props: Props) {
console.error(err);
error("Unable to resync device");
close();
});
})
.finally(() => {
close()
})
};
return (

View file

@ -475,6 +475,37 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
weightConversionKg: 27.2158214642112,
bushelsPerTonne: 36.744
}
],
[
pond.Grain.GRAIN_DRY_BEANS_RED,
{
name: "Dry Beans Red",
group: "Dry Beans",
equation: Equation.halsey,
a: 4.2669,
b: -0.013382,
c: 1.6933,
setTempC: defaultSetTemp,
targetMC: 15.0,
colour: "red",
weightConversionKg: 27.2155,
bushelsPerTonne: 36.744
}
],[
pond.Grain.GRAIN_DRY_BEANS_BLACK,
{
name: "Dry Beans Black",
group: "Dry Beans",
equation: Equation.halsey,
a: 5.2003,
b: -0.022685,
c: 1.9656,
setTempC: defaultSetTemp,
targetMC: 15.0,
colour: "grey",
weightConversionKg: 27.2155,
bushelsPerTonne: 36.744
}
]
]);

View file

@ -36,17 +36,17 @@ const featureVersions: Map<string, FeatureVersionByPlatform> = new Map([
],[
"detectI2C",
{
photon: "NA",
electron: "NA",
v2Wifi: "2.1.6",
v2Cell: "2.1.6",
v2WifiS3: "2.1.6",
v2CellS3: "2.1.6",
v2CellBlack: "2.1.6",
v2CellGreen: "2.1.6",
v2WifiBlue: "2.1.6",
v2CellBlue: "2.1.6",
v2EthBlue: "2.1.6"
photon: "N/A",
electron: "N/A",
v2Wifi: "N/A",
v2Cell: "N/A",
v2WifiS3: "N/A",
v2CellS3: "N/A",
v2CellBlack: "N/A",
v2CellGreen: "N/A",
v2WifiBlue: "N/A",
v2CellBlue: "N/A",
v2EthBlue: "N/A"
}
]
]);

View file

@ -256,5 +256,15 @@ function unitConversion(
});
}
}
if (type === quack.MeasurementType.MEASUREMENT_TYPE_SPEED) {
if (user.settings.distanceUnit === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
newVals.forEach((val, i) => {
val.values.forEach((v, j) => {
//convert m/s to ft/m by multiplying by 196.9
newVals[i].values[j] = v * 196.9
});
});
}
}
return newVals;
}

View file

@ -9,6 +9,7 @@ import { getWhitelabel } from "services/whiteLabel";
import Ventilation from "pages/VentEditor";
import { useGlobalState } from "providers";
import DeviceSupport from "pages/DeviceSupport";
import SignupCallback from "pages/SignupCallback";
const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
const DevicePage = lazy(() => import("pages/Device"));
@ -302,6 +303,8 @@ export default function Router() {
{/* Redirects */}
<Route path="/callback" element={<Navigate to="/" />} />
{/* <Route path="/signupCallback" element={<Navigate to="/" />} /> */}
<Route path="/signupCallback" element={<SignupCallback />} />
{/* <Route path="/login" element={<Login />} /> */}
{/* <Route path="/team/:teamID" element={<Navigate to="/teams/:teamID" />} /> */}
{/* <Route path="/device/:deviceID" element={<Navigate to="/devices/:devicesID" />} /> */}

View file

@ -221,7 +221,7 @@ export default function SideNavigator(props: Props) {
{(isAg || isStreamline || user.hasFeature("admin")) && (
<Tooltip title="Bins" placement="right">
<ListItemButton
id="tour-dashboard"
id="tour-bins"
onClick={() => goTo("/bins")}
classes={getClasses("/bin")}
>
@ -277,7 +277,7 @@ export default function SideNavigator(props: Props) {
{(isMiVent || user.hasFeature("admin")) && (
<Tooltip title="Mines" placement="right">
<ListItemButton
id="tour-dashboard"
id="tour-mines"
onClick={() => goTo("/mines")}
classes={getClasses("/mine")}
>
@ -330,7 +330,7 @@ export default function SideNavigator(props: Props) {
}
<Tooltip title="Teams" placement="right">
<ListItemButton
id="tour-dashboard"
id="tour-teams"
onClick={() => goTo("/teams")}
classes={getClasses("/team")}
>
@ -342,7 +342,7 @@ export default function SideNavigator(props: Props) {
</Tooltip>
<Tooltip title="Users" placement="right">
<ListItemButton
id="tour-dashboard"
id="tour-users"
onClick={() => goTo("/users")}
classes={getClasses("/user")}
>

View file

@ -1107,7 +1107,7 @@ export default function Bin(props: Props) {
};
return (
<PageContainer spacing={1}>
<PageContainer >
{showPlenumError()}
<Box
paddingBottom={isMobile || displayMobile ? 1 : 2}

View file

@ -18,7 +18,6 @@ import {
ImageList,
ImageListItem,
InputLabel,
lighten,
ListItemIcon,
ListItemText,
Menu,
@ -26,8 +25,6 @@ import {
Select,
Skeleton,
Theme,
ToggleButton,
ToggleButtonGroup,
Typography,
useTheme,
} from "@mui/material";
@ -61,7 +58,8 @@ import {
// useBinYardAPI,
useGlobalState,
useInteractionsAPI,
useSnackbar
useSnackbar,
useTeamAPI
} from "providers";
import React, { SetStateAction, useCallback, useEffect, useState } from "react";
// import { getThemeType } from "theme";
@ -72,7 +70,7 @@ import ObjectTable from "objects/ObjectTable";
import ResponsiveDialog from "common/ResponsiveDialog";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { green, yellow } from "@mui/material/colors";
import { makeStyles, styled } from "@mui/styles";
import { makeStyles } from "@mui/styles";
import { useNavigate } from "react-router-dom";
import BinSettings from "bin/BinSettings";
import AddBinFab from "bin/AddBinFab";
@ -82,6 +80,8 @@ import GrainBagSettings from "grainBag/grainBagSettings";
import GrainBagList from "grainBag/grainBagList";
import BinYards from "bin/BinYards";
import ButtonGroup from "common/ButtonGroup";
import TeamDialog from "teams/TeamDialog";
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
// import { useHistory } from "react-router";
const useStyles = makeStyles((theme: Theme) => {
@ -141,11 +141,13 @@ export default function Bins(props: Props) {
const classes = useStyles();
const isMobile = useMobile();
const binAPI = useBinAPI();
const teamAPI = useTeamAPI();
// const binyardAPI = useBinYardAPI();
const interactionsAPI = useInteractionsAPI();
const [{ user, as }] = useGlobalState();
const [allLoading, setAllLoading] = useState(false);
const [binsLoading, setBinsLoading] = useState(false);
const [doneLoadingPage, setDoneLoadingPage] = useState(false)
const [addBinOpen, setAddBinOpen] = useState(false);
const [paginatedBins, setPaginatedBins] = useState<PaginatedBins>({
bins: [],
@ -188,13 +190,16 @@ export default function Bins(props: Props) {
const [qrKeyList, setQrKeyList] = useState<QrCodeKey[]>([]);
const [totalFanControllers, setTotalFanControllers] = useState(0);
const [totalFanControllersOn, setTotalFanControllersOn] = useState(0);
const [yardsLoading, setYardsLoading] = useState(false);
// const [yardsLoading, setYardsLoading] = useState(false);
const [openAddAlerts, setOpenAddAlerts] = useState(false);
const [alertBins, setAlertBins] = useState<Bin[]>([]);
const { openSnack } = useSnackbar();
// const history = useHistory();
const navigate = useNavigate()
const [cardValDisplay, setCardValDisplay] = useState<"high" | "low" | "average">("average");
const [teamsDialog, setTeamsDialog] = useState(false)
// const [scrollTranslations, setScrollTranslations] = useState({
// bins: 0,
// empty: 0,
@ -252,6 +257,18 @@ export default function Bins(props: Props) {
// selected: {}
// }));
useEffect(() => {
if (doneLoadingPage && grainBins.length < 1 && grainBags.length < 1) {
teamAPI.listTeams(1, 0).then(resp => {
// console.log(resp.data.teams)
if (resp.data.teams.length > 0) {
// console.log("should?")
setTeamsDialog(true)
}
})
}
}, [doneLoadingPage, grainBins, grainBags])
useEffect(() => {
let ebt = sessionStorage.getItem("expandBinTotal");
if (ebt === "true") {
@ -295,6 +312,16 @@ export default function Bins(props: Props) {
}
}, [props.yardFilter]);
const getKeys = () => {
if (as) return undefined
return getContextKeys()
}
const getTypes = () => {
if (as) return undefined
return getContextTypes()
}
const loadBins = useCallback(() => {
//let filter = grainFilter;
let filter = yardFilter;
@ -309,7 +336,7 @@ export default function Bins(props: Props) {
setDisplayFert(false);
binAPI
.listBinsAndData(binsLimit, 0, order, orderBy, filter, as, false)
.listBinsAndData(binsLimit, 0, order, orderBy, filter, as, false, getKeys(), getTypes())
.then(resp => {
let grain: Bin[] = [];
let empty: Bin[] = [];
@ -400,7 +427,7 @@ export default function Bins(props: Props) {
const bags = resp.data.bags.map(b => GrainBag.create(b));
setGrainBags(bags);
})
.catch(err => {
.catch(() => {
setPaginatedBins({ bins: [], binsOffset: 0, binsTotal: 0 });
setBinMetrics(pond.BinMetrics.create());
setYards([]);
@ -408,6 +435,7 @@ export default function Bins(props: Props) {
.finally(() => {
setAllLoading(false);
setBinsLoading(false);
setDoneLoadingPage(true)
});
}, [binAPI, yardFilter, order, orderBy, as, loadingAs, loadingFilter]); // eslint-disable-line react-hooks/exhaustive-deps
@ -454,7 +482,7 @@ export default function Bins(props: Props) {
setFertilizerBins([...fert]);
setQrKeyList([...newKeyList]);
})
.catch(err => {
.catch(() => {
setPaginatedBins({ bins: [], binsOffset: 0, binsTotal: 0 });
})
.finally(() => {
@ -462,13 +490,13 @@ export default function Bins(props: Props) {
});
};
const searchYards = (
search?: string,
limit?: number,
offset?: number,
order?: "asc" | "desc"
) => {
setYardsLoading(true);
// const searchYards = (
// search?: string,
// limit?: number,
// offset?: number,
// order?: "asc" | "desc"
// ) => {
// setYardsLoading(true);
// binyardAPI.listBinYards(limit ?? binsLimit, offset ?? 0, order, "name", search).then(resp => {
// let y: pond.BinYardSettings[] = [];
// let p: Dictionary<pond.Permission[]> = {};
@ -493,7 +521,7 @@ export default function Bins(props: Props) {
// }
// setYardsLoading(false);
// });
};
// };
useEffect(() => {
loadBins();
@ -515,8 +543,8 @@ export default function Bins(props: Props) {
}
const duplicateBin = (bin: Bin) => {
binAPI.addBin(bin.settings, as).then(resp => {
loadBins();
binAPI.addBin(bin.settings, as).then(() => {
loadMoreBins(contentFilter);
});
};
@ -556,7 +584,7 @@ export default function Bins(props: Props) {
.then(resp => {
openSnack("Successfully added " + resp.data.numAlertsSet + " alerts");
})
.catch(err => {
.catch(() => {
console.log("errors found");
});
};
@ -606,8 +634,9 @@ export default function Bins(props: Props) {
preLoadedData={paginatedBins.bins}
rowClickFunction={data => {
let bin = data as Bin;
let path = "/bins/" + bin.key();
navigate(path, { state: { bin: bin }});
// let path = "bins/" + bin.key();
// navigate(path, { state: { bin: bin }});
navigate(bin.key(), { state: { bin: bin }, relative: "path"});
}}
customButtons={[
{
@ -1080,7 +1109,7 @@ export default function Bins(props: Props) {
control={
<Checkbox
checked={displayGrain}
onChange={(e, checked) => {
onChange={(_, checked) => {
setDisplayGrain(checked);
}}
/>
@ -1093,7 +1122,7 @@ export default function Bins(props: Props) {
control={
<Checkbox
checked={displayFert}
onChange={(e, checked) => {
onChange={(_, checked) => {
setDisplayFert(checked);
}}
/>
@ -1106,7 +1135,7 @@ export default function Bins(props: Props) {
control={
<Checkbox
checked={displayEmpty}
onChange={(e, checked) => {
onChange={(_, checked) => {
setDisplayEmpty(checked);
}}
/>
@ -1392,15 +1421,17 @@ export default function Bins(props: Props) {
{addALertsDialog()}
<BinYards
yards={yards}
yardsLoading={yardsLoading}
// yardsLoading={yardsLoading}
yardsLoading={false}
yardPerms={yardPermissions}
showTabs={!props.insert}
setYardFilter={setYardFilter}
bins={paginatedBins.bins}
loadBins={loadBins}
loadYards={(search, limit, offset) => {
searchYards(search, limit, offset);
}}
// loadYards={(search, limit, offset) => {
// searchYards(search, limit, offset);
// }}
loadYards={() => {}}
grainBags={grainBags}
setShowing={(val: "all" | "bins" | "bags") => {
setShowing(val);
@ -1473,6 +1504,13 @@ export default function Bins(props: Props) {
/>
{binMenu()}
<AddBinFab onClick={() => setAddBinOpen(true)} pulse={binsTotal < 1 && !allLoading} />
<TeamDialog
open={teamsDialog}
setOpen={setTeamsDialog}
title={"You have teams!"}
info={"Looks like you don't have any bins, but are a part of at least one team. If you want to see your teams bins, please select your team and choose the option \"View as team\""}
hideKey={"bins-teams-notification-dialog"}
/>
</Box>
);
};

View file

@ -3,7 +3,7 @@ import PageContainer from "./PageContainer";
import { pond } from "protobuf-ts/pond";
import LogsDisplay from "common/LogsDisplay";
import { useEffect, useState } from "react";
import { useDeviceAPI, useSnackbar } from "hooks";
import { useDeviceAPI, useMobile, useSnackbar } from "hooks";
import { useLocation, useParams } from "react-router-dom";
import { Component, Device, Interaction } from "models";
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
@ -43,6 +43,7 @@ export default function DeviceSupport() {
const deviceAPI = useDeviceAPI()
const snackbar = useSnackbar()
const theme = useTheme()
const isMobile = useMobile()
const { state } = useLocation();
const [{ user, as }] = useGlobalState()
@ -188,7 +189,7 @@ export default function DeviceSupport() {
}
return (
<PageContainer>
<PageContainer spacing={isMobile ? 0 : 2}>
<Grid container justifyContent={"space-between"}>
<Grid>
<SmartBreadcrumb deviceName={device.name()} state={{devicePageData: devicePageData}} />

View file

@ -5,7 +5,7 @@ import { makeStyles } from "@mui/styles";
import ResponsiveTable, { Column } from "common/ResponsiveTable";
import ProvisionDevice from "device/ProvisionDevice";
import { pond } from "protobuf-ts/pond";
import { useDeviceAPI, useGlobalState, useGroupAPI } from "providers";
import { useDeviceAPI, useGlobalState, useGroupAPI, useTeamAPI } from "providers";
import { useCallback, useEffect, useState } from "react";
import PageContainer from "./PageContainer";
import { useMobile } from "hooks";
@ -28,6 +28,7 @@ import StatusDust from "common/StatusDust";
import StatusGas from "common/StatusGas";
import { cloneDeep } from "lodash";
import LoadingScreen from "app/LoadingScreen";
import TeamDialog from "teams/TeamDialog";
const useStyles = makeStyles((theme: Theme) => {
return ({
@ -98,9 +99,12 @@ export default function Devices() {
const location = useLocation();
const deviceAPI = useDeviceAPI();
const groupAPI = useGroupAPI();
const teamAPI = useTeamAPI();
const [devicesLoading, setDevicesLoading] = useState(false)
const [limit, setLimit] = useState(10);
const [page, setPage] = useState(0);
const [doneLoading, setDoneLoading] = useState(false)
const [teamsDialog, setTeamsDialog] = useState(false)
const [{ user }] = useGlobalState()
@ -186,7 +190,7 @@ export default function Devices() {
const [preferencesAnchor, setPreferencesAnchor] = useState<any>(null)
const [{ as }] = useGlobalState()
const [{ as, team }] = useGlobalState()
const updateGroups = (newGroups: Group[]) => {
setGroups(newGroups);
@ -327,6 +331,7 @@ export default function Devices() {
setTotal(resp.data.total)
}).finally(() => {
setDevicesLoading(false)
setDoneLoading(true)
})
}
@ -338,6 +343,16 @@ export default function Devices() {
loadGroups()
}, [groupLimit, groupPage, orderGroup, orderGroupBy, searchGroup, as])
useEffect(() => {
if (doneLoading && devices.length < 1 && groups.length < 1) {
teamAPI.listTeams(1, 0).then(resp => {
if (resp.data.teams.length > 0 && team.key().length < 1) {
setTeamsDialog(true);
}
})
}
}, [doneLoading, devices, groups])
const handleChange = (event: any) => {
setLimit(event.target.value);
};
@ -826,6 +841,13 @@ export default function Devices() {
addGroupCallback={addGroupCallback}
/>
{preferencesMenu()}
<TeamDialog
open={teamsDialog}
setOpen={setTeamsDialog}
title={"You have teams!"}
info={"Looks like you don't have any devices, but are a part of at least one team. If you want to see your teams devices, please select your team and choose the option \"View as team\""}
hideKey={"devices-teams-notification-dialog"}
/>
</PageContainer>
)
}

View file

@ -12,7 +12,7 @@ import {
TypeBackground
} from "@mui/material";
import { blue, green, orange, red } from "@mui/material/colors";
import { createStyles, makeStyles } from "@mui/styles";
import { makeStyles } from "@mui/styles";
//import { TypeBackground } from "@material-ui/core/styles/createPalette";
import {
ArrowUpward,
@ -267,7 +267,7 @@ export default function Firmware() {
if (selected) {
firmwareAPI
.removeFirmware(selected.settings.platform, selected.settings.version)
.then((response: any) => {
.then(() => {
success("Successfully removed firmware");
})
.catch((err: any) => {

View file

@ -48,7 +48,7 @@ export const PageContainer: React.FunctionComponent<Props> = (props: Props) => {
const { children, fullViewport, isCenterCenter, sx } = props;
// let fullViewport = false
// let isCenterCenter = false
const spacing = props.spacing ? props.spacing : 2;
const spacing = props.spacing ?? 0;
return (
<Container
className={classNames(

View file

@ -0,0 +1,217 @@
import { useDeviceAPI, useMobile, useUserAPI } from "hooks";
import PageContainer from "./PageContainer";
import { useEffect, useState } from "react";
import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Typography } from "@mui/material";
import { useBinAPI, useGlobalState, useTeamAPI } from "providers";
import { CheckCircleOutline } from "@mui/icons-material";
import { green } from "@mui/material/colors";
import Tour, { TourStep } from "common/Tour";
import Emoji from "react-emoji-render";
import { getWhitelabel, IsAdaptiveAgriculture } from "services/whiteLabel";
// import { color } from "framer-motion";
// interface Props {
// }
// export default function SignupCallback (props: Props) {
export default function SignupCallback () {
const userAPI = useUserAPI();
const teamAPI = useTeamAPI()
const deviceAPI = useDeviceAPI();
const binAPI = useBinAPI();
const isMobile = useMobile()
const [{ user }] = useGlobalState();
const whiteLabel = getWhitelabel()
const [loading, setLoading] = useState(false)
const [signupDialogOpen, setSignupDialogOpen] = useState(true)
const [hasTeams, setHasTeams] = useState(false)
const [hasDevices, setHasDevices] = useState(false)
const [hasBins, setHasBins] = useState(false)
const [isTourRunning, setIsTourRunning] = useState(false);
const [stepIndex, setStepIndex] = useState(0)
const [doneBins, setDoneBins] = useState(false)
const [doneTeams, setDoneTeams] = useState(false)
const [doneDevices, setDoneDevices] = useState(false)
useEffect(() => {
setLoading(true)
userAPI.completeSignup().then(() => {
teamAPI.listTeams(1, 0).then(resp => {
setHasTeams(resp.data.teams.length > 0)
}).finally(() => setDoneTeams(true))
deviceAPI.list(1, 0).then(resp => {
setHasDevices(resp.data.devices.length > 0)
}).finally(() => setDoneDevices(true))
binAPI.listBins(1, 0).then(resp => {
setHasBins(resp.data.bins.length > 0)
}).finally(() => setDoneBins(true))
}).finally(() => setLoading(false))
}, [])
const loadingComplete = () => {
return loading === false && doneBins && doneTeams && doneDevices
}
const content = () => {
return (
<DialogContent sx={{ margin: "auto", textAlign: "center" }}>
{ loadingComplete() ?
<CheckCircleOutline sx={{ fontSize: 64, color: green[500] }}/>
:
<CircularProgress size={64}/>
}
<DialogContentText sx={{marginTop: 2}}>
{hasTeams && "Teams found!"}
<br/>
{hasDevices && "Devices found!"}
<br/>
{hasBins && "Bins found!"}
</DialogContentText>
</DialogContent>
)
}
const getTourSteps = (): TourStep[] => {
let steps: TourStep[] = [
{
title: (
<>
Welcome to {whiteLabel.name}
<Emoji text=" 🎉" />
</>
),
content: (
<>
<Typography variant="subtitle2" gutterBottom>
{"Hello " + user.name()}
<Emoji text=" 👋" />
</Typography>
<Typography variant="body2">
Thank you for signing up! Let me give you the tour
<Emoji text=" 😊" />
</Typography>
</>
),
target: "body",
placement: "center",
disableBeacon: true,
},
{
title: "User Menu",
content: (
<>
<Typography variant="body2" >
In the user menu, you can make changes to your profile, and adjust unit preferences.
</Typography>
<br/>
<Typography variant="body2" >
If you are part of a team, you can select it here as well.
</Typography>
</>
),
target: "#tour-user-menu",
placement: "bottom",
disableBeacon: true,
// onNext: () => setDoneFirst(true)
},
// {
// title:
// }
];
if (hasTeams) steps.push({
title: "Teams",
content: (
<>
<Typography variant="body2" >
Here, you can view your team(s), access their settings, and choose your favourite.
</Typography>
</>
),
target: "#tour-teams",
placement: "right",
// spotlightPadding: 50,
})
steps.push({
title: "Devices",
content: (
<>
<Typography variant="body2" >
You can view your devices and their readings here.
</Typography>
<br/>
<Typography>
If you are viewing as a team, your team's devices will be displayed here instead.
</Typography>
</>
),
target: "#tour-dashboard",
placement: "right"
})
if (IsAdaptiveAgriculture()) steps.push({
title: "Bins",
content: (
<>
<Typography variant="body2" >
You can view your bins and their inventory here.
</Typography>
<br/>
<Typography>
If you are viewing as a team, your team's bins will be displayed here instead.
</Typography>
</>
),
target: "#tour-bins",
placement: "right",
disableBeacon: true,
})
return steps;
};
const endTour = () => {
setIsTourRunning(false);
// if (user) {
// user.status.finishedBinIntro = moment().toJSON();
// userAPI
// .updateUser(userID, user.protobuf())
// .then(() => dispatch({ key: "user", value: user }))
// .catch((err: any) => error(err));
// }
};
const closeDialog = () => {
setSignupDialogOpen(false)
setIsTourRunning(true)
}
useEffect(() => {
console.log(stepIndex)
}, [stepIndex])
return (
<PageContainer spacing={1}>
<Dialog open={signupDialogOpen} fullScreen={isMobile} >
<DialogTitle>
Finishing Signup
</DialogTitle>
<Divider sx={{ marginBottom: 2 }}/>
{ content() }
<DialogActions>
{loadingComplete() &&
<Button onClick={closeDialog}>
Done
</Button>
}
</DialogActions>
</Dialog>
<Tour
setStepIndex={setStepIndex}
spotlightStyle={{ marginTop: stepIndex!==1 ? 74 : 1 }}
run={isTourRunning} steps={getTourSteps()}
endTourCallback={endTour}
/>
</PageContainer>
)
}

View file

@ -12,10 +12,10 @@ import { appendToUrl } from "navigation/Router";
import PageContainer from "pages/PageContainer";
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
import { pond } from "protobuf-ts/pond";
import { useSnackbar, useTeamAPI, useUserAPI } from "providers";
import { useGlobalState, useSnackbar, useTeamAPI, useUserAPI } from "providers";
import { useEffect, useState } from "react";
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { getSignatureAccentColour } from "services/whiteLabel";
import { getSignatureAccentColour, IsAdaptiveAgriculture, IsStreamline } from "services/whiteLabel";
import TeamActions from "teams/TeamActions";
import TeamKeyManager from "teams/TeamKeyManager";
import ObjectUsers from "user/ObjectUsers";
@ -88,6 +88,7 @@ export default function TeamPage() {
const [loadingUsers, setLoadingUsers] = useState(false)
const [userDialog, setUserDialog] = useState(false)
const teamKey = teamID ? teamID : "";
const [{ user }] = useGlobalState()
const getTeam = () => {
@ -213,8 +214,16 @@ export default function TeamPage() {
navigate(appendToUrl("groups"))
}
const toBins = () => {
// navigate(appendToUrl("bins"))
navigate("bins")
}
const isAg = IsAdaptiveAgriculture()
const isStreamline = IsStreamline()
return (
<PageContainer padding={isMobile ? 0 : 2}>
<PageContainer spacing={isMobile ? 0 : 2}>
<Box className={classes.container}>
<Box className={classes.leftBox}>
{title()}
@ -237,6 +246,7 @@ export default function TeamPage() {
isDialogOpen={userDialog}
closeDialogCallback={() => setUserDialog(false)}
refreshCallback={() => {}}
useImitation={false}
/>
<Grid2 container spacing={1}>
<Grid2 size={{ xs: 12, sm: 4 }}>
@ -255,6 +265,14 @@ export default function TeamPage() {
<ListItemButton onClick={toGroups}>
Groups
</ListItemButton>
{((isAg || isStreamline) || user.hasFeature("admin")) &&
<>
<Divider />
<ListItemButton onClick={toBins}>
Bins
</ListItemButton>
</>
}
<Divider />
</List>
</Card>

View file

@ -5,7 +5,7 @@ import TeamList from "teams/TeamList";
export default function Teams() {
const isMobile = useMobile()
return (
<PageContainer padding={isMobile ? 0 : 2}>
<PageContainer spacing={isMobile ? 0 : 2}>
<TeamList />
</PageContainer>
);

View file

@ -349,7 +349,7 @@ export default function Users() {
}
return (
<PageContainer padding={isMobile ? 0 : 2}>
<PageContainer spacing={isMobile ? 0 : 2}>
<ResponsiveTable<User>
title="Users"
rows={rows}

View file

@ -146,7 +146,7 @@ export interface ComponentTypeExtension {
interactionResultTypes: Array<quack.InteractionResultType>;
states: Array<string>;
measurements: Array<ComponentMeasurement>;
measurementSummary: Function; //Deprecated: this summary used the old measurement structure
measurementSummary?: Function; //Deprecated: this summary used the old measurement structure
unitMeasurementSummary: (
measurements: convertedUnitMeasurement,
excludedNodes?: number[]
@ -382,7 +382,8 @@ export async function getMeasurementSummary(
measurement: quack.IMeasurement,
filters: GraphFilters
): Promise<Array<Summary>> {
return extension(type, subtype).measurementSummary(measurement, filters);
let sumFunc = extension(type, subtype).measurementSummary
return sumFunc ? sumFunc(measurement, filters) : [];
}
const validNodes = (nodeVals: number[], filters?: GraphFilters) => {

View file

@ -1,14 +1,15 @@
import {
ComponentTypeExtension,
Summary,
simpleMeasurements,
simpleSummaries,
// simpleMeasurements,
// simpleSummaries,
unitMeasurementSummaries,
AreaChartData,
GraphFilters,
simpleAreaChartData,
LineChartData,
simpleLineChartData
simpleLineChartData,
ComponentMeasurement
} from "pbHelpers/ComponentType";
import PressureDarkIcon from "assets/components/pressureDark.png";
import PressureLightIcon from "assets/components/pressureLight.png";
@ -18,14 +19,46 @@ import { convertedUnitMeasurement } from "models/UnitMeasurement";
import { pond } from "protobuf-ts/pond";
export function Airflow(subtype: number = 0): ComponentTypeExtension {
let airflow = describeMeasurement(
let cfm = describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_CFM,
quack.ComponentType.COMPONENT_TYPE_AIRFLOW,
subtype
);
let velocity = describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_CFM,
quack.ComponentType.COMPONENT_TYPE_AIRFLOW,
subtype
);
let measurementTypes = [
{
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PPM,
label: cfm.label(),
colour: cfm.colour(),
graphType: cfm.graph()
} as ComponentMeasurement,
{
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_VOLTAGE,
label: velocity.label(),
colour: velocity.colour(),
graphType: velocity.graph()
} as ComponentMeasurement
];
return {
type: quack.ComponentType.COMPONENT_TYPE_AIRFLOW,
subtypes: [],
subtypes: [
{
key: quack.AirFlowSubtype.AIR_FLOW_SUBTYPE_PROSENSE,
value: "AIR_FLOW_SUBTYPE_PROSENSE",
friendlyName: "Prosense"
},
{
key: quack.AirFlowSubtype.AIR_FLOW_SUBTYPE_ULTRASONIC,
value: "AIR_FLOW_SUBTYPE_ULTRASONIC",
friendlyName: "Ultrasonic"
},
],
friendlyName: "Airflow",
description: "Measure the flow of air though a specified area",
isController: false,
@ -36,10 +69,10 @@ export function Airflow(subtype: number = 0): ComponentTypeExtension {
addressTypes: [quack.AddressType.ADDRESS_TYPE_I2C],
interactionResultTypes: [],
states: [],
measurements: simpleMeasurements(airflow),
measurementSummary: async function(measurement: quack.Measurement): Promise<Array<Summary>> {
return simpleSummaries(measurement, airflow);
},
measurements: measurementTypes,
// measurementSummary: async function(measurement: quack.Measurement): Promise<Array<Summary>> {
// return simpleSummaries(measurement, airflow);
// },
unitMeasurementSummary: (
measurements: convertedUnitMeasurement,
excludedNodes?: number[]

View file

@ -450,8 +450,9 @@ export class MeasurementDescriber {
this.details.max = 100000;
break;
case quack.MeasurementType.MEASUREMENT_TYPE_SPEED:
let speedUnit = getDistanceUnit()
this.details.label = "Speed";
this.details.unit = "km/h";
this.details.unit = speedUnit === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m/s" : "ft/m";
this.details.colour = green["500"];
this.details.path = "edgeTriggered.rises";
this.details.max = 500;

View file

@ -22,7 +22,7 @@ export const MiVentV1Availability: DeviceAvailabilityMap = new Map<
[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]],
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, [0x2a]]
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, [0x6c]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],

View file

@ -9,29 +9,39 @@ interface Props extends PropsWithChildren<any>{
export default function AuthWrapper(props: Props) {
const { children, setToken } = props;
const { isLoading, loginWithRedirect, isAuthenticated, getAccessTokenSilently } = useAuth0();
const {
isLoading,
loginWithRedirect,
isAuthenticated,
getAccessTokenSilently,
// getIdTokenClaims
} = useAuth0();
useEffect(() => {
const url = new URL(window.location.href);
const isManualLoginPage = url.pathname.includes("/login");
const options: RedirectLoginOptions = {
authorizationParams: {} // Initialize here
};
if (isManualLoginPage) {
const options: RedirectLoginOptions = {
authorizationParams: {} // Initialize here
};
if (isManualLoginPage && options.authorizationParams) {
let parsed = queryString.parse(url.search);
if (parsed.signup === "true") {
options.authorizationParams!.screen_hint = "signup";
options.authorizationParams!.prompt = "login";
options.authorizationParams.screen_hint = "signUp";
// options.authorizationParams.m
// options.authorizationParams.prompt = "login";
}
if (parsed.email && parsed.email !== "") {
options.authorizationParams!.login_hint = parsed.email.toString(); // prefill email
options.authorizationParams.login_hint = parsed.email.toString(); // prefill email
}
console.log(url)
options.authorizationParams.redirect_uri = url.origin + "/signupCallback"
console.log(options.authorizationParams.redirect_uri)
loginWithRedirect(options)
} else if (!isAuthenticated && !isLoading) {
@ -40,15 +50,27 @@ export default function AuthWrapper(props: Props) {
}, [isAuthenticated, loginWithRedirect, isLoading]);
useEffect(() => {
if (isAuthenticated) getAccessTokenSilently().then(t => {
if (t.length < 1) {
// const url = new URL(window.location.href);
// const isSignupCallback = url.pathname.includes("/signupCallback");
if (isAuthenticated) {
getAccessTokenSilently().then(t => {
if (t.length < 1) {
loginWithRedirect()
}
setToken(t)
}).catch(() => {
// No token, go back to login
loginWithRedirect()
}
setToken(t)
}).catch(() => {
// No token, go back to login
loginWithRedirect()
})
})
// if (isSignupCallback) {
// getIdTokenClaims().then(id_claims => {
// console.log(id_claims)
// console.log(id_claims?.nickname)
// if (id_claims) console.log(Object.keys(id_claims))
// })
// }
}
}, [setToken, isAuthenticated])
return children;

View file

@ -28,6 +28,7 @@ export interface IUserAPIContext {
prefixSearch?: string,
) => Promise<ListUsersResponse>;
listProfiles: () => Promise<pond.UserProfile[]>;
completeSignup: () => Promise<pond.CompleteSignupUserResponse>;
listObjectUsers: (scope: Scope) => Promise<any>;
updateObjectUsers: (scope: Scope, users: pond.IUser[]) => Promise<any>;
getUser: (id: string, scope?: Scope) => Promise<User>;
@ -55,8 +56,21 @@ export const UserAPIContext = createContext<IUserAPIContext>({} as IUserAPIConte
export default function UserProvider(props: PropsWithChildren<any>) {
const { children } = props;
const { get, put } = useHTTP();
const [{ as }] = useGlobalState();
const { get, put, post } = useHTTP();
const [{ as, user }] = useGlobalState();
const completeSignup = (): Promise<pond.CompleteSignupUserResponse> => {
return new Promise((resolve, reject) => {
let request = pond.CompleteSignupUserRequest.create();
request.avatar = user.settings.avatar
request.email = user.settings.email
request.id = user.id()
request.name = user.name()
post(pondURL("/signups"), request).then(res => {
resolve(pond.CompleteSignupUserResponse.create())
})
})
}
const listUsers = (
limit: number,
@ -234,6 +248,7 @@ export default function UserProvider(props: PropsWithChildren<any>) {
return (
<UserAPIContext.Provider
value={{
completeSignup,
listUsers,
listProfiles,
listObjectUsers,

View file

@ -1,6 +1,9 @@
import {
AppBar,
Button,
Checkbox,
FormControlLabel,
FormGroup,
IconButton,
Theme,
Toolbar,
@ -18,6 +21,9 @@ import { makeStyles } from "@mui/styles";
interface Props {
open: boolean;
setOpen: (value: React.SetStateAction<boolean>) => void;
title?: string;
info?: string;
hideKey?: string;
}
const useStyles = makeStyles((theme: Theme) => {
@ -37,16 +43,22 @@ const useStyles = makeStyles((theme: Theme) => {
});
export default function TeamDialog(props: Props) {
const { open, setOpen } = props;
const { open, setOpen, title, info, hideKey } = props;
const teamAPI = useTeamAPI();
const userAPI = useUserAPI();
const [, dispatch] = useGlobalState();
const [teamKey, setTeamKey] = useState<string>("");
const [{ team, user }] = useGlobalState();
const [viewAsTeam, setViewAsTeam] = useState(user.settings.useTeam)
const [doNotShow, setDoNotShow] = useState(false)
const classes = useStyles();
const shouldHide = hideKey ?
(localStorage.getItem(hideKey) === "true" ? true : false)
: false
const setTeam = () => {
setOpen(false);
onClose()
dispatch({ key: "as", value: teamKey });
if (teamKey === "" || teamKey.includes("auth")) {
dispatch({ key: "team", value: Team.create() });
@ -55,13 +67,19 @@ export default function TeamDialog(props: Props) {
teamAPI.getTeam(teamKey).then(team => {
dispatch({ key: "team", value: team });
user.settings.defaultTeam = team.key();
user.settings.useTeam = true; //when selecting a team now set to view as team by default
user.settings.useTeam = viewAsTeam; //when selecting a team now set to view as team by default
userAPI.updateUser(user.id(), user.protobuf());
});
};
const onClose = () => {
console.log("close")
setOpen(false)
if (hideKey) localStorage.setItem(hideKey, ""+doNotShow)
}
return (
<ResponsiveDialog open={open} onClose={() => setOpen(false)}>
<ResponsiveDialog open={open && !shouldHide} onClose={onClose}>
<AppBar position="relative">
<Toolbar>
<IconButton
@ -72,9 +90,9 @@ export default function TeamDialog(props: Props) {
<CloseIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
Select a Team to View As
{ title ? title : "Select a Team to View As"}
</Typography>
<Button color="inherit" onClick={setTeam}>
<Button color="inherit" onClick={setTeam} >
Save
</Button>
</Toolbar>
@ -84,12 +102,40 @@ export default function TeamDialog(props: Props) {
Viewing as: {team.name()}
</Typography>
)}
{info &&
<Typography variant="body1" className={classes.viewingAs}>
{info}
</Typography>
}
<TeamSearch
//style={{ width: theme.spacing(32) }}
className={classes.searchBar}
loadUsers={user.hasFeature("admin")}
setTeamCallback={setTeamKey}
/>
{/* <Checkbox checked={viewAsTeam} onChange={() => setViewAsTeam(!viewAsTeam)} /> */}
<FormGroup sx={{ margin: 1, marginLeft: 2 }}>
<FormControlLabel
control={
<Checkbox
checked={viewAsTeam}
onChange={() => setViewAsTeam(!viewAsTeam)}
/>
}
label="View as team"
/>
</FormGroup>
{hideKey && <FormGroup sx={{ margin: 1, marginLeft: 2 }}>
<FormControlLabel
control={
<Checkbox
checked={doNotShow}
onChange={() => setDoNotShow(!doNotShow)}
/>
}
label="Do not show again"
/>
</FormGroup>}
</ResponsiveDialog>
);
}

View file

@ -228,7 +228,7 @@ export default function UserMenu() {
<ListItemText primary="Toggle Theme" />
</MenuItem> */}
{hasTeams && (
<MenuItem onClick={openTeamDialog} aria-label="Open Team Menu" dense>
<MenuItem id="tour-teams" onClick={openTeamDialog} aria-label="Open Team Menu" dense>
<ListItemIcon>
<TeamIcon />
</ListItemIcon>