vmui: extend options for app mode (#3252)

* feat: add vmui customization for dbaas

* feat: extends vmui customization for dbaas

* fix: move input tenandId after query

* fix: change serverURL when changing tenantID

* fix: remove options

* docs: add options description
This commit is contained in:
Yury Molodov 2022-10-24 19:45:41 +02:00 committed by GitHub
parent c52c23c272
commit 59199a98dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 299 additions and 177 deletions

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[]>([]);

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,
@ -97,20 +92,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 = {