vmui: grid support for predefined panels (#2386)

* update packages

* feat: add setting width for predefined panels

* docs: update doc by predefined dashboards

* app/vmselect: `make vmui-update`

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
Yury Molodov 2022-04-01 12:48:17 +03:00 committed by GitHub
parent 0fd4c48568
commit f166f80f15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 843 additions and 854 deletions

View file

@ -1,14 +1,14 @@
{
"files": {
"main.css": "./static/css/main.d8362c27.css",
"main.js": "./static/js/main.1c66c512.js",
"static/js/362.1990b49e.chunk.js": "./static/js/362.1990b49e.chunk.js",
"main.js": "./static/js/main.0c1bf008.js",
"static/js/362.1a2113d4.chunk.js": "./static/js/362.1a2113d4.chunk.js",
"static/js/27.939f971b.chunk.js": "./static/js/27.939f971b.chunk.js",
"static/media/README.md": "./static/media/README.a3933343f0099d3929b4.md",
"static/media/README.md": "./static/media/README.5e5724daf3ee333540a3.md",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.d8362c27.css",
"static/js/main.1c66c512.js"
"static/js/main.0c1bf008.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 defer="defer" src="./static/js/main.1c66c512.js"></script><link href="./static/css/main.d8362c27.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 defer="defer" src="./static/js/main.0c1bf008.js"></script><link href="./static/css/main.d8362c27.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

View file

@ -1 +1 @@
"use strict";(self.webpackChunkvmui=self.webpackChunkvmui||[]).push([[362],{8362:function(e,s,u){e.exports=u.p+"static/media/README.a3933343f0099d3929b4.md"}}]);
"use strict";(self.webpackChunkvmui=self.webpackChunkvmui||[]).push([[362],{8362:function(e,s,u){e.exports=u.p+"static/media/README.5e5724daf3ee333540a3.md"}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -20,13 +20,14 @@ DashboardRow:
<br/>
PanelSettings:
| Name | Type | Description |
|:---------------|:----------:|----------------------------------------------------:|
| expr* | `string[]` | Data source queries |
| title | `string` | Panel title |
| description | `string` | Additional information about the panel |
| unit | `string` | Y-axis unit |
| showLegend | `boolean` | If `false`, the legend hide. Default value - `true` |
| Name | Type | Description |
|:------------|:----------:|-------------------------------------------------------------------------------------------:|
| expr* | `string[]` | Data source queries |
| title | `string` | Panel title |
| description | `string` | Additional information about the panel |
| unit | `string` | Y-axis unit |
| showLegend | `boolean` | If `false`, the legend hide. Default value - `true` |
| width | `number` | The number of columns the panel uses.<br/> From 1 (minimum width) to 12 (full width). |
---
@ -73,4 +74,4 @@ PanelSettings:
}
]
}
```
```

View file

@ -41,7 +41,11 @@ module.exports = {
"react/prop-types": 0,
"max-lines": [
"error",
{"max": 150}
{
"max": 150,
"skipBlankLines": true,
"skipComments": true,
}
]
},
"settings": {

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
import React, {FC, useEffect, useMemo, useState} from "preact/compat";
import React, {FC, useEffect, useMemo, useRef, useState} from "preact/compat";
import {MetricResult} from "../../../api/types";
import LineChart from "../../LineChart/LineChart";
import {AlignedData as uPlotData, Series as uPlotSeries} from "uplot";
@ -126,14 +126,18 @@ const GraphView: FC<GraphViewProps> = ({
setLegend(tempLegend);
}, [hideSeries]);
const containerRef = useRef<HTMLDivElement>(null);
return <>
{(data.length > 0)
? <div>
<LineChart data={dataChart} series={series} metrics={data} period={period} yaxis={yaxis} unit={unit} setPeriod={setPeriod}/>
{(data.length > 0) ?
<div style={{width: "100%"}} ref={containerRef}>
{containerRef?.current &&
<LineChart data={dataChart} series={series} metrics={data} period={period} yaxis={yaxis} unit={unit}
setPeriod={setPeriod} container={containerRef?.current}/>}
{showLegend && <Legend labels={legend} query={query} onChange={onChangeLegend}/>}
</div>
: <Alert color="warning" severity="warning" sx={{mt: 2}}>No data to show</Alert>}
</>;
};
export default GraphView;
export default GraphView;

View file

@ -4,10 +4,10 @@ import Box from "@mui/material/Box";
import { Outlet } from "react-router-dom";
const HomeLayout: FC = () => {
return <Box id="homeLayout">
return <Box>
<Header/>
<Outlet/>
</Box>;
};
export default HomeLayout;
export default HomeLayout;

View file

@ -21,16 +21,18 @@ export interface LineChartProps {
series: uPlotSeries[];
unit?: string;
setPeriod: ({from, to}: {from: Date, to: Date}) => void;
container: HTMLDivElement | null
}
enum typeChartUpdate {xRange = "xRange", yRange = "yRange", data = "data"}
const LineChart: FC<LineChartProps> = ({data, series, metrics = [],
period, yaxis, unit, setPeriod}) => {
period, yaxis, unit, setPeriod, container}) => {
const uPlotRef = useRef<HTMLDivElement>(null);
const [isPanning, setPanning] = useState(false);
const [xRange, setXRange] = useState({min: period.start, max: period.end});
const [uPlotInst, setUPlotInst] = useState<uPlot>();
const layoutSize = useResize(document.getElementById("homeLayout"));
const layoutSize = useResize(container);
const tooltip = document.createElement("div");
tooltip.className = "u-tooltip";
@ -105,7 +107,7 @@ const LineChart: FC<LineChartProps> = ({data, series, metrics = [],
series,
axes: getAxes(series, unit),
scales: {...getScales()},
width: layoutSize.width ? layoutSize.width - 64 : 400,
width: layoutSize.width || 400,
plugins: [{hooks: {ready: onReadyChart, setCursor, setSeries: seriesFocus}}],
};

View file

@ -1,21 +1,81 @@
import React, {FC} from "preact/compat";
import React, {FC, useEffect, useMemo, useState} from "preact/compat";
import {MouseEvent as ReactMouseEvent} from "react";
import {DashboardRow} from "../../types";
import Box from "@mui/material/Box";
import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails";
import Grid from "@mui/material/Grid";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import Typography from "@mui/material/Typography";
import PredefinedPanels from "./PredefinedPanels";
import Alert from "@mui/material/Alert";
import {CSSProperties} from "@mui/styles";
import useResize from "../../hooks/useResize";
export interface PredefinedDashboardProps extends DashboardRow {
filename: string;
index: number;
}
const resizerStyle: CSSProperties = {
position: "absolute",
top: 0,
bottom: 0,
width: "10px",
opacity: 0,
cursor: "ew-resize",
};
const PredefinedDashboard: FC<PredefinedDashboardProps> = ({index, title, panels, filename}) => {
const windowSize = useResize(document.body);
const sizeSection = useMemo(() => {
return windowSize.width / 12;
}, [windowSize]);
const [panelsWidth, setPanelsWidth] = useState<number[]>([]);
useEffect(() => {
setPanelsWidth(panels.map(p => p.width || 12));
}, [panels]);
const [resize, setResize] = useState({start: 0, target: 0, enable: false});
const handleMouseMove = (e: MouseEvent) => {
if (!resize.enable) return;
const {start} = resize;
const sectionCount = Math.ceil((start - e.clientX)/sizeSection);
if (Math.abs(sectionCount) >= 12) return;
const width = panelsWidth.map((p, i) => {
return p - (i === resize.target ? sectionCount : 0);
});
setPanelsWidth(width);
};
const handleMouseDown = (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>, i: number) => {
setResize({
start: e.clientX,
target: i,
enable: true,
});
};
const handleMouseUp = () => {
setResize({
...resize,
enable: false
});
};
useEffect(() => {
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};
}, [resize]);
return <Accordion defaultExpanded={!index} sx={{boxShadow: "none"}}>
<AccordionSummary
sx={{px: 3, bgcolor: "rgba(227, 242, 253, 0.6)"}}
@ -29,20 +89,29 @@ const PredefinedDashboard: FC<PredefinedDashboardProps> = ({index, title, panels
</Box>
</AccordionSummary>
<AccordionDetails sx={{display: "grid", gridGap: "10px"}}>
{Array.isArray(panels) && !!panels.length
? panels.map((p, i) => <PredefinedPanels key={i}
title={p.title}
description={p.description}
unit={p.unit}
expr={p.expr}
filename={filename}
showLegend={p.showLegend}/>)
: <Alert color="error" severity="error" sx={{m: 4}}>
<code>&quot;panels&quot;</code> not found. Check the configuration file <b>{filename}</b>.
</Alert>
}
<Grid container spacing={2}>
{Array.isArray(panels) && !!panels.length
? panels.map((p, i) =>
<Grid key={i} item xs={panelsWidth[i]} sx={{transition: "200ms"}}>
<Box position={"relative"} height={"100%"}>
<PredefinedPanels
title={p.title}
description={p.description}
unit={p.unit}
expr={p.expr}
filename={filename}
showLegend={p.showLegend}/>
<button style={{...resizerStyle, right: 0}}
onMouseDown={(e) => handleMouseDown(e, i)}/>
</Box>
</Grid>)
: <Alert color="error" severity="error" sx={{m: 4}}>
<code>&quot;panels&quot;</code> not found. Check the configuration file <b>{filename}</b>.
</Alert>
}
</Grid>
</AccordionDetails>
</Accordion>;
};
export default PredefinedDashboard;
export default PredefinedDashboard;

View file

@ -82,41 +82,34 @@ const PredefinedPanels: FC<PredefinedPanelsProps> = ({
<code>&quot;expr&quot;</code> not found. Check the configuration file <b>{filename}</b>.
</Alert>;
return <Box border="1px solid" borderRadius="2px" borderColor="divider" ref={containerRef}>
<Box px={2} py={1} display="grid" gap={1} gridTemplateColumns="18px 1fr auto"
return <Box border="1px solid" borderRadius="2px" borderColor="divider" width={"100%"} height={"100%"} ref={containerRef}>
<Box px={2} py={1} display="flex" flexWrap={"wrap"}
width={"100%"}
alignItems="center" justifyContent="space-between" borderBottom={"1px solid"} borderColor={"divider"}>
<Tooltip arrow componentsProps={{
tooltip: {
sx: {maxWidth: "100%"}
}
}}
title={<Box sx={{p: 1}}>
{description && <Box mb={2}>
<Typography fontWeight={"500"} sx={{mb: 0.5, textDecoration: "underline"}}>Description:</Typography>
<div className="panelDescription" dangerouslySetInnerHTML={{__html: marked.parse(description)}}/>
</Box>}
<Box>
<Typography fontWeight={"500"} sx={{mb: 0.5, textDecoration: "underline"}}>Queries:</Typography>
<div>
{expr.map((e, i) => <Box key={`${i}_${e}`} mb={0.5}>{e}</Box>)}
</div>
</Box>
</Box>}>
<InfoIcon color="info"/>
<Tooltip arrow componentsProps={{tooltip: {sx: {maxWidth: "100%"}}}}
title={<Box sx={{p: 1}}>
{description && <Box mb={2}>
<Typography fontWeight={"500"} sx={{mb: 0.5, textDecoration: "underline"}}>Description:</Typography>
<div className="panelDescription" dangerouslySetInnerHTML={{__html: marked.parse(description)}}/>
</Box>}
<Box>
<Typography fontWeight={"500"} sx={{mb: 0.5, textDecoration: "underline"}}>Queries:</Typography>
<div>
{expr.map((e, i) => <Box key={`${i}_${e}`} mb={0.5}>{e}</Box>)}
</div>
</Box>
</Box>}>
<InfoIcon color="info" sx={{mr: 1}}/>
</Tooltip>
<Typography variant="subtitle1" gridColumn={2} textAlign={"left"} width={"100%"} fontWeight={500}>
<Typography component={"div"} variant="subtitle1" fontWeight={500} sx={{mr: 2, py: 1, flexGrow: "1"}}>
{title || ""}
</Typography>
<Box display={"grid"} gridTemplateColumns={"repeat(2, auto)"} gap={2} alignItems={"center"}>
<Box mr={2} py={1}>
<StepConfigurator defaultStep={period.step} customStepEnable={customStep.enable}
setStep={(value) => {
setCustomStep({...customStep, value: value});
}}
toggleEnableStep={() => {
setCustomStep({...customStep, enable: !customStep.enable});
}}/>
<GraphSettings yaxis={yaxis} setYaxisLimits={setYaxisLimits} toggleEnableLimits={toggleEnableLimits}/>
setStep={(value) => setCustomStep({...customStep, value: value})}
toggleEnableStep={() => setCustomStep({...customStep, enable: !customStep.enable})}/>
</Box>
<GraphSettings yaxis={yaxis} setYaxisLimits={setYaxisLimits} toggleEnableLimits={toggleEnableLimits}/>
</Box>
<Box px={2} pb={2}>
{isLoading && <Spinner isLoading={true} height={"500px"}/>}
@ -136,4 +129,4 @@ const PredefinedPanels: FC<PredefinedPanelsProps> = ({
</Box>;
};
export default PredefinedPanels;
export default PredefinedPanels;

View file

@ -20,13 +20,14 @@ DashboardRow:
<br/>
PanelSettings:
| Name | Type | Description |
|:---------------|:----------:|----------------------------------------------------:|
| expr* | `string[]` | Data source queries |
| title | `string` | Panel title |
| description | `string` | Additional information about the panel |
| unit | `string` | Y-axis unit |
| showLegend | `boolean` | If `false`, the legend hide. Default value - `true` |
| Name | Type | Description |
|:------------|:----------:|-------------------------------------------------------------------------------------------:|
| expr* | `string[]` | Data source queries |
| title | `string` | Panel title |
| description | `string` | Additional information about the panel |
| unit | `string` | Y-axis unit |
| showLegend | `boolean` | If `false`, the legend hide. Default value - `true` |
| width | `number` | The number of columns the panel uses.<br/> From 1 (minimum width) to 12 (full width). |
---
@ -73,4 +74,4 @@ PanelSettings:
}
]
}
```
```

View file

@ -5,21 +5,25 @@
"panels": [
{
"title": "Per-job CPU usage",
"width": 6,
"expr": ["sum(rate(process_cpu_seconds_total)) by (job)"]
},
{
"title": "Per-job RSS usage",
"width": 6,
"expr": ["sum(process_resident_memory_bytes) by (job)"]
},
{
"title": "Per-job disk read",
"width": 6,
"expr": ["sum(rate(process_io_storage_read_bytes_total)) by (job)"]
},{
"title": "Per-job disk write",
"width": 6,
"expr": ["sum(rate(process_io_storage_written_bytes_total)) by (job)"]
}
]
}
]
}
}

View file

@ -1,44 +1,21 @@
import { useState, useEffect } from "preact/compat";
const getScrollbarWidth = () => {
// Creating invisible container
const outer = document.createElement("div");
outer.style.visibility = "hidden";
outer.style.overflow = "scroll"; // forcing scrollbar to appear
document.body.appendChild(outer);
// Creating inner element and placing it in the container
const inner = document.createElement("div");
outer.appendChild(inner);
// Calculating difference between container's full width and the child width
const scrollbarWidth = (outer.offsetWidth - inner.offsetWidth);
// Removing temporary elements from the DOM
inner.remove();
outer.remove();
return scrollbarWidth;
};
const useResize = (node: HTMLElement | null): {width: number, height: number} => {
const [windowSize, setWindowSize] = useState({
width: 0,
height: 0,
});
useEffect(() => {
if (!node) return;
const handleResize = () => {
setWindowSize({
width: node.offsetWidth - getScrollbarWidth(),
height: node.offsetHeight,
});
const observer = new ResizeObserver((entries) => {
const {width, height} = entries[0].contentRect;
setWindowSize({width, height});
});
if (node) observer.observe(node);
return () => {
if (node) observer.unobserve(node);
};
window.addEventListener("resize", handleResize);
handleResize();
return () => window.removeEventListener("resize", handleResize);
}, []);
return windowSize;
};
export default useResize;
export default useResize;

View file

@ -41,6 +41,7 @@ export interface PanelSettings {
unit?: string;
expr: string[];
showLegend?: boolean;
width?: number
}
export interface DashboardRow {
@ -52,4 +53,4 @@ export interface DashboardSettings {
title?: string;
filename: string;
rows: DashboardRow[];
}
}