mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-01-10 15:14:09 +00: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
9 changed files with 149 additions and 43 deletions
|
@ -1,7 +1,5 @@
|
|||
import React, {FC, useEffect, useState} from "react";
|
||||
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 {useAppDispatch, useAppState} from "../../../state/common/StateContext";
|
||||
import CircularProgressWithLabel from "../../common/CircularProgressWithLabel";
|
||||
|
@ -70,23 +68,19 @@ export const ExecutionControls: FC = () => {
|
|||
};
|
||||
|
||||
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
|
||||
control={<Switch size="small" className={classes.colorizing} checked={autoRefresh} onChange={handleChange} />}
|
||||
label="Auto-refresh"
|
||||
/>}
|
||||
|
||||
{autoRefresh && <>
|
||||
<CircularProgressWithLabel className={classes.colorizing} label={delay} value={progress} onClick={() => {iterateDelays();}} />
|
||||
<Box ml={1}>
|
||||
<IconButton onClick={() => {iterateDelays();}}><EqualizerIcon style={{color: "white"}} /></IconButton>
|
||||
</Box>
|
||||
<CircularProgressWithLabel className={classes.colorizing} label={delay} value={progress}
|
||||
onClick={() => {iterateDelays();}} />
|
||||
<Tooltip title="Change delay refresh">
|
||||
<Box ml={1}>
|
||||
<IconButton onClick={() => {iterateDelays();}}><EqualizerIcon style={{color: "white"}} /></IconButton>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</>}
|
||||
</Box>;
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import React, {FC, useState} from "react";
|
||||
import React, {FC, useRef, useState} from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
|
@ -7,7 +7,10 @@ import {
|
|||
Grid,
|
||||
IconButton,
|
||||
TextField,
|
||||
Typography
|
||||
Typography,
|
||||
FormControlLabel,
|
||||
Tooltip,
|
||||
Switch,
|
||||
} from "@material-ui/core";
|
||||
import QueryEditor from "./QueryEditor";
|
||||
import {TimeSelector} from "./TimeSelector";
|
||||
|
@ -15,14 +18,36 @@ import {useAppDispatch, useAppState} from "../../../state/common/StateContext";
|
|||
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
|
||||
import SecurityIcon from "@material-ui/icons/Security";
|
||||
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 {serverUrl, query, time: {duration}} = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const {queryControls: {autocomplete}} = useAppState();
|
||||
const onChangeAutocomplete = () => {
|
||||
dispatch({type: "TOGGLE_AUTOCOMPLETE"});
|
||||
saveToStorage("AUTOCOMPLETE", !autocomplete);
|
||||
};
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
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 (
|
||||
<>
|
||||
|
@ -35,31 +60,65 @@ const QueryConfigurator: FC = () => {
|
|||
<Box mr={2}>
|
||||
<Typography variant="h6" component="h2">Query Configuration</Typography>
|
||||
</Box>
|
||||
{!expanded && <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})}/>
|
||||
</Box>}
|
||||
<Box flexGrow={1} onClick={e => e.stopPropagation()} onFocusCapture={e => e.stopPropagation()}>
|
||||
<Portal disablePortal={!expanded} container={queryContainer.current}>
|
||||
<QueryEditor server={serverUrl} query={query} oneLiner={!expanded} autocomplete={autocomplete}
|
||||
runQuery={onRunQuery}
|
||||
setQuery={onSetQuery}/>
|
||||
</Portal>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box>
|
||||
<Box py={2} display="flex">
|
||||
<Box py={2} display="flex" alignItems="center">
|
||||
<TextField variant="outlined" fullWidth label="Server URL" value={serverUrl}
|
||||
inputProps={{
|
||||
style: {fontFamily: "Monospace"}
|
||||
}}
|
||||
onChange={(e) => dispatch({type: "SET_SERVER", payload: e.target.value})}/>
|
||||
<Box pl={.5} flexGrow={0}>
|
||||
<IconButton onClick={() => setDialogOpen(true)}>
|
||||
<SecurityIcon/>
|
||||
</IconButton>
|
||||
onChange={onSetServer}/>
|
||||
<Box ml={1}>
|
||||
<Tooltip title="Execute Query">
|
||||
<IconButton onClick={onRunQuery}>
|
||||
<PlayCircleOutlineIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box>
|
||||
<Tooltip title="Request Auth Settings">
|
||||
<IconButton onClick={() => setDialogOpen(true)}>
|
||||
<SecurityIcon/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</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>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Grid item xs={8} md={6} >
|
||||
<Box style={{
|
||||
borderRadius: "4px",
|
||||
borderColor: "#b9b9b9",
|
||||
|
@ -68,7 +127,7 @@ const QueryConfigurator: FC = () => {
|
|||
height: "calc(100% - 18px)",
|
||||
marginTop: "16px"
|
||||
}}>
|
||||
<TimeSelector setDuration={(dur) => dispatch({type: "SET_DURATION", payload: dur})} duration={duration}/>
|
||||
<TimeSelector setDuration={onSetDuration} duration={duration}/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
|
|
@ -4,15 +4,20 @@ import {defaultKeymap} from "@codemirror/next/commands";
|
|||
import React, {FC, useEffect, useRef, useState} from "react";
|
||||
import { PromQLExtension } from "codemirror-promql";
|
||||
import { basicSetup } from "@codemirror/next/basic-setup";
|
||||
import {isMacOs} from "../../../utils/detect-os";
|
||||
|
||||
export interface QueryEditorProps {
|
||||
setQuery: (query: string) => void;
|
||||
runQuery: () => void;
|
||||
query: string;
|
||||
server: string;
|
||||
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);
|
||||
|
||||
|
@ -33,28 +38,41 @@ const QueryEditor: FC<QueryEditorProps> = ({query, setQuery, server, oneLiner =
|
|||
// update state on change of autocomplete server
|
||||
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 => {
|
||||
if (editorUpdate.docChanged) {
|
||||
setQuery(
|
||||
editorUpdate.state.doc.toJSON().map(el => el.trim()).join("")
|
||||
);
|
||||
setQuery(editorUpdate.state.doc.toJSON().map(el => el.trim()).join(""));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
editorView?.setState(EditorState.create({
|
||||
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 (
|
||||
<>
|
||||
{/*Class one-line-scroll and other codemirror stylings are declared in index.css*/}
|
||||
<div ref={ref} className={oneLiner ? "one-line-scroll" : undefined}></div>
|
||||
{/*Class one-line-scroll and other codemirror styles are declared in index.css*/}
|
||||
<div ref={ref} className={oneLiner ? "one-line-scroll" : undefined}/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -21,7 +21,7 @@ const HomeLayout: FC = () => {
|
|||
<>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<Box mr={2} display="flex">
|
||||
<Box display="flex">
|
||||
<Typography variant="h5">
|
||||
<span style={{fontWeight: "bolder"}}>VM</span>
|
||||
<span style={{fontWeight: "lighter"}}>UI</span>
|
||||
|
@ -43,7 +43,7 @@ const HomeLayout: FC = () => {
|
|||
Create an issue
|
||||
</Link>
|
||||
</div>
|
||||
<Box flexGrow={1}>
|
||||
<Box ml={4} flexGrow={1}>
|
||||
<ExecutionControls/>
|
||||
</Box>
|
||||
<DisplayTypeSwitch/>
|
||||
|
|
|
@ -37,7 +37,7 @@ export const ChartTooltip: React.FC<ChartTooltipProps> = ({data, time}) => {
|
|||
<Box>
|
||||
<Typography variant="body2">
|
||||
{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 style={{fontWeight: "bold"}}>{value}</span>
|
||||
</Box>)}
|
||||
|
|
|
@ -31,3 +31,9 @@ code {
|
|||
.one-line-scroll .cm-wrap {
|
||||
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;
|
||||
queryControls: {
|
||||
autoRefresh: boolean;
|
||||
autocomplete: boolean
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,6 +29,7 @@ export type Action =
|
|||
| { type: "RUN_QUERY"}
|
||||
| { type: "RUN_QUERY_TO_NOW"}
|
||||
| { type: "TOGGLE_AUTOREFRESH"}
|
||||
| { type: "TOGGLE_AUTOCOMPLETE"}
|
||||
|
||||
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",
|
||||
|
@ -38,7 +40,8 @@ export const initialState: AppState = {
|
|||
period: getTimeperiodForDuration("1h")
|
||||
},
|
||||
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
|
||||
}
|
||||
};
|
||||
case "TOGGLE_AUTOCOMPLETE":
|
||||
return {
|
||||
...state,
|
||||
queryControls: {
|
||||
...state.queryControls,
|
||||
autocomplete: !state.queryControls.autocomplete
|
||||
}
|
||||
};
|
||||
case "RUN_QUERY":
|
||||
return {
|
||||
...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 => {
|
||||
if (value) {
|
||||
|
|
Loading…
Reference in a new issue