This commit is contained in:
Carter 2025-04-14 16:26:56 -06:00
commit b208cd68f4
11 changed files with 1475 additions and 22 deletions

View file

@ -0,0 +1,265 @@
import {
Box,
Button,
DialogActions,
DialogContent,
Grid2 as Grid,
IconButton
} from "@mui/material";
// import { getTableIcons } from "common/ResponsiveTable";
// import MaterialTable, { Column } from "material-table";
import { Bin } from "models";
import { useBinAPI, useGlobalState, useSnackbar } from "providers";
import fans from "fans/fans_client.json";
import React, { useState, useEffect } from "react";
import { getThemeType } from "theme";
import AerationFanIcon from "products/AgIcons/AerationFanIcon";
import FanPicker from "fans/fanPicker";
import ResponsiveDialog from "common/ResponsiveDialog";
import { useMobile } from "hooks";
import { makeStyles } from "@mui/styles";
import ResponsiveTable, { Column } from "common/ResponsiveTable";
import { cloneDeep } from "lodash";
const useStyles = makeStyles(() => ({
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
}
})
);
interface jsonFan {
id: number;
name: string;
kind: string;
rpm: number;
diameter: number;
horsepower: number;
}
interface BinRow {
bin: Bin;
fan?: jsonFan;
}
interface Props {
isLoading: boolean;
bins: Bin[];
getControllerInfo?: (totalControllers: number, onControllers: number) => void;
}
export default function BinsFansStatusTable(props: Props) {
const [{ as }] = useGlobalState();
const binAPI = useBinAPI();
const { bins, isLoading, getControllerInfo } = props;
const { openSnack } = useSnackbar();
const [tableData, setTableData] = useState<BinRow[]>([]);
const [displayedRows, setDisplayedRows] = useState<BinRow[]>([]);
const [fanMap, setFanMap] = useState<Map<number, jsonFan>>(new Map());
const isMobile = useMobile();
const [tablePage, setTablePage] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [openPickerDialog, setOpenPickerDialog] = useState(false);
const [selectedBinRow, setSelectedBinRow] = useState<BinRow | undefined>();
const [newFanID, setNewFanID] = useState<number | undefined>();
const classes = useStyles();
useEffect(()=>{
let clone: BinRow[] = cloneDeep(tableData)
let rows = clone.splice(tablePage * pageSize, pageSize)
setDisplayedRows(rows)
},[tableData, tablePage, pageSize])
//define the columns for the desktop table
const columns: Column<BinRow>[] = [
{
title: "Set Fan",
cellStyle: {padding: 2},
render: (row) => <Box>
<IconButton
onClick={() => {
setSelectedBinRow(row);
setOpenPickerDialog(true);
}}
>
<AerationFanIcon />
</IconButton>
</Box>
},
{
title: "Bin",
cellStyle: {padding: 2},
render: (row) => <Box>{row.bin.settings.name}</Box>
},
{
title: "Bin Fan",
cellStyle: {padding: 2},
hidden: isMobile,
render: row => {
if (row.fan) {
return <Box>{row.fan.name}</Box>
} else {
return <Box>"No Fan Set on Bin"</Box>
}
}
},
{
title: "Fan Type",
cellStyle: {padding: 2},
hidden: isMobile,
render: (row) => <Box>{row.fan?.kind}</Box>
},
{
title: "Horsepower",
cellStyle: {padding: 2},
hidden: isMobile,
render: (row) => <Box>{row.fan?.horsepower}</Box>
},
//the controllers
{
title: "Fan Controllers",
cellStyle: {padding: 2},
render: row => {
if (row.bin.status.fans.length > 0) {
return (
<Grid container direction="column" alignItems="center">
{row.bin.status.fans.map((binFan, i) => (
<Grid
key={i}
container
direction="row"
justifyContent="space-between"
className={i % 2 === 0 ? classes.dark : classes.light}>
<Grid>{binFan.name}</Grid>
<Grid>{binFan.state ? "On" : "Off"}</Grid>
</Grid>
))}
</Grid>
);
} else {
return <Box>No Controllers attached to this bin</Box>
}
}
}
];
useEffect(() => {
//build the fanmap
let fanMap: Map<number, jsonFan> = new Map();
fans["bulk"].forEach(fan => {
fanMap.set(fan.id, fan);
});
setFanMap(fanMap);
let data: BinRow[] = [];
let dataMap: Map<string, BinRow> = new Map();
let totalControllers = 0;
let onControllers = 0;
bins.forEach(bin => {
//put the bin in the row
let row: BinRow = {
bin: bin
};
//if the bin has a fan id set get it from the json and put it in the row data
if (bin.fanID() !== 0) {
row.fan = fanMap.get(bin.fanID());
}
data.push(row);
dataMap.set(bin.key(), row);
//add to the controller numbers
bin.status.fans.forEach(fan => {
//add to the total for each fan
totalControllers++;
//if the fan is on add to the count for controllers that are on
if (fan.state) {
onControllers++;
}
});
});
setTableData(data);
//setTableDataMap(dataMap)
if (getControllerInfo) {
getControllerInfo(totalControllers, onControllers);
}
}, [as, bins, getControllerInfo]);
const updateBinFan = () => {
if (selectedBinRow && newFanID) {
//use the bin api to update the bin in the selected row
let bin = selectedBinRow.bin;
bin.settings.fanId = newFanID;
binAPI
.updateBin(bin.key(), bin.settings)
.then(resp => {
//when an update succeeds update the table with the fan information
//let dataMap = tableDataMap
selectedBinRow.bin = bin;
selectedBinRow.fan = fanMap.get(newFanID);
openSnack("Updated bin with new fan");
//close dialog
setOpenPickerDialog(false);
})
.catch(err => {
openSnack("failed to update bin with new fan");
});
}
};
const fanPickerDialog = () => {
return (
<ResponsiveDialog
open={openPickerDialog}
onClose={() => {
setOpenPickerDialog(false);
}}>
<DialogContent>
<FanPicker
updateFan={fanID => {
setNewFanID(fanID);
}}
useCardView
/>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
setOpenPickerDialog(false);
}}>
Cancel
</Button>
<Button onClick={updateBinFan} variant="contained" color="primary">
Confirm
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
//function to make the table
const fanStatusTable = () => {
return (
<ResponsiveTable
rows={displayedRows}
isLoading={isLoading}
columns={columns}
page={tablePage}
pageSize={pageSize}
total={tableData.length}
setPage={(page)=>{setTablePage(page)}}
handleRowsPerPageChange={(e)=>{setPageSize(e.target.value)}}
/>
);
};
return (
<React.Fragment>
{fanStatusTable()}
{fanPickerDialog()}
</React.Fragment>
);
}

133
src/common/LogCard.tsx Normal file
View file

@ -0,0 +1,133 @@
import {
Box,
Drawer,
Grid2 as Grid,
Theme,
Typography,
useTheme
} from "@mui/material";
import { grey, lightBlue, red, orange } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import moment from "moment";
import { pond } from "protobuf-ts/pond";
import React, { useState } from "react";
interface Props {
index: number;
datalog: pond.Log;
}
const useStyles = makeStyles((theme: Theme) => ({
logbox: {
width: "100%",
padding: theme.spacing(1),
"&:hover": {
filter: "brightness(1.15)",
cursor: "pointer"
}
}
})
);
export default function LogCard(props: Props) {
const { datalog, index } = props;
let log = datalog;
let attributes = JSON.parse(log.attributes);
// let elementKey = event.target.getAttribute('a-key');
const classes = useStyles();
const theme = useTheme();
const [drawer, setDrawer] = useState(false);
if (log.level === undefined) log.level = "info";
let barColor: string = lightBlue[400];
if (log.level.toLowerCase() === "debug") {
barColor = grey[400];
} else if (log.level.toLowerCase() === "warning") {
barColor = orange[500];
} else if (log.level.toLowerCase() === "error") {
barColor = red[500];
} else if (log.level.toLowerCase() === "critical") {
barColor = red["A100"];
}
let bc = index % 2 === 0 ? theme.palette.background.default : theme.palette.background.paper;
const lineSummary = () => {
return (
<Grid container direction="row" justifyContent="space-around" alignContent="center">
<Grid size={2}>
{moment(log.timestamp).format("MMM Do, h:mm:ss a")}
</Grid>
<Grid size={1}>
{log.service}
</Grid>
<Grid size={1}>
{log.device}
</Grid>
<Grid size={2}>
{log.message}
</Grid>
<Grid>
<Typography noWrap variant="body2">
{log.attributes}
</Typography>
</Grid>
</Grid>
);
};
return (
<React.Fragment>
<Drawer open={drawer} onClose={() => setDrawer(false)} anchor="right">
<Box maxWidth={theme.spacing(68)} minWidth={theme.spacing(62)} padding={2}>
<Grid container direction="column" justifyContent="space-evenly">
<Grid container direction="row">
<Grid size={4}>
<Typography>Device:</Typography>
</Grid>
<Grid>
<Typography>{log.device}</Typography>
</Grid>
</Grid>
<Grid container direction="row">
<Grid size={4}>
<Typography>Timestamp:</Typography>
</Grid>
<Grid>
<Typography>{moment(log.timestamp).format("MMMM ddd Do, h:mm:ss a")}</Typography>
</Grid>
</Grid>
<Grid container direction="row">
<Grid size={4}>
<Typography>Level:</Typography>
</Grid>
<Grid>
<Typography>{log.level}</Typography>
</Grid>
</Grid>
<Grid container direction="row">
<Grid size={4}>
<Typography>Service:</Typography>
</Grid>
<Grid>
<Typography>{log.service}</Typography>
</Grid>
</Grid>
</Grid>
{
<Typography variant="caption">
<pre>{JSON.stringify(attributes, null, 2)}</pre>
</Typography>
}
</Box>
</Drawer>
<Box
className={classes.logbox}
style={{ backgroundColor: bc, borderLeft: "2px solid " + barColor }}
onClick={() => setDrawer(true)}>
{lineSummary()}
</Box>
</React.Fragment>
);
}

609
src/common/LogsDisplay.tsx Normal file
View file

@ -0,0 +1,609 @@
import {
Box,
Button,
Checkbox,
CircularProgress,
FormControlLabel,
FormGroup,
Grid,
IconButton,
List,
ListItem,
MenuItem,
TextField,
Theme,
Typography,
useTheme
} from "@mui/material";
import { pond } from "protobuf-ts/pond";
import { useEffect, useState } from "react";
import LogCard from "./LogCard";
import moment from "moment";
import { useDataDogProxyAPI } from "providers";
import { Cell, Legend, Pie, PieChart, ResponsiveContainer } from "recharts";
import { grey, lightBlue, orange, red } from "@mui/material/colors";
import SearchIcon from "@mui/icons-material/Search";
import { makeStyles } from "@mui/styles";
interface DataAmount {
dataAmount: number;
name: string;
color: string;
}
interface Props {
deviceID?: number;
}
const levelOptions = [
{
key: "All",
val: "all"
},
{
key: "Info",
val: "info"
},
{
key: "Debug",
val: "debug"
},
{
key: "Warning",
val: "warning"
},
{
key: "Error",
val: "error"
},
{
key: "Critical",
val: "critical"
}
];
const sortOptions = [
{
key: "Ascending",
val: "asc"
},
{
key: "Descending",
val: "desc"
}
];
const useStyles = makeStyles((theme: Theme) => ({
sidebox: {
borderRadius: 5,
borderColor: theme.palette.text.primary,
border: "1px solid",
padding: theme.spacing(1),
marginRight: theme.spacing(2)
}
})
);
export default function LogsDisplay(props: Props) {
const { deviceID } = props;
const theme = useTheme();
const classes = useStyles();
const [sort, setSort] = useState("desc");
const [limit, setLimit] = useState(50);
const [level, setLevel] = useState("all");
const [service, setService] = useState("all");
const [logs, setLogs] = useState<pond.Log[]>([]);
const [loading, setLoading] = useState(false);
const [device, setDevice] = useState(0);
const [usage, setUsage] = useState<DataAmount[]>([]);
const [customQuery, setCustomQuery] = useState<string>("");
// const [pieLabel, setPieLabel] = useState("");
// const [pieData, setPieData] = useState("");
const DDAPI = useDataDogProxyAPI();
const [from, setFrom] = useState(
moment()
.subtract(1, "day")
.format("YYYY-MM-DD")
);
const [to, setTo] = useState(
moment()
.add("2", "days")
.format("YYYY-MM-DD")
);
const addToQuery = (query: string, input: string) => {
if (query.length > 0) query = query + " AND ";
query = query + input;
return query;
};
const getLogs = () => {
setLoading(true);
let query = "";
if (level !== "all")
query = addToQuery(query, "@level:(" + level + " OR " + level.toUpperCase() + ")");
if (deviceID) {
query = addToQuery(query, "@device:" + deviceID);
} else if (device > 0) {
query = addToQuery(query, "@device:" + device);
}
if (service !== "all") query = addToQuery(query, "service:" + service);
setCustomQuery(query);
DDAPI.listLogs(from, to, sort, limit, query)
.then(resp => {
setLogs(resp.data.logs);
})
.finally(() => {
setLoading(false);
});
};
const getCustomQuery = () => {
setLoading(true);
DDAPI.listLogs(from, to, sort, limit, customQuery)
.then(resp => {
setLogs(resp.data.logs);
})
.finally(() => {
setLoading(false);
});
};
const titleGrid = () => {
return (
<Grid container direction="row" alignItems="center">
<Grid item xs={2}>
<TextField
fullWidth
variant="outlined"
label="From:"
type="date"
InputLabelProps={{
shrink: true
}}
value={from}
onChange={e => setFrom(e.target.value)}
/>
</Grid>
<Grid item xs={2}>
<TextField
fullWidth
variant="outlined"
label="To:"
type="date"
InputLabelProps={{
shrink: true
}}
value={to}
onChange={e => setTo(e.target.value)}
/>
</Grid>
<Grid item xs={2}>
<TextField
fullWidth
variant="outlined"
select
label="level"
value={level}
onChange={event => setLevel(event.target.value)}>
{levelOptions.map(op => (
<MenuItem key={op.key} value={op.val}>
{op.key}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs>
<TextField
fullWidth
variant="outlined"
label="Number of Logs:"
type="number"
InputLabelProps={{
shrink: true
}}
value={limit}
onChange={e => setLimit(+e.target.value)}
/>
</Grid>
<Grid item xs={2}>
<TextField
fullWidth
variant="outlined"
select
label="Sort Order"
value={sort}
onChange={event => setSort(event.target.value)}>
{sortOptions.map(op => (
<MenuItem key={op.key} value={op.val}>
{op.key}
</MenuItem>
))}
</TextField>
</Grid>
{!deviceID && (
<Grid item xs>
<TextField
fullWidth
variant="outlined"
type="number"
label="Device"
value={device}
onChange={event => setDevice(parseInt(event.target.value))}>
{sortOptions.map(op => (
<MenuItem key={op.key} value={op.val}>
{op.key}
</MenuItem>
))}
</TextField>
</Grid>
)}
<Grid item xs style={{ textAlign: "center" }}>
{loading ? (
<CircularProgress />
) : (
<Button variant="contained" color="primary" onClick={getLogs}>
Get Logs
</Button>
)}
</Grid>
</Grid>
);
};
const endAdornment = () => {
// if (loading) return <CircularProgress/>
return (
<IconButton onClick={getCustomQuery} disabled={loading}>
<SearchIcon />
</IconButton>
);
};
const searchBar = () => {
return (
<Grid container direction="row" alignItems="center" style={{ marginTop: theme.spacing(1) }}>
<Grid item xs>
<TextField
fullWidth
variant="outlined"
label="Query:"
InputLabelProps={{
shrink: true
}}
InputProps={{ endAdornment: endAdornment() }}
value={customQuery}
onChange={e => setCustomQuery(e.target.value)}
onKeyPress={e => {
if (e.key === "Enter" && !loading) {
getCustomQuery();
}
}}
/>
</Grid>
</Grid>
);
};
useEffect(() => {
if (logs.length < 1) {
let data = { dataAmount: 100, name: "Dummy", color: theme.palette.background.paper };
setUsage([data]);
return;
}
let debugData = { dataAmount: 0, name: "Debug", color: grey[500] };
let infoData = { dataAmount: 0, name: "Info", color: lightBlue[200] };
let warningData = { dataAmount: 0, name: "Warning", color: orange[500] };
let errorData = { dataAmount: 0, name: "Error", color: red[500] };
let criticalData = { dataAmount: 0, name: "Critical", color: red["A100"] };
logs.forEach(log => {
switch (log.level.toLowerCase()) {
case "debug":
debugData.dataAmount++;
break;
case "info":
infoData.dataAmount++;
break;
case "warning":
warningData.dataAmount++;
break;
case "error":
errorData.dataAmount++;
break;
case "critical":
criticalData.dataAmount++;
break;
}
});
setUsage([debugData, infoData, warningData, errorData, criticalData]);
}, [logs, theme.palette.background.paper]);
const logPieChart = () => {
return (
<Box width={"100%"} style={{ aspectRatio: "1" }} padding={2} paddingLeft={1}>
<ResponsiveContainer>
<PieChart>
<Legend
verticalAlign="middle"
content={() => (
<Box textAlign="center" marginY={0.5}>
{/* <Typography>
hi
</Typography> */}
{logs.length < 1 && <Typography>No Logs</Typography>}
{/* {device.status.bytesTimestamp.length > 1 && (
<Typography>{pieData}</Typography>
)} */}
</Box>
)}
/>
<Pie
data={usage}
dataKey="dataAmount"
nameKey="name"
innerRadius={0}
cx="50%"
cy="50%"
outerRadius={"100%"}
stroke="none"
//cornerRadius={6}
>
{usage.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.color}
style={{ outline: "none !important" }}
onMouseOver={() => {
// setPieLabel(entry.name);
// setPieData(entry.dataAmount.toString());
}}
/>
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</Box>
);
};
// const check = (label: string) => {
// if (service === label) {
// setService("all");
// } else {
// setService(label);
// }
// };
const serviceSelector = () => {
return (
<Box className={classes.sidebox} padding={2} marginTop={1}>
<Typography>Service</Typography>
<FormGroup aria-label="position">
<FormControlLabel
// value="end"
control={
<Checkbox
color="primary"
checked={service === "all"}
onChange={() => setService("all")}
/>
}
label="all"
labelPlacement="end"
/>
<FormControlLabel
// value="end"
control={
<Checkbox
color="primary"
checked={service === "pond"}
onChange={() => setService("pond")}
/>
}
label="pond"
labelPlacement="end"
/>
<FormControlLabel
// value="end"
control={
<Checkbox
color="primary"
checked={service === "quay"}
onChange={() => setService("quay")}
/>
}
label="quay"
labelPlacement="end"
/>
<FormControlLabel
// value="end"
control={
<Checkbox
color="primary"
checked={service === "traefik"}
onChange={() => setService("traefik")}
/>
}
label="traefik"
labelPlacement="end"
/>
<FormControlLabel
// value="end"
control={
<Checkbox
color="primary"
checked={service === "webui"}
onChange={() => setService("webui")}
/>
}
label="webui"
labelPlacement="end"
/>
<FormControlLabel
// value="end"
control={
<Checkbox
color="primary"
checked={service === "agent"}
onChange={() => setService("agent")}
/>
}
label="agent"
labelPlacement="end"
/>
</FormGroup>
</Box>
);
};
const getPercent = (dataNum: number) => {
if (logs.length < 1 || logs.length < dataNum) return 0 + "%";
let newNum = (dataNum / logs.length) * 100;
return newNum.toFixed(0) + "%";
};
const logLevels = () => {
let debugNum = 0;
let infoNum = 0;
let warningNum = 0;
let errorNum = 0;
let criticalNum = 0;
usage.forEach(data => {
switch (data.name) {
case "Debug":
debugNum = data.dataAmount;
break;
case "Info":
infoNum = data.dataAmount;
break;
case "Warning":
warningNum = data.dataAmount;
break;
case "Error":
errorNum = data.dataAmount;
break;
case "Critical":
criticalNum = data.dataAmount;
break;
}
});
return (
<Box className={classes.sidebox}>
<List dense>
<ListItem style={{ display: "flex", float: "left" }}>
<Typography style={{ color: lightBlue[400] }}></Typography>
<Typography variant="caption" style={{ marginLeft: theme.spacing(1) }}>
Info: {infoNum}
</Typography>
<Typography variant="caption" style={{ marginLeft: "auto" }}>
({getPercent(infoNum)})
</Typography>
</ListItem>
<ListItem>
<Typography style={{ color: grey[500] }}></Typography>
<Typography variant="caption" style={{ marginLeft: theme.spacing(1) }}>
Debug: {debugNum}
</Typography>
<Typography variant="caption" style={{ marginLeft: "auto" }}>
({getPercent(debugNum)})
</Typography>
</ListItem>
<ListItem>
<Typography style={{ color: orange[500] }}></Typography>
<Typography variant="caption" style={{ marginLeft: theme.spacing(1) }}>
Warning: {warningNum}
</Typography>
<Typography variant="caption" style={{ marginLeft: "auto" }}>
({getPercent(warningNum)})
</Typography>
</ListItem>
<ListItem>
<Typography style={{ color: red[500] }}></Typography>
<Typography variant="caption" style={{ marginLeft: theme.spacing(1) }}>
Error: {errorNum}
</Typography>
<Typography variant="caption" align="right" style={{ marginLeft: "auto" }}>
({getPercent(errorNum)})
</Typography>
</ListItem>
<ListItem>
<Typography style={{ color: red[900] }}></Typography>
<Typography variant="caption" style={{ marginLeft: theme.spacing(1) }}>
Critical: {criticalNum}
</Typography>
<Typography variant="caption" style={{ marginLeft: "auto" }}>
({getPercent(criticalNum)})
</Typography>
</ListItem>
</List>
</Box>
);
};
return (
<Box style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden" }}>
{/* Title Section */}
<Box margin={2}>
{titleGrid()}
{searchBar()}
</Box>
{/* Header Section */}
<Grid container style={{ height: "100%" }}>
<Grid item xs={2}>
<Typography>Log Metrics</Typography>
{/* Pie chart and service selector will go here */}
{logPieChart()}
{logLevels()}
{/* <br /> */}
{serviceSelector()}
</Grid>
<Grid item xs={10} style={{ height: "100%", overflow: "hidden" }}>
<Grid container direction="row" justifyContent="space-around" alignItems="center">
<Grid item xs={2}>
Timestamp
</Grid>
<Grid item xs={1}>
Service
</Grid>
<Grid item xs={1}>
Device
</Grid>
<Grid item xs={2}>
Message
</Grid>
<Grid item xs zeroMinWidth>
Content
</Grid>
</Grid>
{/* Scrollable List Section */}
<Box
style={{
height: "100%",
overflowY: "auto",
textAlign: logs.length < 1 ? "center" : "left"
}}>
{logs.map((l, i) => (
<LogCard key={i} datalog={l} index={i} />
))}
{logs.length < 1 && (
<Typography
variant="body1"
color="textSecondary"
style={{ marginTop: theme.spacing(12), alignSelf: "center" }}>
Found logs will be displayed here
</Typography>
)}
</Box>
</Grid>
</Grid>
</Box>
);
}

View file

@ -0,0 +1,300 @@
import { Box, Button, DialogActions, DialogContent, DialogTitle } from "@mui/material";
// import MaterialTable from "material-table";
import React, { useEffect, useState } from "react";
import ResponsiveDialog from "./ResponsiveDialog";
// import { getTableIcons } from "./ResponsiveTable";
import { Document, Image, Page, PDFViewer, StyleSheet, Text, View } from "@react-pdf/renderer";
import { useMobile } from "hooks";
import { cloneDeep } from "lodash";
import ResponsiveTable from "./ResponsiveTable";
export interface QrCodeKey {
name: string;
key: string;
}
interface Props {
open: boolean;
close: () => void;
keyList: QrCodeKey[];
requiredUrlAffix: string;
url?: string;
}
export default function(props: Props) {
const { open, close, url, requiredUrlAffix, keyList } = props;
const [selectedKeys, setSelectedKeys] = useState<QrCodeKey[]>([]);
const [selectedPageRows, setSelectedPageRows] = useState<Map<number, number[]>>(new Map())//key is the page value is an array of row indexes for that page
const [tablePage, setTablePage] = useState(0)
const [pageSize, setPageSize] = useState(10)
const isMobile = useMobile();
const [displayedRows, setDisplayedRows] = useState<QrCodeKey[]>([]);
const codesPerPage = 4;
const pdfStyle = StyleSheet.create({
viewerDesktop: {
height: 500 * 1.44,
width: 500
},
viewerMobile: {
height: "100%",
width: "100%"
},
page: {
padding: "3%",
textAlign: "center",
fontFamily: "Helvetica-Bold",
fontSize: 30
},
quadrant: {
width: "50%",
textAlign: "center",
//margin: 10,
borderRadius: 20
},
code: {
flexDirection: "column",
border: "2px dotted black",
margin: 10,
borderRadius: 20
},
image: {
padding: 20,
paddingBottom: 0
//height: "65%"
//border: "1px solid black", //used just to see a representation of where the view is
},
text: {
marginTop: 30,
marginBottom: 30,
textAlign: "center",
justifyContent: "center"
//border: "1px solid black", //used just to see a representation of where the view is
}
});
useEffect(()=>{
let clone: QrCodeKey[] = cloneDeep(keyList)
let rows = clone.splice(tablePage * pageSize, pageSize)
setDisplayedRows(rows)
},[keyList, tablePage, pageSize])
const handleRowsPerPageChange = (event: any) => {
const newRowsPerPage = parseInt(event.target.value, 10); // Get selected value
setPageSize(newRowsPerPage);
setTablePage(0)
//clear the selected rows
setSelectedPageRows(new Map())
//clear the selected keys
setSelectedKeys([])
}
const rowSelected = (row: QrCodeKey, index: number, checked: boolean) => {
//when a row is selected add it to the map for the page
let selected = cloneDeep(selectedKeys)
let pageMap = cloneDeep(selectedPageRows)
let pageSelect = pageMap.get(tablePage)
if(checked){ //add the page/index to the map
if(pageSelect){
pageSelect.push(index)
}else{
pageMap.set(tablePage, [index])
}
//handle the row data to add it to a list of keys that are selected
selected.push(row)
}else{ //remove the page/index from the map
if(pageSelect){
pageSelect.splice(pageSelect.indexOf(index))
}
//remove the selected object from the array
selected.forEach((codeKey, index) => {
if(row.key === codeKey.key){
selected.splice(index)
}
})
}
setSelectedKeys(selected)
setSelectedPageRows(pageMap)
}
const selectionTable = () => {
return (
<React.Fragment>
<ResponsiveTable
columns={[
{
cellStyle: {
padding: 2
},
title: "Name",
render: (row) => <Box>{row.name}</Box>
}
]}
rowSelect={rowSelected}
selectedRows={selectedPageRows.get(tablePage)}
rows={displayedRows}
page={tablePage}
pageSize={pageSize}
total={keyList.length}
setPage={(page) => {setTablePage(page)}}
handleRowsPerPageChange={handleRowsPerPageChange}
/>
{/* <MaterialTable
icons={getTableIcons()}
title={""}
data={keyList}
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) => {
setSelectedKeys(data);
}}
columns={[
{
cellStyle: {
padding: 0
},
title: "Name",
field: "name"
}
]}></MaterialTable> */}
</React.Fragment>
);
};
const generatedCodes = () => {
let numPages = Math.ceil(selectedKeys.length / codesPerPage);
let keys = cloneDeep(selectedKeys);
let pages: JSX.Element[] = [];
let location = url ?? document.URL; //document.URL will append the ending slash to the url string
if (!location.includes(requiredUrlAffix)) {
location = location + requiredUrlAffix;
}
for (let i = 0; i < numPages; i++) {
let pageKeys = keys.splice(0, codesPerPage);
pages.push(
<React.Fragment key={i}>
<View style={{ height: "50%", flexDirection: "row" }}>
{pageKeys[0] && (
<View style={pdfStyle.quadrant}>
<View style={pdfStyle.code}>
<View style={pdfStyle.image}>
<Image
src={
"https://api.qrserver.com/v1/create-qr-code/?data=" +
location +
"/" +
pageKeys[0].key +
"&amp;size=100x100"
}
/>
</View>
<View style={pdfStyle.text}>
<Text>{pageKeys[0].name}</Text>
</View>
</View>
</View>
)}
{pageKeys[1] && (
<View style={pdfStyle.quadrant}>
<View style={pdfStyle.code}>
<View style={pdfStyle.image}>
<Image
src={
"https://api.qrserver.com/v1/create-qr-code/?data=" +
location +
"/" +
pageKeys[1].key +
"&amp;size=100x100"
}
/>
</View>
<View style={pdfStyle.text}>
<Text>{pageKeys[1].name}</Text>
</View>
</View>
</View>
)}
</View>
<View style={{ height: "50%", flexDirection: "row" }}>
{pageKeys[2] && (
<View style={pdfStyle.quadrant}>
<View style={pdfStyle.code}>
<View style={pdfStyle.image}>
<Image
src={
"https://api.qrserver.com/v1/create-qr-code/?data=" +
location +
"/" +
pageKeys[2].key +
"&amp;size=100x100"
}
/>
</View>
<View style={pdfStyle.text}>
<Text>{pageKeys[2].name}</Text>
</View>
</View>
</View>
)}
{pageKeys[3] && (
<View style={pdfStyle.quadrant}>
<View style={pdfStyle.code}>
<View style={pdfStyle.image}>
<Image
src={
"https://api.qrserver.com/v1/create-qr-code/?data=" +
location +
"/" +
pageKeys[3].key +
"&amp;size=100x100"
}
/>
</View>
<View style={pdfStyle.text}>
<Text>{pageKeys[3].name}</Text>
</View>
</View>
</View>
)}
</View>
</React.Fragment>
);
}
return (
<Document>
<Page style={pdfStyle.page}>{pages}</Page>
</Document>
);
};
return (
<ResponsiveDialog open={open} onClose={close}>
<DialogTitle>Generate QR Codes</DialogTitle>
<DialogContent>
{selectionTable()}
<Box marginTop={5}>
<PDFViewer style={isMobile ? pdfStyle.viewerMobile : pdfStyle.viewerDesktop}>
{generatedCodes()}
</PDFViewer>
</Box>
</DialogContent>
<DialogActions>
<Button variant="contained" color="primary" onClick={close}>
Close
</Button>
</DialogActions>
</ResponsiveDialog>
);
}

View file

@ -37,6 +37,8 @@ const BinCableEstimator = lazy(() => import("pages/BinCableEstimator"));
const APIDocs = lazy(() => import("pages/APIDocs"));
const Fields = lazy(()=> import("pages/Fields"));
const Marketplace = lazy(() => import("userFeatures/UserFeatures"))
const Logs = lazy(() => import("pages/Logs"))
export const appendToUrl = (appendage: number | string) => {
const basePath = location.pathname.replace(/\/$/, "");
return(`${basePath}/${appendage}`);
@ -304,6 +306,9 @@ export default function Router() {
{user.hasFeature("developer") &&
<Route path="api" element={<APIDocs />} />
}
{user.hasFeature("admin") &&
<Route path="logs" element={<Logs />} />
}
<Route path="fields" element={<Fields />} />
<Route path="marketplace" element={<Marketplace />} />

View file

@ -43,6 +43,7 @@ import TasksIcon from "products/Construction/TasksIcon";
import CableIcon from "products/Bindapt/CableIcon";
import FieldListIcon from "products/AgIcons/FieldList";
import MarketplaceIcon from "products/CommonIcons/marketplaceIcon";
import DataDuckIcon from "products/Bindapt/DataDuckIcon";
const drawerWidth = 230;
@ -367,6 +368,20 @@ export default function SideNavigator(props: Props) {
</ListItemButton>
</Tooltip>
}
{user.hasFeature("admin") &&
<Tooltip title="Logs" placement="right">
<ListItemButton
id="tour-logs"
onClick={() => goTo("/logs")}
classes={getClasses("/logs")}
>
<ListItemIcon>
<DataDuckIcon />
</ListItemIcon>
{open && <ListItemText primary="Logs" />}
</ListItemButton>
</Tooltip>
}
<Tooltip title="Marketplace" placement="right">
<ListItemButton
id="tour-marketplace"

View file

@ -40,13 +40,13 @@ import {
ViewComfy,
ViewList
} from "@mui/icons-material";
// import BinsFansStatusTable from "bin/BinFansStatusTable";
import BinsFansStatusTable from "bin/BinFansStatusTable";
// import BinSettings from "bin/BinSettings";
import BinsList from "bin/BinsList";
// import BinYard from "bin/BinYard";
import BinInventoryChart, { GrainAmount } from "charts/BinInventoryChart";
import BinUtilizationChart from "charts/BinUtilizationChart";
// import QrCodeGenerator, { QrCodeKey } from "common/QrCodeGenerator";
import QrCodeGenerator, { QrCodeKey } from "common/QrCodeGenerator";
import GrainDescriber, { grainName } from "grain/GrainDescriber";
// import GrainBagList from "grainBag/grainBagList";
// import GrainBagSettings from "grainBag/grainBagSettings";
@ -184,7 +184,7 @@ export default function Bins(props: Props) {
const [expandUtilization, setExpandUtilization] = useState(false);
const [expandFanTable, setExpandFanTable] = useState(false);
const [openQrGenerator, setOpenQrGenerator] = useState(false);
// const [qrKeyList, setQrKeyList] = useState<QrCodeKey[]>([]);
const [qrKeyList, setQrKeyList] = useState<QrCodeKey[]>([]);
const [totalFanControllers, setTotalFanControllers] = useState(0);
const [totalFanControllersOn, setTotalFanControllersOn] = useState(0);
const [yardsLoading, setYardsLoading] = useState(false);
@ -313,7 +313,7 @@ export default function Bins(props: Props) {
let grain: Bin[] = [];
let empty: Bin[] = [];
let fert: Bin[] = [];
// let qr: QrCodeKey[] = [];
let qr: QrCodeKey[] = [];
let b = resp.data.bins.map(b => Bin.any(b));
b.forEach(bin => {
if (bin.empty()) {
@ -324,12 +324,12 @@ export default function Bins(props: Props) {
grain.push(bin);
}
//build qr keys here
// qr.push({
// key: bin.key(),
// name: bin.name()
// });
qr.push({
key: bin.key(),
name: bin.name()
});
// setQrKeyList(qr);
});
setQrKeyList(qr);
setGrainBins(grain);
if (fert.length > 0) {
setDisplayFert(true);
@ -428,7 +428,7 @@ export default function Bins(props: Props) {
let empty: Bin[] = offset ? emptyBins : [];
let fert: Bin[] = offset ? fertilizerBins : [];
let grain: Bin[] = offset ? grainBins : [];
// let newKeyList: QrCodeKey[] = qrKeyList;
let newKeyList: QrCodeKey[] = qrKeyList;
newBins.forEach(bin => {
if (bin.empty()) {
empty.push(bin);
@ -437,10 +437,10 @@ export default function Bins(props: Props) {
} else {
grain.push(bin);
}
// newKeyList.push({
// key: bin.key(),
// name: bin.name()
// });
newKeyList.push({
key: bin.key(),
name: bin.name()
});
});
if (fert.length > 0) {
setDisplayFert(true);
@ -451,7 +451,7 @@ export default function Bins(props: Props) {
setGrainBins([...grain]);
setEmptyBins([...empty]);
setFertilizerBins([...fert]);
// setQrKeyList([...newKeyList]);
setQrKeyList([...newKeyList]);
})
.catch(err => {
setPaginatedBins({ bins: [], binsOffset: 0, binsTotal: 0 });
@ -1013,14 +1013,14 @@ export default function Bins(props: Props) {
</Typography>
</AccordionSummary>
<AccordionDetails>
{/* <BinsFansStatusTable
<BinsFansStatusTable
bins={paginatedBins.bins}
isLoading={binsLoading}
getControllerInfo={(total, on) => {
setTotalFanControllers(total);
setTotalFanControllersOn(on);
}}
/> */}
/>
</AccordionDetails>
</Accordion>
);
@ -1390,14 +1390,14 @@ export default function Bins(props: Props) {
setAddBagOpen(false);
}}
/>
{/* <QrCodeGenerator
<QrCodeGenerator
open={openQrGenerator}
close={() => {
setOpenQrGenerator(false);
}}
keyList={qrKeyList}
requiredUrlAffix={"bins"}
/> */}
/>
{binMenu()}
<AddBinFab onClick={() => setAddBinOpen(true)} pulse={binsTotal < 1 && !allLoading} />
</Box>

15
src/pages/Logs.tsx Normal file
View file

@ -0,0 +1,15 @@
import { Box } from "@mui/material";
import LogsDisplay from "common/LogsDisplay";
import { useMobile } from "hooks";
import PageContainer from "pages/PageContainer";
export default function Logs() {
const isMobile = useMobile();
return (
<PageContainer>
<Box paddingY={isMobile ? 0.5 : 1} paddingX={isMobile ? 1 : 2} height="100%">
<LogsDisplay />
</Box>
</PageContainer>
);
}

View file

@ -33,7 +33,7 @@ export {
// useUsageAPI,
useUserAPI,
// useStripeAPI,
// useDataDogProxyAPI,
useDataDogProxyAPI,
useMineAPI,
// useKeyManagerAPI,
useTerminalAPI, //TODO: update api with resolve,reject

View file

@ -0,0 +1,107 @@
import { AxiosResponse } from "axios";
import { useHTTP } from "hooks";
import { pond } from "protobuf-ts/pond";
import { createContext, PropsWithChildren, useContext } from "react";
import { pondURL } from "./pond";
export interface IDataDogProxyAPIContext {
listLogs: (
from: string,
to: string,
sort: string,
limit: number,
level?: string
) => Promise<AxiosResponse<pond.DataDogLogsResponse>>;
listDeviceLogs: (
device: string | number,
from: string,
to: string,
sort: string,
limit: number,
level?: string
) => Promise<AxiosResponse<pond.DataDogLogsResponse>>;
}
export const DataDogProxyAPIContext = createContext<IDataDogProxyAPIContext>(
{} as IDataDogProxyAPIContext
);
interface Props {}
export default function DataDogProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { get } = useHTTP();
const listLogs = (from: string, to: string, sort: string, limit: number, query?: string) => {
query = query ? query : "";
// if (level && level !== "all") {
// query = query + " @level:" + level;
// }
return get<pond.DataDogLogsResponse>(
pondURL(
"/datadog/logs?query=" +
query +
"&from=" +
from +
"&to=" +
to +
"&sort=" +
sort +
"&limit=" +
limit
)
);
};
const listDeviceLogs = (
device: string | number,
from: string,
to: string,
sort: string,
limit: number,
level?: string
) => {
let query = "@device:" + device;
if (level && level !== "all") {
query = query + " @level:" + level;
}
// console.log(
// "/datadog/logs?query=" +
// query +
// "&from=" +
// from +
// "&to=" +
// to +
// "&sort=" +
// sort +
// "&limit=" +
// limit
// );
return get<pond.DataDogLogsResponse>(
pondURL(
"/datadog/logs?query=" +
query +
"&from=" +
from +
"&to=" +
to +
"&sort=" +
sort +
"&limit=" +
limit
)
);
};
return (
<DataDogProxyAPIContext.Provider
value={{
listLogs,
listDeviceLogs
}}>
{children}
</DataDogProxyAPIContext.Provider>
);
}
export const useDataDogProxyAPI = () => useContext(DataDogProxyAPIContext);

View file

@ -30,6 +30,7 @@ import ObjectHeaterProvider, {useObjectHeaterAPI} from "./ObjectHeaterAPI";
import MutationProvider, {useMutationAPI} from "./mutationAPI"
import PreferenceProvider, {usePreferenceAPI} from "./preferenceAPI";
import TaskProvider, { useTaskAPI } from "./taskAPI";
import DataDogProvider, { useDataDogProxyAPI } from "./datadogProxyAPI";
// import NoteProvider from "providers/noteAPI";
export const pondURL = (partial: string, demo: boolean = false): string => {
@ -76,7 +77,9 @@ export default function PondProvider(props: PropsWithChildren<any>) {
<MutationProvider>
<PreferenceProvider>
<TaskProvider>
<DataDogProvider>
{children}
</DataDogProvider>
</TaskProvider>
</PreferenceProvider>
</MutationProvider>
@ -139,5 +142,6 @@ export {
useObjectHeaterAPI,
useMutationAPI,
usePreferenceAPI,
useTaskAPI
useTaskAPI,
useDataDogProxyAPI
};