mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-23 12:31:07 +01:00
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:
parent
a0d0ba7219
commit
17c33132df
@ -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"
|
||||
]
|
||||
}
|
@ -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
2
app/vmselect/vmui/static/js/main.a6398eac.js
Normal file
2
app/vmselect/vmui/static/js/main.a6398eac.js
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
135
app/vmui/packages/vmui/src/components/Table/TableSettings.tsx
Normal file
135
app/vmui/packages/vmui/src/components/Table/TableSettings.tsx
Normal 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;
|
@ -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]);
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user