mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
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:
parent
0fd4c48568
commit
f166f80f15
18 changed files with 843 additions and 854 deletions
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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>
|
|
@ -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"}}]);
|
2
app/vmselect/vmui/static/js/main.0c1bf008.js
Normal file
2
app/vmselect/vmui/static/js/main.0c1bf008.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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:
|
|||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
```
|
|
@ -41,7 +41,11 @@ module.exports = {
|
|||
"react/prop-types": 0,
|
||||
"max-lines": [
|
||||
"error",
|
||||
{"max": 150}
|
||||
{
|
||||
"max": 150,
|
||||
"skipBlankLines": true,
|
||||
"skipComments": true,
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
|
|
1417
app/vmui/packages/vmui/package-lock.json
generated
1417
app/vmui/packages/vmui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}}],
|
||||
};
|
||||
|
||||
|
|
|
@ -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>"panels"</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>"panels"</code> not found. Check the configuration file <b>{filename}</b>.
|
||||
</Alert>
|
||||
}
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>;
|
||||
};
|
||||
|
||||
export default PredefinedDashboard;
|
||||
export default PredefinedDashboard;
|
||||
|
|
|
@ -82,41 +82,34 @@ const PredefinedPanels: FC<PredefinedPanelsProps> = ({
|
|||
<code>"expr"</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;
|
||||
|
|
|
@ -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:
|
|||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
```
|
||||
|
|
|
@ -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)"]
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue