Merge branch 'public-single-node' into pmm-6401-read-prometheus-data-files

This commit is contained in:
Aliaksandr Valialkin 2022-10-24 21:30:41 +03:00
commit c4fc87f8b8
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
42 changed files with 350 additions and 230 deletions

View file

@ -1,12 +1,12 @@
{
"files": {
"main.css": "./static/css/main.ba692000.css",
"main.js": "./static/js/main.efa0a7b4.js",
"main.css": "./static/css/main.a8f63142.css",
"main.js": "./static/js/main.d67a62da.js",
"static/js/27.939f971b.chunk.js": "./static/js/27.939f971b.chunk.js",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.ba692000.css",
"static/js/main.efa0a7b4.js"
"static/css/main.a8f63142.css",
"static/js/main.d67a62da.js"
]
}

View file

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="VM-UI is a metric explorer for Victoria Metrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.efa0a7b4.js"></script><link href="./static/css/main.ba692000.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="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap" rel="stylesheet"><script src="./dashboards/index.js" type="module"></script><script defer="defer" src="./static/js/main.d67a62da.js"></script><link href="./static/css/main.a8f63142.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

View file

@ -0,0 +1 @@
body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:Lato,sans-serif}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.MuiAccordionSummary-content{margin:0!important}.shortcut-key{align-items:center;border:1px solid #dedede;border-radius:4px;cursor:default;display:inline-flex;font-size:10px;justify-content:center;line-height:22px;padding:2px 6px 0;text-align:center;white-space:nowrap}.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:700;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 0 0;padding:10px 6px}.legendGroupTitle{align-items:center;border-bottom:1px solid #ecebe6;display:flex;font-size:11px;margin-bottom:5px;padding:0 10px 5px}.legendGroupQuery{font-weight:700;margin-right:4px}.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{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:","}.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}

View file

@ -1 +0,0 @@
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}.shortcut-key{align-items:center;border:1px solid #dedede;border-radius:4px;cursor:default;display:inline-flex;font-size:10px;justify-content:center;line-height:22px;padding:2px 6px 0;text-align:center;white-space:nowrap}.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 0 0;padding:10px 6px}.legendGroupTitle{align-items:center;border-bottom:1px solid #ecebe6;display:flex;font-size:11px;margin-bottom:5px;padding:0 10px 5px}.legendGroupQuery{font-weight:700;margin-right:4px}.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{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:","}.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}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -44,3 +44,59 @@ You dont have to ever use `eject`. The curated feature set is suitable for sm
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
---
# VMUI config options
VMUI can be used to paste into other applications
1. Go to file `index.html`
2. Find root element `<div id="root"></div>`
3. Add `data-params` with the options
#### Options (JSON):
| Name | Default | Description |
|:------------------------|:--------------:|--------------------------------------------------------------------------------------:|
| serverURL | domain name | Can't be changed from the UI |
| inputTenantID | - | If the flag is present, the "Tenant ID" field is displayed |
| headerStyles.background | `#FFFFFF` | Header background color |
| headerStyles.color | `primary.main` | Header font color |
| palette.primary | `#3F51B5` | used to represent primary interface elements for a user |
| palette.secondary | `#F50057` | used to represent secondary interface elements for a user |
| palette.error | `#FF4141` | used to represent interface elements that the user should be made aware of |
| palette.warning | `#FF9800` | used to represent potentially dangerous actions or important messages |
| palette.success | `#4CAF50` | used to indicate the successful completion of an action that user triggered |
| palette.info | `#03A9F4` | used to present information to the user that is neutral and not necessarily important |
#### JSON example:
```json
{
"serverURL": "http://localhost:8428",
"inputTenantID": "true",
"headerStyles": {
"background": "#fff",
"color": "primary.main"
},
"palette": {
"primary": "#538DE8",
"secondary": "#F76F8E",
"error": "#FD151B",
"warning": "#FFB30F",
"success": "#7BE622",
"info": "#0F5BFF"
}
}
```
#### HTML example:
```html
<div id="root" data-params='{"serverURL":"http://localhost:8428","inputTenantID":"true","headerStyles":{"background":"#fff","color":"primary.main"},"palette":{"primary":"#538DE8","secondary":"#F76F8E","error":"#FD151B","warning":"#FFB30F","success":"#7BE622","info":"#0F5BFF"}}'></div>
```

View file

@ -26,7 +26,9 @@
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>VM UI</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap" rel="stylesheet">
<script src="%PUBLIC_URL%/dashboards/index.js" type="module"></script>
</head>
<body>

View file

@ -21,10 +21,10 @@ const classes = {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
background: "#3F51B5",
backgroundColor: "primary.main",
padding: "6px 6px 6px 12px",
borderRadius: "4px 4px 0 0",
color: "#FFF",
color: "primary.contrastText",
},
popoverBody: {
display: "grid",

View file

@ -5,12 +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";
import {useGraphDispatch} from "../../../../state/graph/GraphStateContext";
import {getAppModeParams} from "../../../../utils/app-mode";
import TenantsConfiguration from "../Settings/TenantsConfiguration";
const AdditionalSettings: FC = () => {
const {customStep} = useGraphState();
const graphDispatch = useGraphDispatch();
const {inputTenantID} = getAppModeParams();
const {queryControls: {autocomplete, nocache, isTracingEnabled}, time: {period: {step}}} = useAppState();
const dispatch = useAppDispatch();
@ -46,15 +48,15 @@ const AdditionalSettings: FC = () => {
control={<BasicSwitch checked={isTracingEnabled} onChange={onChangeQueryTracing} />}
/>
</Box>
<Box ml={2} mr={2}>
<StepConfigurator defaultStep={step} customStepEnable={customStep.enable}
<Box ml={2} mr={inputTenantID ? 0 : 2}>
<StepConfigurator
defaultStep={step}
setStep={(value) => {
graphDispatch({type: "SET_CUSTOM_STEP", payload: value});
}}
toggleEnableStep={() => {
graphDispatch({type: "TOGGLE_CUSTOM_STEP"});
}}/>
/>
</Box>
{!!inputTenantID && <Box sx={{mx: 3}}><TenantsConfiguration/></Box>}
</Box>;
};

View file

@ -75,7 +75,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({error, queryOptions}) =>
}
}, [stateQuery]);
return <Box boxShadow="rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;" p={4} pb={2} m={-4} mb={2}>
return <Box>
<Box>
{stateQuery.map((q, i) =>
<Box key={i} display="grid" gridTemplateColumns="1fr auto" gap="4px" width="100%" position="relative"

View file

@ -1,60 +1,41 @@
import React, {FC, useEffect, useState} from "preact/compat";
import React, {FC, useCallback, 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 debounce from "lodash.debounce";
interface StepConfiguratorProps {
defaultStep?: number,
customStepEnable: boolean,
setStep: (step: number) => void,
toggleEnableStep: () => void
}
const StepConfigurator: FC<StepConfiguratorProps> = ({
defaultStep, customStepEnable, setStep, toggleEnableStep
}) => {
const StepConfigurator: FC<StepConfiguratorProps> = ({defaultStep, setStep}) => {
const [customStep, setCustomStep] = useState(defaultStep);
const [error, setError] = useState(false);
useEffect(() => {
setStep(customStep || 1);
}, [customStep]);
const handleApply = (step: number) => setStep(step || 1);
const debouncedHandleApply = useCallback(debounce(handleApply, 700), []);
const onChangeStep = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (!customStepEnable) return;
const value = +e.target.value;
if (value > 0) {
setCustomStep(value);
debouncedHandleApply(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>;
return <TextField
label="Step value"
type="number"
size="small"
variant="outlined"
value={customStep}
error={error}
helperText={error ? "step is out of allowed range" : " "}
onChange={onChangeStep}/>;
};
export default StepConfigurator;
export default StepConfigurator;

View file

@ -9,7 +9,6 @@ import Typography from "@mui/material/Typography";
import CloseIcon from "@mui/icons-material/Close";
import IconButton from "@mui/material/IconButton";
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import {getAppModeEnable} from "../../../../utils/app-mode";
const modalStyle = {
position: "absolute" as const,
@ -27,13 +26,12 @@ const title = "Setting Server URL";
const GlobalSettings: FC = () => {
const appModeEnable = getAppModeEnable();
const {serverUrl} = useAppState();
const dispatch = useAppDispatch();
const [changedServerUrl, setChangedServerUrl] = useState(serverUrl);
const setServer = () => {
if (!appModeEnable) dispatch({type: "SET_SERVER", payload: changedServerUrl});
dispatch({type: "SET_SERVER", payload: changedServerUrl});
handleClose();
};
@ -79,4 +77,4 @@ const GlobalSettings: FC = () => {
</>;
};
export default GlobalSettings;
export default GlobalSettings;

View file

@ -1,8 +1,7 @@
import React, {FC, useEffect, useState} from "preact/compat";
import React, {FC, useState} from "preact/compat";
import TextField from "@mui/material/TextField";
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import {useAppState} from "../../../../state/common/StateContext";
import {ErrorTypes} from "../../../../types";
import {getAppModeEnable, getAppModeParams} from "../../../../utils/app-mode";
import {ChangeEvent} from "react";
export interface ServerConfiguratorProps {
@ -12,30 +11,19 @@ export interface ServerConfiguratorProps {
const ServerConfigurator: FC<ServerConfiguratorProps> = ({error, setServer}) => {
const appModeEnable = getAppModeEnable();
const {serverURL: appServerUrl} = getAppModeParams();
const {serverUrl} = useAppState();
const dispatch = useAppDispatch();
const [changedServerUrl, setChangedServerUrl] = useState(serverUrl);
useEffect(() => {
if (appModeEnable) {
dispatch({type: "SET_SERVER", payload: appServerUrl});
setChangedServerUrl(appServerUrl);
}
}, [appServerUrl]);
const onChangeServer = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
const value = e.target.value || "";
setChangedServerUrl(value);
setServer(value);
};
return <TextField variant="outlined" fullWidth label="Server URL" value={changedServerUrl || ""} disabled={appModeEnable}
return <TextField variant="outlined" fullWidth label="Server URL" value={changedServerUrl || ""}
error={error === ErrorTypes.validServer || error === ErrorTypes.emptyServer}
inputProps={{style: {fontFamily: "Monospace"}}}
onChange={onChangeServer}/>;
};
export default ServerConfigurator;
export default ServerConfigurator;

View file

@ -0,0 +1,60 @@
import React, {FC, useState, useEffect, useCallback} from "preact/compat";
import TextField from "@mui/material/TextField";
import InputAdornment from "@mui/material/InputAdornment";
import Tooltip from "@mui/material/Tooltip";
import InfoIcon from "@mui/icons-material/Info";
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import {ChangeEvent} from "react";
import debounce from "lodash.debounce";
import {getAppModeParams} from "../../../../utils/app-mode";
const TenantsConfiguration: FC = () => {
const {serverURL} = getAppModeParams();
const {tenantId: tenantIdState} = useAppState();
const dispatch = useAppDispatch();
const [tenantId, setTenantId] = useState<string | number>(tenantIdState || 0);
const handleApply = (value: string | number) => {
const tenantId = Number(value);
dispatch({type: "SET_TENANT_ID", payload: tenantId});
if (serverURL) {
const updateServerUrl = serverURL.replace(/(\/select\/)([\d]+)(\/prometheus)/gmi, `$1${tenantId}$3`);
dispatch({type: "SET_SERVER", payload: updateServerUrl});
dispatch({type: "RUN_QUERY"});
}
};
const debouncedHandleApply = useCallback(debounce(handleApply, 700), []);
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setTenantId(e.target.value);
debouncedHandleApply(e.target.value);
};
useEffect(() => {
if (tenantId === tenantIdState) return;
setTenantId(tenantIdState);
}, [tenantIdState]);
return <TextField
label="Tenant ID"
type="number"
size="small"
variant="outlined"
value={tenantId}
onChange={handleChange}
InputProps={{
inputProps: {min: 0},
startAdornment: (
<InputAdornment position="start">
<Tooltip title={"Define tenant id if you need request to another storage"}>
<InfoIcon fontSize={"small"} />
</Tooltip>
</InputAdornment>
),
}}
/>;
};
export default TenantsConfiguration;

View file

@ -11,6 +11,7 @@ import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemText from "@mui/material/ListItemText";
import {useLocation} from "react-router-dom";
import {getAppModeEnable} from "../../../../utils/app-mode";
interface AutoRefreshOption {
seconds: number
@ -35,6 +36,7 @@ const delayOptions: AutoRefreshOption[] = [
export const ExecutionControls: FC = () => {
const dispatch = useAppDispatch();
const appModeEnable = getAppModeEnable();
const {queryControls: {autoRefresh}} = useAppState();
const location = useLocation();
@ -77,7 +79,7 @@ export const ExecutionControls: FC = () => {
sx={{
minWidth: "110px",
color: "white",
border: "1px solid rgba(0, 0, 0, 0.2)",
border: appModeEnable ? "none" : "1px solid rgba(0, 0, 0, 0.2)",
justifyContent: "space-between",
boxShadow: "none",
}}
@ -104,4 +106,4 @@ export const ExecutionControls: FC = () => {
</Paper>
</ClickAwayListener></Popper>
</>;
};
};

View file

@ -15,6 +15,7 @@ import Divider from "@mui/material/Divider";
import ClickAwayListener from "@mui/material/ClickAwayListener";
import Tooltip from "@mui/material/Tooltip";
import AlarmAdd from "@mui/icons-material/AlarmAdd";
import {getAppModeEnable} from "../../../../utils/app-mode";
const formatDate = "YYYY-MM-DD HH:mm:ss";
@ -43,6 +44,7 @@ export const TimeSelector: FC = () => {
const {time: {period: {end, start}, relativeTime}} = useAppState();
const dispatch = useAppDispatch();
const appModeEnable = getAppModeEnable();
useEffect(() => {
setUntil(formatDateForNativeInput(dateFromSeconds(end)));
@ -96,7 +98,7 @@ export const TimeSelector: FC = () => {
<Button variant="contained" color="primary"
sx={{
color: "white",
border: "1px solid rgba(0, 0, 0, 0.2)",
border: appModeEnable ? "none" : "1px solid rgba(0, 0, 0, 0.2)",
boxShadow: "none"
}}
startIcon={<QueryBuilderIcon/>}

View file

@ -62,7 +62,9 @@ const CustomPanel: FC = () => {
return (
<Box p={4} display="grid" gridTemplateRows="auto 1fr" style={{minHeight: "calc(100vh - 64px)"}}>
<QueryConfigurator error={error} queryOptions={queryOptions}/>
<Box boxShadow="rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;" p={4} pb={2} m={-4} mb={2}>
<QueryConfigurator error={error} queryOptions={queryOptions}/>
</Box>
<Box height="100%">
{isLoading && <Spinner isLoading={isLoading} height={"500px"}/>}
{<Box height={"100%"} bgcolor={"#fff"}>

View file

@ -7,12 +7,12 @@ import {getHideSeries, getLegendItem, getSeriesItem} from "../../../utils/uplot/
import {getLimitsYAxis, getTimeSeries} from "../../../utils/uplot/axes";
import {LegendItem} from "../../../utils/uplot/types";
import {TimeParams} from "../../../types";
import {AxisRange, CustomStep, YaxisState} from "../../../state/graph/reducer";
import {AxisRange, YaxisState} from "../../../state/graph/reducer";
export interface GraphViewProps {
data?: MetricResult[];
period: TimeParams;
customStep: CustomStep;
customStep: number;
query: string[];
alias?: string[],
yaxis: YaxisState;
@ -49,7 +49,7 @@ const GraphView: FC<GraphViewProps> = ({
setPeriod,
alias = []
}) => {
const currentStep = useMemo(() => customStep.enable ? customStep.value : period.step || 1, [period.step, customStep]);
const currentStep = useMemo(() => customStep || period.step || 1, [period.step, customStep]);
const [dataChart, setDataChart] = useState<uPlotData>([[]]);
const [series, setSeries] = useState<uPlotSeries[]>([]);
@ -69,16 +69,13 @@ const GraphView: FC<GraphViewProps> = ({
const tempTimes: number[] = [];
const tempValues: {[key: string]: number[]} = {};
const tempLegend: LegendItem[] = [];
const tempSeries: uPlotSeries[] = [];
const tempSeries: uPlotSeries[] = [{}];
data?.forEach((d) => {
const seriesItem = getSeriesItem(d, hideSeries, alias);
tempSeries.push(seriesItem);
tempLegend.push(getLegendItem(seriesItem, d.group));
let tmpValues = tempValues[d.group];
if (!tmpValues) {
tmpValues = [];
}
const tmpValues = tempValues[d.group] || [];
for (const v of d.values) {
tempTimes.push(v[0]);
tmpValues.push(promValueToNumber(v[1]));
@ -87,14 +84,15 @@ const GraphView: FC<GraphViewProps> = ({
});
const timeSeries = getTimeSeries(tempTimes, currentStep, period);
setDataChart([timeSeries, ...data.map(d => {
const timeDataSeries = data.map(d => {
const results = [];
const values = d.values;
const length = values.length;
let j = 0;
for (const t of timeSeries) {
while (j < values.length && values[j][0] < t) j++;
while (j < length && values[j][0] < t) j++;
let v = null;
if (j < values.length && values[j][0] == t) {
if (j < length && values[j][0] == t) {
v = promValueToNumber(values[j][1]);
if (!Number.isFinite(v)) {
// Treat special values as nulls in order to satisfy uPlot.
@ -105,25 +103,24 @@ const GraphView: FC<GraphViewProps> = ({
results.push(v);
}
return results;
})] as uPlotData);
setLimitsYaxis(tempValues);
});
timeDataSeries.unshift(timeSeries);
const newSeries = [{}, ...tempSeries];
if (JSON.stringify(newSeries) !== JSON.stringify(series)) {
setSeries(newSeries);
setLegend(tempLegend);
}
setLimitsYaxis(tempValues);
setDataChart(timeDataSeries as uPlotData);
setSeries(tempSeries);
setLegend(tempLegend);
}, [data]);
useEffect(() => {
const tempLegend: LegendItem[] = [];
const tempSeries: uPlotSeries[] = [];
const tempSeries: uPlotSeries[] = [{}];
data?.forEach(d => {
const seriesItem = getSeriesItem(d, hideSeries, alias);
tempSeries.push(seriesItem);
tempLegend.push(getLegendItem(seriesItem, d.group));
});
setSeries([{}, ...tempSeries]);
setSeries(tempSeries);
setLegend(tempLegend);
}, [hideSeries]);

View file

@ -3,7 +3,6 @@ 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 "../CustomPanel/Configurator/Time/ExecutionControls";
import Logo from "../common/Logo";
import {setQueryStringWithoutPageReload} from "../../utils/query-string";
@ -17,6 +16,7 @@ import DatePicker from "../Main/DatePicker/DatePicker";
import {useCardinalityState, useCardinalityDispatch} from "../../state/cardinality/CardinalityStateContext";
import {useEffect} from "react";
import ShortcutKeys from "../ShortcutKeys/ShortcutKeys";
import {getAppModeEnable, getAppModeParams} from "../../utils/app-mode";
const classes = {
logo: {
@ -25,9 +25,8 @@ const classes = {
alignItems: "center",
color: "#fff",
cursor: "pointer",
"&:hover": {
textDecoration: "underline"
}
width: "100%",
marginBottom: "2px"
},
issueLink: {
textAlign: "center",
@ -58,12 +57,18 @@ const classes = {
const Header: FC = () => {
const appModeEnable = getAppModeEnable();
const {headerStyles: {
background = appModeEnable ? "#FFF" : "primary.main",
color = appModeEnable ? "primary.main" : "#FFF",
} = {}} = getAppModeParams();
const {date} = useCardinalityState();
const cardinalityDispatch = useCardinalityDispatch();
const navigate = useNavigate();
const {search, pathname} = useLocation();
const routes = [
const routes = useMemo(() => ([
{
label: "Custom panel",
value: router.home,
@ -71,6 +76,7 @@ const Header: FC = () => {
{
label: "Dashboards",
value: router.dashboards,
hide: appModeEnable
},
{
label: "Cardinality",
@ -80,7 +86,7 @@ const Header: FC = () => {
label: "Top queries",
value: router.topQueries,
}
];
]), [appModeEnable]);
const [activeMenu, setActiveMenu] = useState(pathname);
@ -102,31 +108,30 @@ const Header: FC = () => {
setActiveMenu(pathname);
}, [pathname]);
return <AppBar position="static" sx={{px: 1, boxShadow: "none"}}>
return <AppBar position="static" sx={{px: 1, boxShadow: "none", bgcolor: background, color}}>
<Toolbar>
<Box display="grid" alignItems="center" justifyContent="center">
<Box onClick={onClickLogo} sx={classes.logo}>
<Logo style={{color: "inherit", marginRight: "6px"}}/>
<Typography variant="h5">
<span style={{fontWeight: "bolder"}}>VM</span>
<span style={{fontWeight: "lighter"}}>UI</span>
</Typography>
{!appModeEnable && (
<Box display="grid" alignItems="center" justifyContent="center">
<Box onClick={onClickLogo} sx={classes.logo}>
<Logo style={{color: "inherit", width: "100%"}}/>
</Box>
<Link sx={classes.issueLink} target="_blank"
href="https://github.com/VictoriaMetrics/VictoriaMetrics/issues/new">
create an issue
</Link>
</Box>
<Link sx={classes.issueLink} target="_blank"
href="https://github.com/VictoriaMetrics/VictoriaMetrics/issues/new">
create an issue
</Link>
</Box>
<Box sx={{ml: 8}}>
<Tabs value={activeMenu} textColor="inherit" TabIndicatorProps={{style: {background: "white"}}}
)}
<Box sx={{ml: appModeEnable ? 0 : 8}}>
<Tabs value={activeMenu} textColor="inherit" TabIndicatorProps={{style: {background: color}}}
onChange={(e, val) => setActiveMenu(val)}>
{routes.map(r => (
{routes.filter(r => !r.hide).map(r => (
<Tab
key={`${r.label}_${r.value}`}
label={r.label}
value={r.value}
component={RouterLink}
to={`${r.value}${search}`}
sx={{color}}
/>
))}
</Tabs>
@ -140,7 +145,7 @@ const Header: FC = () => {
/>
)}
{headerSetup?.executionControls && <ExecutionControls/>}
{headerSetup?.globalSettings && <GlobalSettings/>}
{headerSetup?.globalSettings && !appModeEnable && <GlobalSettings/>}
<ShortcutKeys/>
</Box>
</Toolbar>

View file

@ -9,7 +9,7 @@
color: #fff;
font-size: 10px;
line-height: 1.4em;
font-weight: 500;
font-weight: bold;
word-wrap: break-word;
font-family: monospace;
pointer-events: none;
@ -38,4 +38,4 @@
width: 12px;
height: 12px;
margin-right: 4px;
}
}

View file

@ -10,6 +10,7 @@ import Popper from "@mui/material/Popper";
import ClickAwayListener from "@mui/material/ClickAwayListener";
import Paper from "@mui/material/Paper";
import EventIcon from "@mui/icons-material/Event";
import {getAppModeEnable} from "../../../utils/app-mode";
const formatDate = "YYYY-MM-DD";
@ -20,6 +21,7 @@ interface DatePickerProps {
const DatePicker: FC<DatePickerProps> = ({date, onChange}) => {
const appModeEnable = getAppModeEnable();
const dateFormatted = date ? dayjs(date).format(formatDate) : null;
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
@ -30,7 +32,7 @@ const DatePicker: FC<DatePickerProps> = ({date, onChange}) => {
<Button variant="contained" color="primary"
sx={{
color: "white",
border: "1px solid rgba(0, 0, 0, 0.2)",
border: appModeEnable ? "none" : "1px solid rgba(0, 0, 0, 0.2)",
boxShadow: "none"
}}
startIcon={<EventIcon/>}

View file

@ -12,7 +12,6 @@ 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";
@ -36,7 +35,7 @@ const PredefinedPanels: FC<PredefinedPanelsProps> = ({
const containerRef = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(true);
const [customStep, setCustomStep] = useState<CustomStep>({enable: false, value: period.step || 1});
const [customStep, setCustomStep] = useState<number>(period.step || 1);
const [yaxis, setYaxis] = useState<YaxisState>({
limits: {
enable: false,
@ -106,9 +105,10 @@ const PredefinedPanels: FC<PredefinedPanelsProps> = ({
{title || ""}
</Typography>
<Box mr={2} py={1}>
<StepConfigurator defaultStep={period.step} customStepEnable={customStep.enable}
setStep={(value) => setCustomStep({...customStep, value: value})}
toggleEnableStep={() => setCustomStep({...customStep, enable: !customStep.enable})}/>
<StepConfigurator
defaultStep={period.step}
setStep={(value) => setCustomStep(value)}
/>
</Box>
<GraphSettings yaxis={yaxis} setYaxisLimits={setYaxisLimits} toggleEnableLimits={toggleEnableLimits}/>
</Box>

View file

@ -9,6 +9,7 @@ import KeyboardIcon from "@mui/icons-material/Keyboard";
import CloseIcon from "@mui/icons-material/Close";
import Divider from "@mui/material/Divider";
import {isMacOs} from "../../utils/detect-os";
import {getAppModeEnable} from "../../utils/app-mode";
const modalStyle = {
position: "absolute" as const,
@ -80,13 +81,14 @@ const keyList = [
const ShortcutKeys: FC = () => {
const [openList, setOpenList] = useState(false);
const appModeEnable = getAppModeEnable();
return <>
<Tooltip title={"Shortcut keys"}>
<Button variant="contained" color="primary"
sx={{
color: "white",
border: "1px solid rgba(0, 0, 0, 0.2)",
border: appModeEnable ? "none" : "1px solid rgba(0, 0, 0, 0.2)",
minWidth: "34px",
padding: "6px 8px",
boxShadow: "none",

View file

@ -26,10 +26,10 @@ const classes = {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
background: "#3F51B5",
backgroundColor: "primary.main",
padding: "6px 6px 6px 12px",
borderRadius: "4px 4px 0 0",
color: "#FFF",
color: "primary.contrastText",
},
popoverBody: {
display: "grid",
@ -80,7 +80,7 @@ const TableSettings: FC<TableSettingsProps> = ({data, defaultColumns, onChange})
return <Box>
<Tooltip title={title}>
<IconButton onClick={(e) => setAnchorEl(e.currentTarget)}>
<IconButton onClick={(e) => setAnchorEl(e.currentTarget)} disabled={!columns.length}>
<SettingsIcon/>
</IconButton>
</Tooltip>

View file

@ -7,13 +7,15 @@ interface LogoProps {
}
const Logo: FC<LogoProps> = ({style}) => (
<SvgIcon style={style} viewBox="0 0 20 24">
<SvgIcon style={style} viewBox="0 0 74 20">
<path fillRule="evenodd" clipRule="evenodd"
d="M6.11771 9.47563C6.4774 9.7554 6.91935 9.90875 7.37507 9.9119H7.42685C7.9076 9.90451 8.38836 9.71964 8.67681 9.46823C10.1856 8.18898 14.5568 4.18115 14.5568 4.18115C15.7254 3.09415 12.4637 2.00716 7.42685 1.99977H7.36768C2.33084 2.00716 -0.930893 3.09415 0.237711 4.18115C0.237711 4.18115 4.60888 8.18898 6.11771 9.47563ZM8.67681 11.6422C8.31807 11.9246 7.87603 12.0806 7.41945 12.0859H7.37507C6.91849 12.0806 6.47645 11.9246 6.11771 11.6422C5.08224 10.7549 1.38413 7.41995 0.00103198 6.14809V8.07806C0.00103198 8.2925 0.0823905 8.57349 0.222919 8.70659L0.293358 8.77097L0.293386 8.77099C1.33788 9.72556 4.83907 12.9253 6.11771 14.0159C6.47645 14.2983 6.91849 14.4543 7.37507 14.4595H7.41945C7.9076 14.4447 8.38096 14.2599 8.67681 14.0159C9.98594 12.9067 13.6249 9.57175 14.5642 8.70659C14.7121 8.57349 14.7861 8.2925 14.7861 8.07806V6.14809C12.7662 7.99781 10.7297 9.82926 8.67681 11.6422ZM7.41945 16.6261C7.87517 16.623 8.31712 16.4696 8.67681 16.1898C10.7298 14.3744 12.7663 12.5405 14.7861 10.6883V12.6257C14.7861 12.8327 14.7121 13.1137 14.5642 13.2468C13.6249 14.1194 9.98594 17.4469 8.67681 18.5561C8.38096 18.8075 7.9076 18.9924 7.41945 18.9998H7.37507C6.91935 18.9966 6.4774 18.8433 6.11771 18.5635C4.91431 17.5371 1.74223 14.6362 0.502336 13.5023C0.3934 13.4027 0.299379 13.3167 0.222919 13.2468C0.0823905 13.1137 0.00103198 12.8327 0.00103198 12.6257V10.6883C1.38413 11.9528 5.08224 15.2951 6.11771 16.1825C6.47645 16.4649 6.91849 16.6209 7.37507 16.6261H7.41945Z"
fill="currentColor"
/>
<path
d="M8.27 10.58a2.8 2.8 0 0 0 1.7.59h.07c.65-.01 1.3-.26 1.69-.6 2.04-1.73 7.95-7.15 7.95-7.15C21.26 1.95 16.85.48 10.04.47h-.08C3.15.48-1.26 1.95.32 3.42c0 0 5.91 5.42 7.95 7.16"/>
<path
d="M11.73 13.51a2.8 2.8 0 0 1-1.7.6h-.06a2.8 2.8 0 0 1-1.7-.6C6.87 12.31 1.87 7.8 0 6.08v2.61c0 .29.11.67.3.85 1.28 1.17 6.2 5.67 7.97 7.18a2.8 2.8 0 0 0 1.7.6h.06c.66-.02 1.3-.27 1.7-.6 1.77-1.5 6.69-6.01 7.96-7.18.2-.18.3-.56.3-.85V6.08a615.27 615.27 0 0 1-8.26 7.43"/>
<path
d="M11.73 19.66a2.8 2.8 0 0 1-1.7.59h-.06a2.8 2.8 0 0 1-1.7-.6c-1.4-1.2-6.4-5.72-8.27-7.43v2.62c0 .28.11.66.3.84 1.28 1.17 6.2 5.68 7.97 7.19a2.8 2.8 0 0 0 1.7.59h.06c.66-.01 1.3-.26 1.7-.6 1.77-1.5 6.69-6 7.96-7.18.2-.18.3-.56.3-.84v-2.62a614.96 614.96 0 0 1-8.26 7.44"/>
d="M35 3.54L29.16 18H26.73L20.89 3.54H23.05C23.2833 3.54 23.4733 3.59667 23.62 3.71C23.7667 3.82333 23.8767 3.97 23.95 4.15L27.36 12.97C27.4733 13.2567 27.58 13.5733 27.68 13.92C27.7867 14.26 27.8867 14.6167 27.98 14.99C28.06 14.6167 28.1467 14.26 28.24 13.92C28.3333 13.5733 28.4367 13.2567 28.55 12.97L31.94 4.15C31.9933 3.99667 32.0967 3.85667 32.25 3.73C32.41 3.60333 32.6033 3.54 32.83 3.54H35ZM52.1767 3.54V18H49.8067V8.66C49.8067 8.28667 49.8267 7.88333 49.8667 7.45L45.4967 15.66C45.2901 16.0533 44.9734 16.25 44.5467 16.25H44.1667C43.7401 16.25 43.4234 16.0533 43.2167 15.66L38.7967 7.42C38.8167 7.64 38.8334 7.85667 38.8467 8.07C38.8601 8.28333 38.8667 8.48 38.8667 8.66V18H36.4967V3.54H38.5267C38.6467 3.54 38.7501 3.54333 38.8367 3.55C38.9234 3.55667 39.0001 3.57333 39.0667 3.6C39.1401 3.62667 39.2034 3.67 39.2567 3.73C39.3167 3.79 39.3734 3.87 39.4267 3.97L43.7567 12C43.8701 12.2133 43.9734 12.4333 44.0667 12.66C44.1667 12.8867 44.2634 13.12 44.3567 13.36C44.4501 13.1133 44.5467 12.8767 44.6467 12.65C44.7467 12.4167 44.8534 12.1933 44.9667 11.98L49.2367 3.97C49.2901 3.87 49.3467 3.79 49.4067 3.73C49.4667 3.67 49.5301 3.62667 49.5967 3.6C49.6701 3.57333 49.7501 3.55667 49.8367 3.55C49.9234 3.54333 50.0267 3.54 50.1467 3.54H52.1767ZM61.063 17.27C61.743 17.27 62.3496 17.1533 62.883 16.92C63.423 16.68 63.8796 16.35 64.253 15.93C64.6263 15.51 64.9096 15.0167 65.103 14.45C65.303 13.8767 65.403 13.26 65.403 12.6V3.85H66.423V12.6C66.423 13.38 66.2996 14.11 66.053 14.79C65.8063 15.4633 65.4496 16.0533 64.983 16.56C64.523 17.06 63.9596 17.4533 63.293 17.74C62.633 18.0267 61.8896 18.17 61.063 18.17C60.2363 18.17 59.4896 18.0267 58.823 17.74C58.163 17.4533 57.5996 17.06 57.133 16.56C56.673 16.0533 56.3196 15.4633 56.073 14.79C55.8263 14.11 55.703 13.38 55.703 12.6V3.85H56.733V12.59C56.733 13.25 56.8296 13.8667 57.023 14.44C57.223 15.0067 57.5063 15.5 57.873 15.92C58.2463 16.34 58.6996 16.67 59.233 16.91C59.773 17.15 60.383 17.27 61.063 17.27ZM71.4442 18H70.4142V3.85H71.4442V18Z"
fill="currentColor"
/>
</SvgIcon>
);

View file

@ -2,14 +2,10 @@ import {ErrorTypes} from "../types";
import {useAppState} from "../state/common/StateContext";
import {useEffect, useState} from "preact/compat";
import {CardinalityRequestsParams, getCardinalityInfo} from "../api/tsdb";
import {getAppModeEnable, getAppModeParams} from "../utils/app-mode";
import {TSDBStatus} from "../components/CardinalityPanel/types";
import {useCardinalityState} from "../state/cardinality/CardinalityStateContext";
import AppConfigurator from "../components/CardinalityPanel/appConfigurator";
const appModeEnable = getAppModeEnable();
const {serverURL: appServerUrl} = getAppModeParams();
export const useFetchQuery = (): {
fetchUrl?: string[],
isLoading: boolean,
@ -32,12 +28,11 @@ export const useFetchQuery = (): {
}, [error]);
const fetchCardinalityInfo = async (requestParams: CardinalityRequestsParams) => {
const server = appModeEnable ? appServerUrl : serverUrl;
if (!server) return;
if (!serverUrl) return;
setError("");
setIsLoading(true);
setTSDBStatus(appConfigurator.defaultTSDBStatus);
const url = getCardinalityInfo(server, requestParams);
const url = getCardinalityInfo(serverUrl, requestParams);
try {
const response = await fetch(url);

View file

@ -4,10 +4,8 @@ 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 debounce from "lodash.debounce";
import {DisplayType} from "../components/CustomPanel/Configurator/DisplayTypeSwitch";
import {CustomStep} from "../state/graph/reducer";
import usePrevious from "./usePrevious";
import {arrayEquals} from "../utils/array";
import Trace from "../components/CustomPanel/Trace/Trace";
@ -17,12 +15,9 @@ interface FetchQueryParams {
predefinedQuery?: string[]
visible: boolean
display?: DisplayType,
customStep: CustomStep,
customStep: number,
}
const appModeEnable = getAppModeEnable();
const {serverURL: appServerUrl} = getAppModeParams();
export const useFetchQuery = ({predefinedQuery, visible, display, customStep}: FetchQueryParams): {
fetchUrl?: string[],
isLoading: boolean,
@ -53,38 +48,43 @@ export const useFetchQuery = ({predefinedQuery, visible, display, customStep}: F
const fetchData = async (fetchUrl: string[], fetchQueue: AbortController[], displayType: DisplayType, query: string[]) => {
const controller = new AbortController();
setFetchQueue([...fetchQueue, controller]);
const isDisplayChart = displayType === "chart";
try {
const responses = await Promise.all(fetchUrl.map(url => fetch(url, {signal: controller.signal})));
const isDisplayChart = displayType === "chart";
const seriesLimit = MAX_SERIES[displayType];
const tempData: MetricBase[] = [];
const tempTraces: Trace[] = [];
let counter = 1;
let totalLength = 0;
for await (const response of responses) {
for await (const url of fetchUrl) {
const response = await fetch(url, {signal: controller.signal});
const resp = await response.json();
if (response.ok) {
setError(undefined);
if (resp.trace) {
const trace = new Trace(resp.trace, query[counter-1]);
const trace = new Trace(resp.trace, query[counter - 1]);
tempTraces.push(trace);
}
resp.data.result.forEach((d: MetricBase) => {
const freeTempSize = seriesLimit - tempData.length;
resp.data.result.slice(0, freeTempSize).forEach((d: MetricBase) => {
d.group = counter;
tempData.push(d);
});
totalLength += resp.data.result.length;
counter++;
} else {
setError(`${resp.errorType}\r\n${resp?.error}`);
}
}
const length = tempData.length;
const seriesLimit = MAX_SERIES[displayType];
const result = tempData.slice(0, seriesLimit);
const limitText = `Showing ${seriesLimit} series out of ${length} series due to performance reasons. Please narrow down the query, so it returns less series`;
setWarning(length > seriesLimit ? limitText : "");
const limitText = `Showing ${seriesLimit} series out of ${totalLength} series due to performance reasons. Please narrow down the query, so it returns less series`;
setWarning(totalLength > seriesLimit ? limitText : "");
isDisplayChart ? setGraphData(result as MetricResult[]) : setLiveData(result as InstantMetricResult[]);
isDisplayChart ? setGraphData(tempData as MetricResult[]) : setLiveData(tempData as InstantMetricResult[]);
setTraces(tempTraces);
} catch (e) {
if (e instanceof Error && e.name !== "AbortError") {
@ -97,20 +97,19 @@ export const useFetchQuery = ({predefinedQuery, visible, display, customStep}: F
const throttledFetchData = useCallback(debounce(fetchData, 600), []);
const fetchUrl = useMemo(() => {
const server = appModeEnable ? appServerUrl : serverUrl;
const expr = predefinedQuery ?? query;
const displayChart = (display || displayType) === "chart";
if (!period) return;
if (!server) {
if (!serverUrl) {
setError(ErrorTypes.emptyServer);
} else if (expr.every(q => !q.trim())) {
setError(ErrorTypes.validQuery);
} else if (isValidHttpUrl(server)) {
} else if (isValidHttpUrl(serverUrl)) {
const updatedPeriod = {...period};
if (customStep.enable) updatedPeriod.step = customStep.value;
updatedPeriod.step = customStep;
return expr.filter(q => q.trim()).map(q => displayChart
? getQueryRangeUrl(server, q, updatedPeriod, nocache, isTracingEnabled)
: getQueryUrl(server, q, updatedPeriod, isTracingEnabled));
? getQueryRangeUrl(serverUrl, q, updatedPeriod, nocache, isTracingEnabled)
: getQueryUrl(serverUrl, q, updatedPeriod, isTracingEnabled));
} else {
setError(ErrorTypes.validServer);
}

View file

@ -1,10 +1,6 @@
import {useEffect, useState} from "preact/compat";
import {getQueryOptions} from "../api/query-range";
import {useAppState} from "../state/common/StateContext";
import {getAppModeEnable, getAppModeParams} from "../utils/app-mode";
const appModeEnable = getAppModeEnable();
const {serverURL: appServerUrl} = getAppModeParams();
export const useFetchQueryOptions = (): {
queryOptions: string[],
@ -14,9 +10,8 @@ export const useFetchQueryOptions = (): {
const [queryOptions, setQueryOptions] = useState([]);
const fetchOptions = async () => {
const server = appModeEnable ? appServerUrl : serverUrl;
if (!server) return;
const url = getQueryOptions(server);
if (!serverUrl) return;
const url = getQueryOptions(serverUrl);
try {
const response = await fetch(url);

View file

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import {ErrorTypes} from "../types";
import {getAppModeEnable, getAppModeParams} from "../utils/app-mode";
import {getAppModeParams} from "../utils/app-mode";
import {useAppState} from "../state/common/StateContext";
import {useMemo} from "preact/compat";
import {getTopQueries} from "../api/top-queries";
@ -8,8 +8,6 @@ import {TopQueriesData} from "../types";
import {useTopQueriesState} from "../state/topQueries/TopQueriesStateContext";
export const useFetchTopQueries = () => {
const appModeEnable = getAppModeEnable();
const {serverURL: appServerUrl} = getAppModeParams();
const {serverUrl} = useAppState();
const {topN, maxLifetime, runQuery} = useTopQueriesState();
@ -17,9 +15,7 @@ export const useFetchTopQueries = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorTypes | string>();
const server = useMemo(() => appModeEnable ? appServerUrl : serverUrl,
[appModeEnable, serverUrl, appServerUrl]);
const fetchUrl = useMemo(() => getTopQueries(server, topN, maxLifetime), [server, topN, maxLifetime]);
const fetchUrl = useMemo(() => getTopQueries(serverUrl, topN, maxLifetime), [serverUrl, topN, maxLifetime]);
const fetchData = async () => {
setLoading(true);

View file

@ -1,7 +1,5 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
font-family: 'Lato', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View file

@ -31,7 +31,8 @@ export interface AppState {
displayType: DisplayType;
query: string[];
time: TimeState;
queryHistory: QueryHistory[],
tenantId: number;
queryHistory: QueryHistory[];
queryControls: {
autoRefresh: boolean;
autocomplete: boolean;
@ -51,6 +52,7 @@ export type Action =
| { type: "SET_UNTIL", payload: Date }
| { type: "SET_FROM", payload: Date }
| { type: "SET_PERIOD", payload: TimePeriod }
| { type: "SET_TENANT_ID", payload: number }
| { type: "RUN_QUERY"}
| { type: "RUN_QUERY_TO_NOW"}
| { type: "TOGGLE_AUTOREFRESH"}
@ -72,6 +74,7 @@ export const initialState: AppState = {
displayType: (displayType?.value || "chart") as DisplayType,
query: query, // demo_memory_usage_bytes
queryHistory: query.map(q => ({index: 0, values: [q]})),
tenantId: Number(getQueryStringValue("g0.tenantID", 0)),
time: {
duration,
period: getTimeperiodForDuration(duration, endInput),
@ -174,6 +177,11 @@ export function reducer(state: AppState, action: Action): AppState {
relativeTime: "none"
}
};
case "SET_TENANT_ID":
return {
...state,
tenantId: action.payload
};
case "TOGGLE_AUTOREFRESH":
return {
...state,

View file

@ -9,24 +9,18 @@ export interface YaxisState {
}
}
export interface CustomStep {
enable: boolean,
value: number
}
export interface GraphState {
customStep: CustomStep
customStep: number
yaxis: YaxisState
}
export type GraphAction =
| { type: "TOGGLE_ENABLE_YAXIS_LIMITS" }
| { type: "SET_YAXIS_LIMITS", payload: AxisRange }
| { type: "TOGGLE_CUSTOM_STEP" }
| { type: "SET_CUSTOM_STEP", payload: number}
export const initialGraphState: GraphState = {
customStep: {enable: false, value: 1},
customStep: 1,
yaxis: {
limits: {enable: false, range: {"1": [0, 0]}}
}
@ -45,21 +39,10 @@ export function reducer(state: GraphState, action: GraphAction): GraphState {
}
}
};
case "TOGGLE_CUSTOM_STEP":
return {
...state,
customStep: {
...state.customStep,
enable: !state.customStep.enable
}
};
case "SET_CUSTOM_STEP":
return {
...state,
customStep: {
...state.customStep,
value: action.payload
}
customStep: action.payload
};
case "SET_YAXIS_LIMITS":
return {

View file

@ -1,16 +1,28 @@
import {createTheme} from "@mui/material/styles";
import {getAppModeParams} from "../utils/app-mode";
const {palette} = getAppModeParams();
const THEME = createTheme({
palette: {
primary: {
main: "#3F51B5",
main: palette?.primary || "#3F51B5",
light: "#e3f2fd"
},
secondary: {
main: "#F50057"
main: palette?.secondary || "#F50057"
},
error: {
main: "#FF4141"
main: palette?.error || "#FF4141"
},
warning: {
main: palette?.warning || "#ff9800"
},
info: {
main: palette?.info || "#03a9f4"
},
success: {
main: palette?.success || "#4caf50"
}
},
components: {
@ -104,10 +116,25 @@ const THEME = createTheme({
boxShadow: "rgba(0, 0, 0, 0.08) 0px 4px 12px"
}
}
},
MuiTableCell: {
styleOverrides: {
head: {
fontWeight: 600
}
}
},
MuiTab: {
styleOverrides: {
root: {
fontWeight: 600
}
}
}
},
typography: {
"fontSize": 10
"fontSize": 10,
fontFamily: "'Lato', sans-serif"
}
});

View file

@ -1,10 +1,28 @@
export interface AppParams {
serverURL: string
serverURL?: string
inputTenantID?: boolean
headerStyles?: {
background?: string
color?: string
}
palette?: {
primary?: string
secondary?: string
error?: string
warning?: string
info?: string
success?: string
}
}
const getAppModeParams = (): AppParams => {
const dataParams = document.getElementById("root")?.dataset.params || "{}";
return JSON.parse(dataParams);
try {
return JSON.parse(dataParams);
} catch (e) {
console.error(e);
return {};
}
};
const getAppModeEnable = (): boolean => !!Object.keys(getAppModeParams()).length;

View file

@ -1,3 +1,6 @@
import {getAppModeParams} from "./app-mode";
export const getDefaultServer = (): string => {
return window.location.href.replace(/\/(?:prometheus\/)?(?:graph|vmui)\/.*/, "/prometheus");
const {serverURL} = getAppModeParams();
return serverURL || window.location.href.replace(/\/(?:prometheus\/)?(?:graph|vmui)\/.*/, "/prometheus");
};

View file

@ -9,6 +9,7 @@ const graphStateToUrlParams = {
"time.period.step": "step_input",
"time.relativeTime": "relative_time",
"displayType": "tab",
"tenantId": "tenantID",
};
const stateToUrlParams = {

View file

@ -43,6 +43,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): allow controlling [staleness tracking](https://docs.victoriametrics.com/vmagent.html#prometheus-staleness-markers) on a per-[scrape_config](https://docs.victoriametrics.com/sd_configs.html#scrape_configs) basis by specifying `no_stale_markers: true` or `no_stale_markers: false` option in the corresponding [scrape_config](https://docs.victoriametrics.com/sd_configs.html#scrape_configs).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): limit the number of plotted series. This should prevent from browser crashes or hangs when the query returns big number of time series. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3155).
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): reduce memory usage when querying big number of time series. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3240).
* FEATURE: log error if some environment variables referred at `-promscrape.config` via `%{ENV_VAR}` aren't found. This should prevent from silent using incorrect config files.
* FEATURE: immediately shut down VictoriaMetrics apps on the second SIGINT or SIGTERM signal if they couldn't be finished gracefully for some reason after receiving the first signal.
* FEATURE: improve the performance of [/api/v1/series](https://docs.victoriametrics.com/url-examples.html#apiv1series) endpoint by eliminating loading of unused `TSID` data during the API call.

View file

@ -1,5 +0,0 @@
<p style="text-align: center">
<a href="{{ include.href }}" target="_blank" rel="noopener">
<img src="{{ include.href }}">
</a>
</p>

View file

@ -130,7 +130,7 @@ for serving read queries. This API is used in various integrations such as
by [VMUI](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#vmui) - a graphical User Interface for
querying and visualizing metrics:
{% include img.html href="migrate-from-influx-vmui.png" %}
<img src="migrate-from-influx-vmui.png">
See more about [how to query data in VictoriaMetrics](https://docs.victoriametrics.com/keyConcepts.html#query-data).
@ -159,7 +159,7 @@ The data sample consists data points for a measurement `foo`
and a field `bar` with additional tag `instance=localhost`. If we would like plot this data as a time series in Grafana
it might have the following look:
{% include img.html href="migrate-from-influx-data-sample-in-influx.png" %}
<img src="migrate-from-influx-data-sample-in-influx.png">
The query used for this panel is written in
[InfluxQL](https://docs.influxdata.com/influxdb/v1.8/query_language/):
@ -194,7 +194,7 @@ InfluxQL query might be translated to MetricsQL let's break it into components f
In result, executing the `foo_bar{instance="localhost"}` MetricsQL expression with `step=1m` for the same set of data in
Grafana will have the following form:
{% include img.html href="migrate-from-influx-data-sample-in-vm.png" %}
<img src="migrate-from-influx-data-sample-in-vm.png">
Visualizations from both databases are a bit different - VictoriaMetrics shows some extra points
filling the gaps in the graph. This behavior is described in more

View file

@ -93,7 +93,7 @@ So, the `counter` metric shows the number of observed events since the service s
In programming, `counter` is a variable that you **increment** each time something happens.
{% include img.html href="keyConcepts_counter.png" %}
<img src="keyConcepts_counter.png">
`vm_http_requests_total` is a typical example of a counter. The interpretation of a graph
above is that time series `vm_http_requests_total{instance="localhost:8428", job="victoriametrics", path="api/v1/query_range"}`
@ -119,7 +119,7 @@ by humans from other metric types.
Gauge is used for measuring a value that can go up and down:
{% include img.html href="keyConcepts_gauge.png" %}
<img src="keyConcepts_gauge.png">
The metric `process_resident_memory_anon_bytes` on the graph shows the memory usage of the application at every given time.
It is changing frequently, going up and down showing how the process allocates and frees the memory.
@ -219,7 +219,7 @@ Such a combination of `counter` metrics allows
plotting [Heatmaps in Grafana](https://grafana.com/docs/grafana/latest/visualizations/heatmap/)
and calculating [quantiles](https://prometheus.io/docs/practices/histograms/#quantiles):
{% include img.html href="keyConcepts_histogram.png" %}
<img src="keyConcepts_histogram.png">
Grafana doesn't understand buckets with `vmrange` labels, so the [prometheus_buckets](https://docs.victoriametrics.com/MetricsQL.html#prometheus_buckets)
function must be used for converting buckets with `vmrange` labels to buckets with `le` labels before building heatmaps in Grafana.
@ -261,7 +261,7 @@ go_gc_duration_seconds_count 83
The visualisation of summaries is pretty straightforward:
{% include img.html href="keyConcepts_summary.png" %}
<img src="keyConcepts_summary.png">
Such an approach makes summaries easier to use but also puts significant limitations compared to [histograms](#histogram):
@ -320,7 +320,7 @@ VictoriaMetrics supports both models used in modern monitoring applications: [pu
Client regularly sends the collected metrics to the server in the push model:
{% include img.html href="keyConcepts_push_model.png" %}
<img src="keyConcepts_push_model.png">
The client (application) decides when and where to send its metrics. VictoriaMetrics supports the following protocols
for data ingestion (aka `push protocols`):
@ -378,7 +378,7 @@ The cons of push protocol:
Pull model is an approach popularized by [Prometheus](https://prometheus.io/), where the monitoring system decides when
and where to pull metrics from:
{% include img.html href="keyConcepts_pull_model.png" %}
<img src="keyConcepts_pull_model.png">
In pull model, the monitoring system needs to be aware of all the applications it needs to monitor. The metrics are
scraped (pulled) from the known applications (aka `scrape targets`) via HTTP protocol on a regular basis (aka `scrape_interval`).
@ -409,7 +409,7 @@ models for data collection. Many installations use exclusively one of these mode
The most common approach for data collection is using both models:
{% include img.html href="keyConcepts_data_collection.png" %}
<img src="keyConcepts_data_collection.png">
In this approach the additional component is used - [vmagent](https://docs.victoriametrics.com/vmagent.html). Vmagent is
a lightweight agent whose main purpose is to collect, filter, relabel and deliver metrics to VictoriaMetrics.
@ -424,7 +424,7 @@ installation for querying collected data.
VictoriaMetrics components allow building more advanced topologies. For example, vmagents can push metrics from separate datacenters to the central VictoriaMetrics:
{% include img.html href="keyConcepts_two_dcs.png" %}
<img src="keyConcepts_two_dcs.png">
VictoriaMetrics in this example may be either [single-node VictoriaMetrics](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html)
or [VictoriaMetrics Cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html). Vmagent also allows
@ -854,7 +854,7 @@ VictoriaMetrics has a built-in graphical User Interface for querying and visuali
[VMUI](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#vmui).
Open `http://victoriametrics:8428/vmui` page, type the query and see the results:
{% include img.html href="keyConcepts_vmui.png" %}
<img src="keyConcepts_vmui.png">
VictoriaMetrics supports [Prometheus HTTP API](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#prometheus-querying-api-usage)
which makes it possible to [query it with Grafana](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#grafana-setup)