mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-03-21 15:45:01 +00: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
99402541fb
commit
1ca20caa4b
11 changed files with 174 additions and 17 deletions
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"main.css": "./static/css/main.7e6d0c89.css",
|
"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",
|
"static/js/27.939f971b.chunk.js": "./static/js/27.939f971b.chunk.js",
|
||||||
"index.html": "./index.html"
|
"index.html": "./index.html"
|
||||||
},
|
},
|
||||||
"entrypoints": [
|
"entrypoints": [
|
||||||
"static/css/main.7e6d0c89.css",
|
"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}
|
onKeyDown={handleKeyDown}
|
||||||
onChange={(e) => setQuery(e.target.value, index)}
|
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)}>
|
<ClickAwayListener onClickAway={() => setOpenAutocomplete(false)}>
|
||||||
<Paper elevation={3} sx={{ maxHeight: 300, overflow: "auto" }}>
|
<Paper elevation={3} sx={{ maxHeight: 300, overflow: "auto" }}>
|
||||||
<MenuList ref={wrapperEl} dense>
|
<MenuList ref={wrapperEl} dense>
|
||||||
|
|
|
@ -15,9 +15,11 @@ import Spinner from "../common/Spinner";
|
||||||
import {useFetchQueryOptions} from "../../hooks/useFetchQueryOptions";
|
import {useFetchQueryOptions} from "../../hooks/useFetchQueryOptions";
|
||||||
import TracingsView from "./Views/TracingsView";
|
import TracingsView from "./Views/TracingsView";
|
||||||
import Trace from "./Trace/Trace";
|
import Trace from "./Trace/Trace";
|
||||||
|
import TableSettings from "../Table/TableSettings";
|
||||||
|
|
||||||
const CustomPanel: FC = () => {
|
const CustomPanel: FC = () => {
|
||||||
|
|
||||||
|
const [displayColumns, setDisplayColumns] = useState<string[]>();
|
||||||
const [tracesState, setTracesState] = useState<Trace[]>([]);
|
const [tracesState, setTracesState] = useState<Trace[]>([]);
|
||||||
const {displayType, time: {period}, query, queryControls: {isTracingEnabled}} = useAppState();
|
const {displayType, time: {period}, query, queryControls: {isTracingEnabled}} = useAppState();
|
||||||
const { customStep, yaxis } = useGraphState();
|
const { customStep, yaxis } = useGraphState();
|
||||||
|
@ -73,6 +75,11 @@ const CustomPanel: FC = () => {
|
||||||
setYaxisLimits={setYaxisLimits}
|
setYaxisLimits={setYaxisLimits}
|
||||||
toggleEnableLimits={toggleEnableLimits}
|
toggleEnableLimits={toggleEnableLimits}
|
||||||
/>}
|
/>}
|
||||||
|
{displayType === "table" && <TableSettings
|
||||||
|
data={liveData || []}
|
||||||
|
defaultColumns={displayColumns}
|
||||||
|
onChange={setDisplayColumns}
|
||||||
|
/>}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{error && <Alert color="error" severity="error" sx={{whiteSpace: "pre-wrap", mt: 2}}>{error}</Alert>}
|
{error && <Alert color="error" severity="error" sx={{whiteSpace: "pre-wrap", mt: 2}}>{error}</Alert>}
|
||||||
|
@ -90,7 +97,7 @@ const CustomPanel: FC = () => {
|
||||||
traces={tracesState}
|
traces={tracesState}
|
||||||
onDeleteClick={handleTraceDelete}
|
onDeleteClick={handleTraceDelete}
|
||||||
/>}
|
/>}
|
||||||
<TableView data={liveData}/>
|
<TableView data={liveData} displayColumns={displayColumns}/>
|
||||||
</>}
|
</>}
|
||||||
</Box>}
|
</Box>}
|
||||||
</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 {InstantMetricResult} from "../../../api/types";
|
||||||
import {InstantDataSeries} from "../../../types";
|
import {InstantDataSeries} from "../../../types";
|
||||||
import Table from "@mui/material/Table";
|
import Table from "@mui/material/Table";
|
||||||
|
@ -10,14 +10,16 @@ import TableRow from "@mui/material/TableRow";
|
||||||
import TableSortLabel from "@mui/material/TableSortLabel";
|
import TableSortLabel from "@mui/material/TableSortLabel";
|
||||||
import {useSortedCategories} from "../../../hooks/useSortedCategories";
|
import {useSortedCategories} from "../../../hooks/useSortedCategories";
|
||||||
import Alert from "@mui/material/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
|
import {useAppState} from "../../../state/common/StateContext";
|
||||||
|
|
||||||
export interface GraphViewProps {
|
export interface GraphViewProps {
|
||||||
data: InstantMetricResult[];
|
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 [orderBy, setOrderBy] = useState("");
|
||||||
const [orderDir, setOrderDir] = useState<"asc" | "desc">("asc");
|
const [orderDir, setOrderDir] = useState<"asc" | "desc">("asc");
|
||||||
|
@ -43,16 +45,24 @@ const TableView: FC<GraphViewProps> = ({data}) => {
|
||||||
setOrderBy(key);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{(rows.length > 0)
|
{(rows.length > 0)
|
||||||
? <TableContainer>
|
? <TableContainer ref={tableContainerRef} sx={{width: "calc(100vw - 68px)", height: tableContainerHeight}}>
|
||||||
<Table aria-label="simple table">
|
<Table stickyHeader aria-label="simple table">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{sortedColumns.map((col, index) => (
|
{sortedColumns.map((col, index) => (
|
||||||
<TableCell key={index} style={{textTransform: "capitalize"}}>
|
<TableCell key={index} style={{textTransform: "capitalize", paddingTop: 0}}>
|
||||||
<TableSortLabel
|
<TableSortLabel
|
||||||
active={orderBy === col.key}
|
active={orderBy === col.key}
|
||||||
direction={orderDir}
|
direction={orderDir}
|
||||||
|
@ -79,7 +89,9 @@ const TableView: FC<GraphViewProps> = ({data}) => {
|
||||||
{row.metadata.map((rowMeta, index2) => {
|
{row.metadata.map((rowMeta, index2) => {
|
||||||
const prevRowValue = rows[index - 1] && rows[index - 1].metadata[index2];
|
const prevRowValue = rows[index - 1] && rows[index - 1].metadata[index2];
|
||||||
return (
|
return (
|
||||||
<TableCell sx={prevRowValue === rowMeta ? {opacity: 0.4} : {}}
|
<TableCell
|
||||||
|
sx={prevRowValue === rowMeta ? {opacity: 0.4} : {}}
|
||||||
|
style={{whiteSpace: "nowrap"}}
|
||||||
key={index2}>{rowMeta}</TableCell>
|
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;
|
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> } } = {};
|
const columns: { [key: string]: { options: Set<string> } } = {};
|
||||||
data.forEach(d =>
|
data.forEach(d =>
|
||||||
Object.entries(d.metric).forEach(e =>
|
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],
|
key: e[0],
|
||||||
variations: e[1].options.size
|
variations: e[1].options.size
|
||||||
})).sort((a1, a2) => a1.variations - a2.variations);
|
})).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.
|
* `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: 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: 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:
|
* 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 a new issue