Merge branch 'devices_websocket' into dev_environment
This commit is contained in:
commit
9e921cb39d
1 changed files with 122 additions and 60 deletions
|
|
@ -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,14 +500,12 @@ 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;
|
||||||
dragStartRef.current = event.clientX;
|
dragStartRef.current = event.clientX;
|
||||||
|
|
@ -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]}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue