mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmui: predefined panels (#2243)
* feat: add basic components for predefined dashboards * fix: change display alert * feat: add autosize and unit for axes * feat: add component for CircularProgress * feat: change layout for predefined dashboards * feat: add override step for predefined panels * feat: add override step for predefined panels * feat: change yaxis limits for predefined panels * fix: rename flag for hide legend * feat: add formatted panel description * feat: add README.md for dashboard setup * feat: validate dashboard settings * feat: add unit for y-ticks * fix: correct display error for dashboards * fix: disable auto refresh after route change * update package-lock.json * fix: add basename for BrowserRouter * fix: add dynamic basename for routing * update packages * feat: add a pre-defined dashboard "per-job resource usage" * feat: display unit in the hover-tooltip * fix: change routing and home layout * fix: change axis width calc * updated packages * app/vmselect: `make vmui-update` Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
parent
56c32011c3
commit
7cbf19812d
56 changed files with 2063 additions and 1548 deletions
|
@ -1,12 +1,14 @@
|
|||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.098d452b.css",
|
||||
"main.js": "./static/js/main.523bd341.js",
|
||||
"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",
|
||||
"static/js/27.939f971b.chunk.js": "./static/js/27.939f971b.chunk.js",
|
||||
"static/media/README.md": "./static/media/README.a3933343f0099d3929b4.md",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.098d452b.css",
|
||||
"static/js/main.523bd341.js"
|
||||
"static/css/main.d8362c27.css",
|
||||
"static/js/main.1c66c512.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.523bd341.js"></script><link href="./static/css/main.098d452b.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.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>
|
|
@ -1 +1 @@
|
|||
body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.MuiAccordionSummary-content{margin:0!important}.uplot,.uplot *,.uplot :after,.uplot :before{box-sizing:border-box}.uplot{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;width:-webkit-min-content;width:min-content}.u-title{font-size:18px;font-weight:700;text-align:center}.u-wrap{position:relative;-webkit-user-select:none;-ms-user-select:none;user-select:none}.u-over,.u-under{position:absolute}.u-under{overflow:hidden}.uplot canvas{display:block;height:100%;position:relative;width:100%}.u-axis{position:absolute}.u-legend{font-size:14px;margin:auto;text-align:center}.u-inline{display:block}.u-inline *{display:inline-block}.u-inline tr{margin-right:16px}.u-legend th{font-weight:600}.u-legend th>*{display:inline-block;vertical-align:middle}.u-legend .u-marker{background-clip:padding-box!important;height:1em;margin-right:4px;width:1em}.u-inline.u-live th:after{content:":";vertical-align:middle}.u-inline:not(.u-live) .u-value{display:none}.u-series>*{padding:4px}.u-series th{cursor:pointer}.u-legend .u-off>*{opacity:.3}.u-select{background:rgba(0,0,0,.07)}.u-cursor-x,.u-cursor-y,.u-select{pointer-events:none;position:absolute}.u-cursor-x,.u-cursor-y{left:0;top:0;will-change:transform;z-index:100}.u-hz .u-cursor-x,.u-vt .u-cursor-y{border-right:1px dashed #607d8b;height:100%}.u-hz .u-cursor-y,.u-vt .u-cursor-x{border-bottom:1px dashed #607d8b;width:100%}.u-cursor-pt{background-clip:padding-box!important;border:0 solid;border-radius:50%;left:0;pointer-events:none;position:absolute;top:0;will-change:transform;z-index:100}.u-axis.u-off,.u-cursor-pt.u-off,.u-cursor-x.u-off,.u-cursor-y.u-off,.u-select.u-off,.u-tooltip{display:none}.u-tooltip{grid-gap:12px;word-wrap:break-word;background:rgba(57,57,57,.9);border-radius:4px;color:#fff;font-family:monospace;font-size:10px;font-weight:500;line-height:1.4em;max-width:300px;padding:8px;pointer-events:none;position:absolute;z-index:100}.u-tooltip-data{align-items:center;display:flex;flex-wrap:wrap;font-size:11px;line-height:150%}.u-tooltip-data__value{font-weight:700;padding:4px}.u-tooltip__info{grid-gap:4px;display:grid}.u-tooltip__marker{height:12px;margin-right:4px;width:12px}.legendWrapper{grid-gap:20px;cursor:default;display:grid;grid-template-columns:repeat(auto-fit,minmax(400px,1fr));margin-top:20px;position:relative}.legendGroup{margin-bottom:24px}.legendGroupTitle{align-items:center;display:grid;font-size:11px;grid-template-columns:43px auto;padding:10px}.legendGroupQuery{grid-column:1/3;opacity:.6}.legendGroupLine{margin-right:10px}.legendItem{grid-gap:6px;align-items:start;background-color:#fff;cursor:pointer;display:inline-grid;grid-template-columns:auto auto;justify-content:start;padding:7px 50px 7px 10px;transition:.2s ease}.legendItemHide{opacity:.5;text-decoration:line-through}.legendItem:hover{background-color:rgba(0,0,0,.1)}.legendMarker{border-style:solid;border-width:2px;box-sizing:border-box;height:12px;transition:.2s ease;width:12px}.legendLabel{font-size:11px;font-weight:400;line-height:12px}.legendFreeFields{cursor:pointer;padding:3px}.legendFreeFields:hover{text-decoration:underline}.legendFreeFields:not(:last-child):after{content:","}.legendWrapperHotkey{align-items:center;display:flex;font-size:11px}.legendWrapperHotkey p{margin-right:20px}.legendWrapperHotkey code{word-wrap:break-word;background-color:#f2f2f2;border:1px solid #dedede;border-radius:2px;color:#0a0a0a;display:inline;font-size:10px;font-weight:400;max-width:100%;padding:4px 6px}
|
||||
body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.MuiAccordionSummary-content{margin:0!important}.uplot,.uplot *,.uplot :after,.uplot :before{box-sizing:border-box}.uplot{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;width:-webkit-min-content;width:min-content}.u-title{font-size:18px;font-weight:700;text-align:center}.u-wrap{position:relative;-webkit-user-select:none;-ms-user-select:none;user-select:none}.u-over,.u-under{position:absolute}.u-under{overflow:hidden}.uplot canvas{display:block;height:100%;position:relative;width:100%}.u-axis{position:absolute}.u-legend{font-size:14px;margin:auto;text-align:center}.u-inline{display:block}.u-inline *{display:inline-block}.u-inline tr{margin-right:16px}.u-legend th{font-weight:600}.u-legend th>*{display:inline-block;vertical-align:middle}.u-legend .u-marker{background-clip:padding-box!important;height:1em;margin-right:4px;width:1em}.u-inline.u-live th:after{content:":";vertical-align:middle}.u-inline:not(.u-live) .u-value{display:none}.u-series>*{padding:4px}.u-series th{cursor:pointer}.u-legend .u-off>*{opacity:.3}.u-select{background:rgba(0,0,0,.07)}.u-cursor-x,.u-cursor-y,.u-select{pointer-events:none;position:absolute}.u-cursor-x,.u-cursor-y{left:0;top:0;will-change:transform;z-index:100}.u-hz .u-cursor-x,.u-vt .u-cursor-y{border-right:1px dashed #607d8b;height:100%}.u-hz .u-cursor-y,.u-vt .u-cursor-x{border-bottom:1px dashed #607d8b;width:100%}.u-cursor-pt{background-clip:padding-box!important;border:0 solid;border-radius:50%;left:0;pointer-events:none;position:absolute;top:0;will-change:transform;z-index:100}.u-axis.u-off,.u-cursor-pt.u-off,.u-cursor-x.u-off,.u-cursor-y.u-off,.u-select.u-off,.u-tooltip{display:none}.u-tooltip{grid-gap:12px;word-wrap:break-word;background:rgba(57,57,57,.9);border-radius:4px;color:#fff;font-family:monospace;font-size:10px;font-weight:500;line-height:1.4em;max-width:300px;padding:8px;pointer-events:none;position:absolute;z-index:100}.u-tooltip-data{align-items:center;display:flex;flex-wrap:wrap;font-size:11px;line-height:150%}.u-tooltip-data__value{font-weight:700;padding:4px}.u-tooltip__info{grid-gap:4px;display:grid}.u-tooltip__marker{height:12px;margin-right:4px;width:12px}.legendWrapper{cursor:default;display:flex;flex-wrap:wrap;margin-top:20px;position:relative}.legendGroup{margin:0 12px 24px 0}.legendGroupTitle{align-items:center;display:grid;font-size:11px;grid-template-columns:43px auto;padding:10px}.legendGroupQuery{grid-column:1/3;opacity:.6}.legendGroupLine{margin-right:10px}.legendItem{grid-gap:6px;align-items:start;background-color:#fff;cursor:pointer;display:grid;grid-template-columns:auto auto;justify-content:start;padding:7px 50px 7px 10px;transition:.2s ease}.legendItemHide{opacity:.5;text-decoration:line-through}.legendItem:hover{background-color:rgba(0,0,0,.1)}.legendMarker{border-style:solid;border-width:2px;box-sizing:border-box;height:12px;transition:.2s ease;width:12px}.legendLabel{font-size:11px;font-weight:400;line-height:12px}.legendFreeFields{cursor:pointer;padding:3px}.legendFreeFields:hover{text-decoration:underline}.legendFreeFields:not(:last-child):after{content:","}.legendWrapperHotkey{align-items:center;display:flex;font-size:11px}.legendWrapperHotkey p{margin-right:20px}.legendWrapperHotkey code{word-wrap:break-word;background-color:#f2f2f2;border:1px solid #dedede;border-radius:2px;color:#0a0a0a;display:inline;font-size:10px;font-weight:400;max-width:100%;padding:4px 6px}.panelDescription ul{line-height:2.2}.panelDescription a{color:#fff}.panelDescription code{background-color:rgba(0,0,0,.3);border-radius:2px;color:#fff;display:inline;font-size:inherit;font-weight:400;max-width:100%;padding:4px 6px}
|
1
app/vmselect/vmui/static/js/362.1990b49e.chunk.js
Normal file
1
app/vmselect/vmui/static/js/362.1990b49e.chunk.js
Normal file
|
@ -0,0 +1 @@
|
|||
"use strict";(self.webpackChunkvmui=self.webpackChunkvmui||[]).push([[362],{8362:function(e,s,u){e.exports=u.p+"static/media/README.a3933343f0099d3929b4.md"}}]);
|
2
app/vmselect/vmui/static/js/main.1c66c512.js
Normal file
2
app/vmselect/vmui/static/js/main.1c66c512.js
Normal file
File diff suppressed because one or more lines are too long
|
@ -6,7 +6,29 @@
|
|||
* @license MIT
|
||||
*/
|
||||
|
||||
/** @license MUI v5.4.4
|
||||
/**
|
||||
* React Router DOM v6.2.2
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Router v6.2.2
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/** @license MUI v5.5.2
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,76 @@
|
|||
### Configuration options
|
||||
|
||||
<br/>
|
||||
DashboardSettings:
|
||||
|
||||
| Name | Type | Description |
|
||||
|:----------|:----------------:|---------------------------:|
|
||||
| rows* | `DashboardRow[]` | Sections containing panels |
|
||||
| title | `string` | Dashboard title |
|
||||
|
||||
|
||||
<br/>
|
||||
DashboardRow:
|
||||
|
||||
| Name | Type | Description |
|
||||
|:-----------|:-----------------:|---------------------------:|
|
||||
| panels* | `PanelSettings[]` | List of panels (charts) |
|
||||
| title | `string` | Row title |
|
||||
|
||||
<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` |
|
||||
|
||||
---
|
||||
|
||||
### Example json
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Example",
|
||||
"rows": [
|
||||
{
|
||||
"title": "Performance",
|
||||
"panels": [
|
||||
{
|
||||
"title": "Query duration",
|
||||
"description": "The less time it takes is better.\n* `*` - unsupported query path\n* `/write` - insert into VM\n* `/metrics` - query VM system metrics\n* `/query` - query instant values\n* `/query_range` - query over a range of time\n* `/series` - match a certain label set\n* `/label/{}/values` - query a list of label values (variables mostly)",
|
||||
"unit": "ms",
|
||||
"showLegend": false,
|
||||
"expr": [
|
||||
"max(vm_request_duration_seconds{quantile=~\"(0.5|0.99)\"}) by (path, quantile) > 0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Concurrent flushes on disk",
|
||||
"description": "Shows how many ongoing insertions (not API /write calls) on disk are taking place, where:\n* `max` - equal to number of CPUs;\n* `current` - current number of goroutines busy with inserting rows into underlying storage.\n\nEvery successful API /write call results into flush on disk. However, these two actions are separated and controlled via different concurrency limiters. The `max` on this panel can't be changed and always equal to number of CPUs. \n\nWhen `current` hits `max` constantly, it means storage is overloaded and requires more CPU.\n\n",
|
||||
"expr": [
|
||||
"sum(vm_concurrent_addrows_capacity)",
|
||||
"sum(vm_concurrent_addrows_current)"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Troubleshooting",
|
||||
"panels": [
|
||||
{
|
||||
"title": "Churn rate",
|
||||
"description": "Shows the rate and total number of new series created over last 24h.\n\nHigh churn rate tightly connected with database performance and may result in unexpected OOM's or slow queries. It is recommended to always keep an eye on this metric to avoid unexpected cardinality \"explosions\".\n\nThe higher churn rate is, the more resources required to handle it. Consider to keep the churn rate as low as possible.\n\nGood references to read:\n* https://www.robustperception.io/cardinality-is-key\n* https://www.robustperception.io/using-tsdb-analyze-to-investigate-churn-and-cardinality",
|
||||
"expr": [
|
||||
"sum(rate(vm_new_timeseries_created_total[5m]))",
|
||||
"sum(increase(vm_new_timeseries_created_total[24h]))"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
2507
app/vmui/packages/vmui/package-lock.json
generated
2507
app/vmui/packages/vmui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -17,17 +17,22 @@
|
|||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/lodash.throttle": "^4.1.6",
|
||||
"@types/marked": "^4.0.2",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/react": "^17.0.41",
|
||||
"@types/react-dom": "^17.0.14",
|
||||
"@types/react-measure": "^2.0.8",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/webpack-env": "^1.16.3",
|
||||
"dayjs": "^1.11.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"marked": "^4.0.12",
|
||||
"preact": "^10.6.6",
|
||||
"qs": "^6.10.3",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"typescript": "~4.6.2",
|
||||
"uplot": "^1.6.19",
|
||||
"web-vitals": "^2.1.4"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, {FC} from "preact/compat";
|
||||
import {HashRouter, Route, Routes} from "react-router-dom";
|
||||
import {SnackbarProvider} from "./contexts/Snackbar";
|
||||
import HomeLayout from "./components/Home/HomeLayout";
|
||||
import {StateProvider} from "./state/common/StateContext";
|
||||
import {AuthStateProvider} from "./state/auth/AuthStateContext";
|
||||
import {GraphStateProvider} from "./state/graph/GraphStateContext";
|
||||
|
@ -9,6 +9,11 @@ import { ThemeProvider, StyledEngineProvider } from "@mui/material/styles";
|
|||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
import LocalizationProvider from "@mui/lab/LocalizationProvider";
|
||||
import DayjsUtils from "@date-io/dayjs";
|
||||
import router from "./router/index";
|
||||
|
||||
import CustomPanel from "./components/CustomPanel/CustomPanel";
|
||||
import HomeLayout from "./components/Home/HomeLayout";
|
||||
import DashboardsLayout from "./components/PredefinedPanels/DashboardsLayout";
|
||||
|
||||
|
||||
const App: FC = () => {
|
||||
|
@ -22,7 +27,14 @@ const App: FC = () => {
|
|||
<AuthStateProvider> {/* Auth related info - optionally persisted to Local Storage */}
|
||||
<GraphStateProvider> {/* Graph settings */}
|
||||
<SnackbarProvider> {/* Display various snackbars */}
|
||||
<HomeLayout/>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path={"/"} element={<HomeLayout/>}>
|
||||
<Route path={router.home} element={<CustomPanel/>}/>
|
||||
<Route path={router.dashboards} element={<DashboardsLayout/>}/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</SnackbarProvider>
|
||||
</GraphStateProvider>
|
||||
</AuthStateProvider>
|
||||
|
|
|
@ -3,29 +3,31 @@ import {ChangeEvent} from "react";
|
|||
import Box from "@mui/material/Box";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {useGraphDispatch, useGraphState} from "../../../../state/graph/GraphStateContext";
|
||||
import debounce from "lodash.debounce";
|
||||
import BasicSwitch from "../../../../theme/switch";
|
||||
import {AxisRange, YaxisState} from "../../../../state/graph/reducer";
|
||||
|
||||
const AxesLimitsConfigurator: FC = () => {
|
||||
interface AxesLimitsConfiguratorProps {
|
||||
yaxis: YaxisState,
|
||||
setYaxisLimits: (limits: AxisRange) => void,
|
||||
toggleEnableLimits: () => void
|
||||
}
|
||||
|
||||
const AxesLimitsConfigurator: FC<AxesLimitsConfiguratorProps> = ({yaxis, setYaxisLimits, toggleEnableLimits}) => {
|
||||
|
||||
const { yaxis } = useGraphState();
|
||||
const graphDispatch = useGraphDispatch();
|
||||
const axes = useMemo(() => Object.keys(yaxis.limits.range), [yaxis.limits.range]);
|
||||
|
||||
const onChangeYaxisLimits = () => { graphDispatch({type: "TOGGLE_ENABLE_YAXIS_LIMITS"}); };
|
||||
|
||||
const onChangeLimit = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, axis: string, index: number) => {
|
||||
const newLimits = yaxis.limits.range;
|
||||
newLimits[axis][index] = +e.target.value;
|
||||
if (newLimits[axis][0] === newLimits[axis][1] || newLimits[axis][0] > newLimits[axis][1]) return;
|
||||
graphDispatch({type: "SET_YAXIS_LIMITS", payload: newLimits});
|
||||
setYaxisLimits(newLimits);
|
||||
};
|
||||
const debouncedOnChangeLimit = useCallback(debounce(onChangeLimit, 500), [yaxis.limits.range]);
|
||||
|
||||
return <Box display="grid" alignItems="center" gap={2}>
|
||||
<FormControlLabel
|
||||
control={<BasicSwitch checked={yaxis.limits.enable} onChange={onChangeYaxisLimits}/>}
|
||||
control={<BasicSwitch checked={yaxis.limits.enable} onChange={toggleEnableLimits}/>}
|
||||
label="Fix the limits for y-axis"
|
||||
/>
|
||||
<Box display="grid" alignItems="center" gap={2}>
|
|
@ -10,6 +10,7 @@ import Typography from "@mui/material/Typography";
|
|||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import ClickAwayListener from "@mui/material/ClickAwayListener";
|
||||
import {AxisRange, YaxisState} from "../../../../state/graph/reducer";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
popover: {
|
||||
|
@ -35,7 +36,13 @@ const useStyles = makeStyles({
|
|||
|
||||
const title = "Axes Settings";
|
||||
|
||||
const GraphSettings: FC = () => {
|
||||
interface GraphSettingsProps {
|
||||
yaxis: YaxisState,
|
||||
setYaxisLimits: (limits: AxisRange) => void,
|
||||
toggleEnableLimits: () => void
|
||||
}
|
||||
|
||||
const GraphSettings: FC<GraphSettingsProps> = ({yaxis, setYaxisLimits, toggleEnableLimits}) => {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
|
@ -61,7 +68,11 @@ const GraphSettings: FC = () => {
|
|||
</IconButton>
|
||||
</div>
|
||||
<Box className={classes.popoverBody}>
|
||||
<AxesLimitsConfigurator/>
|
||||
<AxesLimitsConfigurator
|
||||
yaxis={yaxis}
|
||||
setYaxisLimits={setYaxisLimits}
|
||||
toggleEnableLimits={toggleEnableLimits}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
</ClickAwayListener>
|
|
@ -5,10 +5,14 @@ import {saveToStorage} from "../../../../utils/storage";
|
|||
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
|
||||
import BasicSwitch from "../../../../theme/switch";
|
||||
import StepConfigurator from "./StepConfigurator";
|
||||
import {useGraphDispatch, useGraphState} from "../../../../state/graph/GraphStateContext";
|
||||
|
||||
const AdditionalSettings: FC = () => {
|
||||
|
||||
const {queryControls: {autocomplete, nocache}} = useAppState();
|
||||
const {customStep} = useGraphState();
|
||||
const graphDispatch = useGraphDispatch();
|
||||
|
||||
const {queryControls: {autocomplete, nocache}, time: {period: {step}}} = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onChangeAutocomplete = () => {
|
||||
|
@ -33,7 +37,13 @@ const AdditionalSettings: FC = () => {
|
|||
/>
|
||||
</Box>
|
||||
<Box ml={2}>
|
||||
<StepConfigurator/>
|
||||
<StepConfigurator defaultStep={step} customStepEnable={customStep.enable}
|
||||
setStep={(value) => {
|
||||
graphDispatch({type: "SET_CUSTOM_STEP", payload: value});
|
||||
}}
|
||||
toggleEnableStep={() => {
|
||||
graphDispatch({type: "TOGGLE_CUSTOM_STEP"});
|
||||
}}/>
|
||||
</Box>
|
||||
</Box>;
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
import React, {FC, useEffect, useState} from "preact/compat";
|
||||
import {ChangeEvent} from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import BasicSwitch from "../../../../theme/switch";
|
||||
|
||||
interface StepConfiguratorProps {
|
||||
defaultStep?: number,
|
||||
customStepEnable: boolean,
|
||||
setStep: (step: number) => void,
|
||||
toggleEnableStep: () => void
|
||||
}
|
||||
|
||||
const StepConfigurator: FC<StepConfiguratorProps> = ({
|
||||
defaultStep, customStepEnable, setStep, toggleEnableStep
|
||||
}) => {
|
||||
|
||||
const [customStep, setCustomStep] = useState(defaultStep);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setStep(customStep || 1);
|
||||
}, [customStep]);
|
||||
|
||||
const onChangeStep = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
if (!customStepEnable) return;
|
||||
const value = +e.target.value;
|
||||
if (value > 0) {
|
||||
setCustomStep(value);
|
||||
setError(false);
|
||||
} else {
|
||||
setError(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeEnableStep = () => {
|
||||
setError(false);
|
||||
toggleEnableStep();
|
||||
};
|
||||
|
||||
return <Box display="grid" gridTemplateColumns="auto 120px" alignItems="center">
|
||||
<FormControlLabel
|
||||
control={<BasicSwitch checked={customStepEnable} onChange={onChangeEnableStep}/>}
|
||||
label="Override step value"
|
||||
/>
|
||||
<TextField
|
||||
label="Step value"
|
||||
type="number"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
value={customStep}
|
||||
disabled={!customStepEnable}
|
||||
error={error}
|
||||
helperText={error ? "step is out of allowed range" : " "}
|
||||
onChange={onChangeStep}/>
|
||||
</Box>;
|
||||
};
|
||||
|
||||
export default StepConfigurator;
|
|
@ -10,6 +10,7 @@ import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
|||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import {useLocation} from "react-router-dom";
|
||||
|
||||
interface AutoRefreshOption {
|
||||
seconds: number
|
||||
|
@ -36,6 +37,12 @@ export const ExecutionControls: FC = () => {
|
|||
const dispatch = useAppDispatch();
|
||||
const {queryControls: {autoRefresh}} = useAppState();
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (autoRefresh) dispatch({type: "TOGGLE_AUTOREFRESH"});
|
||||
}, [location]);
|
||||
|
||||
const [selectedDelay, setSelectedDelay] = useState<AutoRefreshOption>(delayOptions[0]);
|
||||
|
||||
const handleChange = (d: AutoRefreshOption) => {
|
|
@ -0,0 +1,68 @@
|
|||
import React, {FC} from "preact/compat";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Box from "@mui/material/Box";
|
||||
import GraphView from "./Views/GraphView";
|
||||
import TableView from "./Views/TableView";
|
||||
import {useAppDispatch, useAppState} from "../../state/common/StateContext";
|
||||
import QueryConfigurator from "./Configurator/Query/QueryConfigurator";
|
||||
import {useFetchQuery} from "../../hooks/useFetchQuery";
|
||||
import JsonView from "./Views/JsonView";
|
||||
import {DisplayTypeSwitch} from "./Configurator/DisplayTypeSwitch";
|
||||
import GraphSettings from "./Configurator/Graph/GraphSettings";
|
||||
import {useGraphDispatch, useGraphState} from "../../state/graph/GraphStateContext";
|
||||
import {AxisRange} from "../../state/graph/reducer";
|
||||
import Spinner from "../common/Spinner";
|
||||
|
||||
const CustomPanel: FC = () => {
|
||||
|
||||
const {displayType, time: {period}, query} = useAppState();
|
||||
const { customStep, yaxis } = useGraphState();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const graphDispatch = useGraphDispatch();
|
||||
|
||||
const setYaxisLimits = (limits: AxisRange) => {
|
||||
graphDispatch({type: "SET_YAXIS_LIMITS", payload: limits});
|
||||
};
|
||||
|
||||
const toggleEnableLimits = () => {
|
||||
graphDispatch({type: "TOGGLE_ENABLE_YAXIS_LIMITS"});
|
||||
};
|
||||
|
||||
const setPeriod = ({from, to}: {from: Date, to: Date}) => {
|
||||
dispatch({type: "SET_PERIOD", payload: {from, to}});
|
||||
};
|
||||
|
||||
const {isLoading, liveData, graphData, error, queryOptions} = useFetchQuery({
|
||||
visible: true,
|
||||
customStep
|
||||
});
|
||||
|
||||
return (
|
||||
<Box p={4} display="grid" gridTemplateRows="auto 1fr" style={{minHeight: "calc(100vh - 64px)"}}>
|
||||
<QueryConfigurator error={error} queryOptions={queryOptions}/>
|
||||
<Box height="100%">
|
||||
{isLoading && <Spinner isLoading={isLoading} height={"500px"}/>}
|
||||
{<Box height={"100%"} bgcolor={"#fff"}>
|
||||
<Box display="grid" gridTemplateColumns="1fr auto" alignItems="center" mx={-4} px={4} mb={2}
|
||||
borderBottom={1} borderColor="divider">
|
||||
<DisplayTypeSwitch/>
|
||||
{displayType === "chart" && <GraphSettings
|
||||
yaxis={yaxis}
|
||||
setYaxisLimits={setYaxisLimits}
|
||||
toggleEnableLimits={toggleEnableLimits}
|
||||
/>}
|
||||
</Box>
|
||||
{error && <Alert color="error" severity="error" sx={{whiteSpace: "pre-wrap", mt: 2}}>{error}</Alert>}
|
||||
{graphData && period && (displayType === "chart") &&
|
||||
<GraphView data={graphData} period={period} customStep={customStep} query={query} yaxis={yaxis}
|
||||
setYaxisLimits={setYaxisLimits} setPeriod={setPeriod}/>}
|
||||
{liveData && (displayType === "code") && <JsonView data={liveData}/>}
|
||||
{liveData && (displayType === "table") && <TableView data={liveData}/>}
|
||||
</Box>}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomPanel;
|
|
@ -3,14 +3,23 @@ import {MetricResult} from "../../../api/types";
|
|||
import LineChart from "../../LineChart/LineChart";
|
||||
import {AlignedData as uPlotData, Series as uPlotSeries} from "uplot";
|
||||
import Legend from "../../Legend/Legend";
|
||||
import {useGraphDispatch, useGraphState} from "../../../state/graph/GraphStateContext";
|
||||
import {getHideSeries, getLegendItem, getSeriesItem} from "../../../utils/uplot/series";
|
||||
import {getLimitsYAxis, getTimeSeries} from "../../../utils/uplot/axes";
|
||||
import {LegendItem} from "../../../utils/uplot/types";
|
||||
import {useAppState} from "../../../state/common/StateContext";
|
||||
import {TimeParams} from "../../../types";
|
||||
import {AxisRange, CustomStep, YaxisState} from "../../../state/graph/reducer";
|
||||
import Alert from "@mui/material/Alert";
|
||||
|
||||
export interface GraphViewProps {
|
||||
data?: MetricResult[];
|
||||
period: TimeParams;
|
||||
customStep: CustomStep;
|
||||
query: string[];
|
||||
yaxis: YaxisState;
|
||||
unit?: string;
|
||||
showLegend?: boolean;
|
||||
setYaxisLimits: (val: AxisRange) => void
|
||||
setPeriod: ({from, to}: {from: Date, to: Date}) => void
|
||||
}
|
||||
|
||||
const promValueToNumber = (s: string): number => {
|
||||
|
@ -28,10 +37,17 @@ const promValueToNumber = (s: string): number => {
|
|||
}
|
||||
};
|
||||
|
||||
const GraphView: FC<GraphViewProps> = ({data = []}) => {
|
||||
const graphDispatch = useGraphDispatch();
|
||||
const {time: {period}} = useAppState();
|
||||
const { customStep } = useGraphState();
|
||||
const GraphView: FC<GraphViewProps> = ({
|
||||
data = [],
|
||||
period,
|
||||
customStep,
|
||||
query,
|
||||
yaxis,
|
||||
unit,
|
||||
showLegend= true,
|
||||
setYaxisLimits,
|
||||
setPeriod
|
||||
}) => {
|
||||
const currentStep = useMemo(() => customStep.enable ? customStep.value : period.step || 1, [period.step, customStep]);
|
||||
|
||||
const [dataChart, setDataChart] = useState<uPlotData>([[]]);
|
||||
|
@ -41,7 +57,7 @@ const GraphView: FC<GraphViewProps> = ({data = []}) => {
|
|||
|
||||
const setLimitsYaxis = (values: {[key: string]: number[]}) => {
|
||||
const limits = getLimitsYAxis(values);
|
||||
graphDispatch({type: "SET_YAXIS_LIMITS", payload: limits});
|
||||
setYaxisLimits(limits);
|
||||
};
|
||||
|
||||
const onChangeLegend = (legend: LegendItem, metaKey: boolean) => {
|
||||
|
@ -113,11 +129,11 @@ const GraphView: FC<GraphViewProps> = ({data = []}) => {
|
|||
return <>
|
||||
{(data.length > 0)
|
||||
? <div>
|
||||
<LineChart data={dataChart} series={series} metrics={data}/>
|
||||
<Legend labels={legend} onChange={onChangeLegend}/>
|
||||
<LineChart data={dataChart} series={series} metrics={data} period={period} yaxis={yaxis} unit={unit} setPeriod={setPeriod}/>
|
||||
{showLegend && <Legend labels={legend} query={query} onChange={onChangeLegend}/>}
|
||||
</div>
|
||||
: <div style={{textAlign: "center"}}>No data to show</div>}
|
||||
: <Alert color="warning" severity="warning" sx={{mt: 2}}>No data to show</Alert>}
|
||||
</>;
|
||||
};
|
||||
|
||||
export default GraphView;
|
||||
export default GraphView;
|
|
@ -10,6 +10,7 @@ import TableRow from "@mui/material/TableRow";
|
|||
import TableSortLabel from "@mui/material/TableSortLabel";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import {useSortedCategories} from "../../../hooks/useSortedCategories";
|
||||
import Alert from "@mui/material/Alert";
|
||||
|
||||
export interface GraphViewProps {
|
||||
data: InstantMetricResult[];
|
||||
|
@ -98,7 +99,7 @@ const TableView: FC<GraphViewProps> = ({data}) => {
|
|||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
: <div style={{textAlign: "center"}}>No data to show</div>}
|
||||
: <Alert color="warning" severity="warning" sx={{mt: 2}}>No data to show</Alert>}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,15 +1,19 @@
|
|||
import React, {FC} from "preact/compat";
|
||||
import React, {FC, useState} from "preact/compat";
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import Box from "@mui/material/Box";
|
||||
import Link from "@mui/material/Link";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {ExecutionControls} from "../Home/Configurator/Time/ExecutionControls";
|
||||
import {ExecutionControls} from "../CustomPanel/Configurator/Time/ExecutionControls";
|
||||
import Logo from "../common/Logo";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import {setQueryStringWithoutPageReload} from "../../utils/query-string";
|
||||
import {TimeSelector} from "../Home/Configurator/Time/TimeSelector";
|
||||
import GlobalSettings from "../Home/Configurator/Settings/GlobalSettings";
|
||||
import {TimeSelector} from "../CustomPanel/Configurator/Time/TimeSelector";
|
||||
import GlobalSettings from "../CustomPanel/Configurator/Settings/GlobalSettings";
|
||||
import {Link as RouterLink, useLocation, useNavigate} from "react-router-dom";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import router from "../../router/index";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
logo: {
|
||||
|
@ -32,18 +36,41 @@ const useStyles = makeStyles({
|
|||
"&:hover": {
|
||||
opacity: ".8",
|
||||
}
|
||||
},
|
||||
menuLink: {
|
||||
display: "block",
|
||||
padding: "16px 8px",
|
||||
color: "white",
|
||||
fontSize: "11px",
|
||||
textDecoration: "none",
|
||||
cursor: "pointer",
|
||||
textTransform: "uppercase",
|
||||
borderRadius: "4px",
|
||||
transition: ".2s background",
|
||||
"&:hover": {
|
||||
boxShadow: "rgba(0, 0, 0, 0.15) 0px 2px 8px"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const Header: FC = () => {
|
||||
|
||||
const classes = useStyles();
|
||||
const {search, pathname} = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [activeMenu, setActiveMenu] = useState(pathname);
|
||||
|
||||
const onClickLogo = () => {
|
||||
navigateHandler(router.home);
|
||||
setQueryStringWithoutPageReload("");
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const navigateHandler = (pathname: string) => {
|
||||
navigate({pathname, search: search});
|
||||
};
|
||||
|
||||
return <AppBar position="static" sx={{px: 1, boxShadow: "none"}}>
|
||||
<Toolbar>
|
||||
<Box display="grid" alignItems="center" justifyContent="center">
|
||||
|
@ -59,6 +86,13 @@ const Header: FC = () => {
|
|||
create an issue
|
||||
</Link>
|
||||
</Box>
|
||||
<Box sx={{ml: 8}}>
|
||||
<Tabs value={activeMenu} textColor="inherit" TabIndicatorProps={{style: {background: "white"}}}
|
||||
onChange={(e, val) => setActiveMenu(val)}>
|
||||
<Tab label="Custom panel" value={router.home} component={RouterLink} to={`${router.home}${search}`}/>
|
||||
<Tab label="Dashboards" value={router.dashboards} component={RouterLink} to={`${router.dashboards}${search}`}/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
<Box display="grid" gridTemplateColumns="repeat(3, auto)" gap={1} alignItems="center" ml="auto" mr={0}>
|
||||
<TimeSelector/>
|
||||
<ExecutionControls/>
|
||||
|
@ -68,4 +102,4 @@ const Header: FC = () => {
|
|||
</AppBar>;
|
||||
};
|
||||
|
||||
export default Header;
|
||||
export default Header;
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
import React, {FC, useCallback, useEffect, useState} from "preact/compat";
|
||||
import {ChangeEvent} from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import BasicSwitch from "../../../../theme/switch";
|
||||
import {useGraphDispatch, useGraphState} from "../../../../state/graph/GraphStateContext";
|
||||
import {useAppState} from "../../../../state/common/StateContext";
|
||||
import debounce from "lodash.debounce";
|
||||
|
||||
const StepConfigurator: FC = () => {
|
||||
const {customStep} = useGraphState();
|
||||
const graphDispatch = useGraphDispatch();
|
||||
const [error, setError] = useState(false);
|
||||
const {time: {period: {step}}} = useAppState();
|
||||
|
||||
const onChangeStep = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const value = +e.target.value;
|
||||
if (value > 0) {
|
||||
graphDispatch({type: "SET_CUSTOM_STEP", payload: value});
|
||||
setError(false);
|
||||
} else {
|
||||
setError(true);
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedOnChangeStep = useCallback(debounce(onChangeStep, 500), [customStep.value]);
|
||||
|
||||
const onChangeEnableStep = () => {
|
||||
setError(false);
|
||||
graphDispatch({type: "TOGGLE_CUSTOM_STEP"});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!customStep.enable) graphDispatch({type: "SET_CUSTOM_STEP", payload: step || 1});
|
||||
}, [step]);
|
||||
|
||||
return <Box display="grid" gridTemplateColumns="auto 120px" alignItems="center">
|
||||
<FormControlLabel
|
||||
control={<BasicSwitch checked={customStep.enable} onChange={onChangeEnableStep}/>}
|
||||
label="Override step value"
|
||||
/>
|
||||
{customStep.enable &&
|
||||
<TextField label="Step value" type="number" size="small" variant="outlined"
|
||||
defaultValue={customStep.value}
|
||||
error={error}
|
||||
helperText={error ? "step is out of allowed range" : " "}
|
||||
onChange={debouncedOnChangeStep}/>
|
||||
}
|
||||
</Box>;
|
||||
};
|
||||
|
||||
export default StepConfigurator;
|
|
@ -1,62 +1,13 @@
|
|||
import React, {FC} from "preact/compat";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Box from "@mui/material/Box";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Fade from "@mui/material/Fade";
|
||||
import GraphView from "./Views/GraphView";
|
||||
import TableView from "./Views/TableView";
|
||||
import {useAppState} from "../../state/common/StateContext";
|
||||
import QueryConfigurator from "./Configurator/Query/QueryConfigurator";
|
||||
import {useFetchQuery} from "./Configurator/Query/useFetchQuery";
|
||||
import JsonView from "./Views/JsonView";
|
||||
import Header from "../Header/Header";
|
||||
import {DisplayTypeSwitch} from "./Configurator/DisplayTypeSwitch";
|
||||
import GraphSettings from "./Configurator/Graph/GraphSettings";
|
||||
import React, {FC} from "preact/compat";
|
||||
import Box from "@mui/material/Box";
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
const HomeLayout: FC = () => {
|
||||
|
||||
const {displayType, time: {period}} = useAppState();
|
||||
|
||||
const {isLoading, liveData, graphData, error, queryOptions} = useFetchQuery();
|
||||
|
||||
return (
|
||||
<Box id="homeLayout">
|
||||
<Header/>
|
||||
<Box p={4} display="grid" gridTemplateRows="auto 1fr" style={{minHeight: "calc(100vh - 64px)"}}>
|
||||
<QueryConfigurator error={error} queryOptions={queryOptions}/>
|
||||
<Box height="100%">
|
||||
{isLoading && <Fade in={isLoading} style={{
|
||||
transitionDelay: isLoading ? "300ms" : "0ms",
|
||||
}}>
|
||||
<Box alignItems="center" justifyContent="center" flexDirection="column" display="flex"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "calc(100vw - 64px)",
|
||||
position: "absolute",
|
||||
height: "50%",
|
||||
background: "linear-gradient(rgba(255,255,255,.7), rgba(255,255,255,.7), rgba(255,255,255,0))"
|
||||
}}>
|
||||
<CircularProgress/>
|
||||
</Box>
|
||||
</Fade>}
|
||||
{<Box height={"100%"} bgcolor={"#fff"}>
|
||||
<Box display="grid" gridTemplateColumns="1fr auto" alignItems="center" mx={-4} px={4} mb={2}
|
||||
borderBottom={1} borderColor="divider">
|
||||
<DisplayTypeSwitch/>
|
||||
{displayType === "chart" && <GraphSettings/>}
|
||||
</Box>
|
||||
{error && <Alert color="error" severity="error"
|
||||
style={{fontSize: "14px", whiteSpace: "pre-wrap", marginTop: "20px"}}>
|
||||
{error}
|
||||
</Alert>}
|
||||
{graphData && period && (displayType === "chart") && <GraphView data={graphData}/>}
|
||||
{liveData && (displayType === "code") && <JsonView data={liveData}/>}
|
||||
{liveData && (displayType === "table") && <TableView data={liveData}/>}
|
||||
</Box>}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
return <Box id="homeLayout">
|
||||
<Header/>
|
||||
<Outlet/>
|
||||
</Box>;
|
||||
};
|
||||
|
||||
export default HomeLayout;
|
|
@ -1,6 +1,5 @@
|
|||
import React, {FC, useMemo, useState} from "preact/compat";
|
||||
import {hexToRGB} from "../../utils/color";
|
||||
import {useAppState} from "../../state/common/StateContext";
|
||||
import {LegendItem} from "../../utils/uplot/types";
|
||||
import "./legend.css";
|
||||
import {getDashLine} from "../../utils/uplot/helpers";
|
||||
|
@ -8,12 +7,11 @@ import Tooltip from "@mui/material/Tooltip";
|
|||
|
||||
export interface LegendProps {
|
||||
labels: LegendItem[];
|
||||
query: string[];
|
||||
onChange: (item: LegendItem, metaKey: boolean) => void;
|
||||
}
|
||||
|
||||
const Legend: FC<LegendProps> = ({labels, onChange}) => {
|
||||
const {query} = useAppState();
|
||||
|
||||
const Legend: FC<LegendProps> = ({labels, query, onChange}) => {
|
||||
const [copiedValue, setCopiedValue] = useState("");
|
||||
|
||||
const groups = useMemo(() => {
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
.legendWrapper {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
grid-gap: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 20px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.legendGroup {
|
||||
margin-bottom: 24px;
|
||||
margin: 0 12px 24px 0;
|
||||
}
|
||||
|
||||
.legendGroupTitle {
|
||||
|
@ -29,7 +28,7 @@
|
|||
}
|
||||
|
||||
.legendItem {
|
||||
display: inline-grid;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-gap: 6px;
|
||||
align-items: start;
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import React, {FC, useCallback, useEffect, useRef, useState} from "preact/compat";
|
||||
import {useAppDispatch, useAppState} from "../../state/common/StateContext";
|
||||
import uPlot, {AlignedData as uPlotData, Options as uPlotOptions, Series as uPlotSeries, Range, Scales, Scale} from "uplot";
|
||||
import {useGraphState} from "../../state/graph/GraphStateContext";
|
||||
import {defaultOptions} from "../../utils/uplot/helpers";
|
||||
import {dragChart} from "../../utils/uplot/events";
|
||||
import {getAxes, getMinMaxBuffer} from "../../utils/uplot/axes";
|
||||
|
@ -12,18 +10,22 @@ import throttle from "lodash.throttle";
|
|||
import "uplot/dist/uPlot.min.css";
|
||||
import "./tooltip.css";
|
||||
import useResize from "../../hooks/useResize";
|
||||
import {TimeParams} from "../../types";
|
||||
import {YaxisState} from "../../state/graph/reducer";
|
||||
|
||||
export interface LineChartProps {
|
||||
metrics: MetricResult[];
|
||||
data: uPlotData;
|
||||
series: uPlotSeries[];
|
||||
metrics: MetricResult[];
|
||||
data: uPlotData;
|
||||
period: TimeParams;
|
||||
yaxis: YaxisState;
|
||||
series: uPlotSeries[];
|
||||
unit?: string;
|
||||
setPeriod: ({from, to}: {from: Date, to: Date}) => void;
|
||||
}
|
||||
enum typeChartUpdate {xRange = "xRange", yRange = "yRange", data = "data"}
|
||||
|
||||
const LineChart: FC<LineChartProps> = ({data, series, metrics = []}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const {time: {period}} = useAppState();
|
||||
const {yaxis} = useGraphState();
|
||||
const LineChart: FC<LineChartProps> = ({data, series, metrics = [],
|
||||
period, yaxis, unit, setPeriod}) => {
|
||||
const uPlotRef = useRef<HTMLDivElement>(null);
|
||||
const [isPanning, setPanning] = useState(false);
|
||||
const [xRange, setXRange] = useState({min: period.start, max: period.end});
|
||||
|
@ -36,7 +38,7 @@ const LineChart: FC<LineChartProps> = ({data, series, metrics = []}) => {
|
|||
const tooltipOffset = {left: 0, top: 0};
|
||||
|
||||
const setScale = ({min, max}: { min: number, max: number }): void => {
|
||||
dispatch({type: "SET_PERIOD", payload: {from: new Date(min * 1000), to: new Date(max * 1000)}});
|
||||
setPeriod({from: new Date(min * 1000), to: new Date(max * 1000)});
|
||||
};
|
||||
const throttledSetScale = useCallback(throttle(setScale, 500), []);
|
||||
const setPlotScale = ({u, min, max}: { u: uPlot, min: number, max: number }) => {
|
||||
|
@ -73,7 +75,7 @@ const LineChart: FC<LineChartProps> = ({data, series, metrics = []}) => {
|
|||
if (tooltipIdx.dataIdx === u.cursor.idx) return;
|
||||
tooltipIdx.dataIdx = u.cursor.idx || 0;
|
||||
if (tooltipIdx.seriesIdx !== null && tooltipIdx.dataIdx !== undefined) {
|
||||
setTooltip({u, tooltipIdx, metrics, series, tooltip, tooltipOffset});
|
||||
setTooltip({u, tooltipIdx, metrics, series, tooltip, tooltipOffset, unit});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -81,7 +83,7 @@ const LineChart: FC<LineChartProps> = ({data, series, metrics = []}) => {
|
|||
if (tooltipIdx.seriesIdx === sidx) return;
|
||||
tooltipIdx.seriesIdx = sidx;
|
||||
sidx && tooltipIdx.dataIdx !== undefined
|
||||
? setTooltip({u, tooltipIdx, metrics, series, tooltip, tooltipOffset})
|
||||
? setTooltip({u, tooltipIdx, metrics, series, tooltip, tooltipOffset, unit})
|
||||
: tooltip.style.display = "none";
|
||||
};
|
||||
const getRangeX = (): Range.MinMax => [xRange.min, xRange.max];
|
||||
|
@ -101,7 +103,7 @@ const LineChart: FC<LineChartProps> = ({data, series, metrics = []}) => {
|
|||
const options: uPlotOptions = {
|
||||
...defaultOptions,
|
||||
series,
|
||||
axes: getAxes(series),
|
||||
axes: getAxes(series, unit),
|
||||
scales: {...getScales()},
|
||||
width: layoutSize.width ? layoutSize.width - 64 : 400,
|
||||
plugins: [{hooks: {ready: onReadyChart, setCursor, setSeries: seriesFocus}}],
|
||||
|
@ -123,7 +125,7 @@ const LineChart: FC<LineChartProps> = ({data, series, metrics = []}) => {
|
|||
uPlotInst.setData(data);
|
||||
break;
|
||||
}
|
||||
uPlotInst.redraw();
|
||||
if (!isPanning) uPlotInst.redraw();
|
||||
};
|
||||
|
||||
useEffect(() => setXRange({min: period.start, max: period.end}), [period]);
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import React, {FC, useEffect, useMemo, useState} from "preact/compat";
|
||||
import getDashboardSettings from "./getDashboardSettings";
|
||||
import {DashboardRow, DashboardSettings} from "../../types";
|
||||
import Box from "@mui/material/Box";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import PredefinedDashboard from "./PredefinedDashboard";
|
||||
import get from "lodash.get";
|
||||
|
||||
const DashboardLayout: FC = () => {
|
||||
|
||||
const [dashboards, setDashboards] = useState<DashboardSettings[]>();
|
||||
const [tab, setTab] = useState(0);
|
||||
|
||||
const filename = useMemo(() => get(dashboards, [tab, "filename"], ""), [dashboards, tab]);
|
||||
|
||||
const rows = useMemo(() => {
|
||||
return get(dashboards, [tab, "rows"], []) as DashboardRow[];
|
||||
}, [dashboards, tab]);
|
||||
|
||||
useEffect(() => {
|
||||
getDashboardSettings().then(d => d.length && setDashboards(d));
|
||||
}, []);
|
||||
|
||||
return <>
|
||||
{!dashboards && <Alert color="info" severity="info" sx={{m: 4}}>Dashboards not found</Alert>}
|
||||
{dashboards && <>
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||
<Tabs value={tab} onChange={(e, val) => setTab(val)} aria-label="dashboard-tabs">
|
||||
{dashboards && dashboards.map((d, i) =>
|
||||
<Tab key={i} label={d.title || d.filename} id={`tab-${i}`} aria-controls={`tabpanel-${i}`}/>
|
||||
)}
|
||||
</Tabs>
|
||||
</Box>
|
||||
<Box>
|
||||
{Array.isArray(rows) && !!rows.length
|
||||
? rows.map((r,i) =>
|
||||
<PredefinedDashboard
|
||||
key={`${tab}_${i}`}
|
||||
index={i}
|
||||
filename={filename}
|
||||
title={r.title}
|
||||
panels={r.panels}/>)
|
||||
: <Alert color="error" severity="error" sx={{m: 4}}>
|
||||
<code>"rows"</code> not found. Check the configuration file <b>{filename}</b>.
|
||||
</Alert>}
|
||||
</Box>
|
||||
</>}
|
||||
</>;
|
||||
};
|
||||
|
||||
export default DashboardLayout;
|
|
@ -0,0 +1,48 @@
|
|||
import React, {FC} from "preact/compat";
|
||||
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 ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import PredefinedPanels from "./PredefinedPanels";
|
||||
import Alert from "@mui/material/Alert";
|
||||
|
||||
export interface PredefinedDashboardProps extends DashboardRow {
|
||||
filename: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const PredefinedDashboard: FC<PredefinedDashboardProps> = ({index, title, panels, filename}) => {
|
||||
|
||||
return <Accordion defaultExpanded={!index} sx={{boxShadow: "none"}}>
|
||||
<AccordionSummary
|
||||
sx={{px: 3, bgcolor: "rgba(227, 242, 253, 0.6)"}}
|
||||
aria-controls={`panel${index}-content`}
|
||||
id={`panel${index}-header`}
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
>
|
||||
<Box display="flex" alignItems="center" width={"100%"}>
|
||||
{title && <Typography variant="h6" fontWeight="bold" sx={{mr: 2}}>{title}</Typography>}
|
||||
{panels && <Typography variant="body2" fontStyle="italic">({panels.length} panels)</Typography>}
|
||||
</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>
|
||||
}
|
||||
</AccordionDetails>
|
||||
</Accordion>;
|
||||
};
|
||||
|
||||
export default PredefinedDashboard;
|
|
@ -0,0 +1,139 @@
|
|||
import React, {FC, useEffect, useMemo, useRef, useState} from "preact/compat";
|
||||
import Box from "@mui/material/Box";
|
||||
import {PanelSettings} from "../../types";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import InfoIcon from "@mui/icons-material/Info";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {useAppDispatch, useAppState} from "../../state/common/StateContext";
|
||||
import {AxisRange, YaxisState} from "../../state/graph/reducer";
|
||||
import GraphView from "../CustomPanel/Views/GraphView";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import {useFetchQuery} from "../../hooks/useFetchQuery";
|
||||
import Spinner from "../common/Spinner";
|
||||
import StepConfigurator from "../CustomPanel/Configurator/Query/StepConfigurator";
|
||||
import GraphSettings from "../CustomPanel/Configurator/Graph/GraphSettings";
|
||||
import {CustomStep} from "../../state/graph/reducer";
|
||||
import {marked} from "marked";
|
||||
import "./dashboard.css";
|
||||
|
||||
export interface PredefinedPanelsProps extends PanelSettings {
|
||||
filename: string;
|
||||
}
|
||||
|
||||
const PredefinedPanels: FC<PredefinedPanelsProps> = ({
|
||||
title,
|
||||
description,
|
||||
unit,
|
||||
expr,
|
||||
showLegend,
|
||||
filename
|
||||
}) => {
|
||||
|
||||
const {time: {period}} = useAppState();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [customStep, setCustomStep] = useState<CustomStep>({enable: false, value: period.step || 1});
|
||||
const [yaxis, setYaxis] = useState<YaxisState>({
|
||||
limits: {
|
||||
enable: false,
|
||||
range: {"1": [0, 0]}
|
||||
}
|
||||
});
|
||||
|
||||
const validExpr = useMemo(() => Array.isArray(expr) && expr.every(q => typeof q === "string"), [expr]);
|
||||
|
||||
const {isLoading, graphData, error} = useFetchQuery({
|
||||
predefinedQuery: validExpr ? expr : [],
|
||||
display: "chart",
|
||||
visible,
|
||||
customStep,
|
||||
});
|
||||
|
||||
const setYaxisLimits = (limits: AxisRange) => {
|
||||
const tempYaxis = {...yaxis};
|
||||
tempYaxis.limits.range = limits;
|
||||
setYaxis(tempYaxis);
|
||||
};
|
||||
|
||||
const toggleEnableLimits = () => {
|
||||
const tempYaxis = {...yaxis};
|
||||
tempYaxis.limits.enable = !tempYaxis.limits.enable;
|
||||
setYaxis(tempYaxis);
|
||||
};
|
||||
|
||||
const setPeriod = ({from, to}: {from: Date, to: Date}) => {
|
||||
dispatch({type: "SET_PERIOD", payload: {from, to}});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => setVisible(entry.isIntersecting));
|
||||
}, { threshold: 0.1 });
|
||||
if (containerRef.current) observer.observe(containerRef.current);
|
||||
return () => {
|
||||
if (containerRef.current) observer.unobserve(containerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!validExpr) return <Alert color="error" severity="error" sx={{m: 4}}>
|
||||
<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"
|
||||
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>
|
||||
<Typography variant="subtitle1" gridColumn={2} textAlign={"left"} width={"100%"} fontWeight={500}>
|
||||
{title || ""}
|
||||
</Typography>
|
||||
<Box display={"grid"} gridTemplateColumns={"repeat(2, auto)"} gap={2} alignItems={"center"}>
|
||||
<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}/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box px={2} pb={2}>
|
||||
{isLoading && <Spinner isLoading={true} height={"500px"}/>}
|
||||
{error && <Alert color="error" severity="error" sx={{whiteSpace: "pre-wrap", mt: 2}}>{error}</Alert>}
|
||||
{graphData && <GraphView
|
||||
data={graphData}
|
||||
period={period}
|
||||
customStep={customStep}
|
||||
query={expr}
|
||||
yaxis={yaxis}
|
||||
unit={unit}
|
||||
showLegend={showLegend}
|
||||
setYaxisLimits={setYaxisLimits}
|
||||
setPeriod={setPeriod}/>
|
||||
}
|
||||
</Box>
|
||||
</Box>;
|
||||
};
|
||||
|
||||
export default PredefinedPanels;
|
|
@ -0,0 +1,18 @@
|
|||
.panelDescription ul {
|
||||
line-height: 2.2;
|
||||
}
|
||||
|
||||
.panelDescription a {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.panelDescription code {
|
||||
display: inline;
|
||||
max-width: 100%;
|
||||
padding: 4px 6px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 2px;
|
||||
font-weight: 400;
|
||||
font-size: inherit;
|
||||
color: #FFFFFF;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import {DashboardSettings} from "../../types";
|
||||
|
||||
const importModule = async (filename: string) => {
|
||||
const module = await import(`../../dashboards/${filename}`);
|
||||
module.default.filename = filename;
|
||||
return module.default as DashboardSettings;
|
||||
};
|
||||
|
||||
export default async () => {
|
||||
const context = require.context("../../dashboards", true, /\.json$/);
|
||||
const filenames = context.keys().map(r => r.replace("./", ""));
|
||||
return await Promise.all(filenames.map(async f => importModule(f)));
|
||||
};
|
||||
|
30
app/vmui/packages/vmui/src/components/common/Spinner.tsx
Normal file
30
app/vmui/packages/vmui/src/components/common/Spinner.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import React, {FC} from "preact/compat";
|
||||
import Fade from "@mui/material/Fade";
|
||||
import Box from "@mui/material/Box";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
|
||||
interface SpinnerProps {
|
||||
isLoading: boolean;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
const Spinner: FC<SpinnerProps> = ({isLoading, height}) => {
|
||||
return <Fade in={isLoading} style={{
|
||||
transitionDelay: isLoading ? "300ms" : "0ms",
|
||||
}}>
|
||||
<Box alignItems="center" justifyContent="center" flexDirection="column" display="flex"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "calc(100vw - 64px)",
|
||||
position: "absolute",
|
||||
height: height ?? "50%",
|
||||
background: "rgba(255, 255, 255, 0.7)",
|
||||
pointerEvents: "none",
|
||||
zIndex: 2,
|
||||
}}>
|
||||
<CircularProgress/>
|
||||
</Box>
|
||||
</Fade>;
|
||||
};
|
||||
|
||||
export default Spinner;
|
76
app/vmui/packages/vmui/src/dashboards/README.md
Normal file
76
app/vmui/packages/vmui/src/dashboards/README.md
Normal file
|
@ -0,0 +1,76 @@
|
|||
### Configuration options
|
||||
|
||||
<br/>
|
||||
DashboardSettings:
|
||||
|
||||
| Name | Type | Description |
|
||||
|:----------|:----------------:|---------------------------:|
|
||||
| rows* | `DashboardRow[]` | Sections containing panels |
|
||||
| title | `string` | Dashboard title |
|
||||
|
||||
|
||||
<br/>
|
||||
DashboardRow:
|
||||
|
||||
| Name | Type | Description |
|
||||
|:-----------|:-----------------:|---------------------------:|
|
||||
| panels* | `PanelSettings[]` | List of panels (charts) |
|
||||
| title | `string` | Row title |
|
||||
|
||||
<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` |
|
||||
|
||||
---
|
||||
|
||||
### Example json
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Example",
|
||||
"rows": [
|
||||
{
|
||||
"title": "Performance",
|
||||
"panels": [
|
||||
{
|
||||
"title": "Query duration",
|
||||
"description": "The less time it takes is better.\n* `*` - unsupported query path\n* `/write` - insert into VM\n* `/metrics` - query VM system metrics\n* `/query` - query instant values\n* `/query_range` - query over a range of time\n* `/series` - match a certain label set\n* `/label/{}/values` - query a list of label values (variables mostly)",
|
||||
"unit": "ms",
|
||||
"showLegend": false,
|
||||
"expr": [
|
||||
"max(vm_request_duration_seconds{quantile=~\"(0.5|0.99)\"}) by (path, quantile) > 0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Concurrent flushes on disk",
|
||||
"description": "Shows how many ongoing insertions (not API /write calls) on disk are taking place, where:\n* `max` - equal to number of CPUs;\n* `current` - current number of goroutines busy with inserting rows into underlying storage.\n\nEvery successful API /write call results into flush on disk. However, these two actions are separated and controlled via different concurrency limiters. The `max` on this panel can't be changed and always equal to number of CPUs. \n\nWhen `current` hits `max` constantly, it means storage is overloaded and requires more CPU.\n\n",
|
||||
"expr": [
|
||||
"sum(vm_concurrent_addrows_capacity)",
|
||||
"sum(vm_concurrent_addrows_current)"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Troubleshooting",
|
||||
"panels": [
|
||||
{
|
||||
"title": "Churn rate",
|
||||
"description": "Shows the rate and total number of new series created over last 24h.\n\nHigh churn rate tightly connected with database performance and may result in unexpected OOM's or slow queries. It is recommended to always keep an eye on this metric to avoid unexpected cardinality \"explosions\".\n\nThe higher churn rate is, the more resources required to handle it. Consider to keep the churn rate as low as possible.\n\nGood references to read:\n* https://www.robustperception.io/cardinality-is-key\n* https://www.robustperception.io/using-tsdb-analyze-to-investigate-churn-and-cardinality",
|
||||
"expr": [
|
||||
"sum(rate(vm_new_timeseries_created_total[5m]))",
|
||||
"sum(increase(vm_new_timeseries_created_total[24h]))"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
25
app/vmui/packages/vmui/src/dashboards/perJobUsage.json
Normal file
25
app/vmui/packages/vmui/src/dashboards/perJobUsage.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"title": "per-job resource usage",
|
||||
"rows": [
|
||||
{
|
||||
"panels": [
|
||||
{
|
||||
"title": "Per-job CPU usage",
|
||||
"expr": ["sum(rate(process_cpu_seconds_total)) by (job)"]
|
||||
},
|
||||
{
|
||||
"title": "Per-job RSS usage",
|
||||
"expr": ["sum(process_resident_memory_bytes) by (job)"]
|
||||
},
|
||||
{
|
||||
"title": "Per-job disk read",
|
||||
"expr": ["sum(rate(process_io_storage_read_bytes_total)) by (job)"]
|
||||
},{
|
||||
"title": "Per-job disk write",
|
||||
"expr": ["sum(rate(process_io_storage_written_bytes_total)) by (job)"]
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,18 +1,25 @@
|
|||
import {useEffect, useMemo, useCallback, useState} from "preact/compat";
|
||||
import {getQueryOptions, getQueryRangeUrl, getQueryUrl} from "../../../../api/query-range";
|
||||
import {useAppState} from "../../../../state/common/StateContext";
|
||||
import {InstantMetricResult, MetricBase, MetricResult} from "../../../../api/types";
|
||||
import {isValidHttpUrl} from "../../../../utils/url";
|
||||
import {ErrorTypes} from "../../../../types";
|
||||
import {useGraphState} from "../../../../state/graph/GraphStateContext";
|
||||
import {getAppModeEnable, getAppModeParams} from "../../../../utils/app-mode";
|
||||
import {getQueryOptions, getQueryRangeUrl, getQueryUrl} from "../api/query-range";
|
||||
import {useAppState} from "../state/common/StateContext";
|
||||
import {InstantMetricResult, MetricBase, MetricResult} from "../api/types";
|
||||
import {isValidHttpUrl} from "../utils/url";
|
||||
import {ErrorTypes} from "../types";
|
||||
import {getAppModeEnable, getAppModeParams} from "../utils/app-mode";
|
||||
import throttle from "lodash.throttle";
|
||||
import {DisplayType} from "../DisplayTypeSwitch";
|
||||
import {DisplayType} from "../components/CustomPanel/Configurator/DisplayTypeSwitch";
|
||||
import {CustomStep} from "../state/graph/reducer";
|
||||
|
||||
interface FetchQueryParams {
|
||||
predefinedQuery?: string[]
|
||||
visible: boolean
|
||||
display?: DisplayType,
|
||||
customStep: CustomStep,
|
||||
}
|
||||
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const {serverURL: appServerUrl} = getAppModeParams();
|
||||
|
||||
export const useFetchQuery = (): {
|
||||
export const useFetchQuery = ({predefinedQuery, visible, display, customStep}: FetchQueryParams): {
|
||||
fetchUrl?: string[],
|
||||
isLoading: boolean,
|
||||
graphData?: MetricResult[],
|
||||
|
@ -22,8 +29,6 @@ export const useFetchQuery = (): {
|
|||
} => {
|
||||
const {query, displayType, serverUrl, time: {period}, queryControls: {nocache}} = useAppState();
|
||||
|
||||
const {customStep} = useGraphState();
|
||||
|
||||
const [queryOptions, setQueryOptions] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [graphData, setGraphData] = useState<MetricResult[]>();
|
||||
|
@ -67,11 +72,10 @@ export const useFetchQuery = (): {
|
|||
setError(`${e.name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const throttledFetchData = useCallback(throttle(fetchData, 300), []);
|
||||
const throttledFetchData = useCallback(throttle(fetchData, 1000), []);
|
||||
|
||||
const fetchOptions = async () => {
|
||||
const server = appModeEnable ? appServerUrl : serverUrl;
|
||||
|
@ -91,16 +95,19 @@ export const useFetchQuery = (): {
|
|||
|
||||
const fetchUrl = useMemo(() => {
|
||||
const server = appModeEnable ? appServerUrl : serverUrl;
|
||||
const expr = predefinedQuery ?? query;
|
||||
const displayChart = (display || displayType) === "chart";
|
||||
if (!period) return;
|
||||
if (!server) {
|
||||
setError(ErrorTypes.emptyServer);
|
||||
} else if (query.every(q => !q.trim())) {
|
||||
} else if (expr.every(q => !q.trim())) {
|
||||
setError(ErrorTypes.validQuery);
|
||||
} else if (isValidHttpUrl(server)) {
|
||||
if (customStep.enable) period.step = customStep.value;
|
||||
return query.filter(q => q.trim()).map(q => displayType === "chart"
|
||||
? getQueryRangeUrl(server, q, period, nocache)
|
||||
: getQueryUrl(server, q, period));
|
||||
const updatedPeriod = {...period};
|
||||
if (customStep.enable) updatedPeriod.step = customStep.value;
|
||||
return expr.filter(q => q.trim()).map(q => displayChart
|
||||
? getQueryRangeUrl(server, q, updatedPeriod, nocache)
|
||||
: getQueryUrl(server, q, updatedPeriod));
|
||||
} else {
|
||||
setError(ErrorTypes.validServer);
|
||||
}
|
||||
|
@ -111,10 +118,10 @@ export const useFetchQuery = (): {
|
|||
fetchOptions();
|
||||
}, [serverUrl]);
|
||||
|
||||
// TODO: this should depend on query as well, but need to decide when to do the request. Doing it on each query change - looks to be a bad idea. Probably can be done on blur
|
||||
useEffect(() => {
|
||||
throttledFetchData(fetchUrl, fetchQueue, displayType);
|
||||
}, [fetchUrl]);
|
||||
if (!visible) return;
|
||||
throttledFetchData(fetchUrl, fetchQueue, (display || displayType));
|
||||
}, [fetchUrl, visible]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPast = fetchQueue.slice(0, -1);
|
4
app/vmui/packages/vmui/src/router/index.ts
Normal file
4
app/vmui/packages/vmui/src/router/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
home: "/",
|
||||
dashboards: "/dashboards"
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint max-lines: 0 */
|
||||
import {DisplayType} from "../../components/Home/Configurator/DisplayTypeSwitch";
|
||||
import {DisplayType} from "../../components/CustomPanel/Configurator/DisplayTypeSwitch";
|
||||
import {TimeParams, TimePeriod} from "../../types";
|
||||
import {
|
||||
dateFromSeconds,
|
||||
|
|
|
@ -99,6 +99,7 @@ const THEME = createTheme({
|
|||
MuiAlert: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
fontSize: "14px",
|
||||
boxShadow: "rgba(0, 0, 0, 0.08) 0px 4px 12px"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,4 +33,23 @@ export enum ErrorTypes {
|
|||
emptyServer = "Please enter Server URL",
|
||||
validServer = "Please provide a valid Server URL",
|
||||
validQuery = "Please enter a valid Query and execute it"
|
||||
}
|
||||
|
||||
export interface PanelSettings {
|
||||
title?: string;
|
||||
description?: string;
|
||||
unit?: string;
|
||||
expr: string[];
|
||||
showLegend?: boolean;
|
||||
}
|
||||
|
||||
export interface DashboardRow {
|
||||
title?: string;
|
||||
panels: PanelSettings[];
|
||||
}
|
||||
|
||||
export interface DashboardSettings {
|
||||
title?: string;
|
||||
filename: string;
|
||||
rows: DashboardRow[];
|
||||
}
|
|
@ -31,7 +31,7 @@ const stateToUrlParams = {
|
|||
export const setQueryStringWithoutPageReload = (qsValue: string): void => {
|
||||
const w = window;
|
||||
if (w) {
|
||||
const newurl = `${w.location.protocol}//${w.location.host}${w.location.pathname}?${qsValue}`;
|
||||
const newurl = `${w.location.protocol}//${w.location.host}${w.location.pathname}?${qsValue}${w.location.hash}`;
|
||||
w.history.pushState({ path: newurl }, "", newurl);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import {Axis, Series} from "uplot";
|
||||
import uPlot, {Axis, Series} from "uplot";
|
||||
import {getMaxFromArray, getMinFromArray} from "../math";
|
||||
import {roundToMilliseconds} from "../time";
|
||||
import {AxisRange} from "../../state/graph/reducer";
|
||||
import {formatTicks} from "./helpers";
|
||||
import {formatTicks, sizeAxis} from "./helpers";
|
||||
import {TimeParams} from "../../types";
|
||||
|
||||
export const getAxes = (series: Series[]): Axis[] => Array.from(new Set(series.map(s => s.scale))).map(a => {
|
||||
const axis = {scale: a, show: true, font: "10px Arial", values: formatTicks};
|
||||
export const getAxes = (series: Series[], unit?: string): Axis[] => Array.from(new Set(series.map(s => s.scale))).map(a => {
|
||||
const axis = {
|
||||
scale: a,
|
||||
show: true,
|
||||
size: sizeAxis,
|
||||
font: "10px Arial",
|
||||
values: (u: uPlot, ticks: number[]) => formatTicks(u, ticks, unit)
|
||||
};
|
||||
if (!a) return {space: 80};
|
||||
if (!(Number(a) % 2)) return {...axis, side: 1};
|
||||
return axis;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import uPlot from "uplot";
|
||||
import uPlot, {Axis} from "uplot";
|
||||
import {getColorFromString} from "../color";
|
||||
|
||||
export const defaultOptions = {
|
||||
|
@ -28,16 +28,40 @@ export const defaultOptions = {
|
|||
},
|
||||
};
|
||||
|
||||
export const formatTicks = (u: uPlot, ticks: number[]): string[] => {
|
||||
export const formatTicks = (u: uPlot, ticks: number[], unit = ""): string[] => {
|
||||
return ticks.map(v => {
|
||||
const n = Math.abs(v);
|
||||
if (n > 1e-3 && n < 1e4) {
|
||||
return v.toString();
|
||||
}
|
||||
return v.toExponential(1);
|
||||
return `${n > 1e-3 && n < 1e4 ? v.toString() : v.toExponential(1)} ${unit}`;
|
||||
});
|
||||
};
|
||||
|
||||
interface AxisExtend extends Axis {
|
||||
_size?: number;
|
||||
}
|
||||
|
||||
const getTextWidth = (val: string, font: string): number => {
|
||||
const span = document.createElement("span");
|
||||
span.innerText = val;
|
||||
span.style.cssText = `position: absolute; z-index: -1; pointer-events: none; opacity: 0; font: ${font}`;
|
||||
document.body.appendChild(span);
|
||||
const width = span.offsetWidth;
|
||||
span.remove();
|
||||
return width;
|
||||
};
|
||||
|
||||
export const sizeAxis = (u: uPlot, values: string[], axisIdx: number, cycleNum: number): number => {
|
||||
const axis = u.axes[axisIdx] as AxisExtend;
|
||||
|
||||
if (cycleNum > 1) return axis._size || 60;
|
||||
|
||||
let axisSize = 6 + (axis?.ticks?.size || 0) + (axis.gap || 0);
|
||||
|
||||
const longestVal = (values ?? []).reduce((acc, val) => val.length > acc.length ? val : acc, "");
|
||||
if (longestVal != "") axisSize += getTextWidth(longestVal, u.ctx.font);
|
||||
|
||||
return Math.ceil(axisSize);
|
||||
};
|
||||
|
||||
export const getColorLine = (scale: number, label: string): string => getColorFromString(`${scale}${label}`);
|
||||
|
||||
export const getDashLine = (group: number): number[] => group <= 1 ? [] : [group*4, group*1.2];
|
||||
|
|
|
@ -2,7 +2,7 @@ import dayjs from "dayjs";
|
|||
import {SetupTooltip} from "./types";
|
||||
import {getColorLine} from "./helpers";
|
||||
|
||||
export const setTooltip = ({u, tooltipIdx, metrics, series, tooltip, tooltipOffset}: SetupTooltip): void => {
|
||||
export const setTooltip = ({u, tooltipIdx, metrics, series, tooltip, tooltipOffset, unit = ""}: SetupTooltip): void => {
|
||||
const {seriesIdx, dataIdx} = tooltipIdx;
|
||||
if (seriesIdx === null || dataIdx === undefined) return;
|
||||
const dataSeries = u.data[seriesIdx][dataIdx];
|
||||
|
@ -25,7 +25,7 @@ export const setTooltip = ({u, tooltipIdx, metrics, series, tooltip, tooltipOffs
|
|||
const marker = `<div class="u-tooltip__marker" style="background: ${color}"></div>`;
|
||||
tooltip.innerHTML = `<div>${date}</div>
|
||||
<div class="u-tooltip-data">
|
||||
${marker}${metric.__name__ || ""}: <b class="u-tooltip-data__value">${dataSeries}</b>
|
||||
${marker}${metric.__name__ || ""}: <b class="u-tooltip-data__value">${dataSeries}</b> ${unit}
|
||||
</div>
|
||||
<div class="u-tooltip__info">${info}</div>`;
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ export interface SetupTooltip {
|
|||
metrics: MetricResult[],
|
||||
series: Series[],
|
||||
tooltip: HTMLDivElement,
|
||||
unit?: string,
|
||||
tooltipOffset: {
|
||||
left: number,
|
||||
top: number
|
||||
|
|
|
@ -15,6 +15,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
|
|||
|
||||
## tip
|
||||
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add pre-defined dasbhoards for per-job CPU usage, memory usage and disk IO usage. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2243) for details.
|
||||
* FEATURE: add the following command-line flags, which can be used for fine-grained limiting of CPU and memory usage during various API calls:
|
||||
|
||||
* `-search.maxFederateSeries` for limiting the number of time series, which can be returned from [/federate](https://docs.victoriametrics.com/#federation).
|
||||
|
|
Loading…
Reference in a new issue