mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-01-20 07:19:17 +01:00
Vmui/query editor (#1472)
* fix: move request button to server input * feat: add switch for query autocomplete * refactor: rename state for popover open * feat: add detect os by userAgent * fix: change hotkey to run query for mac * fix: change detect mac os * fix: change div to span inside Typography Co-authored-by: yury <yurymolodov@victoriametrics.com>
This commit is contained in:
parent
05672ffc32
commit
a91d41f12a
@ -1,7 +1,5 @@
|
|||||||
import React, {FC, useEffect, useState} from "react";
|
import React, {FC, useEffect, useState} from "react";
|
||||||
import {Box, FormControlLabel, IconButton, Switch, Tooltip} from "@material-ui/core";
|
import {Box, FormControlLabel, IconButton, Switch, Tooltip} from "@material-ui/core";
|
||||||
import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline";
|
|
||||||
|
|
||||||
import EqualizerIcon from "@material-ui/icons/Equalizer";
|
import EqualizerIcon from "@material-ui/icons/Equalizer";
|
||||||
import {useAppDispatch, useAppState} from "../../../state/common/StateContext";
|
import {useAppDispatch, useAppState} from "../../../state/common/StateContext";
|
||||||
import CircularProgressWithLabel from "../../common/CircularProgressWithLabel";
|
import CircularProgressWithLabel from "../../common/CircularProgressWithLabel";
|
||||||
@ -70,23 +68,19 @@ export const ExecutionControls: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return <Box display="flex" alignItems="center">
|
return <Box display="flex" alignItems="center">
|
||||||
<Box mr={2}>
|
|
||||||
<Tooltip title="Execute Query">
|
|
||||||
<IconButton onClick={()=>dispatch({type: "RUN_QUERY"})}>
|
|
||||||
<PlayCircleOutlineIcon className={classes.colorizing} fontSize="large"/>
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
{<FormControlLabel
|
{<FormControlLabel
|
||||||
control={<Switch size="small" className={classes.colorizing} checked={autoRefresh} onChange={handleChange} />}
|
control={<Switch size="small" className={classes.colorizing} checked={autoRefresh} onChange={handleChange} />}
|
||||||
label="Auto-refresh"
|
label="Auto-refresh"
|
||||||
/>}
|
/>}
|
||||||
|
|
||||||
{autoRefresh && <>
|
{autoRefresh && <>
|
||||||
<CircularProgressWithLabel className={classes.colorizing} label={delay} value={progress} onClick={() => {iterateDelays();}} />
|
<CircularProgressWithLabel className={classes.colorizing} label={delay} value={progress}
|
||||||
|
onClick={() => {iterateDelays();}} />
|
||||||
|
<Tooltip title="Change delay refresh">
|
||||||
<Box ml={1}>
|
<Box ml={1}>
|
||||||
<IconButton onClick={() => {iterateDelays();}}><EqualizerIcon style={{color: "white"}} /></IconButton>
|
<IconButton onClick={() => {iterateDelays();}}><EqualizerIcon style={{color: "white"}} /></IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
</>}
|
</>}
|
||||||
</Box>;
|
</Box>;
|
||||||
};
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import React, {FC, useState} from "react";
|
import React, {FC, useRef, useState} from "react";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
@ -7,7 +7,10 @@ import {
|
|||||||
Grid,
|
Grid,
|
||||||
IconButton,
|
IconButton,
|
||||||
TextField,
|
TextField,
|
||||||
Typography
|
Typography,
|
||||||
|
FormControlLabel,
|
||||||
|
Tooltip,
|
||||||
|
Switch,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import QueryEditor from "./QueryEditor";
|
import QueryEditor from "./QueryEditor";
|
||||||
import {TimeSelector} from "./TimeSelector";
|
import {TimeSelector} from "./TimeSelector";
|
||||||
@ -15,14 +18,36 @@ import {useAppDispatch, useAppState} from "../../../state/common/StateContext";
|
|||||||
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
|
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
|
||||||
import SecurityIcon from "@material-ui/icons/Security";
|
import SecurityIcon from "@material-ui/icons/Security";
|
||||||
import {AuthDialog} from "./AuthDialog";
|
import {AuthDialog} from "./AuthDialog";
|
||||||
|
import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline";
|
||||||
|
import Portal from "@material-ui/core/Portal";
|
||||||
|
import Popover from "@material-ui/core/Popover";
|
||||||
|
import SettingsIcon from "@material-ui/icons/Settings";
|
||||||
|
import {saveToStorage} from "../../../utils/storage";
|
||||||
|
|
||||||
const QueryConfigurator: FC = () => {
|
const QueryConfigurator: FC = () => {
|
||||||
|
|
||||||
const {serverUrl, query, time: {duration}} = useAppState();
|
const {serverUrl, query, time: {duration}} = useAppState();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const {queryControls: {autocomplete}} = useAppState();
|
||||||
|
const onChangeAutocomplete = () => {
|
||||||
|
dispatch({type: "TOGGLE_AUTOCOMPLETE"});
|
||||||
|
saveToStorage("AUTOCOMPLETE", !autocomplete);
|
||||||
|
};
|
||||||
|
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [expanded, setExpanded] = useState(true);
|
const [expanded, setExpanded] = useState(true);
|
||||||
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
|
const refSettings = useRef<SVGGElement | any>(null);
|
||||||
|
|
||||||
|
const queryContainer = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const onSetDuration = (dur: string) => dispatch({type: "SET_DURATION", payload: dur});
|
||||||
|
const onRunQuery = () => dispatch({type: "RUN_QUERY"});
|
||||||
|
const onSetQuery = (query: string) => dispatch({type: "SET_QUERY", payload: query});
|
||||||
|
const onSetServer = ({target: {value}}: {target: {value: string}}) => {
|
||||||
|
dispatch({type: "SET_SERVER", payload: value});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -35,31 +60,65 @@ const QueryConfigurator: FC = () => {
|
|||||||
<Box mr={2}>
|
<Box mr={2}>
|
||||||
<Typography variant="h6" component="h2">Query Configuration</Typography>
|
<Typography variant="h6" component="h2">Query Configuration</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
{!expanded && <Box flexGrow={1} onClick={e => e.stopPropagation()} onFocusCapture={e => e.stopPropagation()}>
|
<Box flexGrow={1} onClick={e => e.stopPropagation()} onFocusCapture={e => e.stopPropagation()}>
|
||||||
<QueryEditor server={serverUrl} query={query} oneLiner setQuery={(query) => dispatch({type: "SET_QUERY", payload: query})}/>
|
<Portal disablePortal={!expanded} container={queryContainer.current}>
|
||||||
</Box>}
|
<QueryEditor server={serverUrl} query={query} oneLiner={!expanded} autocomplete={autocomplete}
|
||||||
|
runQuery={onRunQuery}
|
||||||
|
setQuery={onSetQuery}/>
|
||||||
|
</Portal>
|
||||||
|
</Box>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
<Box>
|
<Box>
|
||||||
<Box py={2} display="flex">
|
<Box py={2} display="flex" alignItems="center">
|
||||||
<TextField variant="outlined" fullWidth label="Server URL" value={serverUrl}
|
<TextField variant="outlined" fullWidth label="Server URL" value={serverUrl}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
style: {fontFamily: "Monospace"}
|
style: {fontFamily: "Monospace"}
|
||||||
}}
|
}}
|
||||||
onChange={(e) => dispatch({type: "SET_SERVER", payload: e.target.value})}/>
|
onChange={onSetServer}/>
|
||||||
<Box pl={.5} flexGrow={0}>
|
<Box ml={1}>
|
||||||
|
<Tooltip title="Execute Query">
|
||||||
|
<IconButton onClick={onRunQuery}>
|
||||||
|
<PlayCircleOutlineIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Tooltip title="Request Auth Settings">
|
||||||
<IconButton onClick={() => setDialogOpen(true)}>
|
<IconButton onClick={() => setDialogOpen(true)}>
|
||||||
<SecurityIcon/>
|
<SecurityIcon/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<QueryEditor server={serverUrl} query={query} setQuery={(query) => dispatch({type: "SET_QUERY", payload: query})}/>
|
<Box py={2} display="flex">
|
||||||
|
<Box flexGrow={1} mr={2}>
|
||||||
|
{/* for portal QueryEditor */}
|
||||||
|
<div ref={queryContainer} />
|
||||||
|
</Box>
|
||||||
|
<div>
|
||||||
|
<Tooltip title="Query Editor Settings">
|
||||||
|
<IconButton onClick={() => setPopoverOpen(!popoverOpen)}>
|
||||||
|
<SettingsIcon ref={refSettings}/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Popover open={popoverOpen} transformOrigin={{vertical: -20, horizontal: "left"}}
|
||||||
|
onClose={() => setPopoverOpen(false)}
|
||||||
|
anchorEl={refSettings.current}>
|
||||||
|
<Box p={2}>
|
||||||
|
{<FormControlLabel
|
||||||
|
control={<Switch size="small" checked={autocomplete} onChange={onChangeAutocomplete}/>}
|
||||||
|
label="Autocomplete"
|
||||||
|
/>}
|
||||||
|
</Box>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={8} md={6} >
|
||||||
<Box style={{
|
<Box style={{
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
borderColor: "#b9b9b9",
|
borderColor: "#b9b9b9",
|
||||||
@ -68,7 +127,7 @@ const QueryConfigurator: FC = () => {
|
|||||||
height: "calc(100% - 18px)",
|
height: "calc(100% - 18px)",
|
||||||
marginTop: "16px"
|
marginTop: "16px"
|
||||||
}}>
|
}}>
|
||||||
<TimeSelector setDuration={(dur) => dispatch({type: "SET_DURATION", payload: dur})} duration={duration}/>
|
<TimeSelector setDuration={onSetDuration} duration={duration}/>
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -4,15 +4,20 @@ import {defaultKeymap} from "@codemirror/next/commands";
|
|||||||
import React, {FC, useEffect, useRef, useState} from "react";
|
import React, {FC, useEffect, useRef, useState} from "react";
|
||||||
import { PromQLExtension } from "codemirror-promql";
|
import { PromQLExtension } from "codemirror-promql";
|
||||||
import { basicSetup } from "@codemirror/next/basic-setup";
|
import { basicSetup } from "@codemirror/next/basic-setup";
|
||||||
|
import {isMacOs} from "../../../utils/detect-os";
|
||||||
|
|
||||||
export interface QueryEditorProps {
|
export interface QueryEditorProps {
|
||||||
setQuery: (query: string) => void;
|
setQuery: (query: string) => void;
|
||||||
|
runQuery: () => void;
|
||||||
query: string;
|
query: string;
|
||||||
server: string;
|
server: string;
|
||||||
oneLiner?: boolean;
|
oneLiner?: boolean;
|
||||||
|
autocomplete: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const QueryEditor: FC<QueryEditorProps> = ({query, setQuery, server, oneLiner = false}) => {
|
const QueryEditor: FC<QueryEditorProps> = ({
|
||||||
|
query, setQuery, runQuery, server, oneLiner = false, autocomplete
|
||||||
|
}) => {
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -33,28 +38,41 @@ const QueryEditor: FC<QueryEditorProps> = ({query, setQuery, server, oneLiner =
|
|||||||
// update state on change of autocomplete server
|
// update state on change of autocomplete server
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
const promQL = new PromQLExtension().setComplete({url: server});
|
const promQL = new PromQLExtension();
|
||||||
|
promQL.activateCompletion(autocomplete);
|
||||||
|
promQL.setComplete({url: server});
|
||||||
|
|
||||||
const listenerExtension = EditorView.updateListener.of(editorUpdate => {
|
const listenerExtension = EditorView.updateListener.of(editorUpdate => {
|
||||||
if (editorUpdate.docChanged) {
|
if (editorUpdate.docChanged) {
|
||||||
setQuery(
|
setQuery(editorUpdate.state.doc.toJSON().map(el => el.trim()).join(""));
|
||||||
editorUpdate.state.doc.toJSON().map(el => el.trim()).join("")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
editorView?.setState(EditorState.create({
|
editorView?.setState(EditorState.create({
|
||||||
doc: query,
|
doc: query,
|
||||||
extensions: [basicSetup, keymap(defaultKeymap), listenerExtension, promQL.asExtension()]
|
extensions: [
|
||||||
|
basicSetup,
|
||||||
|
keymap(defaultKeymap),
|
||||||
|
listenerExtension,
|
||||||
|
promQL.asExtension(),
|
||||||
|
keymap([
|
||||||
|
{
|
||||||
|
key: isMacOs() ? "Cmd-Enter" : "Ctrl-Enter",
|
||||||
|
run: (): boolean => {
|
||||||
|
runQuery();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
}, [server, editorView]);
|
}, [server, editorView, autocomplete]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/*Class one-line-scroll and other codemirror stylings are declared in index.css*/}
|
{/*Class one-line-scroll and other codemirror styles are declared in index.css*/}
|
||||||
<div ref={ref} className={oneLiner ? "one-line-scroll" : undefined}></div>
|
<div ref={ref} className={oneLiner ? "one-line-scroll" : undefined}/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -21,7 +21,7 @@ const HomeLayout: FC = () => {
|
|||||||
<>
|
<>
|
||||||
<AppBar position="static">
|
<AppBar position="static">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<Box mr={2} display="flex">
|
<Box display="flex">
|
||||||
<Typography variant="h5">
|
<Typography variant="h5">
|
||||||
<span style={{fontWeight: "bolder"}}>VM</span>
|
<span style={{fontWeight: "bolder"}}>VM</span>
|
||||||
<span style={{fontWeight: "lighter"}}>UI</span>
|
<span style={{fontWeight: "lighter"}}>UI</span>
|
||||||
@ -43,7 +43,7 @@ const HomeLayout: FC = () => {
|
|||||||
Create an issue
|
Create an issue
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Box flexGrow={1}>
|
<Box ml={4} flexGrow={1}>
|
||||||
<ExecutionControls/>
|
<ExecutionControls/>
|
||||||
</Box>
|
</Box>
|
||||||
<DisplayTypeSwitch/>
|
<DisplayTypeSwitch/>
|
||||||
|
@ -37,7 +37,7 @@ export const ChartTooltip: React.FC<ChartTooltipProps> = ({data, time}) => {
|
|||||||
<Box>
|
<Box>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
{data.metrics.map(({key, value}) =>
|
{data.metrics.map(({key, value}) =>
|
||||||
<Box mb={.25} key={key} display="flex" flexDirection="row" alignItems="center">
|
<Box component="span" mb={.25} key={key} display="flex" flexDirection="row" alignItems="center">
|
||||||
<span>{key}: </span>
|
<span>{key}: </span>
|
||||||
<span style={{fontWeight: "bold"}}>{value}</span>
|
<span style={{fontWeight: "bold"}}>{value}</span>
|
||||||
</Box>)}
|
</Box>)}
|
||||||
|
@ -31,3 +31,9 @@ code {
|
|||||||
.one-line-scroll .cm-wrap {
|
.one-line-scroll .cm-wrap {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
.cm-content, .cm-gutter { min-height: 51px; }
|
||||||
|
|
||||||
|
.one-line-scroll .cm-content,
|
||||||
|
.one-line-scroll .cm-gutter {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
@ -15,6 +15,7 @@ export interface AppState {
|
|||||||
time: TimeState;
|
time: TimeState;
|
||||||
queryControls: {
|
queryControls: {
|
||||||
autoRefresh: boolean;
|
autoRefresh: boolean;
|
||||||
|
autocomplete: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ export type Action =
|
|||||||
| { type: "RUN_QUERY"}
|
| { type: "RUN_QUERY"}
|
||||||
| { type: "RUN_QUERY_TO_NOW"}
|
| { type: "RUN_QUERY_TO_NOW"}
|
||||||
| { type: "TOGGLE_AUTOREFRESH"}
|
| { type: "TOGGLE_AUTOREFRESH"}
|
||||||
|
| { type: "TOGGLE_AUTOCOMPLETE"}
|
||||||
|
|
||||||
export const initialState: AppState = {
|
export const initialState: AppState = {
|
||||||
serverUrl: getFromStorage("PREFERRED_URL") as string || "https://", // https://demo.promlabs.com or https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus",
|
serverUrl: getFromStorage("PREFERRED_URL") as string || "https://", // https://demo.promlabs.com or https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/prometheus",
|
||||||
@ -38,7 +40,8 @@ export const initialState: AppState = {
|
|||||||
period: getTimeperiodForDuration("1h")
|
period: getTimeperiodForDuration("1h")
|
||||||
},
|
},
|
||||||
queryControls: {
|
queryControls: {
|
||||||
autoRefresh: false
|
autoRefresh: false,
|
||||||
|
autocomplete: getFromStorage("AUTOCOMPLETE") as boolean || false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -99,6 +102,14 @@ export function reducer(state: AppState, action: Action): AppState {
|
|||||||
autoRefresh: !state.queryControls.autoRefresh
|
autoRefresh: !state.queryControls.autoRefresh
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
case "TOGGLE_AUTOCOMPLETE":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
queryControls: {
|
||||||
|
...state.queryControls,
|
||||||
|
autocomplete: !state.queryControls.autocomplete
|
||||||
|
}
|
||||||
|
};
|
||||||
case "RUN_QUERY":
|
case "RUN_QUERY":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
13
app/vmui/packages/vmui/src/utils/detect-os.ts
Normal file
13
app/vmui/packages/vmui/src/utils/detect-os.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const desktopOs = {
|
||||||
|
windows: "Windows",
|
||||||
|
mac: "Mac OS",
|
||||||
|
linux: "Linux"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOs = () : string => {
|
||||||
|
return Object.values(desktopOs).find(os => navigator.userAgent.indexOf(os) >= 0) || "unknown";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isMacOs = (): boolean => {
|
||||||
|
return getOs() === desktopOs.mac;
|
||||||
|
};
|
@ -1,4 +1,9 @@
|
|||||||
export type StorageKeys = "PREFERRED_URL" | "LAST_QUERY" | "BASIC_AUTH_DATA" | "BEARER_AUTH_DATA" | "AUTH_TYPE";
|
export type StorageKeys = "PREFERRED_URL"
|
||||||
|
| "LAST_QUERY"
|
||||||
|
| "BASIC_AUTH_DATA"
|
||||||
|
| "BEARER_AUTH_DATA"
|
||||||
|
| "AUTH_TYPE"
|
||||||
|
| "AUTOCOMPLETE";
|
||||||
|
|
||||||
export const saveToStorage = (key: StorageKeys, value: string | boolean | Record<string, unknown>): void => {
|
export const saveToStorage = (key: StorageKeys, value: string | boolean | Record<string, unknown>): void => {
|
||||||
if (value) {
|
if (value) {
|
||||||
|
Loading…
Reference in New Issue
Block a user