Merge branch 'table_column_resize' into dev_environment
This commit is contained in:
commit
b3313128e8
5 changed files with 168 additions and 46 deletions
14
indexV2.html
Normal file
14
indexV2.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" id="favicon-link" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title id="title-id">Adaptive Dashboard</title>
|
||||
<link rel="manifest" id="manifest-link" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/app/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -61,7 +61,12 @@ http {
|
|||
expires $cache_expires;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri /index.html;
|
||||
try_files $uri /indexV2.html;
|
||||
|
||||
location = /index.html {
|
||||
return 301 /indexV2.html;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
# Healthcheck
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ export default function CableEstimator() {
|
|||
},
|
||||
{
|
||||
title: "Peak Height" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"),
|
||||
sortKey: "PeakHeight",
|
||||
sortKey: "Peak",
|
||||
render: (row: jsonBin) => <Box padding={2}>{distanceConversion(row.Peak).toFixed(2)}</Box>
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Box, Button, Card, Checkbox, CircularProgress, Collapse, darken, Divider, Grid2, IconButton, InputAdornment, ListItemButton, ListItemIcon, ListItemText, Menu, MenuItem, Paper, SxProps, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TextField, Theme, Typography } from "@mui/material";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { ArrowDownward, ArrowUpward, ChevronRight, Close, Search, ViewColumn } from "@mui/icons-material"
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import React from "react";
|
||||
import { useMobile } from "hooks";
|
||||
import classNames from "classnames";
|
||||
|
|
@ -99,7 +99,8 @@ interface Props<T> {
|
|||
order?: string;
|
||||
setOrder?: React.Dispatch<React.SetStateAction<"asc" | "desc">>;
|
||||
endTitleElement?: string | JSX.Element | (() => string | JSX.Element);
|
||||
loadMore?: () => void
|
||||
loadMore?: () => void;
|
||||
resizeable?: boolean;
|
||||
}
|
||||
|
||||
export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
||||
|
|
@ -133,14 +134,15 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
setOrderBy,
|
||||
order,
|
||||
setOrder,
|
||||
loadMore
|
||||
loadMore,
|
||||
// resizeable,
|
||||
} = props
|
||||
const classes = useStyles();
|
||||
const columns = typeof(props.columns) === "function" ? props.columns() : props.columns
|
||||
const subtitle = typeof(props.subtitle) === "function" ? props.subtitle() : props.subtitle
|
||||
const title = typeof(props.title) === "function" ? props.title() : props.title
|
||||
// const order = props.order ? props.order : "asc"
|
||||
const endTitleElement = typeof(props.endTitleElement) === "function" ? props.endTitleElement() : props.endTitleElement
|
||||
const resizeable = props.resizeable === undefined ? false : props.resizeable
|
||||
|
||||
const isMobile = useMobile()
|
||||
|
||||
|
|
@ -149,6 +151,19 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
const [handler, setHandler] = useState<NodeJS.Timeout | undefined>(undefined);
|
||||
const [columnAnchor, setColumnAnchor] = useState<any>(null)
|
||||
|
||||
const rowKey = window.location.pathname.split('/').pop() + "-table-row-widths";
|
||||
const [rowWidths, setRowWidths] = useState<number[]>(() => {
|
||||
const saved = localStorage.getItem(rowKey);
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
})
|
||||
useEffect(() => {
|
||||
localStorage.setItem(rowKey, JSON.stringify(rowWidths));
|
||||
}, [rowWidths])
|
||||
|
||||
const dragIndexRef = useRef<number | undefined>(undefined);
|
||||
const dragStartRef = useRef<number | undefined>(undefined);
|
||||
const rowWidthsRef = useRef<number[]>([]);
|
||||
|
||||
const filterKey = window.location.pathname.split('/').pop() + "-table-filter-list";
|
||||
|
||||
const [filterList, setFilterList] = useState<string[]>(() => {
|
||||
|
|
@ -274,9 +289,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
}
|
||||
|
||||
const renderMobileRow = (row: T, index: number) => {
|
||||
if (renderMobile) return (
|
||||
renderMobile(row, index)
|
||||
)
|
||||
if (renderMobile) return renderMobile(row, index)
|
||||
if (columns) {
|
||||
return (
|
||||
<Grid2 container direction={"row"} spacing={1} >
|
||||
|
|
@ -299,8 +312,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
}
|
||||
return (
|
||||
Object.keys(row).map((key, index) => {
|
||||
let value = row[key as keyof typeof row]
|
||||
|
||||
const value = row[key as keyof typeof row]
|
||||
return (
|
||||
<Grid2 container direction={"row"} key={"mobile-list-"+index} spacing={1} padding={1}>
|
||||
<Grid2 size={{ xs: 4 }} alignContent={"center"} >
|
||||
|
|
@ -346,7 +358,6 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
padding: gutterPadding
|
||||
}}
|
||||
onClick={(event) => handleCollapse(index, event)}
|
||||
// style={{pre}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
|
|
@ -401,7 +412,6 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
|
||||
const columnClick = (column: Column<T>) => {
|
||||
const sortKey = column.sortKey ? column.sortKey : column.title.toLowerCase()
|
||||
// console.log(column.sortKey)
|
||||
if (setOrderBy) {
|
||||
if (orderBy === sortKey && setOrder && order) {
|
||||
if (order === "desc") {
|
||||
|
|
@ -409,13 +419,11 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
} else {
|
||||
setOrderBy("")
|
||||
}
|
||||
// setOrder(order === "asc" ? "desc" : "asc")
|
||||
} else {
|
||||
if (setOrder) setOrder("desc")
|
||||
setOrderBy(sortKey)
|
||||
}
|
||||
}
|
||||
// if (setOrder && order) setOrder(order === "asc" ? "desc" : "asc")
|
||||
}
|
||||
|
||||
const showOrderIcon = (column: Column<T>) => {
|
||||
|
|
@ -423,14 +431,60 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
const sortKey = column.sortKey ? column.sortKey : column.title.toLowerCase()
|
||||
if (sortKey === orderBy) {
|
||||
if (order === "asc") {
|
||||
return <ArrowDownward fontSize="small"/>
|
||||
return <ArrowDownward fontSize="small" sx={{ marginLeft: 1 }} />
|
||||
} else {
|
||||
return <ArrowUpward fontSize="small"/>
|
||||
return <ArrowUpward fontSize="small" sx={{ marginLeft: 1 }} />
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const resizeClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>, index: number) => {
|
||||
|
||||
let newWidths: number[] = []
|
||||
event.currentTarget.parentElement?.parentElement?.parentElement?.childNodes.forEach(node => {
|
||||
if (node instanceof HTMLElement) {
|
||||
newWidths.push(node.clientWidth)
|
||||
}
|
||||
})
|
||||
|
||||
rowWidthsRef.current = newWidths
|
||||
dragIndexRef.current = index;
|
||||
dragStartRef.current = event.clientX;
|
||||
|
||||
window.addEventListener('mousemove', resizeDrag);
|
||||
window.addEventListener('mouseup', resizeUnclick);
|
||||
}
|
||||
|
||||
const resizeUnclick = () => {
|
||||
dragIndexRef.current = undefined;
|
||||
dragStartRef.current = undefined;
|
||||
window.removeEventListener('mousemove', resizeDrag);
|
||||
window.removeEventListener('mouseup', resizeUnclick);
|
||||
}
|
||||
|
||||
const resizeDrag = (e: any) => {
|
||||
const index = dragIndexRef.current;
|
||||
const dragStart = dragStartRef.current
|
||||
const rowWidths = rowWidthsRef.current
|
||||
if (index === undefined) return
|
||||
if (rowWidths[index] === undefined) return
|
||||
if (rowWidths[index+1] === undefined) return
|
||||
if (!dragStart) return
|
||||
|
||||
let newWidths: number[] = [...rowWidths]
|
||||
newWidths[index] = newWidths[index] - (dragStart - e.clientX)
|
||||
newWidths.every((width, j) => {
|
||||
if (j <= index || width === 0) return true
|
||||
else if (newWidths[j] !== undefined) {
|
||||
newWidths[j] = newWidths[j] + (dragStart - e.clientX)
|
||||
return false
|
||||
}
|
||||
})
|
||||
setRowWidths(newWidths)
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer className={classes.tableContainer} component={Paper}>
|
||||
<Table>
|
||||
|
|
@ -483,16 +537,29 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
{ renderGutter && <TableCell></TableCell>}
|
||||
{ columns ?
|
||||
columns.map((column, index) => {
|
||||
if (filterList.includes(column.title) || column.hidden) return null
|
||||
if (filterList.includes(column.title) || column.hidden) return (
|
||||
<TableCell key={"row-"+index+"cell-"+index} sx={{ width: 0, margin: 0, padding: 0}} ></TableCell>
|
||||
)
|
||||
return (
|
||||
<TableCell
|
||||
key={"header-cell-"+index}
|
||||
<TableCell key={"header-cell-"+index} width={rowWidths[index] ? rowWidths[index] : undefined}>
|
||||
<Grid2 container spacing={1} justifyContent={"space-between"}>
|
||||
<Grid2
|
||||
alignItems="center"
|
||||
display="flex"
|
||||
onClick={() => !column.disableSort && columnClick(column)}
|
||||
sx={{ cursor: column.disableSort ? "default" : "pointer" }}
|
||||
>
|
||||
<Grid2 container spacing={1} alignItems={"center"}>
|
||||
{column.title} {showOrderIcon(column)}
|
||||
</Grid2>
|
||||
{index < columns.length-1 && resizeable &&
|
||||
<Grid2
|
||||
sx={{ cursor: "col-resize", userSelect: "none" }}
|
||||
onMouseDown={event => resizeClick(event, index)}
|
||||
>
|
||||
|
|
||||
</Grid2>
|
||||
}
|
||||
</Grid2>
|
||||
</TableCell>
|
||||
)
|
||||
})
|
||||
|
|
@ -547,15 +614,17 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
}
|
||||
{columns ?
|
||||
columns.map((column, j) => {
|
||||
if (filterList.includes(column.title) || column.hidden) return null
|
||||
if (filterList.includes(column.title) || column.hidden) return (
|
||||
<TableCell key={"row-"+index+"cell-"+j} sx={{ width: 0, padding: 0, margin: 0}}></TableCell>
|
||||
)
|
||||
if (column.render) return (
|
||||
<TableCell key={"row-"+index+"cell-"+j} sx={column.cellStyle ? column.cellStyle : {padding: 0}}>
|
||||
<TableCell key={"row-"+index+"cell-"+j} sx={{padding: 0, paddingRight: 2, width: rowWidths[j] ? rowWidths[j] : undefined}}>
|
||||
{column.render(row)}
|
||||
</TableCell>
|
||||
)
|
||||
return (
|
||||
<TableCell key={"row-"+index+"cell-"+j}>
|
||||
{Object.values(row)[j].toString()}
|
||||
{Object.values(row)[j] && Object.values(row)[j].toString()}
|
||||
</TableCell>
|
||||
)
|
||||
})
|
||||
|
|
@ -615,9 +684,8 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
<MenuItem
|
||||
onClick={() => filter(column.title)}
|
||||
key={"column-list-"+index}
|
||||
aria-label="Open User Settings"
|
||||
dense
|
||||
sx={{ marginLeft: -0.75 }}
|
||||
sx={{ marginLeft: -0.75, paddingRight: 3 }}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
|
|
@ -628,6 +696,11 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
<MenuItem onClick={() => setRowWidths([])}>
|
||||
<Typography variant="body2" padding={1}>
|
||||
Reset Widths
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</TableContainer>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import StatusSen5x from "common/StatusSen5x";
|
|||
import StatusDust from "common/StatusDust";
|
||||
import StatusGas from "common/StatusGas";
|
||||
import { cloneDeep } from "lodash";
|
||||
import LoadingScreen from "app/LoadingScreen";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return ({
|
||||
|
|
@ -56,6 +57,11 @@ const useStyles = makeStyles((theme: Theme) => {
|
|||
width: theme.spacing(10)
|
||||
// justifyContent: "center",
|
||||
},
|
||||
deviceContainer: {
|
||||
margin: theme.spacing(1),
|
||||
display: "flex",
|
||||
width: theme.spacing(10)
|
||||
},
|
||||
descriptionCellContainer: {
|
||||
margin: theme.spacing(1),
|
||||
marginLeft: theme.spacing(2),
|
||||
|
|
@ -67,7 +73,7 @@ const useStyles = makeStyles((theme: Theme) => {
|
|||
margin: theme.spacing(1),
|
||||
marginLeft: theme.spacing(2),
|
||||
display: "flex",
|
||||
width: theme.spacing(18)
|
||||
minWidth: theme.spacing(10)
|
||||
// justifyContent: "center",
|
||||
},
|
||||
green: {
|
||||
|
|
@ -140,9 +146,22 @@ export default function Devices() {
|
|||
// const [totalGroups, setTotalGroups] = useState(0);
|
||||
|
||||
const [groupPermissions, setGroupPermissions] = useState<pond.Permission[]>([])
|
||||
const [groupsLoaded, setGroupsLoaded] = useState(false)
|
||||
|
||||
const groupID = useParams<{ groupID: string }>()?.groupID ?? "all";
|
||||
const [tab, setTab] = useState(groupID==="all" ? groupID : groupID);
|
||||
// const [tab, setTab] = useState(groupID==="all" ? groupID : groupID);
|
||||
const [tab, setTab] = useState(() => {
|
||||
console.log("groupID: "+groupID)
|
||||
if (groupID!=="all") return groupID
|
||||
const stored = sessionStorage.getItem(location.pathname+"-groups-tab");
|
||||
console.log("stored: "+stored)
|
||||
return stored !== null ? stored : "all";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log(tab)
|
||||
sessionStorage.setItem(location.pathname+"-groups-tab", tab);
|
||||
}, [tab]);
|
||||
|
||||
const [selectedGroup, setSelectedGroup] = useState<Group | undefined>(undefined);
|
||||
const [groupSettingsMode, setGroupSettingsMode] = useState<
|
||||
|
|
@ -184,7 +203,7 @@ export default function Devices() {
|
|||
|
||||
useEffect(() => {
|
||||
setGroupPermissions([])
|
||||
if (tab === "all") return
|
||||
if (tab === "all" || !getGroup()) return
|
||||
groupAPI.getGroupPermissions(getGroup().id()).then(resp => {
|
||||
setGroupPermissions(resp.data.permissions)
|
||||
})
|
||||
|
|
@ -232,7 +251,7 @@ export default function Devices() {
|
|||
};
|
||||
|
||||
const getKeys = () => {
|
||||
if (tab !== "all") {
|
||||
if (tab !== "all" && getGroup()) {
|
||||
let keys = getContextKeys()
|
||||
//keys.splice(keys.length - 1, 0, getGroup().id().toString());
|
||||
keys.push(getGroup().id().toString());
|
||||
|
|
@ -274,11 +293,13 @@ export default function Devices() {
|
|||
updateGroups(newGroups)
|
||||
}).finally(() => {
|
||||
setGroupsLoading(false)
|
||||
setGroupsLoaded(true)
|
||||
})
|
||||
}
|
||||
|
||||
const loadDevices = () => {
|
||||
setDevicesLoading(true)
|
||||
if (!groupsLoaded) return
|
||||
deviceAPI.list(
|
||||
limit,
|
||||
page*limit,
|
||||
|
|
@ -316,7 +337,7 @@ export default function Devices() {
|
|||
|
||||
useEffect(() => {
|
||||
loadDevices()
|
||||
}, [limit, page, order, orderBy, fieldContains, search, tab, as])
|
||||
}, [limit, page, order, orderBy, fieldContains, search, tab, as, groupsLoaded])
|
||||
|
||||
useEffect(() => {
|
||||
loadGroups()
|
||||
|
|
@ -371,12 +392,14 @@ export default function Devices() {
|
|||
let columns: Column<Device>[] = [
|
||||
{
|
||||
title: "State",
|
||||
// cellStyle: {width: 100000},
|
||||
// sortKey: "state",
|
||||
render: (device: Device) => {
|
||||
const status = device.status ?? pond.DeviceStatus.create()
|
||||
const deviceStateHelper = getDeviceStateHelper(status.state);
|
||||
return (
|
||||
<Box className={classes.cellContainer} style={{ width: theme.spacing(0.5)}}>
|
||||
{/* <Box > */}
|
||||
<Tooltip title={deviceStateHelper.description}>{deviceStateHelper.icon}</Tooltip>
|
||||
</Box>
|
||||
)
|
||||
|
|
@ -391,10 +414,12 @@ export default function Devices() {
|
|||
},
|
||||
{
|
||||
title: "Device",
|
||||
cellStyle: { width: 160 },
|
||||
sortKey: "name",
|
||||
render: (device: Device) => {
|
||||
return (
|
||||
<Box className={classes.cellContainer}>
|
||||
<Box className={classes.deviceContainer}>
|
||||
{/* // <Box > */}
|
||||
<Chip
|
||||
variant="outlined"
|
||||
label={device.settings?.name ? device.settings.name : "Device " + device.settings?.deviceId}
|
||||
|
|
@ -406,11 +431,12 @@ export default function Devices() {
|
|||
},
|
||||
{
|
||||
title: "Description",
|
||||
cellStyle: { width: 400 },
|
||||
render: (device: Device) => {
|
||||
const description = device.settings?.description ?? ""
|
||||
let size = (hasCo2 && !groupGas) ? 26 : 36
|
||||
return (
|
||||
<Box className={classes.descriptionCellContainer} width={size*6}>
|
||||
<Box className={classes.descriptionCellContainer} >
|
||||
<Tooltip title={description}>
|
||||
<span>
|
||||
{description.length > size
|
||||
|
|
@ -680,9 +706,7 @@ export default function Devices() {
|
|||
dense
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
checked={!separateDust}
|
||||
/>
|
||||
<Checkbox checked={!separateDust} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={"Group Dust"} />
|
||||
</MenuItem>
|
||||
|
|
@ -692,10 +716,7 @@ export default function Devices() {
|
|||
dense
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
checked={groupGas}
|
||||
|
||||
/>
|
||||
<Checkbox checked={groupGas} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={"Group Gas"} />
|
||||
</MenuItem>
|
||||
|
|
@ -703,6 +724,14 @@ export default function Devices() {
|
|||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Current tab value:', tab, typeof tab);
|
||||
}, [tab]);
|
||||
|
||||
if (!groupsLoaded) return (
|
||||
<LoadingScreen message="Loading groups" />
|
||||
)
|
||||
|
||||
return(
|
||||
<PageContainer padding={isMobile ? 0 : 2}>
|
||||
<Box
|
||||
|
|
@ -725,7 +754,7 @@ export default function Devices() {
|
|||
<Tab value={"loading"} label={<Skeleton variant="rectangular" height={24} width={128} animation="pulse" />}/>
|
||||
: groups.map((group, index) => {
|
||||
if (group.id()) return (
|
||||
<Tab className={classes.tab} wrapped value={index} label={<Typography variant="inherit" noWrap>{group.name()}</Typography>} key={"group-tab-"+index}/>
|
||||
<Tab className={classes.tab} wrapped value={index.toString()} label={<Typography variant="inherit" noWrap>{group.name()}</Typography>} key={"group-tab-"+index}/>
|
||||
)
|
||||
})}
|
||||
<Tab wrapped value={"never"} label={<CreateGroupIcon className={classes.green} />} onClick={() => openGroupSettings(undefined, "add")} />
|
||||
|
|
@ -753,6 +782,7 @@ export default function Devices() {
|
|||
orderBy={orderBy}
|
||||
setOrderBy={setOrderBy}
|
||||
endTitleElement={preferencesButton}
|
||||
resizeable
|
||||
loadMore={()=>{
|
||||
let currentRows = cloneDeep(devices)
|
||||
deviceAPI.list(
|
||||
|
|
@ -784,7 +814,7 @@ export default function Devices() {
|
|||
newDevices.push(Device.create(device))
|
||||
})
|
||||
setDevices(currentRows.concat(newDevices))
|
||||
}).catch(err => {
|
||||
}).catch(_err => {
|
||||
|
||||
})
|
||||
}}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue