Merge branch 'table_column_resize' into dev_environment

This commit is contained in:
Carter 2025-06-04 16:03:38 -06:00
commit b3313128e8
5 changed files with 168 additions and 46 deletions

14
indexV2.html Normal file
View 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>

View file

@ -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

View file

@ -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>
},
{

View file

@ -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()
@ -148,6 +150,19 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
const [openGutters, setOpenGutters] = useState<number[]>([])
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";
@ -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,15 +537,28 @@ 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}
onClick={() => !column.disableSort && columnClick(column)}
sx={{ cursor: column.disableSort ? "default" : "pointer" }}
>
<Grid2 container spacing={1} alignItems={"center"}>
{column.title} {showOrderIcon(column)}
<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" }}
>
{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>
)

View file

@ -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 => {
})
}}