merged
This commit is contained in:
commit
b208cd68f4
11 changed files with 1475 additions and 22 deletions
265
src/bin/BinFansStatusTable.tsx
Normal file
265
src/bin/BinFansStatusTable.tsx
Normal 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
133
src/common/LogCard.tsx
Normal 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
609
src/common/LogsDisplay.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
300
src/common/QrCodeGenerator.tsx
Normal file
300
src/common/QrCodeGenerator.tsx
Normal 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 +
|
||||
"&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 +
|
||||
"&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 +
|
||||
"&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 +
|
||||
"&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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 />} />
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
15
src/pages/Logs.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ export {
|
|||
// useUsageAPI,
|
||||
useUserAPI,
|
||||
// useStripeAPI,
|
||||
// useDataDogProxyAPI,
|
||||
useDataDogProxyAPI,
|
||||
useMineAPI,
|
||||
// useKeyManagerAPI,
|
||||
useTerminalAPI, //TODO: update api with resolve,reject
|
||||
|
|
|
|||
107
src/providers/pond/datadogProxyAPI.tsx
Normal file
107
src/providers/pond/datadogProxyAPI.tsx
Normal 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);
|
||||
|
|
@ -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>
|
||||
{children}
|
||||
<DataDogProvider>
|
||||
{children}
|
||||
</DataDogProvider>
|
||||
</TaskProvider>
|
||||
</PreferenceProvider>
|
||||
</MutationProvider>
|
||||
|
|
@ -139,5 +142,6 @@ export {
|
|||
useObjectHeaterAPI,
|
||||
useMutationAPI,
|
||||
usePreferenceAPI,
|
||||
useTaskAPI
|
||||
useTaskAPI,
|
||||
useDataDogProxyAPI
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue