vmui: optimize table view (#2867)

* feat: optimize table view

* fix: add column display setting

* app/vmselect: `make vmui-update`

Also document the change at docs/CHANGELOG.md

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
Yury Molodov 2022-07-14 00:15:43 +03:00 committed by GitHub
parent a0d0ba7219
commit 17c33132df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 174 additions and 17 deletions

View File

@ -1,12 +1,12 @@
{
"files": {
"main.css": "./static/css/main.7e6d0c89.css",
"main.js": "./static/js/main.6cbf53db.js",
"main.js": "./static/js/main.a6398eac.js",
"static/js/27.939f971b.chunk.js": "./static/js/27.939f971b.chunk.js",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.7e6d0c89.css",
"static/js/main.6cbf53db.js"
"static/js/main.a6398eac.js"
]
}

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.6cbf53db.js"></script><link href="./static/css/main.7e6d0c89.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.a6398eac.js"></script><link href="./static/css/main.7e6d0c89.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -113,7 +113,7 @@ const QueryEditor: FC<QueryEditorProps> = ({
onKeyDown={handleKeyDown}
onChange={(e) => setQuery(e.target.value, index)}
/>
<Popper open={openAutocomplete} anchorEl={autocompleteAnchorEl.current} placement="bottom-start">
<Popper open={openAutocomplete} anchorEl={autocompleteAnchorEl.current} placement="bottom-start" sx={{zIndex: 3}}>
<ClickAwayListener onClickAway={() => setOpenAutocomplete(false)}>
<Paper elevation={3} sx={{ maxHeight: 300, overflow: "auto" }}>
<MenuList ref={wrapperEl} dense>

View File

@ -15,9 +15,11 @@ import Spinner from "../common/Spinner";
import {useFetchQueryOptions} from "../../hooks/useFetchQueryOptions";
import TracingsView from "./Views/TracingsView";
import Trace from "./Trace/Trace";
import TableSettings from "../Table/TableSettings";
const CustomPanel: FC = () => {
const [displayColumns, setDisplayColumns] = useState<string[]>();
const [tracesState, setTracesState] = useState<Trace[]>([]);
const {displayType, time: {period}, query, queryControls: {isTracingEnabled}} = useAppState();
const { customStep, yaxis } = useGraphState();
@ -73,6 +75,11 @@ const CustomPanel: FC = () => {
setYaxisLimits={setYaxisLimits}
toggleEnableLimits={toggleEnableLimits}
/>}
{displayType === "table" && <TableSettings
data={liveData || []}
defaultColumns={displayColumns}
onChange={setDisplayColumns}
/>}
</Box>
</Box>
{error && <Alert color="error" severity="error" sx={{whiteSpace: "pre-wrap", mt: 2}}>{error}</Alert>}
@ -90,7 +97,7 @@ const CustomPanel: FC = () => {
traces={tracesState}
onDeleteClick={handleTraceDelete}
/>}
<TableView data={liveData}/>
<TableView data={liveData} displayColumns={displayColumns}/>
</>}
</Box>}
</Box>

View File

@ -1,4 +1,4 @@
import React, {FC, useMemo, useState} from "preact/compat";
import React, {FC, useEffect, useMemo, useRef, useState} from "preact/compat";
import {InstantMetricResult} from "../../../api/types";
import {InstantDataSeries} from "../../../types";
import Table from "@mui/material/Table";
@ -10,14 +10,16 @@ import TableRow from "@mui/material/TableRow";
import TableSortLabel from "@mui/material/TableSortLabel";
import {useSortedCategories} from "../../../hooks/useSortedCategories";
import Alert from "@mui/material/Alert";
import {useAppState} from "../../../state/common/StateContext";
export interface GraphViewProps {
data: InstantMetricResult[];
displayColumns?: string[]
}
const TableView: FC<GraphViewProps> = ({data}) => {
const TableView: FC<GraphViewProps> = ({data, displayColumns}) => {
const sortedColumns = useSortedCategories(data);
const sortedColumns = useSortedCategories(data, displayColumns);
const [orderBy, setOrderBy] = useState("");
const [orderDir, setOrderDir] = useState<"asc" | "desc">("asc");
@ -43,16 +45,24 @@ const TableView: FC<GraphViewProps> = ({data}) => {
setOrderBy(key);
};
const {query} = useAppState();
const [tableContainerHeight, setTableContainerHeight] = useState("");
const tableContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!tableContainerRef.current) return;
const {top} = tableContainerRef.current.getBoundingClientRect();
setTableContainerHeight(`calc(100vh - ${top + 32}px)`);
}, [tableContainerRef, query]);
return (
<>
{(rows.length > 0)
? <TableContainer>
<Table aria-label="simple table">
? <TableContainer ref={tableContainerRef} sx={{width: "calc(100vw - 68px)", height: tableContainerHeight}}>
<Table stickyHeader aria-label="simple table">
<TableHead>
<TableRow>
{sortedColumns.map((col, index) => (
<TableCell key={index} style={{textTransform: "capitalize"}}>
<TableCell key={index} style={{textTransform: "capitalize", paddingTop: 0}}>
<TableSortLabel
active={orderBy === col.key}
direction={orderDir}
@ -79,7 +89,9 @@ const TableView: FC<GraphViewProps> = ({data}) => {
{row.metadata.map((rowMeta, index2) => {
const prevRowValue = rows[index - 1] && rows[index - 1].metadata[index2];
return (
<TableCell sx={prevRowValue === rowMeta ? {opacity: 0.4} : {}}
<TableCell
sx={prevRowValue === rowMeta ? {opacity: 0.4} : {}}
style={{whiteSpace: "nowrap"}}
key={index2}>{rowMeta}</TableCell>
);
}

View File

@ -0,0 +1,135 @@
import SettingsIcon from "@mui/icons-material/Settings";
import React, {FC, useEffect, useState} from "preact/compat";
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton";
import Paper from "@mui/material/Paper";
import Popper from "@mui/material/Popper";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import CloseIcon from "@mui/icons-material/Close";
import ClickAwayListener from "@mui/material/ClickAwayListener";
import {useSortedCategories} from "../../hooks/useSortedCategories";
import {InstantMetricResult} from "../../api/types";
import FormControl from "@mui/material/FormControl";
import {FormGroup, FormLabel} from "@mui/material";
import FormControlLabel from "@mui/material/FormControlLabel";
import Checkbox from "@mui/material/Checkbox";
import Button from "@mui/material/Button";
const classes = {
popover: {
display: "grid",
gridGap: "16px",
padding: "0 0 25px",
},
popoverHeader: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
background: "#3F51B5",
padding: "6px 6px 6px 12px",
borderRadius: "4px 4px 0 0",
color: "#FFF",
},
popoverBody: {
display: "grid",
gridGap: "6px",
padding: "0 14px",
minWidth: "200px",
}
};
const title = "Table Settings";
interface TableSettingsProps {
data: InstantMetricResult[];
defaultColumns?: string[]
onChange: (arr: string[]) => void
}
const TableSettings: FC<TableSettingsProps> = ({data, defaultColumns, onChange}) => {
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl);
const columns = useSortedCategories(data);
const [checkedColumns, setCheckedColumns] = useState(columns.map(col => col.key));
const handleChange = (key: string) => {
setCheckedColumns(prev => checkedColumns.includes(key) ? prev.filter(col => col !== key) : [...prev, key]);
};
const handleClose = () => {
setAnchorEl(null);
setCheckedColumns(defaultColumns || columns.map(col => col.key));
};
const handleReset = () => {
setAnchorEl(null);
const value = columns.map(col => col.key);
setCheckedColumns(value);
onChange(value);
};
const handleApply = () => {
setAnchorEl(null);
onChange(checkedColumns);
};
useEffect(() => {
setCheckedColumns(columns.map(col => col.key));
}, [columns]);
return <Box>
<Tooltip title={title}>
<IconButton onClick={(e) => setAnchorEl(e.currentTarget)}>
<SettingsIcon/>
</IconButton>
</Tooltip>
<Popper
open={open}
anchorEl={anchorEl}
placement="left-start"
sx={{zIndex: 3}}
modifiers={[{name: "offset", options: {offset: [0, 6]}}]}>
<ClickAwayListener onClickAway={() => handleClose()}>
<Paper elevation={3} sx={classes.popover}>
<Box id="handle" sx={classes.popoverHeader}>
<Typography variant="body1"><b>{title}</b></Typography>
<IconButton size="small" onClick={() => handleClose()}>
<CloseIcon style={{color: "white"}}/>
</IconButton>
</Box>
<Box sx={classes.popoverBody}>
<FormControl component="fieldset" variant="standard">
<FormLabel component="legend">Display columns</FormLabel>
<FormGroup sx={{display: "grid", maxHeight: "350px", overflow: "auto"}}>
{columns.map(col => (
<FormControlLabel
key={col.key}
label={col.key}
sx={{textTransform: "capitalize"}}
control={
<Checkbox
checked={checkedColumns.includes(col.key)}
onChange={() => handleChange(col.key)}
name={col.key} />
}
/>
))}
</FormGroup>
</FormControl>
<Box display="grid" gridTemplateColumns="1fr 1fr" gap={1} justifyContent="center" mt={2}>
<Button variant="outlined" onClick={handleReset}>
Reset
</Button>
<Button variant="contained" onClick={handleApply}>
apply
</Button>
</Box>
</Box>
</Paper>
</ClickAwayListener>
</Popper>
</Box>;
};
export default TableSettings;

View File

@ -6,7 +6,7 @@ export type MetricCategory = {
variations: number;
}
export const useSortedCategories = (data: MetricBase[]): MetricCategory[] => useMemo(() => {
export const useSortedCategories = (data: MetricBase[], displayColumns?: string[]): MetricCategory[] => useMemo(() => {
const columns: { [key: string]: { options: Set<string> } } = {};
data.forEach(d =>
Object.entries(d.metric).forEach(e =>
@ -14,8 +14,10 @@ export const useSortedCategories = (data: MetricBase[]): MetricCategory[] => us
)
);
return Object.entries(columns).map(e => ({
const sortedColumns = Object.entries(columns).map(e => ({
key: e[0],
variations: e[1].options.size
})).sort((a1, a2) => a1.variations - a2.variations);
}, [data]);
return displayColumns ? sortedColumns.filter(col => displayColumns.includes(col.key)) : sortedColumns;
}, [data, displayColumns]);

View File

@ -53,6 +53,7 @@ scrape_configs:
* `vm_series_read_per_query` - the number of series read per query.
* FEATURE: publish binaries for FreeBSD and OpenBSD at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): allow selecting the needed columns at table view. This functionaly may help when the selected time series contain many different labels. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2817) and [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2867).
* BUGFIX: consistently name binaries at [releases page](https://github.com/VictoriaMetrics/VictoriaMetrics/releases) in the form `$(APP_NAME)-$(GOOS)-$(GOARCH)-$(VERSION).tar.gz`. For example, `victoria-metrics-linux-amd64-v1.79.0.tar.gz`. Previously the `$(GOOS)` part was missing in binaries for Linux.
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): allow using `__name__` label (aka [metric name](https://prometheus.io/docs/prometheus/latest/querying/basics/#time-series-selectors)) in alerting annotations. For example: