device history implemented
This commit is contained in:
parent
53a430d525
commit
2acdd59ac7
12 changed files with 3292 additions and 22 deletions
593
src/common/DiffHistory.tsx
Normal file
593
src/common/DiffHistory.tsx
Normal file
|
|
@ -0,0 +1,593 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
// import { createStyles, Theme, makeStyles, fade } from "@material-ui/core/styles";
|
||||||
|
import { detailedDiff } from "deep-object-diff";
|
||||||
|
import {
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
ListItemIcon,
|
||||||
|
Chip,
|
||||||
|
Avatar,
|
||||||
|
Grid2 as Grid
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Add, ChangeHistory, Remove, Face, Sync } from "@mui/icons-material";
|
||||||
|
import { amber, red, blue, green } from "@mui/material/colors";
|
||||||
|
// import MaterialTable from "material-table";
|
||||||
|
import moment from "moment";
|
||||||
|
// import { getTableIcons } from "common/ResponsiveTable";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { useUserAPI } from "hooks";
|
||||||
|
import { useMobile } from "hooks";
|
||||||
|
import ObjectDescriber from "objects/ObjectDescriber";
|
||||||
|
import { makeStyles } from "@mui/styles";
|
||||||
|
import { alpha, Theme } from "@mui/material";
|
||||||
|
import ResponsiveTable, { Column } from "./ResponsiveTable";
|
||||||
|
|
||||||
|
const addedColour = alpha(green[600], 0.25);
|
||||||
|
const deletedColour = alpha(red[600], 0.25);
|
||||||
|
const updatedColour = alpha(blue[600], 0.25);
|
||||||
|
const resyncedColour = alpha(amber[600], 0.25);
|
||||||
|
const useStyles = makeStyles((theme: Theme) => {
|
||||||
|
return ({
|
||||||
|
added: {
|
||||||
|
backgroundColor: addedColour
|
||||||
|
},
|
||||||
|
deleted: {
|
||||||
|
backgroundColor: deletedColour
|
||||||
|
},
|
||||||
|
updated: {
|
||||||
|
backgroundColor: updatedColour
|
||||||
|
},
|
||||||
|
resynced: {
|
||||||
|
backgroundColor: resyncedColour
|
||||||
|
},
|
||||||
|
chipContainer: {
|
||||||
|
maxWidth: "100%",
|
||||||
|
maxHeight: "100%",
|
||||||
|
flexWrap: "wrap"
|
||||||
|
},
|
||||||
|
translateContainer: {
|
||||||
|
borderLeft: "2px solid white",
|
||||||
|
borderRight: "2px solid white",
|
||||||
|
borderRadius: "6px",
|
||||||
|
padding: "0 8px 0 8px",
|
||||||
|
whiteSpace: "pre",
|
||||||
|
margin: "8px"
|
||||||
|
},
|
||||||
|
translateBox: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "0px"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Diff {
|
||||||
|
added: any;
|
||||||
|
deleted: any;
|
||||||
|
updated: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RowData {
|
||||||
|
timestamp: string;
|
||||||
|
user: string;
|
||||||
|
changes: number;
|
||||||
|
before: object;
|
||||||
|
after: object;
|
||||||
|
diff: Diff;
|
||||||
|
status: string;
|
||||||
|
type?: pond.ObjectType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SummaryProps {
|
||||||
|
changes: number;
|
||||||
|
diff: Diff;
|
||||||
|
before: any;
|
||||||
|
after: any;
|
||||||
|
kind: string;
|
||||||
|
translateKey: (key: keyof any, type?: pond.ObjectType) => string;
|
||||||
|
translateValue: (key: keyof any, obj: any, type?: pond.ObjectType) => string;
|
||||||
|
variant?: "expanded" | "inline";
|
||||||
|
type?: pond.ObjectType;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Summary(props: SummaryProps) {
|
||||||
|
const classes = useStyles();
|
||||||
|
const { changes, diff, before, after, translateKey, translateValue, variant, type } = props;
|
||||||
|
//console.log(diff)
|
||||||
|
const { added, updated, deleted } = diff;
|
||||||
|
|
||||||
|
const translate = (key: keyof any, changeType: "added" | "deleted" | "updated"): any => {
|
||||||
|
switch (changeType) {
|
||||||
|
case "updated":
|
||||||
|
return (
|
||||||
|
<div className={classes.translateBox}>
|
||||||
|
<div>{translateKey(key, type) + ":"}</div>
|
||||||
|
<div className={classes.translateContainer}>{translateValue(key, before, type)}</div>
|
||||||
|
<div style={{ fontSize: "20px" }}>{"→"}</div>
|
||||||
|
<div className={classes.translateContainer}>{translateValue(key, after, type)}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "deleted":
|
||||||
|
return translateKey(key, type);
|
||||||
|
default:
|
||||||
|
let val = translateValue(key, after, type);
|
||||||
|
if (isBool(val)) {
|
||||||
|
return translateKey(key, type);
|
||||||
|
}
|
||||||
|
return translateKey(key, type) + ": " + val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isBool = (val: string) => {
|
||||||
|
return val.toLowerCase() === "true" || val.toLowerCase() === "false";
|
||||||
|
};
|
||||||
|
|
||||||
|
const icon = (changeType: "added" | "deleted" | "updated") => {
|
||||||
|
switch (changeType) {
|
||||||
|
case "added":
|
||||||
|
return <Add />;
|
||||||
|
case "deleted":
|
||||||
|
return <Remove />;
|
||||||
|
default:
|
||||||
|
return <ChangeHistory />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const inlineItem = (key: any, changeType: "added" | "deleted" | "updated") => {
|
||||||
|
if (key !== "key") {
|
||||||
|
return (
|
||||||
|
<Grid key={key} style={{ minWidth: "8rem" }}>
|
||||||
|
{key !== "key" && (
|
||||||
|
<Chip
|
||||||
|
className={
|
||||||
|
changeType === "added"
|
||||||
|
? classes.added
|
||||||
|
: changeType === "deleted"
|
||||||
|
? classes.deleted
|
||||||
|
: classes.updated
|
||||||
|
}
|
||||||
|
icon={icon(changeType)}
|
||||||
|
size="small"
|
||||||
|
style={{ height: "100%", margin: "auto", padding: "0.25rem" }}
|
||||||
|
label={<div style={{ whiteSpace: "pre" }}>{translate(key, changeType)}</div>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMobile = useMobile();
|
||||||
|
|
||||||
|
const listItem = (key: any, changeType: "added" | "deleted" | "updated") => {
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={key}
|
||||||
|
className={
|
||||||
|
changeType === "added"
|
||||||
|
? classes.added
|
||||||
|
: changeType === "deleted"
|
||||||
|
? classes.deleted
|
||||||
|
: classes.updated
|
||||||
|
}>
|
||||||
|
<div style={{ marginRight: "1rem" }}>{icon(changeType)}</div>
|
||||||
|
{isMobile ? (
|
||||||
|
<div style={{ fontSize: "12px" }}>{translate(key, changeType)}</div>
|
||||||
|
) : (
|
||||||
|
<ListItemText>{translate(key, changeType)}</ListItemText>
|
||||||
|
)}
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (variant) {
|
||||||
|
case "inline":
|
||||||
|
if (changes <= 0) {
|
||||||
|
return <Chip className={classes.resynced} icon={<Sync />} size="small" label="Resynced" />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Grid container direction="row" spacing={1} className={classes.chipContainer}>
|
||||||
|
{added &&
|
||||||
|
Object.keys(added).length > 0 &&
|
||||||
|
Object.keys(added).map(key => inlineItem(key, "added"))}
|
||||||
|
{deleted &&
|
||||||
|
Object.keys(deleted).length > 0 &&
|
||||||
|
Object.keys(deleted).map(key => inlineItem(key, "deleted"))}
|
||||||
|
{updated &&
|
||||||
|
Object.keys(updated).length > 0 &&
|
||||||
|
Object.keys(updated).map(key => inlineItem(key, "updated"))}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
if (changes <= 0) {
|
||||||
|
return (
|
||||||
|
<List disablePadding>
|
||||||
|
<ListItem key={"resynced"} className={classes.resynced}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Sync />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>
|
||||||
|
Setting were resynced to ensure consistency with the cloud
|
||||||
|
</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<List disablePadding>
|
||||||
|
{added && Object.keys(added).length > 0 && (
|
||||||
|
<React.Fragment>{Object.keys(added).map(key => listItem(key, "added"))}</React.Fragment>
|
||||||
|
)}
|
||||||
|
{deleted && Object.keys(deleted).length > 0 && (
|
||||||
|
<React.Fragment>
|
||||||
|
{Object.keys(deleted).map(key => listItem(key, "deleted"))}
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
{updated && Object.keys(updated).length > 0 && (
|
||||||
|
<React.Fragment>
|
||||||
|
{Object.keys(updated).map(key => listItem(key, "updated"))}
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Record {
|
||||||
|
timestamp: string;
|
||||||
|
data: any;
|
||||||
|
user: string;
|
||||||
|
status: string;
|
||||||
|
type?: pond.ObjectType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListResult {
|
||||||
|
records: Record[];
|
||||||
|
offset: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
name: string;
|
||||||
|
kind: string;
|
||||||
|
showTitle: boolean;
|
||||||
|
list: (limit: number, offset: number) => Promise<ListResult>;
|
||||||
|
translateKey: (key: keyof any, type?: pond.ObjectType) => string;
|
||||||
|
translateValue: (key: keyof any, obj: any, type?: pond.ObjectType) => string;
|
||||||
|
headerStyle?: React.CSSProperties;
|
||||||
|
cellStyle?: React.CSSProperties | ((data: RowData[], rowData: RowData) => React.CSSProperties);
|
||||||
|
noPaging?: boolean;
|
||||||
|
sortingEnabled?: boolean;
|
||||||
|
filteringEnabled?: boolean;
|
||||||
|
drawer?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DiffHistory(props: Props) {
|
||||||
|
const {
|
||||||
|
kind,
|
||||||
|
name,
|
||||||
|
headerStyle,
|
||||||
|
cellStyle,
|
||||||
|
list,
|
||||||
|
translateKey,
|
||||||
|
translateValue,
|
||||||
|
showTitle,
|
||||||
|
noPaging,
|
||||||
|
sortingEnabled,
|
||||||
|
filteringEnabled,
|
||||||
|
drawer
|
||||||
|
} = props;
|
||||||
|
const userAPI = useUserAPI();
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
const [profiles, setProfiles] = useState(new Map<string, pond.UserProfile>());
|
||||||
|
const isMobile = useMobile();
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [data, setData] = useState<RowData[]>([]);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
new Promise(resolve => {
|
||||||
|
list(pageSize, page * pageSize)
|
||||||
|
.then((res: ListResult) => {
|
||||||
|
let records = res.records;
|
||||||
|
let uniqueUsers = Array.from(
|
||||||
|
new Set(records.map((record: Record) => record.user).filter(id => !profiles.has(id)))
|
||||||
|
);
|
||||||
|
let req =
|
||||||
|
uniqueUsers.length > 0
|
||||||
|
? userAPI.getProfiles(uniqueUsers)
|
||||||
|
: new Promise<pond.UserProfile[]>(resolve => resolve([]));
|
||||||
|
req
|
||||||
|
.then((res: pond.UserProfile[]) => {
|
||||||
|
res.forEach((profile: pond.UserProfile) => profiles.set(profile.id, profile));
|
||||||
|
setProfiles(profiles);
|
||||||
|
})
|
||||||
|
.catch((err: any) => console.log(err))
|
||||||
|
.finally(() => {
|
||||||
|
let data: RowData[] = [];
|
||||||
|
for (let i = 0; i < records.length; i++) {
|
||||||
|
let record = records[i];
|
||||||
|
let timestamp = record.timestamp;
|
||||||
|
let user = record.user;
|
||||||
|
//let typeString = record.type ? ObjectDescriber(record.type).name : kind;
|
||||||
|
let before = {};
|
||||||
|
if (i < records.length - 1) {
|
||||||
|
if (record.type) {
|
||||||
|
let found = false;
|
||||||
|
let k = i;
|
||||||
|
while (!found && k < records.length - 1) {
|
||||||
|
let nextRecord = records[k + 1];
|
||||||
|
if (record.type === nextRecord.type) {
|
||||||
|
if (record.type === pond.ObjectType.OBJECT_TYPE_DEVICE) {
|
||||||
|
if (record.data.deviceId === nextRecord.data.deviceId) {
|
||||||
|
before = nextRecord.data;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
record.type === pond.ObjectType.OBJECT_TYPE_COMPONENT ||
|
||||||
|
record.type === pond.ObjectType.OBJECT_TYPE_INTERACTION
|
||||||
|
) {
|
||||||
|
if (record.data.key === nextRecord.data.key) {
|
||||||
|
before = nextRecord.data;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
before = records[i + 1].data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let after = record.data;
|
||||||
|
let diff = detailedDiff(before, after) as Diff;
|
||||||
|
let changes = countNumChanges(diff);
|
||||||
|
let type = record.type;
|
||||||
|
let status = record.status;
|
||||||
|
|
||||||
|
data.push({
|
||||||
|
timestamp,
|
||||||
|
user,
|
||||||
|
changes,
|
||||||
|
before,
|
||||||
|
after,
|
||||||
|
diff,
|
||||||
|
status,
|
||||||
|
type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setData(data);
|
||||||
|
setTotal(res.total);
|
||||||
|
resolve({ data: data, page: page, totalCount: res.total });
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
setData([]);
|
||||||
|
resolve({ data: [], page: 0, totalCount: 0 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [pageSize, page, kind, list, profiles, userAPI]);
|
||||||
|
|
||||||
|
const countNumChanges = (diff: Diff): number => {
|
||||||
|
const { added, updated, deleted } = diff;
|
||||||
|
let count = 0;
|
||||||
|
count = count + Object.keys(added).length;
|
||||||
|
count = count + Object.keys(deleted).length;
|
||||||
|
count = count + Object.keys(updated).length;
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProfile = (id: string): pond.UserProfile => {
|
||||||
|
if (id === "system") {
|
||||||
|
return pond.UserProfile.create({ id: "system", name: "System" });
|
||||||
|
}
|
||||||
|
let profile = profiles.get(id);
|
||||||
|
return profile ? profile : pond.UserProfile.create();
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = (): Column<RowData>[] => {
|
||||||
|
return [{
|
||||||
|
title: "Time",
|
||||||
|
render: (row: RowData) => (
|
||||||
|
<div style={isMobile || drawer ? { minWidth: "4rem" } : {}}>
|
||||||
|
{moment(row.timestamp).calendar()}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
// defaultSort: "asc",
|
||||||
|
// cellStyle: cellStyle,
|
||||||
|
// customSort: (a, b) => moment(b.timestamp).diff(moment(a.timestamp))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Object Type",
|
||||||
|
render: row => {
|
||||||
|
if (row.type) {
|
||||||
|
return (<div>ObjectDescriber(row.type).name</div>)
|
||||||
|
}
|
||||||
|
return <div></div>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "User",
|
||||||
|
// cellStyle: cellStyle,
|
||||||
|
// field: "user",
|
||||||
|
render: row => {
|
||||||
|
const profile = getProfile(row.user);
|
||||||
|
const avatarURL = profile.avatar;
|
||||||
|
const name = profile.name ? profile.name : "User " + profile.id;
|
||||||
|
const avatar = avatarURL ? (
|
||||||
|
<Avatar alt={name} src={avatarURL} />
|
||||||
|
) : (
|
||||||
|
<Avatar alt={name}>
|
||||||
|
<Face />
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div style={isMobile || drawer ? { minWidth: "4rem" } : {}}>
|
||||||
|
<Chip variant="outlined" avatar={avatar} label={name} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// customSort: (a, b) => a.user.localeCompare(b.user)
|
||||||
|
//customFilterAndSearch: (filt: string, row: RowData) => row.user.toLowerCase().includes(filt.toLowerCase())
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Status",
|
||||||
|
render: row => <div>row.status</div>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Summary",
|
||||||
|
// cellStyle: cellStyle,
|
||||||
|
// hidden: isMobile || drawer,
|
||||||
|
render: row => (
|
||||||
|
<Summary
|
||||||
|
variant="inline"
|
||||||
|
kind={kind}
|
||||||
|
changes={row.changes}
|
||||||
|
before={row.before}
|
||||||
|
after={row.after}
|
||||||
|
diff={row.diff}
|
||||||
|
translateKey={translateKey}
|
||||||
|
translateValue={translateValue}
|
||||||
|
type={row.type}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
// sorting: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRowsPerPageChange = (event: any) => {
|
||||||
|
const newRowsPerPage = parseInt(event.target.value, 10); // Get selected value
|
||||||
|
setPageSize(newRowsPerPage);
|
||||||
|
setPage(0); // Reset to the first page
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveTable<RowData>
|
||||||
|
title={name + " History"}
|
||||||
|
rows={data}
|
||||||
|
columns={columns()}
|
||||||
|
total={0}
|
||||||
|
pageSize={0}
|
||||||
|
page={0}
|
||||||
|
setPage={setPage}
|
||||||
|
handleRowsPerPageChange={handleRowsPerPageChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <MaterialTable
|
||||||
|
// title={name + " History"}
|
||||||
|
// onChangePage={e => {
|
||||||
|
// setPage(e);
|
||||||
|
// }}
|
||||||
|
// options={{
|
||||||
|
// paging: noPaging ? false : true,
|
||||||
|
// pageSize: pageSize,
|
||||||
|
// pageSizeOptions: [5, 10, 20],
|
||||||
|
// showTitle: showTitle,
|
||||||
|
// toolbar: showTitle,
|
||||||
|
// search: false,
|
||||||
|
// sorting: sortingEnabled ? true : false,
|
||||||
|
// columnsButton: showTitle,
|
||||||
|
// header: true,
|
||||||
|
// headerStyle: headerStyle,
|
||||||
|
// filtering: filteringEnabled
|
||||||
|
// }}
|
||||||
|
// localization={{
|
||||||
|
// body: { emptyDataSourceMessage: "No records found" }
|
||||||
|
// }}
|
||||||
|
// onChangeRowsPerPage={pageSize => setPageSize(pageSize)}
|
||||||
|
// columns={[
|
||||||
|
// {
|
||||||
|
// title: "Time",
|
||||||
|
// render: (row: RowData) => (
|
||||||
|
// <div style={isMobile || drawer ? { minWidth: "4rem" } : {}}>
|
||||||
|
// {moment(row.timestamp).calendar()}
|
||||||
|
// </div>
|
||||||
|
// ),
|
||||||
|
// defaultSort: "asc",
|
||||||
|
// cellStyle: cellStyle,
|
||||||
|
// customSort: (a, b) => moment(b.timestamp).diff(moment(a.timestamp))
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "Object Type",
|
||||||
|
// render: row => {
|
||||||
|
// if (row.type) {
|
||||||
|
// return ObjectDescriber(row.type).name;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "User",
|
||||||
|
// cellStyle: cellStyle,
|
||||||
|
// field: "user",
|
||||||
|
// render: row => {
|
||||||
|
// const profile = getProfile(row.user);
|
||||||
|
// const avatarURL = profile.avatar;
|
||||||
|
// const name = profile.name ? profile.name : "User " + profile.id;
|
||||||
|
// const avatar = avatarURL ? (
|
||||||
|
// <Avatar alt={name} src={avatarURL} />
|
||||||
|
// ) : (
|
||||||
|
// <Avatar alt={name}>
|
||||||
|
// <Face />
|
||||||
|
// </Avatar>
|
||||||
|
// );
|
||||||
|
// return (
|
||||||
|
// <div style={isMobile || drawer ? { minWidth: "4rem" } : {}}>
|
||||||
|
// <Chip variant="outlined" avatar={avatar} label={name} />
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// customSort: (a, b) => a.user.localeCompare(b.user)
|
||||||
|
// //customFilterAndSearch: (filt: string, row: RowData) => row.user.toLowerCase().includes(filt.toLowerCase())
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "Status",
|
||||||
|
// render: row => row.status
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "Summary",
|
||||||
|
// cellStyle: cellStyle,
|
||||||
|
// hidden: isMobile || drawer,
|
||||||
|
// render: row => (
|
||||||
|
// <Summary
|
||||||
|
// variant="inline"
|
||||||
|
// kind={kind}
|
||||||
|
// changes={row.changes}
|
||||||
|
// before={row.before}
|
||||||
|
// after={row.after}
|
||||||
|
// diff={row.diff}
|
||||||
|
// translateKey={translateKey}
|
||||||
|
// translateValue={translateValue}
|
||||||
|
// type={row.type}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
// sorting: false
|
||||||
|
// }
|
||||||
|
// ]}
|
||||||
|
// icons={getTableIcons()}
|
||||||
|
// data={data}
|
||||||
|
// page={page}
|
||||||
|
// totalCount={total}
|
||||||
|
// detailPanel={row => {
|
||||||
|
// return (
|
||||||
|
// <Summary
|
||||||
|
// kind={kind}
|
||||||
|
// changes={row.changes}
|
||||||
|
// before={row.before}
|
||||||
|
// after={row.after}
|
||||||
|
// diff={row.diff}
|
||||||
|
// translateKey={translateKey}
|
||||||
|
// translateValue={translateValue}
|
||||||
|
// type={row.type}
|
||||||
|
// />
|
||||||
|
// );
|
||||||
|
// }}
|
||||||
|
// onRowClick={(event, row, togglePanel) => togglePanel && togglePanel()}
|
||||||
|
// />
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { Box } from "@material-ui/core";
|
import { Box } from "@mui/material";
|
||||||
import { useComponentAPI } from "hooks";
|
import { useComponentAPI } from "hooks";
|
||||||
import { Component, Device } from "models";
|
import { Component, Device } from "models";
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
import React from "react";
|
|
||||||
import { or } from "utils/types";
|
import { or } from "utils/types";
|
||||||
import { TranslateKey, TranslateValue } from "pbHelpers/Component";
|
import { TranslateKey, TranslateValue } from "pbHelpers/Component";
|
||||||
import DiffHistory, { ListResult, Record } from "common/DiffHistory";
|
import DiffHistory, { ListResult, Record } from "common/DiffHistory";
|
||||||
|
|
|
||||||
65
src/device/DeviceHistory.tsx
Normal file
65
src/device/DeviceHistory.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import { useDeviceAPI } from "hooks";
|
||||||
|
import { Device } from "models";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { or } from "utils/types";
|
||||||
|
import { TranslateKey, TranslateValue } from "pbHelpers/Device";
|
||||||
|
import DiffHistory, { ListResult, Record } from "common/DiffHistory";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
device: Device;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DeviceHistory(props: Props) {
|
||||||
|
const deviceAPI = useDeviceAPI();
|
||||||
|
|
||||||
|
let list = (limit: number, offset: number): Promise<ListResult> => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
deviceAPI
|
||||||
|
.listHistory(props.device.id(), limit, offset)
|
||||||
|
.then((res: any) => {
|
||||||
|
let records: Record[] = or(res.data.history, []).map((record: any) => {
|
||||||
|
//console.log(record)
|
||||||
|
return {
|
||||||
|
timestamp: or(record.timestamp, ""),
|
||||||
|
user: or(record.user, ""),
|
||||||
|
data: or(record.device, {}),
|
||||||
|
status: or(record.progress, "Unknown")
|
||||||
|
} as Record;
|
||||||
|
});
|
||||||
|
resolve({
|
||||||
|
records: records,
|
||||||
|
total: or(res.data.total, 0),
|
||||||
|
offset: or(res.data.nextOffset, 0)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((_err: any) => {
|
||||||
|
resolve({
|
||||||
|
records: [] as Record[],
|
||||||
|
total: 0,
|
||||||
|
offset: 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let translateKey = (key: keyof any): string => {
|
||||||
|
return TranslateKey(key as keyof pond.DeviceSettings);
|
||||||
|
};
|
||||||
|
|
||||||
|
let translateValue = (key: keyof any, obj: any): string => {
|
||||||
|
return TranslateValue(key as keyof pond.DeviceSettings, pond.DeviceSettings.fromObject(obj));
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<DiffHistory
|
||||||
|
name={props.device.name()}
|
||||||
|
kind="device"
|
||||||
|
list={list}
|
||||||
|
translateKey={translateKey}
|
||||||
|
translateValue={translateValue}
|
||||||
|
showTitle={true}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Suspense } from "react";
|
import { lazy, Suspense } from "react";
|
||||||
import LoadingScreen from "../app/LoadingScreen";
|
import LoadingScreen from "../app/LoadingScreen";
|
||||||
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||||
import { Typography } from "@mui/material";
|
import { Typography } from "@mui/material";
|
||||||
|
|
@ -14,6 +14,8 @@ 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";
|
||||||
|
|
||||||
|
const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
toggleTheme: () => void;
|
toggleTheme: () => void;
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +72,11 @@ export default function Router(props: Props) {
|
||||||
path="/:deviceID" // "/settings/basic"
|
path="/:deviceID" // "/settings/basic"
|
||||||
element={<DevicePage />}
|
element={<DevicePage />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
|
path="/:deviceID/history" // "/settings/basic"
|
||||||
|
element={<DeviceHistory />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
path="/:deviceID/*" // "/settings/basic"
|
path="/:deviceID/*" // "/settings/basic"
|
||||||
element={<RelativeRoutes />}
|
element={<RelativeRoutes />}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
976
src/objects/ObjectAlerts.tsx
Normal file
976
src/objects/ObjectAlerts.tsx
Normal file
|
|
@ -0,0 +1,976 @@
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Checkbox,
|
||||||
|
createStyles,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
FormHelperText,
|
||||||
|
FormLabel,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
InputAdornment,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
ListSubheader,
|
||||||
|
makeStyles,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
TextField,
|
||||||
|
Theme,
|
||||||
|
Tooltip,
|
||||||
|
Typography
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { ExpandMore } from "@material-ui/icons";
|
||||||
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||||
|
import { Option } from "common/SearchSelect";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { Component, Interaction } from "models";
|
||||||
|
import moment from "moment";
|
||||||
|
import { getFriendlyName, getMeasurements } from "pbHelpers/ComponentType";
|
||||||
|
import { Measurement, Operator } from "pbHelpers/Enums";
|
||||||
|
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { quack } from "protobuf-ts/quack";
|
||||||
|
import { useGlobalState, useInteractionsAPI, useSnackbar } from "providers";
|
||||||
|
import { useNotificationAPI } from "providers/pond/notificationAPI";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import RemoveIcon from "@material-ui/icons/RemoveCircle";
|
||||||
|
import AddIcon from "@material-ui/icons/AddCircle";
|
||||||
|
import { green, red } from "@material-ui/core/colors";
|
||||||
|
import { capitalize, cloneDeep } from "lodash";
|
||||||
|
import { timeOfDayDescriptor } from "pbHelpers/Interaction";
|
||||||
|
import { TimePicker } from "@material-ui/pickers";
|
||||||
|
import { getThemeType } from "theme";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
linkedComponents: Map<string, Component>;
|
||||||
|
componentDevices: Map<string, number>;
|
||||||
|
objectType: pond.ObjectType;
|
||||||
|
objectKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Alert {
|
||||||
|
conditions: pond.InteractionCondition[];
|
||||||
|
components: Component[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => {
|
||||||
|
return createStyles({
|
||||||
|
borderedContainer: {
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
border: "1px solid rgba(255, 255, 255, 0.12)",
|
||||||
|
borderRadius: "4px"
|
||||||
|
},
|
||||||
|
greenButton: {
|
||||||
|
color: green["600"]
|
||||||
|
},
|
||||||
|
redButton: {
|
||||||
|
color: red["600"]
|
||||||
|
},
|
||||||
|
noPadding: {
|
||||||
|
padding: 0
|
||||||
|
},
|
||||||
|
timeRange: {
|
||||||
|
fontSize: "0.875em",
|
||||||
|
marginTop: "20px"
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)",
|
||||||
|
padding: 5
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)",
|
||||||
|
padding: 5
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function ObjectAlerts(props: Props) {
|
||||||
|
const { linkedComponents, componentDevices, objectKey, objectType } = props;
|
||||||
|
const classes = useStyles();
|
||||||
|
const { openSnack } = useSnackbar();
|
||||||
|
const [{ as }] = useGlobalState();
|
||||||
|
const interactionsAPI = useInteractionsAPI();
|
||||||
|
const [interactions, setInteractions] = useState<Interaction[]>([]);
|
||||||
|
const [typeOptions, setTypeOptions] = useState<Option[]>([]);
|
||||||
|
const [alerts, setAlerts] = useState<Alert[]>([]);
|
||||||
|
//the interaction key to the component it is set on
|
||||||
|
const [interactionComponent, setInteractionComponent] = useState<Map<string, string>>(new Map());
|
||||||
|
|
||||||
|
//state variables for notifications
|
||||||
|
const notificationAPI = useNotificationAPI();
|
||||||
|
const [recentNotifications, setRecentNotifications] = useState<pond.Notification[]>([]);
|
||||||
|
const [notificationsLoading, setNotificationsLoading] = useState(false);
|
||||||
|
const [totalNotifications, setTotalNotifications] = useState(0);
|
||||||
|
|
||||||
|
//stat variables for adding new alerts
|
||||||
|
const [newAlertOpen, setNewAlertOpen] = useState(false);
|
||||||
|
const [selectedAlertComponents, setSelectedAlertComponents] = useState<string[]>([]);
|
||||||
|
const [newAlertComponentType, setNewAlertComponentType] = useState<quack.ComponentType>(
|
||||||
|
quack.ComponentType.COMPONENT_TYPE_INVALID
|
||||||
|
);
|
||||||
|
const [conditions, setConditions] = useState<pond.InteractionCondition[]>([]);
|
||||||
|
const [nodeOptions, setNodeOptions] = useState<JSX.Element[]>([]);
|
||||||
|
const [subtypeDropdown, setSubtypeDropdown] = useState<number>(0);
|
||||||
|
//condition value strings - so that decimals are easier to type into the field
|
||||||
|
const [valStrings, setValStrings] = useState(["0", "0"]);
|
||||||
|
//the nodes for the subnode interactions
|
||||||
|
const [nodeOne, setNodeOne] = useState(0);
|
||||||
|
const [nodeTwo, setNodeTwo] = useState(0);
|
||||||
|
|
||||||
|
//schedule variables
|
||||||
|
const [schedule, setSchedule] = useState(
|
||||||
|
pond.InteractionSchedule.create({
|
||||||
|
weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//loop through the linked components to get each of their interactions
|
||||||
|
//TODO-CS: consider making an api function to get the interactions for multiple components, possibly based off objects, in one call,
|
||||||
|
//to get all interactions for bin components this would solve the issue of making multiple calls as well as not having to use async await
|
||||||
|
let newInteractions: Interaction[] = [];
|
||||||
|
let icMap: Map<string, string> = new Map();
|
||||||
|
let includedOps: quack.ComponentType[] = [];
|
||||||
|
let typeOps: Option[] = [];
|
||||||
|
let index = 0;
|
||||||
|
//use async to make sure to wait for the response from the api call and have the interactions set before going to get the next one
|
||||||
|
//this prevents seperate responses from assigning to the array without knowing about interactions from those seperate responses
|
||||||
|
//for example: without async two responses come back at the same time, one has two interactions and another has one but, neither have put their interactions
|
||||||
|
//into the array yet so they both reference an empty array, the first response puts its two into an empty array and sets it but then the next one also puts
|
||||||
|
//its response into an empty array as well and sets it causing the first two to be lost
|
||||||
|
linkedComponents.forEach(async (comp, key) => {
|
||||||
|
index++;
|
||||||
|
//get the interactions for the component
|
||||||
|
let device = componentDevices.get(key);
|
||||||
|
if (device !== undefined) {
|
||||||
|
await interactionsAPI.listInteractionsByComponent(device, comp.location()).then(resp => {
|
||||||
|
if (resp.length > 0) {
|
||||||
|
resp.forEach(interaction => {
|
||||||
|
icMap.set(interaction.key(), key);
|
||||||
|
});
|
||||||
|
newInteractions = newInteractions.concat(resp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//set the state variables if it is the last one in the list
|
||||||
|
if (index === linkedComponents.size) {
|
||||||
|
setInteractionComponent(icMap);
|
||||||
|
setInteractions([...newInteractions]);
|
||||||
|
}
|
||||||
|
//use the component type to the type options if it is not already present
|
||||||
|
if (!includedOps.includes(comp.type())) {
|
||||||
|
includedOps.push(comp.type());
|
||||||
|
typeOps.push({
|
||||||
|
label: getFriendlyName(comp.type()),
|
||||||
|
value: comp.type()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setTypeOptions(typeOps);
|
||||||
|
}, [linkedComponents, interactionsAPI, componentDevices]);
|
||||||
|
|
||||||
|
const matchConditions = (interaction: Interaction, alert: Alert) => {
|
||||||
|
//if the number of conditions are not the same they are different
|
||||||
|
if (interaction.settings.conditions.length !== alert.conditions.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//continure with the comparison
|
||||||
|
let matching = true;
|
||||||
|
interaction.settings.conditions.forEach((condition, i) => {
|
||||||
|
if (
|
||||||
|
condition.comparison !== alert.conditions[i].comparison ||
|
||||||
|
condition.measurementType !== alert.conditions[i].measurementType ||
|
||||||
|
condition.value !== alert.conditions[i].value
|
||||||
|
) {
|
||||||
|
matching = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return matching;
|
||||||
|
};
|
||||||
|
|
||||||
|
//build the alerts to display
|
||||||
|
useEffect(() => {
|
||||||
|
//loop through the interactions
|
||||||
|
let currentAlerts: Alert[] = [];
|
||||||
|
interactions.forEach(interaction => {
|
||||||
|
//if the interaction sends notifications
|
||||||
|
if (interaction.settings.notifications && interaction.settings.notifications.notify) {
|
||||||
|
//determine if the interaction already has an alert in the alerts array
|
||||||
|
let similarAlert: number = -1;
|
||||||
|
currentAlerts.forEach((alert, i) => {
|
||||||
|
if (matchConditions(interaction, alert)) {
|
||||||
|
similarAlert = i;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//if no current alert matched the conditions add a new alert to the array
|
||||||
|
let compKey = interactionComponent.get(interaction.key());
|
||||||
|
if (compKey) {
|
||||||
|
let component = linkedComponents.get(compKey);
|
||||||
|
if (component) {
|
||||||
|
if (similarAlert === -1) {
|
||||||
|
let newAlert: Alert = {
|
||||||
|
conditions: interaction.settings.conditions,
|
||||||
|
components: [component]
|
||||||
|
};
|
||||||
|
currentAlerts.push(newAlert);
|
||||||
|
} else {
|
||||||
|
currentAlerts[similarAlert].components.push(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setAlerts(currentAlerts);
|
||||||
|
}, [interactions, interactionComponent, linkedComponents]);
|
||||||
|
|
||||||
|
//get the notifications for the components on an object
|
||||||
|
useEffect(() => {
|
||||||
|
if (notificationsLoading) return;
|
||||||
|
setNotificationsLoading(true);
|
||||||
|
notificationAPI
|
||||||
|
.listObjectNotifications(objectKey, objectType, 10, 0, undefined, undefined, undefined, as)
|
||||||
|
.then(resp => {
|
||||||
|
setRecentNotifications(resp.data.notifications);
|
||||||
|
setTotalNotifications(resp.data.total);
|
||||||
|
})
|
||||||
|
.catch(err => {})
|
||||||
|
.finally(() => {
|
||||||
|
setNotificationsLoading(false);
|
||||||
|
});
|
||||||
|
}, [objectKey, objectType, notificationAPI]); //eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
//use the selected components to determine the lowest amount of nodes for the options
|
||||||
|
useEffect(() => {
|
||||||
|
setNodeOne(0);
|
||||||
|
setNodeTwo(0);
|
||||||
|
setSubtypeDropdown(0);
|
||||||
|
let nodeCount = 12; //start with the maximum number of nodes for a cable
|
||||||
|
linkedComponents.forEach(comp => {
|
||||||
|
if (selectedAlertComponents.includes(comp.key())) {
|
||||||
|
if (comp.lastMeasurement[0] && comp.lastMeasurement[0].values[0]) {
|
||||||
|
let nodes = comp.lastMeasurement[0].values[0].values.length;
|
||||||
|
nodeCount = nodes < nodeCount ? nodes : nodeCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let options = [];
|
||||||
|
for (let i = 0; i <= nodeCount; i++) {
|
||||||
|
options.push(
|
||||||
|
<MenuItem key={i} value={i}>
|
||||||
|
{i === 0 ? "None" : "Node " + i}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setNodeOptions(options);
|
||||||
|
}, [linkedComponents, selectedAlertComponents]);
|
||||||
|
|
||||||
|
const loadMoreNotifications = () => {
|
||||||
|
console.log("load more");
|
||||||
|
let current = recentNotifications;
|
||||||
|
setNotificationsLoading(true);
|
||||||
|
notificationAPI
|
||||||
|
.listObjectNotifications(
|
||||||
|
objectKey,
|
||||||
|
objectType,
|
||||||
|
10,
|
||||||
|
current.length,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
as
|
||||||
|
)
|
||||||
|
.then(resp => {
|
||||||
|
if (resp.data.notifications.length > 0) {
|
||||||
|
let c = current.concat(resp.data.notifications);
|
||||||
|
setRecentNotifications([...c]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {})
|
||||||
|
.finally(() => {
|
||||||
|
setNotificationsLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const readableCondition = (condition: pond.InteractionCondition) => {
|
||||||
|
let describer = describeMeasurement(condition.measurementType);
|
||||||
|
let type = describer.label();
|
||||||
|
let comparison =
|
||||||
|
condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO
|
||||||
|
? "Exactly"
|
||||||
|
: condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
|
||||||
|
? "Above"
|
||||||
|
: "Below";
|
||||||
|
let value = describer.toDisplay(condition.value);
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography>{type + " " + comparison + " " + value + describer.GetUnit()}</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const alertAccordion = (alert: Alert) => {
|
||||||
|
return (
|
||||||
|
<Accordion style={{ width: "100%" }}>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||||
|
<Grid container direction="column">
|
||||||
|
{alert.conditions.map((condition, i) => (
|
||||||
|
<Grid item key={i}>
|
||||||
|
{readableCondition(condition)}
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<List>
|
||||||
|
<ListSubheader>Reporting Components</ListSubheader>
|
||||||
|
{alert.components.map((comp, i) => (
|
||||||
|
<ListItem key={i}>{comp.name()}</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addInteractionToComponents = () => {
|
||||||
|
//array of device and component ids ie, [4:9-5-12, 4:9-5-13, 3:9-5-12]
|
||||||
|
let compIds: string[] = [];
|
||||||
|
selectedAlertComponents.forEach(comp => {
|
||||||
|
let devID = componentDevices.get(comp);
|
||||||
|
let component = linkedComponents.get(comp);
|
||||||
|
if (devID && component) {
|
||||||
|
compIds.push(devID + ":" + component.locationString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let newAlert = pond.InteractionSettings.create();
|
||||||
|
newAlert.conditions = conditions;
|
||||||
|
newAlert.instance = 1;
|
||||||
|
newAlert.schedule = schedule;
|
||||||
|
newAlert.subtype = Interaction.create().subtypeFromNodes(nodeOne, nodeTwo);
|
||||||
|
newAlert.result = pond.InteractionResult.create({
|
||||||
|
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT
|
||||||
|
});
|
||||||
|
newAlert.notifications = pond.InteractionNotifications.create({
|
||||||
|
notify: true
|
||||||
|
});
|
||||||
|
interactionsAPI
|
||||||
|
.addInteractionToComponents(compIds, newAlert)
|
||||||
|
.then(resp => {
|
||||||
|
openSnack("interaction added to selected components");
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
openSnack("there was a problem adding the interaction to the selected components");
|
||||||
|
});
|
||||||
|
setNewAlertOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
//display the components that match the type that was selected for the new alert
|
||||||
|
const listMatchingComponents = () => {
|
||||||
|
let matching: Component[] = [];
|
||||||
|
linkedComponents.forEach(comp => {
|
||||||
|
if (comp.type() === newAlertComponentType) {
|
||||||
|
matching.push(comp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<List>
|
||||||
|
{matching.map(comp => (
|
||||||
|
<ListItem key={comp.key()}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedAlertComponents.includes(comp.key())}
|
||||||
|
onChange={(_, checked) => {
|
||||||
|
let selected = selectedAlertComponents;
|
||||||
|
if (checked) {
|
||||||
|
selected.push(comp.key());
|
||||||
|
} else {
|
||||||
|
selected.splice(selected.indexOf(comp.key()), 1);
|
||||||
|
}
|
||||||
|
setSelectedAlertComponents([...selected]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>{comp.name()}</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const availableMeasurementTypes = (type: quack.ComponentType): quack.MeasurementType[] => {
|
||||||
|
return getMeasurements(type).map(m => m.measurementType);
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialConditions = (type: quack.ComponentType): pond.InteractionCondition[] => {
|
||||||
|
let measurementType = availableMeasurementTypes(type)[0];
|
||||||
|
let condition = pond.InteractionCondition.create({
|
||||||
|
measurementType: measurementType,
|
||||||
|
comparison: measurementType === Measurement.boolean ? Operator.equals : Operator.less,
|
||||||
|
value: 0
|
||||||
|
});
|
||||||
|
return [condition];
|
||||||
|
};
|
||||||
|
|
||||||
|
const measurementTypeMenuItems = () => {
|
||||||
|
return availableMeasurementTypes(newAlertComponentType).map(measurementType => (
|
||||||
|
<MenuItem key={measurementType} value={measurementType}>
|
||||||
|
{describeMeasurement(measurementType).label()}
|
||||||
|
</MenuItem>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeCondition = (newCondition: pond.InteractionCondition, index: number) => {
|
||||||
|
let c = conditions;
|
||||||
|
c[index] = newCondition;
|
||||||
|
setConditions([...c]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const conditionGroup = (condition: pond.InteractionCondition, index: number) => {
|
||||||
|
let measurementType = condition.measurementType;
|
||||||
|
let describer = describeMeasurement(measurementType, newAlertComponentType);
|
||||||
|
let isBoolean = condition.measurementType === Measurement.boolean;
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<Grid
|
||||||
|
key={"interaction"}
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
justify="center"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={2}>
|
||||||
|
{index > 0 && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography align="center">AND</Typography>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
<Grid item xs={10} sm={4}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
id="measurementType"
|
||||||
|
required={true}
|
||||||
|
label=""
|
||||||
|
helperText="Measurement"
|
||||||
|
//error={!isMeasurementTypeValid(interaction.settings.conditions[index].measurementType)}
|
||||||
|
//disabled={!isSourceValid(interaction.settings.source) || !canEdit}
|
||||||
|
value={condition.measurementType}
|
||||||
|
onChange={event => {
|
||||||
|
condition.measurementType = +event.target.value;
|
||||||
|
changeCondition(condition, index);
|
||||||
|
}}
|
||||||
|
autoFocus={false}
|
||||||
|
margin="dense"
|
||||||
|
variant="standard"
|
||||||
|
fullWidth
|
||||||
|
InputLabelProps={{ shrink: true }}>
|
||||||
|
{measurementTypeMenuItems()}
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={10} sm={3}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
id="comparison"
|
||||||
|
required={true}
|
||||||
|
label=""
|
||||||
|
helperText="Comparator"
|
||||||
|
//disabled={!isSourceValid(interaction.settings.source) || isBoolean || !canEdit}
|
||||||
|
value={condition.comparison}
|
||||||
|
onChange={event => {
|
||||||
|
condition.comparison = +event.target.value;
|
||||||
|
changeCondition(condition, index);
|
||||||
|
}}
|
||||||
|
autoFocus={false}
|
||||||
|
margin="dense"
|
||||||
|
variant="standard"
|
||||||
|
fullWidth
|
||||||
|
InputLabelProps={{ shrink: true }}>
|
||||||
|
{!isBoolean && [
|
||||||
|
<MenuItem key={2} value={Operator.less}>
|
||||||
|
{"is below"}
|
||||||
|
</MenuItem>,
|
||||||
|
<MenuItem key={3} value={Operator.greater}>
|
||||||
|
{"is above"}
|
||||||
|
</MenuItem>
|
||||||
|
]}
|
||||||
|
<MenuItem key={1} value={Operator.equals}>
|
||||||
|
{"is exactly"}
|
||||||
|
</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={8} sm={4}>
|
||||||
|
<TextField
|
||||||
|
select={isBoolean}
|
||||||
|
id="value"
|
||||||
|
required={true}
|
||||||
|
type={"text"}
|
||||||
|
label=""
|
||||||
|
helperText="Value"
|
||||||
|
error={isNaN(+valStrings[index])}
|
||||||
|
value={valStrings[index]}
|
||||||
|
onChange={event => {
|
||||||
|
let strings = valStrings;
|
||||||
|
strings[index] = event.target.value;
|
||||||
|
setValStrings([...strings]);
|
||||||
|
if (!isNaN(+valStrings[index])) {
|
||||||
|
condition.value = describer.toStored(+event.target.value);
|
||||||
|
changeCondition(condition, index);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus={false}
|
||||||
|
margin="dense"
|
||||||
|
variant="standard"
|
||||||
|
fullWidth
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: <InputAdornment position="end">{describer.unit()}</InputAdornment>
|
||||||
|
}}
|
||||||
|
InputLabelProps={{ shrink: true }}>
|
||||||
|
{isBoolean && [
|
||||||
|
<MenuItem key={0} value={0}>
|
||||||
|
{describer.enumerations()[0]}
|
||||||
|
</MenuItem>,
|
||||||
|
<MenuItem key={1} value={1}>
|
||||||
|
{describer.enumerations()[1]}
|
||||||
|
</MenuItem>
|
||||||
|
]}
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={2} sm={1}>
|
||||||
|
{index === 0 ? (
|
||||||
|
<Tooltip title="Add another condition">
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
disabled={conditions.length > 1}
|
||||||
|
aria-label="Add another condition"
|
||||||
|
onClick={() => {
|
||||||
|
let c = cloneDeep(conditions);
|
||||||
|
let newConditions = c.concat(initialConditions(newAlertComponentType));
|
||||||
|
setConditions(newConditions);
|
||||||
|
}}
|
||||||
|
className={classNames(classes.greenButton, classes.noPadding)}>
|
||||||
|
<AddIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Tooltip title="Remove this condition">
|
||||||
|
<IconButton
|
||||||
|
color="default"
|
||||||
|
//disabled={!canEdit}
|
||||||
|
aria-label="Remove condition"
|
||||||
|
onClick={() => {
|
||||||
|
let c = conditions;
|
||||||
|
c.splice(index, 1);
|
||||||
|
setConditions([...c]);
|
||||||
|
}}
|
||||||
|
className={classNames(classes.redButton, classes.noPadding)}>
|
||||||
|
<RemoveIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
{nodeOptions.length > 2 && (
|
||||||
|
<Grid
|
||||||
|
key={"nodes"}
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
justify="center"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={2}>
|
||||||
|
<Grid item xs={10} sm={4}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
id="subtype"
|
||||||
|
label="Subtype"
|
||||||
|
//disabled={!isSourceValid(interaction.settings.source) || !canEdit}
|
||||||
|
value={subtypeDropdown}
|
||||||
|
onChange={event => {
|
||||||
|
setSubtypeDropdown(+event.target.value);
|
||||||
|
}}
|
||||||
|
margin="dense"
|
||||||
|
variant="standard"
|
||||||
|
fullWidth
|
||||||
|
InputLabelProps={{ shrink: true }}>
|
||||||
|
<MenuItem key={0} value={0}>
|
||||||
|
Any
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem key={1} value={1}>
|
||||||
|
Average
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem key={2} value={2}>
|
||||||
|
Single Node
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem key={3} value={3}>
|
||||||
|
Node Diff
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem key={4} value={4}>
|
||||||
|
Up To
|
||||||
|
</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={10} sm={4}>
|
||||||
|
{(subtypeDropdown === 2 || subtypeDropdown === 3 || subtypeDropdown === 4) && (
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
id="interactionType"
|
||||||
|
label="Node 1"
|
||||||
|
//disabled={!isSourceValid(interaction.settings.source) || !canEdit}
|
||||||
|
value={nodeOne}
|
||||||
|
onChange={event => {
|
||||||
|
setNodeOne(+event.target.value);
|
||||||
|
}}
|
||||||
|
margin="dense"
|
||||||
|
variant="standard"
|
||||||
|
fullWidth
|
||||||
|
InputLabelProps={{ shrink: true }}>
|
||||||
|
{nodeOptions}
|
||||||
|
</TextField>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={10} sm={4}>
|
||||||
|
{subtypeDropdown === 3 && (
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
id="interactionType"
|
||||||
|
label="Node 2"
|
||||||
|
//disabled={!isSourceValid(interaction.settings.source) || !canEdit}
|
||||||
|
value={nodeTwo}
|
||||||
|
onChange={event => {
|
||||||
|
setNodeTwo(+event.target.value);
|
||||||
|
}}
|
||||||
|
margin="dense"
|
||||||
|
variant="standard"
|
||||||
|
fullWidth
|
||||||
|
InputLabelProps={{ shrink: true }}>
|
||||||
|
{nodeOptions}
|
||||||
|
</TextField>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const conditionInput = () => {
|
||||||
|
// const conditionGroups: JSX.Element[] = [];
|
||||||
|
// for (let i = 0; i < conditions.length; i++) {
|
||||||
|
// conditionGroups[i] = conditionGroup(i);
|
||||||
|
// }
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
component={"fieldset" as "div"}
|
||||||
|
className={classes.borderedContainer}
|
||||||
|
fullWidth
|
||||||
|
disabled={selectedAlertComponents.length === 0}>
|
||||||
|
<FormLabel component={"legend" as "caption"}>Conditions</FormLabel>
|
||||||
|
{selectedAlertComponents.length > 0 ? (
|
||||||
|
<React.Fragment>
|
||||||
|
{conditions.map((condition, i) => conditionGroup(condition, i))}
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
<FormHelperText>You must select a source before adding conditions</FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setScheduleTime = (id: string, event: any) => {
|
||||||
|
let updatedSchedule = cloneDeep(schedule);
|
||||||
|
if (id === "shortcut") {
|
||||||
|
let value = event.target.value;
|
||||||
|
if (value === "allDay") {
|
||||||
|
updatedSchedule.timeOfDayStart = "00:00";
|
||||||
|
updatedSchedule.timeOfDayEnd = "24:00";
|
||||||
|
} else if (value === "before") {
|
||||||
|
updatedSchedule.timeOfDayStart = "00:00";
|
||||||
|
if (!updatedSchedule.timeOfDayEnd || updatedSchedule.timeOfDayEnd === "24:00") {
|
||||||
|
updatedSchedule.timeOfDayEnd = "23:59";
|
||||||
|
}
|
||||||
|
} else if (value === "after") {
|
||||||
|
updatedSchedule.timeOfDayEnd = "24:00";
|
||||||
|
if (updatedSchedule.timeOfDayStart === "00:00") {
|
||||||
|
updatedSchedule.timeOfDayStart = "00:01";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (updatedSchedule.timeOfDayStart === "00:00") {
|
||||||
|
updatedSchedule.timeOfDayStart = "00:01";
|
||||||
|
}
|
||||||
|
if (updatedSchedule.timeOfDayEnd === "24:00") {
|
||||||
|
updatedSchedule.timeOfDayEnd = "23:59";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let timeOfDay =
|
||||||
|
event
|
||||||
|
.hour()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0") +
|
||||||
|
":" +
|
||||||
|
event
|
||||||
|
.minute()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0");
|
||||||
|
if (id === "start") {
|
||||||
|
updatedSchedule.timeOfDayStart = timeOfDay;
|
||||||
|
} else if (id === "end") {
|
||||||
|
if (timeOfDay === "00:00") {
|
||||||
|
timeOfDay = "24:00";
|
||||||
|
}
|
||||||
|
updatedSchedule.timeOfDayEnd = timeOfDay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatedSchedule.timezone = moment.tz.guess();
|
||||||
|
setSchedule(updatedSchedule);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDaySelected = (day: string, event: any) => {
|
||||||
|
let updatedSchedule = cloneDeep(schedule);
|
||||||
|
if (event.target.checked) {
|
||||||
|
if (!updatedSchedule.weekdays.includes(day)) {
|
||||||
|
updatedSchedule.weekdays.push(day);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updatedSchedule.weekdays.splice(updatedSchedule.weekdays.indexOf(day), 1);
|
||||||
|
}
|
||||||
|
setSchedule(updatedSchedule);
|
||||||
|
};
|
||||||
|
|
||||||
|
const daySelector = (day: string, size: any) => {
|
||||||
|
//const schedule = or(interaction.settings.schedule, pond.InteractionSchedule.create());
|
||||||
|
return (
|
||||||
|
<Grid item container xs={size} sm={1} justify="center" alignItems="center">
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
id={day}
|
||||||
|
checked={schedule.weekdays.includes(day)}
|
||||||
|
onChange={event => toggleDaySelected(day, event)}
|
||||||
|
value={day}
|
||||||
|
color="secondary"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={capitalize(day.substr(0, 2))}
|
||||||
|
labelPlacement="bottom"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduler = () => {
|
||||||
|
//let schedule = pond.InteractionSchedule.create();
|
||||||
|
let timezone = moment.tz.guess();
|
||||||
|
let todStart = "00:00".split(":");
|
||||||
|
let todEnd = "23:59".split(":");
|
||||||
|
let start = moment
|
||||||
|
.tz(timezone)
|
||||||
|
.startOf("day")
|
||||||
|
.add(todStart[0], "hours")
|
||||||
|
.add(todStart[1], "minutes")
|
||||||
|
.local();
|
||||||
|
let end = moment
|
||||||
|
.tz(timezone)
|
||||||
|
.startOf("day")
|
||||||
|
.add(todEnd[0], "hours")
|
||||||
|
.add(todEnd[1], "minutes")
|
||||||
|
.local();
|
||||||
|
let descriptor = timeOfDayDescriptor(schedule);
|
||||||
|
let showStart = descriptor === "after" || descriptor === "from";
|
||||||
|
let showEnd = descriptor === "before" || descriptor === "from";
|
||||||
|
let showBoth = showStart && showEnd;
|
||||||
|
return (
|
||||||
|
<FormControl component={"fieldset" as "div"} className={classes.borderedContainer} fullWidth>
|
||||||
|
<FormLabel component={"legend" as "caption"}>Schedule</FormLabel>
|
||||||
|
<Grid container direction="row" spacing={0} justify="flex-start">
|
||||||
|
{daySelector("sunday", 3)}
|
||||||
|
{daySelector("monday", 3)}
|
||||||
|
{daySelector("tuesday", 3)}
|
||||||
|
{daySelector("wednesday", 3)}
|
||||||
|
{daySelector("thursday", 4)}
|
||||||
|
{daySelector("friday", 4)}
|
||||||
|
{daySelector("saturday", 4)}
|
||||||
|
</Grid>
|
||||||
|
<Grid container direction="row" spacing={2} alignItems="center">
|
||||||
|
<Grid item xs={12} sm={3}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
id="timeOfDayShortcut"
|
||||||
|
value={descriptor}
|
||||||
|
onChange={(event: any) => setScheduleTime("shortcut", event)}
|
||||||
|
fullWidth
|
||||||
|
autoFocus={false}
|
||||||
|
margin="normal"
|
||||||
|
variant="standard"
|
||||||
|
InputLabelProps={{ shrink: true }}>
|
||||||
|
<MenuItem key="allDay" value="allDay">
|
||||||
|
All Day
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem key="before" value="before">
|
||||||
|
Before
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem key="after" value="after">
|
||||||
|
After
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem key="from" value="from">
|
||||||
|
From
|
||||||
|
</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
{showStart && (
|
||||||
|
<Grid item xs={5} sm={3}>
|
||||||
|
<TimePicker
|
||||||
|
renderInput={props => <TextField {...props} helperText="" />}
|
||||||
|
value={start}
|
||||||
|
onChange={(event: any) => setScheduleTime("start", event)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{showBoth && (
|
||||||
|
<Grid item xs={2} sm={1}>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
color="textSecondary"
|
||||||
|
className={classes.timeRange}
|
||||||
|
align="right">
|
||||||
|
to
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{showEnd && (
|
||||||
|
<Grid item xs={5} sm={3}>
|
||||||
|
<TimePicker
|
||||||
|
renderInput={props => <TextField {...props} helperText="" />}
|
||||||
|
value={end}
|
||||||
|
onChange={(event: any) => setScheduleTime("end", event)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const newAlertDialog = () => {
|
||||||
|
return (
|
||||||
|
<ResponsiveDialog
|
||||||
|
open={newAlertOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setNewAlertOpen(false);
|
||||||
|
}}>
|
||||||
|
<DialogTitle>Set New Alert</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
{/* Dropdown to select the component type to add the interaction to */}
|
||||||
|
<Select
|
||||||
|
id="componentType"
|
||||||
|
label="Component Type"
|
||||||
|
fullWidth
|
||||||
|
displayEmpty
|
||||||
|
value={newAlertComponentType}
|
||||||
|
onChange={e => {
|
||||||
|
setNewAlertComponentType(e.target.value as quack.ComponentType);
|
||||||
|
setSelectedAlertComponents([]);
|
||||||
|
setConditions(initialConditions(e.target.value as quack.ComponentType));
|
||||||
|
}}>
|
||||||
|
<MenuItem key={0} value={0}>
|
||||||
|
Select Component Type
|
||||||
|
</MenuItem>
|
||||||
|
{typeOptions.map(option => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
{/* list of checkboxes for the components that match that type */}
|
||||||
|
{listMatchingComponents()}
|
||||||
|
{/* have the conditions set here */}
|
||||||
|
{conditionInput()}
|
||||||
|
{/* have the schedule set here */}
|
||||||
|
{scheduler()}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setNewAlertOpen(false)} variant="contained">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={selectedAlertComponents.length === 0}
|
||||||
|
onClick={addInteractionToComponents}
|
||||||
|
variant="contained"
|
||||||
|
color="primary">
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</ResponsiveDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const alertDisplay = () => {
|
||||||
|
return (
|
||||||
|
<List style={{ margin: 5 }}>
|
||||||
|
<ListSubheader>Alerts</ListSubheader>
|
||||||
|
{alerts.map((alert, i) => (
|
||||||
|
<ListItem key={i}>{alertAccordion(alert)}</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const notificationList = () => {
|
||||||
|
return (
|
||||||
|
<List style={{ margin: 5 }}>
|
||||||
|
<ListSubheader>Notifications</ListSubheader>
|
||||||
|
{recentNotifications && (
|
||||||
|
<React.Fragment>
|
||||||
|
{recentNotifications.map((notification, i) => (
|
||||||
|
<ListItem key={i} className={i % 2 === 0 ? classes.dark : classes.light}>
|
||||||
|
<Typography style={{ fontWeight: 650 }}>
|
||||||
|
{moment(notification.status?.timestamp).format("MMMM DD, YYYY - h:mm a")}
|
||||||
|
</Typography>{" "}
|
||||||
|
:{" "}
|
||||||
|
<Typography>
|
||||||
|
{notification.settings?.title + " - " + notification.settings?.subtitle}
|
||||||
|
</Typography>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
{recentNotifications.length < totalNotifications && (
|
||||||
|
<ListItem key={"button"}>
|
||||||
|
<Button onClick={loadMoreNotifications} disabled={notificationsLoading}>
|
||||||
|
Get More
|
||||||
|
</Button>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card raised style={{ margin: 5 }}>
|
||||||
|
<Box padding={1} display="flex" justifyContent="space-between">
|
||||||
|
<Typography style={{ fontWeight: 650, fontSize: 25 }}>Alerts & Notifications</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setNewAlertOpen(true);
|
||||||
|
}}>
|
||||||
|
New Alert
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
{alertDisplay()}
|
||||||
|
{notificationList()}
|
||||||
|
{newAlertDialog()}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
746
src/objects/ObjectDescriber.ts
Normal file
746
src/objects/ObjectDescriber.ts
Normal file
|
|
@ -0,0 +1,746 @@
|
||||||
|
import { Option } from "common/SearchSelect";
|
||||||
|
import GrainDescriber from "grain/GrainDescriber";
|
||||||
|
import { Column } from "material-table";
|
||||||
|
import { Bin, BinYard, Field } from "models";
|
||||||
|
import { Gate } from "models/Gate";
|
||||||
|
import { GrainBag } from "models/GrainBag";
|
||||||
|
import { ObjectHeater } from "models/ObjectHeater";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { getDistanceUnit, getTemperatureUnit } from "utils";
|
||||||
|
|
||||||
|
interface Sort {
|
||||||
|
order: string; //what to send to the backend list to sort by
|
||||||
|
numerical: boolean; //whether to use a numerical or text sort
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ObjectExtension {
|
||||||
|
name: string;
|
||||||
|
inventoryGroup: string;
|
||||||
|
isTransactionObject: boolean;
|
||||||
|
//this will define the columns to be used for the object in a material table
|
||||||
|
tableColumns: Column<any>[];
|
||||||
|
//this map will match the title of the column to what the backend needs to perform the ordering
|
||||||
|
tableSort: Map<string, Sort>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultObject: ObjectExtension = {
|
||||||
|
name: "None",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ObjectExtensions: Map<pond.ObjectType, ObjectExtension> = new Map([
|
||||||
|
[pond.ObjectType.OBJECT_TYPE_UNKNOWN, defaultObject],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_BACKPACK,
|
||||||
|
{
|
||||||
|
name: "Backpack",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_BIN,
|
||||||
|
{
|
||||||
|
name: "Bin",
|
||||||
|
inventoryGroup: "grain",
|
||||||
|
isTransactionObject: true,
|
||||||
|
tableColumns: [
|
||||||
|
{
|
||||||
|
title: "Name",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Grain",
|
||||||
|
render: row => {
|
||||||
|
let grain = row.settings.inventory?.grainType;
|
||||||
|
if (grain) {
|
||||||
|
return GrainDescriber(grain).name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Bin Height",
|
||||||
|
render: row => {
|
||||||
|
let d = row.settings.specs?.heightCm;
|
||||||
|
if (d) {
|
||||||
|
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
|
||||||
|
return (d / 100).toFixed(2) + " m";
|
||||||
|
} else {
|
||||||
|
return (d / 30.48).toFixed(2) + " ft";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Bin Diameter",
|
||||||
|
render: row => {
|
||||||
|
let d = row.settings.specs?.diameterCm;
|
||||||
|
if (d) {
|
||||||
|
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
|
||||||
|
return (d / 100).toFixed(2) + " m";
|
||||||
|
} else {
|
||||||
|
return (d / 30.48).toFixed(2) + " ft";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Custom Grain Name",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.inventory?.customTypeName;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Grain Variant",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.inventory?.grainSubtype;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Grain Bushels",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.inventory?.grainBushels;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Grain Capacity",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.specs?.bushelCapacity;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "High Temp Warning",
|
||||||
|
render: row => {
|
||||||
|
let t = row.settings.highTemp;
|
||||||
|
if (t) {
|
||||||
|
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||||
|
return (t * 1.8 + 32).toFixed(2) + " °F";
|
||||||
|
} else {
|
||||||
|
return t.toFixed(2) + " °C";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Low Temp Warning",
|
||||||
|
render: row => {
|
||||||
|
let t = row.settings.lowTemp;
|
||||||
|
if (t) {
|
||||||
|
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||||
|
return (t * 1.8 + 32).toFixed(2) + " °F";
|
||||||
|
} else {
|
||||||
|
return t.toFixed(2) + " °C";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
] as Column<Bin>[],
|
||||||
|
tableSort: new Map([
|
||||||
|
["Name", { order: "name", numerical: false }],
|
||||||
|
["Grain", { order: "inventory.grainType", numerical: false }],
|
||||||
|
["Bin Height", { order: "specs.heightCm", numerical: true }],
|
||||||
|
["Bin Diameter", { order: "specs.diameterCm", numerical: true }],
|
||||||
|
["Custom Grain Name", { order: "inventory.customTypeName", numerical: false }],
|
||||||
|
["Grain Variant", { order: "inventory.grainSubtype", numerical: false }],
|
||||||
|
["Grain Bushels", { order: "inventory.grainBushels", numerical: true }],
|
||||||
|
["Grain Capacity", { order: "specs.bushelCapacity", numerical: true }],
|
||||||
|
["High Temp Warning", { order: "highTemp", numerical: true }],
|
||||||
|
["Low Temp Warning", { order: "lowTemp", numerical: true }]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_BINYARD,
|
||||||
|
{
|
||||||
|
name: "Binyard",
|
||||||
|
inventoryGroup: "grain",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [
|
||||||
|
{
|
||||||
|
title: "Name",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Description",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
] as Column<BinYard>[],
|
||||||
|
tableSort: new Map([
|
||||||
|
["Name", { order: "name", numerical: false }],
|
||||||
|
["Description", { order: "description", numerical: false }]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_COMPONENT,
|
||||||
|
{
|
||||||
|
name: "Component",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_DEVICE,
|
||||||
|
{
|
||||||
|
name: "Device",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_FIELD,
|
||||||
|
{
|
||||||
|
name: "Field",
|
||||||
|
inventoryGroup: "grain",
|
||||||
|
isTransactionObject: true,
|
||||||
|
tableColumns: [
|
||||||
|
{
|
||||||
|
title: "Name",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.fieldName;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Land Location",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.landLocation;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Crop",
|
||||||
|
render: row => {
|
||||||
|
return GrainDescriber(row.settings.crop).name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Custom",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.customGrain;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Grain Variant",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.grainSubtype;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
// title: "Sowing Date",
|
||||||
|
// render: row => {
|
||||||
|
// return row.settings.sowingDate
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
] as Column<Field>[],
|
||||||
|
tableSort: new Map([
|
||||||
|
["Name", { order: "fieldName", numerical: false }],
|
||||||
|
["Land Location", { order: "landLocation", numerical: false }],
|
||||||
|
["Crop", { order: "crop", numerical: false }],
|
||||||
|
["Custom", { order: "customGrain", numerical: false }],
|
||||||
|
["Grain Variant", { order: "grainSubtype", numerical: false }]
|
||||||
|
//["Sowing Date", { order: "sowingDate", numerical: true}]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_FIELDMARKER,
|
||||||
|
{
|
||||||
|
name: "Field Marker",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_FIRMWARE,
|
||||||
|
{
|
||||||
|
name: "Firmware",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_GATE,
|
||||||
|
{
|
||||||
|
name: "Gate",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [
|
||||||
|
{
|
||||||
|
title: "Name",
|
||||||
|
render: row => {
|
||||||
|
if (row.name) {
|
||||||
|
return row.name.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "PCA Type",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.pcaType;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Hourly APU Cost",
|
||||||
|
render: row => {
|
||||||
|
if (row.settings.hourlyApuCost) {
|
||||||
|
return "$" + row.settings.hourlyApuCost.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Hourly PCA Cost",
|
||||||
|
render: row => {
|
||||||
|
if (row.settings.hourlyPcaCost) {
|
||||||
|
return "$" + row.settings.hourlyPcaCost.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Upper Flow",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.upperFlow;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Lower Flow",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.lowerFlow;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Duct Name",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.ductName;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Duct Length",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.ductLength + " m";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Duct Diameter",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.ductDiameter + " mm";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Friction Factor",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.frictionFactor;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Thermal Conductivity",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.thermalConductivity + " W/mK";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Thermal Resistance",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.thermalResistance + " K/W";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
] as Column<Gate>[],
|
||||||
|
tableSort: new Map([
|
||||||
|
["Name", { order: "name", numerical: false }],
|
||||||
|
["PCA Type", { order: "pcaType", numerical: false }],
|
||||||
|
["Hourly APU Cost", { order: "hourlyApuCost", numerical: true }],
|
||||||
|
["Hourly PCA Cost", { order: "hourlyPcaCost", numerical: true }],
|
||||||
|
["Upper Flow", { order: "upperFlow", numerical: true }],
|
||||||
|
["Lower Flow", { order: "lowerFlow", numerical: true }],
|
||||||
|
["Duct Name", { order: "ductName", numerical: false }],
|
||||||
|
["Duct Length", { order: "ductLength", numerical: true }],
|
||||||
|
["Duct Diameter", { order: "ductDiameter", numerical: true }],
|
||||||
|
["Friction Factor", { order: "frictionFactor", numerical: true }],
|
||||||
|
["Thermal Conductivity", { order: "thermalConductivity", numerical: true }],
|
||||||
|
["Thermal Resistance", { order: "thermalResistance", numerical: true }]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_GRAIN_BAG,
|
||||||
|
{
|
||||||
|
name: "Grainbag",
|
||||||
|
inventoryGroup: "grain",
|
||||||
|
isTransactionObject: true,
|
||||||
|
tableColumns: [
|
||||||
|
{
|
||||||
|
title: "Name",
|
||||||
|
render: row => {
|
||||||
|
if (row.title) {
|
||||||
|
return row.title.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Length",
|
||||||
|
render: row => {
|
||||||
|
if (row.settings.length) {
|
||||||
|
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
|
||||||
|
return row.settings.length.toFixed(2) + " m";
|
||||||
|
} else {
|
||||||
|
return (row.settings.length * 3.281).toFixed(2) + " ft";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Diameter",
|
||||||
|
render: row => {
|
||||||
|
if (row.settings.diameter) {
|
||||||
|
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
|
||||||
|
return row.settings.diameter.toFixed(2) + " m";
|
||||||
|
} else {
|
||||||
|
return (row.settings.diameter * 3.281).toFixed(2) + " ft";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Grain",
|
||||||
|
render: row => {
|
||||||
|
if (row.settings.supportedGrain) {
|
||||||
|
return GrainDescriber(row.settings.supportedGrain).name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Custom Grain",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.customGrain;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Grain Variant",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.grainSubtype;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Capacity",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.bushelCapacity + " bu";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Bushels",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.currentBushels + " bu";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Fill Date",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.fillDate;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Initial Moisture",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.initialMoisture + "%";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
] as Column<GrainBag>[],
|
||||||
|
tableSort: new Map([
|
||||||
|
["Name", { order: "name", numerical: false }],
|
||||||
|
["Length", { order: "length", numerical: true }],
|
||||||
|
["Diameter", { order: "diameter", numerical: true }],
|
||||||
|
["Grain", { order: "supportedGrain", numerical: false }],
|
||||||
|
["Custom Grain", { order: "customGrain", numerical: false }],
|
||||||
|
["Grain Variant", { order: "grainSubtype", numerical: false }],
|
||||||
|
["Capacity", { order: "bushelCapacity", numerical: true }],
|
||||||
|
["Bushels", { order: "currentBushels", numerical: true }],
|
||||||
|
["Fill Data", { order: "fillDate", numerical: false }],
|
||||||
|
["Initial Moisture", { order: "initialMoisture", numerical: true }]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_GROUP,
|
||||||
|
{
|
||||||
|
name: "Group",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_HARVESTPLAN,
|
||||||
|
{
|
||||||
|
name: "Harvest Plan",
|
||||||
|
inventoryGroup: "grain",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_HEATER,
|
||||||
|
{
|
||||||
|
name: "Heater",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [
|
||||||
|
{
|
||||||
|
title: "Name",
|
||||||
|
render: row => {
|
||||||
|
if (row.name) {
|
||||||
|
return row.name.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Make",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.make;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Model",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.model;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Fuel Type",
|
||||||
|
render: row => {
|
||||||
|
let fuelMap = new Map<pond.FuelType, string>([
|
||||||
|
[pond.FuelType.FUEL_TYPE_DIESEL, "Diesel"],
|
||||||
|
[pond.FuelType.FUEL_TYPE_GASOLINE, "Gasoline"],
|
||||||
|
[pond.FuelType.FUEL_TYPE_PROPANE, "Propane"]
|
||||||
|
]);
|
||||||
|
return fuelMap.get(row.settings.fuelType);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Tank Size",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.tankSize + " G";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Fuel Consumption",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.fuelConsumption + " G/hr";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Air Circulation",
|
||||||
|
render: row => {
|
||||||
|
return row.settings.airCirculation + " CFM";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
] as Column<ObjectHeater>[],
|
||||||
|
tableSort: new Map([
|
||||||
|
["Name", { order: "name", numerical: false }],
|
||||||
|
["Make", { order: "make", numerical: false }],
|
||||||
|
["Model", { order: "model", numerical: false }],
|
||||||
|
["Fuel Type", { order: "fuelType", numerical: false }],
|
||||||
|
["Tank Size", { order: "tankSize", numerical: true }],
|
||||||
|
["Fuel Consumption", { order: "fuelConsumption", numerical: true }],
|
||||||
|
["Air Circulation", { order: "airCirculation", numerical: true }]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_HOMEMARKER,
|
||||||
|
{
|
||||||
|
name: "Home Marker",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_INTERACTION,
|
||||||
|
{
|
||||||
|
name: "Interaction",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_LINK,
|
||||||
|
{
|
||||||
|
name: "Link",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_MINE,
|
||||||
|
{
|
||||||
|
name: "Mine",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_NOTE,
|
||||||
|
{
|
||||||
|
name: "Note",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_SITE,
|
||||||
|
{
|
||||||
|
name: "Site",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_TAG,
|
||||||
|
{
|
||||||
|
name: "Tag",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_TASK,
|
||||||
|
{
|
||||||
|
name: "Task",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_TEAM,
|
||||||
|
{
|
||||||
|
name: "Team",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_TERMINAL,
|
||||||
|
{
|
||||||
|
name: "Terminal",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_UPGRADE,
|
||||||
|
{
|
||||||
|
name: "Upgrade",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_USER,
|
||||||
|
{
|
||||||
|
name: "User",
|
||||||
|
inventoryGroup: "",
|
||||||
|
isTransactionObject: false,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
pond.ObjectType.OBJECT_TYPE_CONTRACT,
|
||||||
|
{
|
||||||
|
name: "Contract",
|
||||||
|
inventoryGroup: "grain",
|
||||||
|
isTransactionObject: true,
|
||||||
|
tableColumns: [],
|
||||||
|
tableSort: new Map()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default function ObjectDescriber(type: pond.ObjectType): ObjectExtension {
|
||||||
|
let describer = ObjectExtensions.get(type);
|
||||||
|
return describer ? describer : defaultObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* takes in an object type enum and returns the string of that object type that is used in the relative_objects table
|
||||||
|
* ie "Bin" -> "bin" or "Home Marker" -> "home_marker"
|
||||||
|
* @param type pond.ObjectType enum
|
||||||
|
*/
|
||||||
|
export function ObjectTypeString(type: pond.ObjectType): string {
|
||||||
|
return ObjectDescriber(type)
|
||||||
|
.name.toLocaleLowerCase()
|
||||||
|
.replace(/ /g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TransactionOptions(): Option[] {
|
||||||
|
let options: Option[] = [];
|
||||||
|
Object.values(pond.ObjectType).forEach(obj => {
|
||||||
|
if (typeof obj !== "string") {
|
||||||
|
let ext = ObjectDescriber(obj);
|
||||||
|
if (ext.isTransactionObject) {
|
||||||
|
options.push({
|
||||||
|
label: ext.name,
|
||||||
|
value: obj,
|
||||||
|
group: ext.inventoryGroup
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchableObjects(): Option[] {
|
||||||
|
let options: Option[] = [];
|
||||||
|
Object.values(pond.ObjectType).forEach(obj => {
|
||||||
|
if (typeof obj !== "string") {
|
||||||
|
let ext = ObjectDescriber(obj);
|
||||||
|
if (ext.tableColumns.length > 0) {
|
||||||
|
options.push({
|
||||||
|
label: ext.name,
|
||||||
|
value: obj
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
}
|
||||||
378
src/objects/ObjectTable.tsx
Normal file
378
src/objects/ObjectTable.tsx
Normal file
|
|
@ -0,0 +1,378 @@
|
||||||
|
import { Box, Button, Grid } from "@material-ui/core";
|
||||||
|
import { getTableIcons } from "common/ResponsiveTable";
|
||||||
|
import SearchBar from "common/SearchBar";
|
||||||
|
import SearchSelect, { Option } from "common/SearchSelect";
|
||||||
|
import MaterialTable, { MTableToolbar } from "material-table";
|
||||||
|
import { Bin, BinYard, Field } from "models";
|
||||||
|
import { Gate } from "models/Gate";
|
||||||
|
import { GrainBag } from "models/GrainBag";
|
||||||
|
import { ObjectHeater } from "models/ObjectHeater";
|
||||||
|
import ObjectDescriber, { SearchableObjects } from "objects/ObjectDescriber";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import {
|
||||||
|
useBinAPI,
|
||||||
|
useBinYardAPI,
|
||||||
|
useFieldAPI,
|
||||||
|
useGateAPI,
|
||||||
|
useGlobalState,
|
||||||
|
useGrainBagAPI,
|
||||||
|
useObjectHeaterAPI
|
||||||
|
} from "providers";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import TeamSearch from "teams/TeamSearch";
|
||||||
|
import BulkBinSettings from "./bulkEditForms/bulkBinSettings";
|
||||||
|
import BulkGrainBagSettings from "./bulkEditForms/bulkGrainBagSettings";
|
||||||
|
//import BulkGateSettings from "./bulkEditForms/bulkGateSettings";
|
||||||
|
|
||||||
|
interface customButton {
|
||||||
|
label: string;
|
||||||
|
function: (selectedObjects: any[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
startingObject?: pond.ObjectType;
|
||||||
|
showControls?: boolean;
|
||||||
|
preLoadedData?: any[];
|
||||||
|
customButtons?: customButton[];
|
||||||
|
rowClickFunction?: (rowData: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ObjectTable(props: Props) {
|
||||||
|
const { startingObject, showControls, preLoadedData, customButtons, rowClickFunction } = props;
|
||||||
|
const [currentType, setCurrentType] = useState(
|
||||||
|
startingObject ?? pond.ObjectType.OBJECT_TYPE_UNKNOWN
|
||||||
|
);
|
||||||
|
const [selectedType, setSelectedType] = useState<Option | null>();
|
||||||
|
const [objectTotal, setObjectTotal] = useState(0);
|
||||||
|
const [pageSize, setPageSize] = useState(10); //offset would be determined by the pageSize * tablePage, limit is just pageSize
|
||||||
|
const [tablePage, setTablePage] = useState(0);
|
||||||
|
const [tableData, setTableData] = useState<any[]>([]);
|
||||||
|
const [selectedObjects, setSelectedObjects] = useState<any[]>([]);
|
||||||
|
const [currentDirection, setCurrentDirection] = useState<"asc" | "desc">("asc");
|
||||||
|
const [currentSearchText, setCurrentSearchText] = useState<string | undefined>();
|
||||||
|
const [currentOrder, setCurrentOrder] = useState<string | undefined>();
|
||||||
|
const [isNumerical, setIsNumerical] = useState<boolean | undefined>();
|
||||||
|
const [{ user, as }] = useGlobalState();
|
||||||
|
const [selectedUser, setSelectedUser] = useState<string>(as ?? user.id());
|
||||||
|
const [tableLoading, setTableLoading] = useState(false);
|
||||||
|
//the api's to load all the objects available to look at in this table
|
||||||
|
const binAPI = useBinAPI();
|
||||||
|
const binyardAPI = useBinYardAPI();
|
||||||
|
const fieldAPI = useFieldAPI();
|
||||||
|
const gateAPI = useGateAPI();
|
||||||
|
const grainBagAPI = useGrainBagAPI();
|
||||||
|
const heaterAPI = useObjectHeaterAPI();
|
||||||
|
|
||||||
|
const load = useCallback(() => {
|
||||||
|
if (tableLoading) return;
|
||||||
|
/**
|
||||||
|
* switch case to use the correct api to load the object that was selected.
|
||||||
|
* In the future create a generic object api that takes the object type as a variable along with
|
||||||
|
* the rest that the backend then uses to load from the correct place
|
||||||
|
*/
|
||||||
|
//add a new case for each object that will be viewable in the table
|
||||||
|
let search;
|
||||||
|
if (currentSearchText) {
|
||||||
|
search = currentSearchText?.replace(/\s/g, "<->");
|
||||||
|
}
|
||||||
|
if (currentType !== pond.ObjectType.OBJECT_TYPE_UNKNOWN) {
|
||||||
|
setTableLoading(true);
|
||||||
|
}
|
||||||
|
switch (currentType) {
|
||||||
|
case pond.ObjectType.OBJECT_TYPE_BIN:
|
||||||
|
binAPI
|
||||||
|
.listBins(
|
||||||
|
pageSize,
|
||||||
|
pageSize * tablePage,
|
||||||
|
currentDirection,
|
||||||
|
currentOrder,
|
||||||
|
search,
|
||||||
|
selectedUser,
|
||||||
|
undefined,
|
||||||
|
isNumerical
|
||||||
|
)
|
||||||
|
.then(resp => {
|
||||||
|
setTableData(resp.data.bins.map(b => Bin.create(b)));
|
||||||
|
setObjectTotal(resp.data.total);
|
||||||
|
setTableLoading(false);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case pond.ObjectType.OBJECT_TYPE_BINYARD:
|
||||||
|
binyardAPI
|
||||||
|
.listBinYards(
|
||||||
|
pageSize,
|
||||||
|
pageSize * tablePage,
|
||||||
|
currentDirection,
|
||||||
|
currentOrder,
|
||||||
|
search,
|
||||||
|
undefined,
|
||||||
|
selectedUser
|
||||||
|
)
|
||||||
|
.then(resp => {
|
||||||
|
setTableData(resp.data.yard.map(b => BinYard.create(b)));
|
||||||
|
setObjectTotal(resp.data.total);
|
||||||
|
setTableLoading(false);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case pond.ObjectType.OBJECT_TYPE_FIELD:
|
||||||
|
fieldAPI
|
||||||
|
.listFields(
|
||||||
|
pageSize,
|
||||||
|
pageSize * tablePage,
|
||||||
|
currentDirection,
|
||||||
|
currentOrder,
|
||||||
|
search,
|
||||||
|
selectedUser
|
||||||
|
)
|
||||||
|
.then(resp => {
|
||||||
|
setTableData(resp.data.fields.map(f => Field.create(f)));
|
||||||
|
setObjectTotal(resp.data.total);
|
||||||
|
setTableLoading(false);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case pond.ObjectType.OBJECT_TYPE_GATE:
|
||||||
|
gateAPI
|
||||||
|
.listGates(
|
||||||
|
pageSize,
|
||||||
|
pageSize * tablePage,
|
||||||
|
currentDirection,
|
||||||
|
currentOrder,
|
||||||
|
search,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
isNumerical,
|
||||||
|
selectedUser
|
||||||
|
)
|
||||||
|
.then(resp => {
|
||||||
|
setTableData(resp.data.gates.map(g => Gate.create(g)));
|
||||||
|
setObjectTotal(resp.data.total);
|
||||||
|
setTableLoading(false);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case pond.ObjectType.OBJECT_TYPE_GRAIN_BAG:
|
||||||
|
grainBagAPI
|
||||||
|
.listGrainBags(
|
||||||
|
pageSize,
|
||||||
|
pageSize * tablePage,
|
||||||
|
currentDirection,
|
||||||
|
currentOrder,
|
||||||
|
search,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
isNumerical,
|
||||||
|
selectedUser
|
||||||
|
)
|
||||||
|
.then(resp => {
|
||||||
|
setTableData(resp.data.grainBags.map(gb => GrainBag.create(gb)));
|
||||||
|
setObjectTotal(resp.data.total);
|
||||||
|
setTableLoading(false);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case pond.ObjectType.OBJECT_TYPE_HEATER:
|
||||||
|
heaterAPI
|
||||||
|
.listObjectHeaters(
|
||||||
|
pageSize,
|
||||||
|
pageSize * tablePage,
|
||||||
|
currentDirection,
|
||||||
|
currentOrder,
|
||||||
|
search,
|
||||||
|
undefined,
|
||||||
|
selectedUser,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
isNumerical
|
||||||
|
)
|
||||||
|
.then(resp => {
|
||||||
|
setTableData(resp.data.heaters.map(h => ObjectHeater.create(h)));
|
||||||
|
setObjectTotal(resp.data.total);
|
||||||
|
setTableLoading(false);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [
|
||||||
|
currentDirection,
|
||||||
|
currentOrder,
|
||||||
|
currentType,
|
||||||
|
selectedUser,
|
||||||
|
isNumerical,
|
||||||
|
pageSize,
|
||||||
|
tablePage,
|
||||||
|
currentSearchText,
|
||||||
|
binAPI,
|
||||||
|
binyardAPI,
|
||||||
|
fieldAPI,
|
||||||
|
gateAPI,
|
||||||
|
grainBagAPI,
|
||||||
|
heaterAPI
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (preLoadedData) {
|
||||||
|
setTableData(preLoadedData);
|
||||||
|
setObjectTotal(preLoadedData.length);
|
||||||
|
} else {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
}, [load, preLoadedData]);
|
||||||
|
|
||||||
|
const bulkEditForm = (type: pond.ObjectType) => {
|
||||||
|
switch (type) {
|
||||||
|
case pond.ObjectType.OBJECT_TYPE_BIN:
|
||||||
|
return (
|
||||||
|
<BulkBinSettings
|
||||||
|
selectedBins={selectedObjects as pond.Bin[]}
|
||||||
|
refreshCallback={reLoad => {
|
||||||
|
if (reLoad) {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case pond.ObjectType.OBJECT_TYPE_GRAIN_BAG:
|
||||||
|
return (
|
||||||
|
<BulkGrainBagSettings
|
||||||
|
selectedBags={selectedObjects as GrainBag[]}
|
||||||
|
refreshCallback={reLoad => {
|
||||||
|
if (reLoad) {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionToolbar = () => {
|
||||||
|
return (
|
||||||
|
<Box width="100%" padding={2}>
|
||||||
|
<SearchBar
|
||||||
|
value={currentSearchText ?? ""}
|
||||||
|
onChange={val => {
|
||||||
|
setCurrentSearchText(val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{selectedObjects.length > 0 && <Box paddingTop={2}>{bulkEditForm(currentType)}</Box>}
|
||||||
|
<Grid container direction="row" spacing={2} style={{ marginTop: 2 }}>
|
||||||
|
{customButtons &&
|
||||||
|
selectedObjects.length > 0 &&
|
||||||
|
customButtons.map((button, i) => (
|
||||||
|
<Grid item xs={3} key={i}>
|
||||||
|
<Button
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => button.function(selectedObjects)}>
|
||||||
|
{button.label}
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box padding={2}>
|
||||||
|
{showControls && (
|
||||||
|
<Grid container direction="row" spacing={2} style={{ padding: 10 }}>
|
||||||
|
<Grid item>
|
||||||
|
<SearchSelect
|
||||||
|
style={{ width: 200 }}
|
||||||
|
label="From"
|
||||||
|
selected={selectedType}
|
||||||
|
options={SearchableObjects()}
|
||||||
|
changeSelection={op => {
|
||||||
|
setSelectedObjects([]);
|
||||||
|
setSelectedType(op);
|
||||||
|
if (op?.value) {
|
||||||
|
setCurrentType(op.value);
|
||||||
|
setTablePage(0);
|
||||||
|
setCurrentSearchText(undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item style={{ width: 300 }}>
|
||||||
|
<TeamSearch
|
||||||
|
label="User/Team"
|
||||||
|
loadUsers
|
||||||
|
setTeamCallback={user => {
|
||||||
|
setSelectedUser(user);
|
||||||
|
//dispatch({ key: "as", value: user.toString() });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<MaterialTable
|
||||||
|
isLoading={tableLoading}
|
||||||
|
key="objectList"
|
||||||
|
title={ObjectDescriber(currentType).name}
|
||||||
|
icons={getTableIcons()}
|
||||||
|
columns={ObjectDescriber(currentType).tableColumns}
|
||||||
|
data={tableData}
|
||||||
|
page={tablePage}
|
||||||
|
totalCount={objectTotal}
|
||||||
|
onRowClick={
|
||||||
|
rowClickFunction
|
||||||
|
? (_, data) => {
|
||||||
|
rowClickFunction(data);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onChangePage={page => {
|
||||||
|
setTablePage(page);
|
||||||
|
}}
|
||||||
|
onOrderChange={(by, direction) => {
|
||||||
|
if (by !== -1) {
|
||||||
|
let colName = ObjectDescriber(currentType).tableColumns[by].title?.toString();
|
||||||
|
let order;
|
||||||
|
if (colName) {
|
||||||
|
order = ObjectDescriber(currentType).tableSort.get(colName);
|
||||||
|
setCurrentOrder(order?.order);
|
||||||
|
setCurrentDirection(direction);
|
||||||
|
setIsNumerical(order?.numerical);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
paging: true,
|
||||||
|
pageSize: pageSize,
|
||||||
|
pageSizeOptions: [5, 10, 20],
|
||||||
|
padding: "dense",
|
||||||
|
showTitle: true,
|
||||||
|
toolbar: true,
|
||||||
|
sorting: true,
|
||||||
|
columnsButton: true,
|
||||||
|
search: false,
|
||||||
|
selection: true
|
||||||
|
}}
|
||||||
|
onSelectionChange={(data, rowData) => {
|
||||||
|
setSelectedObjects(data);
|
||||||
|
}}
|
||||||
|
// actions={[
|
||||||
|
// {
|
||||||
|
// tooltip: "Update Selected Objects",
|
||||||
|
// icon: "edit",
|
||||||
|
// onClick: (evt, data) => {console.log(data)}
|
||||||
|
// }
|
||||||
|
// ]}
|
||||||
|
onChangeRowsPerPage={pageSize => {
|
||||||
|
setPageSize(pageSize);
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
Toolbar: props => (
|
||||||
|
<React.Fragment>
|
||||||
|
<MTableToolbar {...props} />
|
||||||
|
{actionToolbar()}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
246
src/objects/bulkEditForms/bulkBinSettings.tsx
Normal file
246
src/objects/bulkEditForms/bulkBinSettings.tsx
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
import { Button, Grid, InputAdornment, TextField } from "@material-ui/core";
|
||||||
|
import SearchSelect, { Option } from "common/SearchSelect";
|
||||||
|
import { GrainOptions } from "grain";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { useBinAPI, useSnackbar } from "providers";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { fahrenheitToCelsius, getDistanceUnit, getTemperatureUnit } from "utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
selectedBins: pond.Bin[];
|
||||||
|
refreshCallback: (reLoad: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BulkBinSettings(props: Props) {
|
||||||
|
const { selectedBins, refreshCallback } = props;
|
||||||
|
const binAPI = useBinAPI();
|
||||||
|
const { openSnack } = useSnackbar();
|
||||||
|
const gridItemWidth = 3;
|
||||||
|
// bin settings variables
|
||||||
|
const [name, setName] = useState<string | undefined>();
|
||||||
|
const [grainType, setGrainType] = useState<pond.Grain | undefined>();
|
||||||
|
const [grainOption, setGrainOption] = useState<Option | null>();
|
||||||
|
const [height, setHeight] = useState<number | undefined>();
|
||||||
|
const [diameter, setDiameter] = useState<number | undefined>();
|
||||||
|
const [customGrain, setCustomGrain] = useState<string | undefined>();
|
||||||
|
const [variant, setVariant] = useState<string | undefined>();
|
||||||
|
const [bushels, setBushels] = useState<number | undefined>();
|
||||||
|
const [capacity, setCapacity] = useState<number | undefined>();
|
||||||
|
const [highTemp, setHighTemp] = useState<number | undefined>();
|
||||||
|
const [lowTemp, setLowTemp] = useState<number | undefined>();
|
||||||
|
|
||||||
|
const convertedDistance = (enteredDistance: number) => {
|
||||||
|
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
|
||||||
|
return enteredDistance * 100;
|
||||||
|
}
|
||||||
|
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
|
||||||
|
return enteredDistance * 30.48;
|
||||||
|
}
|
||||||
|
return enteredDistance;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertedTemp = (enteredTemp: number) => {
|
||||||
|
let t = enteredTemp;
|
||||||
|
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||||
|
t = fahrenheitToCelsius(enteredTemp);
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBins = () => {
|
||||||
|
let binsToEdit: pond.BinSettings[] = [];
|
||||||
|
selectedBins.forEach(bin => {
|
||||||
|
if (bin.settings) {
|
||||||
|
if (bin.settings.inventory) {
|
||||||
|
if (grainType) bin.settings.inventory.grainType = grainType;
|
||||||
|
if (bushels) bin.settings.inventory.grainBushels = bushels;
|
||||||
|
if (customGrain) bin.settings.inventory.customTypeName = customGrain;
|
||||||
|
if (variant) bin.settings.inventory.grainSubtype = variant;
|
||||||
|
}
|
||||||
|
if (bin.settings.specs) {
|
||||||
|
if (height) bin.settings.specs.heightCm = convertedDistance(height);
|
||||||
|
if (diameter) bin.settings.specs.diameterCm = convertedDistance(diameter);
|
||||||
|
if (capacity) bin.settings.specs.bushelCapacity = capacity;
|
||||||
|
}
|
||||||
|
if (name) bin.settings.name = name;
|
||||||
|
if (highTemp) bin.settings.highTemp = convertedTemp(highTemp);
|
||||||
|
if (lowTemp) bin.settings.lowTemp = convertedTemp(lowTemp);
|
||||||
|
binsToEdit.push(bin.settings);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
binAPI
|
||||||
|
.bulkBinUpdate(binsToEdit)
|
||||||
|
.then(resp => {
|
||||||
|
if (resp.data.successfull > 0) {
|
||||||
|
refreshCallback(true);
|
||||||
|
openSnack("Successfully updated " + resp.data.successfull + " bins");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
openSnack("Failed to update bins");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Grid container direction="row" spacing={2} alignContent="center" alignItems="center">
|
||||||
|
{/* first row */}
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Bin Name"
|
||||||
|
value={name ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setName(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<SearchSelect
|
||||||
|
label="Grain Type"
|
||||||
|
selected={grainOption}
|
||||||
|
changeSelection={option => {
|
||||||
|
let newGrainType = option
|
||||||
|
? pond.Grain[option.value as keyof typeof pond.Grain]
|
||||||
|
: pond.Grain.GRAIN_INVALID;
|
||||||
|
setGrainType(newGrainType);
|
||||||
|
setGrainOption(option);
|
||||||
|
}}
|
||||||
|
group
|
||||||
|
options={GrainOptions()}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Grain Variant"
|
||||||
|
value={variant ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setVariant(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
{/* second row */}
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Custom Grain"
|
||||||
|
value={customGrain ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setCustomGrain(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Bushels"
|
||||||
|
type="number"
|
||||||
|
value={bushels ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setBushels(parseFloat(e.target.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Capacity"
|
||||||
|
type="number"
|
||||||
|
value={capacity ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setCapacity(parseFloat(e.target.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
{/* last row */}
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Bin Height"
|
||||||
|
type="number"
|
||||||
|
value={height ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setHeight(parseFloat(e.target.value));
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
{getDistanceUnit() !== pond.DistanceUnit.DISTANCE_UNIT_FEET ? "m" : "ft"}
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Bin Diameter"
|
||||||
|
type="number"
|
||||||
|
value={diameter ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setDiameter(parseFloat(e.target.value));
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
{getDistanceUnit() !== pond.DistanceUnit.DISTANCE_UNIT_FEET ? "m" : "ft"}
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="High Temp Warning"
|
||||||
|
type="number"
|
||||||
|
value={highTemp ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setHighTemp(parseFloat(e.target.value));
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
{getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
|
||||||
|
? "°F"
|
||||||
|
: "°C"}
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Low Temp Warning"
|
||||||
|
type="number"
|
||||||
|
value={lowTemp ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setLowTemp(parseFloat(e.target.value));
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
{getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
|
||||||
|
? "°F"
|
||||||
|
: "°C"}
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
updateBins();
|
||||||
|
}}>
|
||||||
|
Update selected Bins
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
207
src/objects/bulkEditForms/bulkGrainBagSettings.tsx
Normal file
207
src/objects/bulkEditForms/bulkGrainBagSettings.tsx
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
import { Button, Grid, TextField } from "@material-ui/core";
|
||||||
|
import SearchSelect, { Option } from "common/SearchSelect";
|
||||||
|
import { GrainOptions } from "grain";
|
||||||
|
import { GrainBag } from "models/GrainBag";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { useGrainBagAPI, useSnackbar } from "providers";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { getDistanceUnit } from "utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
selectedBags: GrainBag[];
|
||||||
|
refreshCallback: (reLoad: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BulkGrainBagSettings(props: Props) {
|
||||||
|
const { selectedBags, refreshCallback } = props;
|
||||||
|
const gridItemWidth = 3;
|
||||||
|
const grainBagAPI = useGrainBagAPI();
|
||||||
|
const { openSnack } = useSnackbar();
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [length, setLength] = useState<number | undefined>(); //stored in meters
|
||||||
|
const [diameter, setDiameter] = useState<number | undefined>(); //stored in meters
|
||||||
|
const [grainType, setGrainType] = useState<pond.Grain | undefined>();
|
||||||
|
const [grainOption, setGrainOption] = useState<Option | null>();
|
||||||
|
const [customGrain, setCustomGrain] = useState<string | undefined>();
|
||||||
|
const [variant, setVariant] = useState<string | undefined>();
|
||||||
|
const [capacity, setCapacity] = useState<number | undefined>();
|
||||||
|
const [bushels, setBushels] = useState<number | undefined>();
|
||||||
|
const [fillDate, setFillDate] = useState<string | undefined>();
|
||||||
|
const [initialMoisture, setInitialMoisture] = useState<number | undefined>();
|
||||||
|
|
||||||
|
const convertedDistance = (enteredDistance: number) => {
|
||||||
|
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
|
||||||
|
return enteredDistance / 3.281;
|
||||||
|
}
|
||||||
|
return enteredDistance;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBags = () => {
|
||||||
|
let bagsToUpdate: pond.GrainBag[] = [];
|
||||||
|
selectedBags.forEach(b => {
|
||||||
|
let bag = pond.GrainBag.create({
|
||||||
|
key: b.key(),
|
||||||
|
name: b.name(),
|
||||||
|
settings: b.settings
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bag.settings) {
|
||||||
|
if (name) bag.name = name;
|
||||||
|
if (length) bag.settings.length = convertedDistance(length);
|
||||||
|
if (diameter) bag.settings.diameter = convertedDistance(diameter); //diameter
|
||||||
|
if (grainType) bag.settings.supportedGrain = grainType; //grain Type
|
||||||
|
if (customGrain) bag.settings.customGrain = customGrain; //custom grain
|
||||||
|
if (variant) bag.settings.grainSubtype = variant; //variant
|
||||||
|
if (capacity) bag.settings.bushelCapacity = capacity; //capacity
|
||||||
|
if (bushels) bag.settings.currentBushels = bushels; //bushels
|
||||||
|
if (fillDate) bag.settings.fillDate = fillDate; //fill date
|
||||||
|
if (initialMoisture) bag.settings.initialMoisture = initialMoisture; //initial moisture
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
grainBagAPI
|
||||||
|
.bulkUpdateGrainBags(bagsToUpdate)
|
||||||
|
.then(resp => {
|
||||||
|
if (resp.data.successfull > 0) {
|
||||||
|
refreshCallback(true);
|
||||||
|
openSnack("Successfully updated " + resp.data.successfull + " bins");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
openSnack("Failed to update bins");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Grid container direction="row" spacing={2} alignContent="center" alignItems="center">
|
||||||
|
{/* first row */}
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Name"
|
||||||
|
value={name ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setName(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Length"
|
||||||
|
type="number"
|
||||||
|
value={length ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setLength(parseFloat(e.target.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Diameter"
|
||||||
|
type="number"
|
||||||
|
value={diameter ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setDiameter(parseFloat(e.target.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Capacity"
|
||||||
|
type="number"
|
||||||
|
value={capacity ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setCapacity(parseFloat(e.target.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* second row */}
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<SearchSelect
|
||||||
|
label="Grain Type"
|
||||||
|
selected={grainOption}
|
||||||
|
changeSelection={option => {
|
||||||
|
let newGrainType = option
|
||||||
|
? pond.Grain[option.value as keyof typeof pond.Grain]
|
||||||
|
: pond.Grain.GRAIN_INVALID;
|
||||||
|
setGrainType(newGrainType);
|
||||||
|
setGrainOption(option);
|
||||||
|
}}
|
||||||
|
group
|
||||||
|
options={GrainOptions()}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Grain Variant"
|
||||||
|
value={variant ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setVariant(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Custom Grain"
|
||||||
|
value={customGrain ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setCustomGrain(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Bushels"
|
||||||
|
type="number"
|
||||||
|
value={bushels ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setBushels(parseFloat(e.target.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* last row */}
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
type="date"
|
||||||
|
label="Fill Date"
|
||||||
|
value={fillDate ?? ""}
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
onChange={e => setFillDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Initial Moisture"
|
||||||
|
type="number"
|
||||||
|
value={initialMoisture ?? ""}
|
||||||
|
onChange={e => {
|
||||||
|
setInitialMoisture(parseFloat(e.target.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={gridItemWidth}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
updateBags();
|
||||||
|
}}>
|
||||||
|
Update selected Grain Bags
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
src/pages/DeviceHistory.tsx
Normal file
54
src/pages/DeviceHistory.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Theme,
|
||||||
|
Toolbar
|
||||||
|
} from "@mui/material";
|
||||||
|
import SmartBreadcrumb from "common/SmartBreadcrumb";
|
||||||
|
import { useDeviceAPI, useSnackbar } from "hooks";
|
||||||
|
import { Device } from "models";
|
||||||
|
import PageContainer from "pages/PageContainer";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import DeviceHistory from "device/DeviceHistory";
|
||||||
|
import { makeStyles } from "@mui/styles";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => {
|
||||||
|
return ({
|
||||||
|
gutter: {
|
||||||
|
marginBottom: theme.spacing(1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
|
||||||
|
export default function DeviceHistoryPage(_props: Props) {
|
||||||
|
const classes = useStyles();
|
||||||
|
const { error } = useSnackbar();
|
||||||
|
const deviceAPI = useDeviceAPI();
|
||||||
|
const deviceID = useParams<{ deviceID: string }>()?.deviceID ?? "";
|
||||||
|
const [device, setDevice] = useState(Device.any({ settings: { deviceId: deviceID } }));
|
||||||
|
const deviceName = device.name();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async function load() {
|
||||||
|
await deviceAPI
|
||||||
|
.get(deviceID)
|
||||||
|
.then((response: any) => {
|
||||||
|
setDevice(Device.any(response.data));
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
error(err);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}, [deviceAPI, deviceID, error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<Box className={classes.gutter}>
|
||||||
|
<SmartBreadcrumb deviceName={deviceName} />
|
||||||
|
</Box>
|
||||||
|
<DeviceHistory device={device} />
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { DeveloperBoard as ProvisionIcon, LibraryAdd as CreateGroupIcon, CheckCircle } from "@mui/icons-material";
|
import { DeveloperBoard as ProvisionIcon, LibraryAdd as CreateGroupIcon } from "@mui/icons-material";
|
||||||
import { Box, Card, Chip, CircularProgress, Grid2, IconButton, Tab, Tabs, Theme, Tooltip, Typography } from "@mui/material";
|
import { Box, Chip, CircularProgress, IconButton, Tab, Tabs, Theme, Tooltip, Typography } from "@mui/material";
|
||||||
import { blue, green } from "@mui/material/colors";
|
import { blue, green } from "@mui/material/colors";
|
||||||
import { makeStyles } from "@mui/styles";
|
import { makeStyles } from "@mui/styles";
|
||||||
import ResponsiveTable, { Column } from "common/ResponsiveTable";
|
import ResponsiveTable, { Column } from "common/ResponsiveTable";
|
||||||
|
|
@ -16,7 +16,7 @@ import { Device, Group } from "models";
|
||||||
import GroupSettings from "group/GroupSettings";
|
import GroupSettings from "group/GroupSettings";
|
||||||
import SmartBreadcrumb from "common/SmartBreadcrumb";
|
import SmartBreadcrumb from "common/SmartBreadcrumb";
|
||||||
import GroupActions from "group/GroupActions";
|
import GroupActions from "group/GroupActions";
|
||||||
import CircleGraphIcon from "common/CircleGraphIcon";
|
// import CircleGraphIcon from "common/CircleGraphIcon";
|
||||||
import DevicesSummary from "device/DevicesSummary";
|
import DevicesSummary from "device/DevicesSummary";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => {
|
const useStyles = makeStyles((theme: Theme) => {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export interface IUserAPIContext {
|
||||||
) => Promise<AxiosResponse<pond.GetUserWithTeamResponse>>;
|
) => Promise<AxiosResponse<pond.GetUserWithTeamResponse>>;
|
||||||
getProfile: (id: string) => Promise<pond.UserProfile>;
|
getProfile: (id: string) => Promise<pond.UserProfile>;
|
||||||
// getUsers: (ids: string[]) => Promise<User[]>;
|
// getUsers: (ids: string[]) => Promise<User[]>;
|
||||||
// getProfiles: (ids: string[]) => Promise<pond.UserProfile[]>;
|
getProfiles: (ids: string[]) => Promise<pond.UserProfile[]>;
|
||||||
// getUserSettings: (userID: string) => Promise<any>;
|
// getUserSettings: (userID: string) => Promise<any>;
|
||||||
// updateUserSettings: (userID: string, body: pond.IUserSettings) => Promise<any>;
|
// updateUserSettings: (userID: string, body: pond.IUserSettings) => Promise<any>;
|
||||||
// updateUserStatus: (userID: string, body: pond.IUserStatus) => Promise<any>;
|
// updateUserStatus: (userID: string, body: pond.IUserStatus) => Promise<any>;
|
||||||
|
|
@ -183,19 +183,19 @@ export default function UserProvider(props: PropsWithChildren<any>) {
|
||||||
// });
|
// });
|
||||||
// };
|
// };
|
||||||
|
|
||||||
// const getProfiles = (ids: string[]): Promise<pond.UserProfile[]> => {
|
const getProfiles = (ids: string[]): Promise<pond.UserProfile[]> => {
|
||||||
// return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// get(pondURL("/profiles/?ids=" + ids.join(",")))
|
get(pondURL("/profiles/?ids=" + ids.join(",")))
|
||||||
// .then((res: any) => {
|
.then((res: any) => {
|
||||||
// let profiles: pond.UserProfile[] = [];
|
let profiles: pond.UserProfile[] = [];
|
||||||
// if (res && res.data && res.data.users) {
|
if (res && res.data && res.data.users) {
|
||||||
// profiles = res.data.users.map((raw: any) => pond.UserProfile.fromObject(raw));
|
profiles = res.data.users.map((raw: any) => pond.UserProfile.fromObject(raw));
|
||||||
// }
|
}
|
||||||
// resolve(profiles);
|
resolve(profiles);
|
||||||
// })
|
})
|
||||||
// .catch((err: any) => reject(err));
|
.catch((err: any) => reject(err));
|
||||||
// });
|
});
|
||||||
// };
|
};
|
||||||
|
|
||||||
// const getUserSettings = (userID: string) => {
|
// const getUserSettings = (userID: string) => {
|
||||||
// return get(pondURL("/users/" + userID + "/settings"));
|
// return get(pondURL("/users/" + userID + "/settings"));
|
||||||
|
|
@ -239,7 +239,7 @@ export default function UserProvider(props: PropsWithChildren<any>) {
|
||||||
getUserWithTeam,
|
getUserWithTeam,
|
||||||
getProfile,
|
getProfile,
|
||||||
// getUsers,
|
// getUsers,
|
||||||
// getProfiles,
|
getProfiles,
|
||||||
// getUserSettings,
|
// getUserSettings,
|
||||||
// updateUserSettings,
|
// updateUserSettings,
|
||||||
// updateUserStatus,
|
// updateUserStatus,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue