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:
Yury Molodov 2021-07-23 12:00:44 +03:00 committed by GitHub
parent 05672ffc32
commit a91d41f12a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 149 additions and 43 deletions

View file

@ -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>;
};

View file

@ -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>

View file

@ -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}/>
</>
);
};

View file

@ -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/>

View file

@ -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}:&nbsp;</span>
<span style={{fontWeight: "bold"}}>{value}</span>
</Box>)}

View file

@ -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;
}

View file

@ -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,

View 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;
};

View file

@ -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) {