Merge branch 'devices_websocket' into dev_environment

This commit is contained in:
Carter 2026-03-13 15:37:49 -06:00
commit 9e921cb39d

View file

@ -1,11 +1,11 @@
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 { 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 { alpha } from "@mui/system/colorManipulator";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { ArrowDownward, ArrowUpward, ChevronRight, Close, Search, ViewColumn } from "@mui/icons-material" import { ArrowDownward, ArrowUpward, ChevronRight, Close, Search, ViewColumn } from "@mui/icons-material"
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import React from "react"; import React from "react";
import { useMobile } from "hooks"; import { useMobile } from "hooks";
import classNames from "classnames"; import classNames from "classnames";
import { getThemeType } from "theme";
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
//const isMobile = useMobile() //const isMobile = useMobile()
@ -15,11 +15,10 @@ const useStyles = makeStyles((theme: Theme) => {
border: "none" border: "none"
}, },
tableContainer: { tableContainer: {
// margin: theme.spacing(1), width: "100%",
// [theme.breakpoints.up("sm")]: { maxWidth: "100%",
// margin: theme.spacing(2) overflow: "hidden",
// }, minWidth: 0,
// width: "auto"
}, },
title: { title: {
padding: theme.spacing(1), padding: theme.spacing(1),
@ -59,10 +58,14 @@ const useStyles = makeStyles((theme: Theme) => {
}, },
stickyHeader: { stickyHeader: {
position: "sticky", position: "sticky",
backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(40, 40, 40)", backgroundColor: theme.palette.background.paper,
// Match Paper's elevation overlay so header matches search bar area
backgroundImage: theme.palette.mode === "dark"
? `linear-gradient(${alpha("#fff", 0.05)}, ${alpha("#fff", 0.05)})`
: undefined,
top: 0, top: 0,
zIndex: 300 //giving a really high z-index to make sure nothing is in front of it zIndex: 300 //giving a really high z-index to make sure nothing is in front of it
} },
})}, })},
); );
@ -75,6 +78,53 @@ export interface Column<T> {
disableSort?: boolean; disableSort?: boolean;
} }
function ResizableHeaderTitle<T>(props: {
column: Column<T>;
index: number;
columnsLength: number;
resizeable: boolean;
onColumnClick: (column: Column<T>) => void;
showOrderIcon: (column: Column<T>) => React.ReactNode;
}) {
const { column, index, columnsLength, resizeable, onColumnClick, showOrderIcon } = props;
const titleRef = useRef<HTMLDivElement>(null);
const [isOverflowing, setIsOverflowing] = useState(false);
useEffect(() => {
const el = titleRef.current;
if (!el) return;
const checkOverflow = () => setIsOverflowing(el.scrollWidth > el.clientWidth);
checkOverflow();
const ro = new ResizeObserver(checkOverflow);
ro.observe(el);
return () => ro.disconnect();
}, []);
return (
<Grid2
ref={titleRef}
alignItems="center"
display="flex"
onClick={() => !column.disableSort && onColumnClick(column)}
sx={{
cursor: column.disableSort ? "default" : "pointer",
minWidth: 0,
overflow: "hidden",
...(isOverflowing && resizeable && index < columnsLength - 1
? {
maskImage: "linear-gradient(to right, black 75%, transparent 100%)",
WebkitMaskImage: "linear-gradient(to right, black 75%, transparent 100%)",
}
: {}),
}}
>
<Box component="span" sx={{ whiteSpace: "nowrap" }}>
{column.title} {showOrderIcon(column)}
</Box>
</Grid2>
);
}
interface Props<T> { interface Props<T> {
rows: T[], rows: T[],
columns?: Column<T>[] | (() => Column<T>[]), columns?: Column<T>[] | (() => Column<T>[]),
@ -450,13 +500,11 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
} }
const resizeClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>, index: number) => { const resizeClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>, index: number) => {
const row = event.currentTarget.closest("tr")
let newWidths: number[] = [] if (!row) return
event.currentTarget.parentElement?.parentElement?.parentElement?.childNodes.forEach(node => { const cells = Array.from(row.querySelectorAll("th, td"))
if (node instanceof HTMLElement) { const offset = (rowSelect ? 1 : 0) + (renderGutter ? 1 : 0)
newWidths.push(node.clientWidth) const newWidths = cells.slice(offset).map((cell) => cell.clientWidth)
}
})
rowWidthsRef.current = newWidths rowWidthsRef.current = newWidths
dragIndexRef.current = index; dragIndexRef.current = index;
@ -483,11 +531,15 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
if (!dragStart) return if (!dragStart) return
let newWidths: number[] = [...rowWidths] let newWidths: number[] = [...rowWidths]
newWidths[index] = newWidths[index] - (dragStart - e.clientX) const delta = dragStart - e.clientX
const newWidthForIndex = newWidths[index] - delta
const clampedWidth = Math.max(MIN_COLUMN_WIDTH, newWidthForIndex)
const actualDelta = newWidths[index] - clampedWidth
newWidths[index] = clampedWidth
newWidths.every((width, j) => { newWidths.every((width, j) => {
if (j <= index || width === 0) return true if (j <= index || width === 0) return true
else if (newWidths[j] !== undefined) { else if (newWidths[j] !== undefined) {
newWidths[j] = newWidths[j] + (dragStart - e.clientX) newWidths[j] = newWidths[j] + actualDelta
return false return false
} }
}) })
@ -495,45 +547,49 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
} }
const MIN_COLUMN_WIDTH = 24;
return ( return (
<Box className={classes.tableContainer} component={Paper}> <Box className={classes.tableContainer} component={Paper}>
<Table> {(title || setSearchText) && (
<TableHead className={stickyHeader ? classes.stickyHeader : undefined}> <Box>
<TableRow sx={ !title && !setSearchText ? { display: "none" } : undefined}> <Grid2 container justifyContent="space-between" alignItems="center" sx={{ padding: 1 }}>
<TableCell colSpan={10000} sx={{ border: "none" }}> <Grid2>
<Grid2 container justifyContent="space-between"> <Box sx={{ marginTop: -1 }}>
{renderTitle()}
{renderSubtitle()}
</Box>
</Grid2>
<Grid2 size={{ xs: "grow" }} sx={{ minWidth: 0, display: "flex", justifyContent: "flex-end" }}>
<Grid2 container alignItems="center" spacing={1} wrap="nowrap">
<Grid2> <Grid2>
<Box sx={{ marginTop: -1 }}> {actions && actions}
{renderTitle()}
{renderSubtitle()}
</Box>
</Grid2> </Grid2>
<Grid2> <Grid2>
<Grid2 container alignItems={"center"} spacing={1}> {endTitleElement}
<Grid2> </Grid2>
{actions && actions} <Grid2 sx={{ minWidth: 200, maxWidth: 320 }}>
</Grid2> {setSearchText && (
<Grid2> <Box className={classes.searchContainer} sx={{ width: "100%" }}>
{endTitleElement} {searchBar()}
</Grid2> </Box>
<Grid2> )}
{setSearchText &&<Box className={classes.searchContainer}> </Grid2>
{searchBar()} <Grid2>
</Box>} { columns && !hideKeys &&
</Grid2> <IconButton onClick={openColumnMenu} >
<Grid2> <ViewColumn />
{ columns && !hideKeys && </IconButton>
<IconButton onClick={openColumnMenu} > }
<ViewColumn />
</IconButton>
}
</Grid2>
</Grid2>
</Grid2> </Grid2>
</Grid2> </Grid2>
</TableCell> </Grid2>
</TableRow> </Grid2>
</Box>
)}
<TableContainer sx={{ overflowX: "auto", paddingBottom: 1 }}>
<Table>
<TableHead className={stickyHeader ? classes.stickyHeader : undefined}>
{customElement && {customElement &&
<TableRow> <TableRow>
<TableCell colSpan={10000} sx={{ border: "none" }}> <TableCell colSpan={10000} sx={{ border: "none" }}>
@ -552,18 +608,23 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
) )
return ( return (
<TableCell key={"header-cell-"+index} width={rowWidths[index] ? rowWidths[index] : undefined}> <TableCell key={"header-cell-"+index} width={rowWidths[index] ? rowWidths[index] : undefined}>
<Grid2 container spacing={1} justifyContent={"space-between"}> <Grid2
<Grid2 container
alignItems="center" spacing={1}
display="flex" justifyContent={"space-between"}
onClick={() => !column.disableSort && columnClick(column)} flexWrap="nowrap"
sx={{ cursor: column.disableSort ? "default" : "pointer" }} >
> <ResizableHeaderTitle
{column.title} {showOrderIcon(column)} column={column}
</Grid2> index={index}
columnsLength={columns.length}
resizeable={!!resizeable}
onColumnClick={columnClick}
showOrderIcon={showOrderIcon}
/>
{index < columns.length-1 && resizeable && {index < columns.length-1 && resizeable &&
<Grid2 <Grid2
sx={{ cursor: "col-resize", userSelect: "none" }} sx={{ cursor: "col-resize", userSelect: "none", flexShrink: 0 }}
onMouseDown={event => resizeClick(event, index)} onMouseDown={event => resizeClick(event, index)}
> >
| |
@ -672,6 +733,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
} }
</TableBody> </TableBody>
</Table> </Table>
</TableContainer>
{!hidePagination && {!hidePagination &&
<TablePagination <TablePagination
rowsPerPageOptions={rowsPerPageOptions ? rowsPerPageOptions : [5, 10, 20]} rowsPerPageOptions={rowsPerPageOptions ? rowsPerPageOptions : [5, 10, 20]}