mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmui: change layout (#2054)
* fix: change query reset * feat: replace @codemirror to text field * feat: switch to Preact from React * fix: optimize mui imports * feat: move time selector to Header * checkout * fix: remove unused vars * update package-lock.json * fix: correct styles * app/vmselect/vmui: `make vmui-update` Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
parent
c2a3911bb5
commit
fcd33fc409
25 changed files with 511 additions and 527 deletions
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"files": {
|
||||
"main.css": "./static/css/main.79ff1ad2.css",
|
||||
"main.js": "./static/js/main.31aed9a0.js",
|
||||
"main.js": "./static/js/main.2473acb3.js",
|
||||
"static/js/27.cc1b69f7.chunk.js": "./static/js/27.cc1b69f7.chunk.js",
|
||||
"index.html": "./index.html"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.79ff1ad2.css",
|
||||
"static/js/main.31aed9a0.js"
|
||||
"static/js/main.2473acb3.js"
|
||||
]
|
||||
}
|
|
@ -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 defer="defer" src="./static/js/main.31aed9a0.js"></script><link href="./static/css/main.79ff1ad2.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="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/><script defer="defer" src="./static/js/main.2473acb3.js"></script><link href="./static/css/main.79ff1ad2.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
2
app/vmselect/vmui/static/js/main.2473acb3.js
Normal file
2
app/vmselect/vmui/static/js/main.2473acb3.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -5,10 +5,11 @@ import Link from "@mui/material/Link";
|
|||
import Toolbar from "@mui/material/Toolbar";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {ExecutionControls} from "../Home/Configurator/Time/ExecutionControls";
|
||||
import {DisplayTypeSwitch} from "../Home/Configurator/DisplayTypeSwitch";
|
||||
import Logo from "../common/Logo";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import {setQueryStringWithoutPageReload} from "../../utils/query-string";
|
||||
import {TimeSelector} from "../Home/Configurator/Time/TimeSelector";
|
||||
import GlobalSettings from "../Home/Configurator/Settings/GlobalSettings";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
logo: {
|
||||
|
@ -22,8 +23,6 @@ const useStyles = makeStyles({
|
|||
}
|
||||
},
|
||||
issueLink: {
|
||||
position: "absolute",
|
||||
bottom: "6px",
|
||||
textAlign: "center",
|
||||
fontSize: "10px",
|
||||
opacity: ".4",
|
||||
|
@ -45,7 +44,7 @@ const Header: FC = () => {
|
|||
window.location.reload();
|
||||
};
|
||||
|
||||
return <AppBar position="static">
|
||||
return <AppBar position="static" sx={{px: 1, boxShadow: "none"}}>
|
||||
<Toolbar>
|
||||
<Box display="grid" alignItems="center" justifyContent="center">
|
||||
<Box onClick={onClickLogo} className={classes.logo}>
|
||||
|
@ -60,10 +59,11 @@ const Header: FC = () => {
|
|||
create an issue
|
||||
</Link>
|
||||
</Box>
|
||||
<Box ml={4} flexGrow={1}>
|
||||
<Box display="grid" gridTemplateColumns="repeat(3, auto)" gap={1} alignItems="center" ml="auto" mr={0}>
|
||||
<TimeSelector/>
|
||||
<ExecutionControls/>
|
||||
<GlobalSettings/>
|
||||
</Box>
|
||||
<DisplayTypeSwitch/>
|
||||
</Toolbar>
|
||||
</AppBar>;
|
||||
};
|
||||
|
|
|
@ -2,48 +2,39 @@ import React, {FC} from "preact/compat";
|
|||
import TableChartIcon from "@mui/icons-material/TableChart";
|
||||
import ShowChartIcon from "@mui/icons-material/ShowChart";
|
||||
import CodeIcon from "@mui/icons-material/Code";
|
||||
import ToggleButton from "@mui/material/ToggleButton";
|
||||
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import {useAppDispatch, useAppState} from "../../../state/common/StateContext";
|
||||
import withStyles from "@mui/styles/withStyles";
|
||||
import {SyntheticEvent} from "react";
|
||||
|
||||
export type DisplayType = "table" | "chart" | "code";
|
||||
|
||||
const StylizedToggleButton = withStyles({
|
||||
root: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "18px auto",
|
||||
gridGap: 6,
|
||||
padding: "8px 12px",
|
||||
color: "white",
|
||||
lineHeight: "19px",
|
||||
"&.Mui-selected": {
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
})(ToggleButton);
|
||||
const tabs = [
|
||||
{value: "chart", icon: <ShowChartIcon/>, label: "Graph"},
|
||||
{value: "code", icon: <CodeIcon/>, label: "JSON"},
|
||||
{value: "table", icon: <TableChartIcon/>, label: "Table"}
|
||||
];
|
||||
|
||||
export const DisplayTypeSwitch: FC = () => {
|
||||
|
||||
const {displayType} = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return <ToggleButtonGroup
|
||||
const handleChange = (event: SyntheticEvent, newValue: DisplayType) => {
|
||||
dispatch({type: "SET_DISPLAY_TYPE", payload: newValue ?? displayType});
|
||||
};
|
||||
|
||||
return <Tabs
|
||||
value={displayType}
|
||||
exclusive
|
||||
onChange={
|
||||
(e, val) =>
|
||||
// Toggle Button Group returns null in case of click on selected element, avoiding it
|
||||
dispatch({type: "SET_DISPLAY_TYPE", payload: val ?? displayType})
|
||||
}>
|
||||
<StylizedToggleButton value="chart" aria-label="display as chart">
|
||||
<ShowChartIcon/><span>Query Range as Chart</span>
|
||||
</StylizedToggleButton>
|
||||
<StylizedToggleButton value="code" aria-label="display as code">
|
||||
<CodeIcon/><span>Instant Query as JSON</span>
|
||||
</StylizedToggleButton>
|
||||
<StylizedToggleButton value="table" aria-label="display as table">
|
||||
<TableChartIcon/><span>Instant Query as Table</span>
|
||||
</StylizedToggleButton>
|
||||
</ToggleButtonGroup>;
|
||||
onChange={handleChange}
|
||||
sx={{minHeight: "0", marginBottom: "-1px"}}
|
||||
>
|
||||
{tabs.map(t =>
|
||||
<Tab key={t.value}
|
||||
icon={t.icon}
|
||||
iconPosition="start"
|
||||
label={t.label} value={t.value}
|
||||
sx={{minHeight: "41px"}}
|
||||
/>)}
|
||||
</Tabs>;
|
||||
};
|
|
@ -2,13 +2,14 @@ import SettingsIcon from "@mui/icons-material/Settings";
|
|||
import React, {FC, useState} from "preact/compat";
|
||||
import AxesLimitsConfigurator from "./AxesLimitsConfigurator";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Popover from "@mui/material/Popover";
|
||||
import Popper from "@mui/material/Popper";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import ClickAwayListener from "@mui/material/ClickAwayListener";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
popover: {
|
||||
|
@ -32,40 +33,39 @@ const useStyles = makeStyles({
|
|||
}
|
||||
});
|
||||
|
||||
const title = "Axes Settings";
|
||||
|
||||
const GraphSettings: FC = () => {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
return <Box display="flex" px={2}>
|
||||
<Button variant="outlined" aria-describedby="settings-popover"
|
||||
onClick={(e) => setAnchorEl(e.currentTarget)} >
|
||||
<SettingsIcon sx={{fontSize: 16, marginRight: "4px"}}/>
|
||||
<span style={{lineHeight: 1, paddingTop: "1px"}}>{open ? "Hide" : "Show"} graph settings</span>
|
||||
</Button>
|
||||
<Popover
|
||||
id="settings-popover"
|
||||
return <Box>
|
||||
<Tooltip title={title}>
|
||||
<IconButton onClick={(e) => setAnchorEl(e.currentTarget)}>
|
||||
<SettingsIcon/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Popper
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: anchorEl ? anchorEl.offsetWidth + 15 : 200
|
||||
}}
|
||||
>
|
||||
<Paper elevation={3} className={classes.popover}>
|
||||
<div id="handle" className={classes.popoverHeader}>
|
||||
<Typography variant="body1"><b>Graph Settings</b></Typography>
|
||||
<IconButton size="small" onClick={() => setAnchorEl(null)}>
|
||||
<CloseIcon style={{color: "white"}}/>
|
||||
</IconButton>
|
||||
</div>
|
||||
<Box className={classes.popoverBody}>
|
||||
<AxesLimitsConfigurator/>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Popover>
|
||||
placement="left-start"
|
||||
modifiers={[{name: "offset", options: {offset: [0, 6]}}]}>
|
||||
<ClickAwayListener onClickAway={() => setAnchorEl(null)}>
|
||||
<Paper elevation={3} className={classes.popover}>
|
||||
<div id="handle" className={classes.popoverHeader}>
|
||||
<Typography variant="body1"><b>{title}</b></Typography>
|
||||
<IconButton size="small" onClick={() => setAnchorEl(null)}>
|
||||
<CloseIcon style={{color: "white"}}/>
|
||||
</IconButton>
|
||||
</div>
|
||||
<Box className={classes.popoverBody}>
|
||||
<AxesLimitsConfigurator/>
|
||||
</Box>
|
||||
</Paper>
|
||||
</ClickAwayListener>
|
||||
</Popper>
|
||||
</Box>;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,21 +1,12 @@
|
|||
import React, {FC, useEffect, useRef, useState} from "preact/compat";
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||
import React, {FC, useEffect, useRef} from "preact/compat";
|
||||
import Box from "@mui/material/Box";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Button from "@mui/material/Button";
|
||||
import Portal from "@mui/material/Portal";
|
||||
import QueryEditor from "./QueryEditor";
|
||||
import {TimeSelector} from "../Time/TimeSelector";
|
||||
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import HighlightOffIcon from "@mui/icons-material/HighlightOff";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
|
||||
import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline";
|
||||
import ServerConfigurator from "./ServerConfigurator";
|
||||
import AdditionalSettings from "./AdditionalSettings";
|
||||
import {ErrorTypes} from "../../../../types";
|
||||
|
||||
|
@ -28,15 +19,11 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({error, queryOptions}) =>
|
|||
|
||||
const {query, queryHistory, queryControls: {autocomplete}} = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const queryContainer = useRef<HTMLDivElement>(null);
|
||||
const queryRef = useRef(query);
|
||||
useEffect(() => {
|
||||
queryRef.current = query;
|
||||
}, [query]);
|
||||
|
||||
const onSetDuration = (dur: string) => dispatch({type: "SET_DURATION", payload: dur});
|
||||
|
||||
const updateHistory = () => {
|
||||
dispatch({
|
||||
type: "SET_QUERY_HISTORY", payload: query.map((q, i) => {
|
||||
|
@ -80,62 +67,34 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({error, queryOptions}) =>
|
|||
payload: {value: {values, index: newIndexHistory}, queryNumber: indexQuery}
|
||||
});
|
||||
};
|
||||
|
||||
return <>
|
||||
<Accordion expanded={expanded} onChange={() => setExpanded(prev => !prev)}>
|
||||
<AccordionSummary
|
||||
expandIcon={<IconButton><ExpandMoreIcon/></IconButton>}
|
||||
aria-controls="panel1a-content"
|
||||
id="panel1a-header"
|
||||
sx={{alignItems: "flex-start", padding: "15px"}}
|
||||
>
|
||||
<Box mr={2}>
|
||||
<Typography variant="h6" component="h2">Query Configuration</Typography>
|
||||
</Box>
|
||||
<Box flexGrow={1} onClick={e => e.stopPropagation()} onFocusCapture={e => e.stopPropagation()}>
|
||||
<Portal disablePortal={!expanded} container={queryContainer.current}>
|
||||
{query.map((q, i) =>
|
||||
<Box key={i} display="grid" gridTemplateColumns="1fr auto" gap="4px" width="100%"
|
||||
mb={i === query.length - 1 ? 0 : 2}>
|
||||
<QueryEditor query={query[i]} index={i} autocomplete={autocomplete} queryOptions={queryOptions}
|
||||
error={error} setHistoryIndex={setHistoryIndex} runQuery={onRunQuery} setQuery={onSetQuery}/>
|
||||
{i === 0 && <Tooltip title="Execute Query">
|
||||
<IconButton onClick={onRunQuery}>
|
||||
<PlayCircleOutlineIcon/>
|
||||
</IconButton>
|
||||
</Tooltip>}
|
||||
{i > 0 && <Tooltip title="Remove Query">
|
||||
<IconButton onClick={() => onRemoveQuery(i)}>
|
||||
<HighlightOffIcon/>
|
||||
</IconButton>
|
||||
</Tooltip>}
|
||||
</Box>)}
|
||||
</Portal>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Box display="flex" flexWrap="wrap" gap={2}>
|
||||
<Box flexGrow="2" minWidth="50%">
|
||||
<ServerConfigurator error={error}/>
|
||||
{/* for portal QueryEditor */}
|
||||
<div ref={queryContainer}/>
|
||||
{query.length < 2 && <Box display="inline-block" minHeight="40px" mt={2}>
|
||||
<Button onClick={onAddQuery} variant="outlined">
|
||||
<AddIcon sx={{fontSize: 16, marginRight: "4px"}}/>
|
||||
<span style={{lineHeight: 1, paddingTop: "1px"}}>Query</span>
|
||||
</Button>
|
||||
</Box>}
|
||||
</Box>
|
||||
<Box flexGrow="1">
|
||||
<TimeSelector setDuration={onSetDuration}/>
|
||||
</Box>
|
||||
<Box flexBasis="100%" pt={1}>
|
||||
<AdditionalSettings/>
|
||||
</Box>
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</>;
|
||||
return <Box boxShadow="rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;" p={4} pb={2} m={-4} mb={2}>
|
||||
<Box>
|
||||
{query.map((q, i) =>
|
||||
<Box key={i} display="grid" gridTemplateColumns="1fr auto auto" gap="4px" width="100%"
|
||||
mb={i === query.length - 1 ? 0 : 2.5}>
|
||||
<QueryEditor query={query[i]} index={i} autocomplete={autocomplete} queryOptions={queryOptions}
|
||||
error={error} setHistoryIndex={setHistoryIndex} runQuery={onRunQuery} setQuery={onSetQuery}/>
|
||||
{i === 0 && <Tooltip title="Execute Query">
|
||||
<IconButton onClick={onRunQuery} sx={{height: "49px", width: "49px"}}>
|
||||
<PlayCircleOutlineIcon/>
|
||||
</IconButton>
|
||||
</Tooltip>}
|
||||
{query.length < 2 && <Tooltip title="Add Query">
|
||||
<IconButton onClick={onAddQuery} sx={{height: "49px", width: "49px"}}>
|
||||
<AddCircleOutlineIcon/>
|
||||
</IconButton>
|
||||
</Tooltip>}
|
||||
{i > 0 && <Tooltip title="Remove Query">
|
||||
<IconButton onClick={() => onRemoveQuery(i)} sx={{height: "49px", width: "49px"}}>
|
||||
<HighlightOffIcon/>
|
||||
</IconButton>
|
||||
</Tooltip>}
|
||||
</Box>)}
|
||||
</Box>
|
||||
<Box mt={3}>
|
||||
<AdditionalSettings/>
|
||||
</Box>
|
||||
</Box>;
|
||||
};
|
||||
|
||||
export default QueryConfigurator;
|
|
@ -1,51 +0,0 @@
|
|||
import React, {FC, useEffect, useState} from "preact/compat";
|
||||
import Box from "@mui/material/Box";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import SecurityIcon from "@mui/icons-material/Security";
|
||||
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
|
||||
import {AuthDialog} from "../Auth/AuthDialog";
|
||||
import {ErrorTypes} from "../../../../types";
|
||||
import {getAppModeEnable, getAppModeParams} from "../../../../utils/app-mode";
|
||||
|
||||
export interface ServerConfiguratorProps {
|
||||
error?: ErrorTypes | string;
|
||||
}
|
||||
|
||||
const ServerConfigurator: FC<ServerConfiguratorProps> = ({error}) => {
|
||||
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const {serverURL: appServerUrl} = getAppModeParams();
|
||||
|
||||
const {serverUrl} = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onSetServer = ({target: {value}}: {target: {value: string}}) => {
|
||||
dispatch({type: "SET_SERVER", payload: value});
|
||||
};
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (appModeEnable) dispatch({type: "SET_SERVER", payload: appServerUrl});
|
||||
}, [appServerUrl]);
|
||||
|
||||
return <>
|
||||
<Box display="grid" gridTemplateColumns="1fr auto" gap="4px" alignItems="center" width="100%" mb={2} minHeight={50}>
|
||||
<TextField variant="outlined" fullWidth label="Server URL" value={serverUrl || ""} disabled={appModeEnable}
|
||||
error={error === ErrorTypes.validServer || error === ErrorTypes.emptyServer}
|
||||
inputProps={{style: {fontFamily: "Monospace"}}}
|
||||
onChange={onSetServer}/>
|
||||
<Box>
|
||||
<Tooltip title="Request Auth Settings">
|
||||
<IconButton onClick={() => setDialogOpen(true)}>
|
||||
<SecurityIcon/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<AuthDialog open={dialogOpen} onClose={() => setDialogOpen(false)}/>
|
||||
</>;
|
||||
};
|
||||
|
||||
export default ServerConfigurator;
|
|
@ -0,0 +1,82 @@
|
|||
import React, {FC, useState} from "preact/compat";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import Button from "@mui/material/Button";
|
||||
import Box from "@mui/material/Box";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import ServerConfigurator from "./ServerConfigurator";
|
||||
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,
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
bgcolor: "background.paper",
|
||||
p: 3,
|
||||
borderRadius: "4px",
|
||||
width: "80%",
|
||||
maxWidth: "800px"
|
||||
};
|
||||
|
||||
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});
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleOpen = () => setOpen(true);
|
||||
const handleClose = () => setOpen(false);
|
||||
|
||||
return <>
|
||||
<Tooltip title={title}>
|
||||
<Button variant="contained" color="primary"
|
||||
sx={{
|
||||
color: "white",
|
||||
border: "1px solid rgba(0, 0, 0, 0.2)",
|
||||
minWidth: "34px",
|
||||
padding: "6px 8px",
|
||||
boxShadow: "none",
|
||||
}}
|
||||
startIcon={<SettingsIcon style={{marginRight: "-8px", marginLeft: "4px"}}/>}
|
||||
onClick={handleOpen}>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Modal open={open} onClose={handleClose}>
|
||||
<Box sx={modalStyle}>
|
||||
<Box display="grid" gridTemplateColumns="1fr auto" alignItems="center" mb={4}>
|
||||
<Typography id="modal-modal-title" variant="h6" component="h2">
|
||||
{title}
|
||||
</Typography>
|
||||
<IconButton size="small" onClick={handleClose}>
|
||||
<CloseIcon/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
<ServerConfigurator setServer={setChangedServerUrl}/>
|
||||
<Box display="grid" gridTemplateColumns="auto auto" gap={1} justifyContent="end" mt={4}>
|
||||
<Button variant="outlined" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" onClick={setServer}>
|
||||
apply
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
</>;
|
||||
};
|
||||
|
||||
export default GlobalSettings;
|
|
@ -0,0 +1,41 @@
|
|||
import React, {FC, useEffect, useState} from "preact/compat";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
|
||||
import {ErrorTypes} from "../../../../types";
|
||||
import {getAppModeEnable, getAppModeParams} from "../../../../utils/app-mode";
|
||||
import {ChangeEvent} from "react";
|
||||
|
||||
export interface ServerConfiguratorProps {
|
||||
error?: ErrorTypes | string;
|
||||
setServer: (url: string) => void
|
||||
}
|
||||
|
||||
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}
|
||||
error={error === ErrorTypes.validServer || error === ErrorTypes.emptyServer}
|
||||
inputProps={{style: {fontFamily: "Monospace"}}}
|
||||
onChange={onChangeServer}/>;
|
||||
};
|
||||
|
||||
export default ServerConfigurator;
|
|
@ -1,92 +1,100 @@
|
|||
import React, {FC, useEffect, useState} from "preact/compat";
|
||||
import Box from "@mui/material/Box";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import EqualizerIcon from "@mui/icons-material/Equalizer";
|
||||
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
|
||||
import CircularProgressWithLabel from "../../../common/CircularProgressWithLabel";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import BasicSwitch from "../../../../theme/switch";
|
||||
import Button from "@mui/material/Button";
|
||||
import Popper from "@mui/material/Popper";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import ClickAwayListener from "@mui/material/ClickAwayListener";
|
||||
import AutorenewIcon from "@mui/icons-material/Autorenew";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
colorizing: {
|
||||
color: "white"
|
||||
}
|
||||
});
|
||||
interface AutoRefreshOption {
|
||||
seconds: number
|
||||
title: string
|
||||
}
|
||||
|
||||
const delayOptions: AutoRefreshOption[] = [
|
||||
{seconds: 0, title: "Off"},
|
||||
{seconds: 1, title: "1s"},
|
||||
{seconds: 2, title: "2s"},
|
||||
{seconds: 5, title: "5s"},
|
||||
{seconds: 10, title: "10s"},
|
||||
{seconds: 30, title: "30s"},
|
||||
{seconds: 60, title: "1m"},
|
||||
{seconds: 300, title: "5m"},
|
||||
{seconds: 900, title: "15m"},
|
||||
{seconds: 1800, title: "30m"},
|
||||
{seconds: 3600, title: "1h"},
|
||||
{seconds: 7200, title: "2h"}
|
||||
];
|
||||
|
||||
export const ExecutionControls: FC = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const {queryControls: {autoRefresh}} = useAppState();
|
||||
|
||||
const [delay, setDelay] = useState<(1|2|5)>(5);
|
||||
const [lastUpdate, setLastUpdate] = useState<number|undefined>();
|
||||
const [progress, setProgress] = React.useState(100);
|
||||
const [selectedDelay, setSelectedDelay] = useState<AutoRefreshOption>(delayOptions[0]);
|
||||
|
||||
const handleChange = () => {
|
||||
dispatch({type: "TOGGLE_AUTOREFRESH"});
|
||||
const handleChange = (d: AutoRefreshOption) => {
|
||||
if ((autoRefresh && !d.seconds) || (!autoRefresh && d.seconds)) {
|
||||
dispatch({type: "TOGGLE_AUTOREFRESH"});
|
||||
}
|
||||
setSelectedDelay(d);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const delay = selectedDelay.seconds;
|
||||
let timer: number;
|
||||
if (autoRefresh) {
|
||||
setLastUpdate(new Date().valueOf());
|
||||
timer = setInterval(() => {
|
||||
setLastUpdate(new Date().valueOf());
|
||||
dispatch({type: "RUN_QUERY_TO_NOW"});
|
||||
}, delay * 1000) as unknown as number;
|
||||
} else {
|
||||
setSelectedDelay(delayOptions[0]);
|
||||
}
|
||||
return () => {
|
||||
timer && clearInterval(timer);
|
||||
};
|
||||
}, [delay, autoRefresh]);
|
||||
}, [selectedDelay, autoRefresh]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
if (autoRefresh && lastUpdate) {
|
||||
const delta = (new Date().valueOf() - lastUpdate) / 1000; //s
|
||||
const nextValue = Math.floor(delta / delay * 100);
|
||||
setProgress(nextValue);
|
||||
}
|
||||
}, 16);
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [autoRefresh, lastUpdate, delay]);
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const iterateDelays = () => {
|
||||
setDelay(prev => {
|
||||
switch (prev) {
|
||||
case 1:
|
||||
return 2;
|
||||
case 2:
|
||||
return 5;
|
||||
case 5:
|
||||
return 1;
|
||||
default:
|
||||
return 5;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return <Box display="flex" alignItems="center">
|
||||
{<FormControlLabel
|
||||
control={<BasicSwitch className={classes.colorizing} checked={autoRefresh} onChange={handleChange} />}
|
||||
label="Auto-refresh"
|
||||
/>}
|
||||
|
||||
{autoRefresh && <>
|
||||
<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>;
|
||||
return <>
|
||||
<Tooltip title="Auto-refresh control">
|
||||
<Button variant="contained" color="primary"
|
||||
sx={{
|
||||
minWidth: "110px",
|
||||
color: "white",
|
||||
border: "1px solid rgba(0, 0, 0, 0.2)",
|
||||
justifyContent: "space-between",
|
||||
boxShadow: "none",
|
||||
}}
|
||||
startIcon={<AutorenewIcon/>}
|
||||
endIcon={<KeyboardArrowDownIcon sx={{transform: open ? "rotate(180deg)" : "none"}}/>}
|
||||
onClick={(e) => setAnchorEl(e.currentTarget)}
|
||||
>
|
||||
{selectedDelay.title}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Popper
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
placement="bottom-end"
|
||||
modifiers={[{name: "offset", options: {offset: [0, 6]}}]}>
|
||||
<ClickAwayListener onClickAway={() => setAnchorEl(null)}>
|
||||
<Paper elevation={3}>
|
||||
<List style={{minWidth: "110px",maxHeight: "208px", overflow: "auto", padding: "20px 0"}}>
|
||||
{delayOptions.map(d =>
|
||||
<ListItem key={d.seconds} button onClick={() => handleChange(d)}>
|
||||
<ListItemText primary={d.title}/>
|
||||
</ListItem>)}
|
||||
</List>
|
||||
</Paper>
|
||||
</ClickAwayListener></Popper>
|
||||
</>;
|
||||
};
|
|
@ -1,31 +0,0 @@
|
|||
import React, {FC} from "preact/compat";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import {supportedDurations} from "../../../../utils/time";
|
||||
|
||||
export const TimeDurationPopover: FC = () => {
|
||||
|
||||
return <TableContainer component={Paper}>
|
||||
<Table aria-label="simple table" size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Long</TableCell>
|
||||
<TableCell>Short</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{supportedDurations.map((row, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell component="th" scope="row">{row.long}</TableCell>
|
||||
<TableCell>{row.short}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>;
|
||||
};
|
|
@ -1,103 +1,47 @@
|
|||
import React, {FC, useEffect, useState} from "preact/compat";
|
||||
import {ChangeEvent, MouseEvent, KeyboardEvent} from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Popover from "@mui/material/Popover";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {checkDurationLimit} from "../../../../utils/time";
|
||||
import {TimeDurationPopover} from "./TimeDurationPopover";
|
||||
import {InlineBtn} from "../../../common/InlineBtn";
|
||||
import {useAppState} from "../../../../state/common/StateContext";
|
||||
import React, {FC} from "preact/compat";
|
||||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
interface TimeDurationSelector {
|
||||
setDuration: (str: string) => void;
|
||||
setDuration: (str: string, from: Date) => void;
|
||||
}
|
||||
|
||||
interface DurationOption {
|
||||
duration: string,
|
||||
title?: string,
|
||||
from?: () => Date,
|
||||
}
|
||||
|
||||
const durationOptions: DurationOption[] = [
|
||||
{duration: "5m", title: "Last 5 minutes"},
|
||||
{duration: "15m", title: "Last 15 minutes"},
|
||||
{duration: "30m", title: "Last 30 minutes"},
|
||||
{duration: "1h", title: "Last 1 hour"},
|
||||
{duration: "3h", title: "Last 3 hours"},
|
||||
{duration: "6h", title: "Last 6 hours"},
|
||||
{duration: "12h", title: "Last 12 hours"},
|
||||
{duration: "24h", title: "Last 24 hours"},
|
||||
{duration: "2d", title: "Last 2 days"},
|
||||
{duration: "7d", title: "Last 7 days"},
|
||||
{duration: "30d", title: "Last 30 days"},
|
||||
{duration: "90d", title: "Last 90 days"},
|
||||
{duration: "6m", title: "Last 6 months"},
|
||||
{duration: "1y", title: "Last 1 year"},
|
||||
{duration: "1d", from: () => dayjs().subtract(1, "day").endOf("day").toDate(), title: "Yesterday"},
|
||||
{duration: "1d", from: () => dayjs().endOf("day").toDate(), title: "Today"},
|
||||
];
|
||||
|
||||
const TimeDurationSelector: FC<TimeDurationSelector> = ({setDuration}) => {
|
||||
const {time: {duration}} = useAppState();
|
||||
// setDurationString("5m"))
|
||||
|
||||
const [anchorEl, setAnchorEl] = React.useState<Element | null>(null);
|
||||
const [durationString, setDurationString] = useState<string>(duration);
|
||||
const [durationStringFocused, setFocused] = useState(false);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleDurationChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setDurationString(event.target.value);
|
||||
};
|
||||
|
||||
const handlePopoverOpen = (event: MouseEvent<HTMLSpanElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handlePopoverClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const onKeyUp = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key !== "Enter") return;
|
||||
const target = event.target as HTMLInputElement;
|
||||
target.blur();
|
||||
setDurationString(target.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDurationString(duration);
|
||||
}, [duration]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!durationStringFocused) {
|
||||
const value = checkDurationLimit(durationString);
|
||||
setDurationString(value);
|
||||
setDuration(value);
|
||||
}
|
||||
}, [durationString, durationStringFocused]);
|
||||
|
||||
return <>
|
||||
<Box>
|
||||
<TextField label="Duration" value={durationString} onChange={handleDurationChange}
|
||||
variant="standard"
|
||||
fullWidth={true}
|
||||
onKeyUp={onKeyUp}
|
||||
onBlur={() => {
|
||||
setFocused(false);
|
||||
}}
|
||||
onFocus={() => {
|
||||
setFocused(true);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box mt={2}>
|
||||
<Typography variant="body2">
|
||||
<span aria-owns={open ? "mouse-over-popover" : undefined}
|
||||
aria-haspopup="true"
|
||||
style={{cursor: "pointer"}}
|
||||
onMouseEnter={handlePopoverOpen}
|
||||
onMouseLeave={handlePopoverClose}>
|
||||
Possible options:
|
||||
</span>
|
||||
<Popover
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
style={{pointerEvents: "none"}} // important
|
||||
onClose={handlePopoverClose}
|
||||
disableRestoreFocus
|
||||
>
|
||||
<TimeDurationPopover/>
|
||||
</Popover>
|
||||
<InlineBtn handler={() => setDurationString("5m")} text="5m"/>,
|
||||
<InlineBtn handler={() => setDurationString("1h")} text="1h"/>,
|
||||
<InlineBtn handler={() => setDurationString("1h 30m")} text="1h 30m"/>
|
||||
</Typography>
|
||||
</Box>
|
||||
</>;
|
||||
return <List style={{maxHeight: "168px", overflow: "auto", paddingRight: "15px"}}>
|
||||
{durationOptions.map(d =>
|
||||
<ListItem key={d.duration} button onClick={() => setDuration(d.duration, d.from ? d.from() : new Date())}>
|
||||
<ListItemText primary={d.title || d.duration}/>
|
||||
</ListItem>)}
|
||||
</List>;
|
||||
};
|
||||
|
||||
export default TimeDurationSelector;
|
|
@ -1,40 +1,32 @@
|
|||
import React, {FC, useEffect, useState} from "preact/compat";
|
||||
import Box from "@mui/material/Box";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import DateTimePicker from "@mui/lab/DateTimePicker";
|
||||
import React, {FC, useEffect, useState, useMemo} from "preact/compat";
|
||||
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
|
||||
import {dateFromSeconds, formatDateForNativeInput} from "../../../../utils/time";
|
||||
import {InlineBtn} from "../../../common/InlineBtn";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import TimeDurationSelector from "./TimeDurationSelector";
|
||||
import dayjs from "dayjs";
|
||||
import QueryBuilderIcon from "@mui/icons-material/QueryBuilder";
|
||||
import Box from "@mui/material/Box";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import DateTimePicker from "@mui/lab/DateTimePicker";
|
||||
import Button from "@mui/material/Button";
|
||||
import Popper from "@mui/material/Popper";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import ClickAwayListener from "@mui/material/ClickAwayListener";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
|
||||
interface TimeSelectorProps {
|
||||
setDuration: (str: string) => void;
|
||||
}
|
||||
const formatDate = "YYYY-MM-DD HH:mm:ss";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "200px 1fr",
|
||||
gridGap: "20px",
|
||||
height: "100%",
|
||||
gridTemplateColumns: "200px auto 200px",
|
||||
gridGap: "10px",
|
||||
padding: "20px",
|
||||
borderRadius: "4px",
|
||||
borderColor: "#b9b9b9",
|
||||
borderStyle: "solid",
|
||||
borderWidth: "1px"
|
||||
},
|
||||
timeControls: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr",
|
||||
gridTemplateRows: "auto 1fr",
|
||||
gridGap: "16px 0",
|
||||
},
|
||||
datePickers: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, 200px)",
|
||||
gridTemplateRows: "auto 1fr auto",
|
||||
gridGap: "16px 0",
|
||||
},
|
||||
datePickerItem: {
|
||||
|
@ -42,7 +34,7 @@ const useStyles = makeStyles({
|
|||
},
|
||||
});
|
||||
|
||||
export const TimeSelector: FC<TimeSelectorProps> = ({setDuration}) => {
|
||||
export const TimeSelector: FC = () => {
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
|
@ -60,46 +52,88 @@ export const TimeSelector: FC<TimeSelectorProps> = ({setDuration}) => {
|
|||
setFrom(formatDateForNativeInput(dateFromSeconds(start)));
|
||||
}, [start]);
|
||||
|
||||
return <Box className={classes.container}>
|
||||
{/*setup duration*/}
|
||||
<Box>
|
||||
<TimeDurationSelector setDuration={setDuration}/>
|
||||
</Box>
|
||||
{/*setup end time*/}
|
||||
<Box className={classes.timeControls}>
|
||||
<Box className={classes.datePickers}>
|
||||
<Box className={classes.datePickerItem}>
|
||||
<DateTimePicker
|
||||
label="From"
|
||||
ampm={false}
|
||||
value={from}
|
||||
onChange={date => dispatch({type: "SET_FROM", payload: date as unknown as Date})}
|
||||
onError={console.log}
|
||||
inputFormat="DD/MM/YYYY HH:mm:ss"
|
||||
mask="__/__/____ __:__:__"
|
||||
renderInput={(params) => <TextField {...params} variant="standard"/>}
|
||||
maxDate={dayjs(until)}
|
||||
/>
|
||||
</Box>
|
||||
<Box className={classes.datePickerItem}>
|
||||
<DateTimePicker
|
||||
label="Until"
|
||||
ampm={false}
|
||||
value={until}
|
||||
onChange={date => dispatch({type: "SET_UNTIL", payload: date as unknown as Date})}
|
||||
onError={console.log}
|
||||
inputFormat="DD/MM/YYYY HH:mm:ss"
|
||||
mask="__/__/____ __:__:__"
|
||||
renderInput={(params) => <TextField {...params} variant="standard"/>}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2">
|
||||
Will be changed to current time for auto-refresh mode.
|
||||
<InlineBtn handler={() => dispatch({type: "RUN_QUERY_TO_NOW"})} text="Switch to now"/>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>;
|
||||
const setDuration = (dur: string, from: Date) => {
|
||||
dispatch({type: "SET_UNTIL", payload: from});
|
||||
setAnchorEl(null);
|
||||
dispatch({type: "SET_DURATION", payload: dur});
|
||||
};
|
||||
|
||||
const formatRange = useMemo(() => {
|
||||
const startFormat = dayjs(dateFromSeconds(start)).format(formatDate);
|
||||
const endFormat = dayjs(dateFromSeconds(end)).format(formatDate);
|
||||
return {
|
||||
start: startFormat,
|
||||
end: endFormat
|
||||
};
|
||||
}, [start, end]);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
return <>
|
||||
<Tooltip title="Time range controls">
|
||||
<Button variant="contained" color="primary"
|
||||
sx={{
|
||||
color: "white",
|
||||
border: "1px solid rgba(0, 0, 0, 0.2)",
|
||||
boxShadow: "none"
|
||||
}}
|
||||
startIcon={<QueryBuilderIcon/>}
|
||||
onClick={(e) => setAnchorEl(e.currentTarget)}>
|
||||
{formatRange.start} - {formatRange.end}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Popper
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
placement="bottom-end"
|
||||
modifiers={[{name: "offset", options: {offset: [0, 6]}}]}>
|
||||
<ClickAwayListener onClickAway={() => setAnchorEl(null)}>
|
||||
<Paper elevation={3}>
|
||||
<Box className={classes.container}>
|
||||
<Box className={classes.timeControls}>
|
||||
<Box className={classes.datePickerItem}>
|
||||
<DateTimePicker
|
||||
label="From"
|
||||
ampm={false}
|
||||
value={from}
|
||||
onChange={date => dispatch({type: "SET_FROM", payload: date as unknown as Date})}
|
||||
onError={console.log}
|
||||
inputFormat={formatDate}
|
||||
mask="____-__-__ __:__:__"
|
||||
renderInput={(params) => <TextField {...params} variant="standard"/>}
|
||||
maxDate={dayjs(until)}
|
||||
PopperProps={{disablePortal: true}}/>
|
||||
</Box>
|
||||
<Box className={classes.datePickerItem}>
|
||||
<DateTimePicker
|
||||
label="To"
|
||||
ampm={false}
|
||||
value={until}
|
||||
onChange={date => dispatch({type: "SET_UNTIL", payload: date as unknown as Date})}
|
||||
onError={console.log}
|
||||
inputFormat={formatDate}
|
||||
mask="____-__-__ __:__:__"
|
||||
renderInput={(params) => <TextField {...params} variant="standard"/>}
|
||||
PopperProps={{disablePortal: true}}/>
|
||||
</Box>
|
||||
<Box display="grid" gridTemplateColumns="auto 1fr" gap={1}>
|
||||
<Button variant="outlined" onClick={() => setAnchorEl(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" onClick={() => dispatch({type: "RUN_QUERY_TO_NOW"})}>
|
||||
switch to now
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
{/*setup duration*/}
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Box>
|
||||
<TimeDurationSelector setDuration={setDuration}/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</ClickAwayListener>
|
||||
</Popper>
|
||||
</>;
|
||||
};
|
||||
|
|
|
@ -10,6 +10,8 @@ import QueryConfigurator from "./Configurator/Query/QueryConfigurator";
|
|||
import {useFetchQuery} from "./Configurator/Query/useFetchQuery";
|
||||
import JsonView from "./Views/JsonView";
|
||||
import Header from "../Header/Header";
|
||||
import {DisplayTypeSwitch} from "./Configurator/DisplayTypeSwitch";
|
||||
import GraphSettings from "./Configurator/Graph/GraphSettings";
|
||||
|
||||
const HomeLayout: FC = () => {
|
||||
|
||||
|
@ -20,18 +22,16 @@ const HomeLayout: FC = () => {
|
|||
return (
|
||||
<Box id="homeLayout">
|
||||
<Header/>
|
||||
<Box p={4} display="grid" gridTemplateRows="auto 1fr" gap={"20px"} style={{minHeight: "calc(100vh - 64px)"}}>
|
||||
<Box>
|
||||
<QueryConfigurator error={error} queryOptions={queryOptions}/>
|
||||
</Box>
|
||||
<Box height={"100%"}>
|
||||
<Box p={4} display="grid" gridTemplateRows="auto 1fr" style={{minHeight: "calc(100vh - 64px)"}}>
|
||||
<QueryConfigurator error={error} queryOptions={queryOptions}/>
|
||||
<Box height="100%">
|
||||
{isLoading && <Fade in={isLoading} style={{
|
||||
transitionDelay: isLoading ? "300ms" : "0ms",
|
||||
}}>
|
||||
<Box alignItems="center" justifyContent="center" flexDirection="column" display="flex"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "calc(100vw - 32px)",
|
||||
maxWidth: "calc(100vw - 64px)",
|
||||
position: "absolute",
|
||||
height: "50%",
|
||||
background: "linear-gradient(rgba(255,255,255,.7), rgba(255,255,255,.7), rgba(255,255,255,0))"
|
||||
|
@ -40,12 +40,16 @@ const HomeLayout: FC = () => {
|
|||
</Box>
|
||||
</Fade>}
|
||||
{<Box height={"100%"} bgcolor={"#fff"}>
|
||||
{error &&
|
||||
<Alert color="error" severity="error" style={{fontSize: "14px", whiteSpace: "pre-wrap"}}>
|
||||
{error}
|
||||
</Alert>}
|
||||
{graphData && period && (displayType === "chart") &&
|
||||
<GraphView data={graphData}/>}
|
||||
<Box display="grid" gridTemplateColumns="1fr auto" alignItems="center" mx={-4} px={4} mb={2}
|
||||
borderBottom={1} borderColor="divider">
|
||||
<DisplayTypeSwitch/>
|
||||
{displayType === "chart" && <GraphSettings/>}
|
||||
</Box>
|
||||
{error && <Alert color="error" severity="error"
|
||||
style={{fontSize: "14px", whiteSpace: "pre-wrap", marginTop: "20px"}}>
|
||||
{error}
|
||||
</Alert>}
|
||||
{graphData && period && (displayType === "chart") && <GraphView data={graphData}/>}
|
||||
{liveData && (displayType === "code") && <JsonView data={liveData}/>}
|
||||
{liveData && (displayType === "table") && <TableView data={liveData}/>}
|
||||
</Box>}
|
||||
|
|
|
@ -7,7 +7,6 @@ import {useGraphDispatch, useGraphState} from "../../../state/graph/GraphStateCo
|
|||
import {getHideSeries, getLegendItem, getSeriesItem} from "../../../utils/uplot/series";
|
||||
import {getLimitsYAxis, getTimeSeries} from "../../../utils/uplot/axes";
|
||||
import {LegendItem} from "../../../utils/uplot/types";
|
||||
import GraphSettings from "../Configurator/Graph/GraphSettings";
|
||||
import {useAppState} from "../../../state/common/StateContext";
|
||||
|
||||
export interface GraphViewProps {
|
||||
|
@ -82,7 +81,6 @@ const GraphView: FC<GraphViewProps> = ({data = []}) => {
|
|||
return <>
|
||||
{(data.length > 0)
|
||||
? <div>
|
||||
<GraphSettings/>
|
||||
<LineChart data={dataChart} series={series} metrics={data}/>
|
||||
<Legend labels={legend} onChange={onChangeLegend}/>
|
||||
</div>
|
||||
|
|
|
@ -15,18 +15,24 @@ const JsonView: FC<JsonViewProps> = ({data}) => {
|
|||
|
||||
return (
|
||||
<Box position="relative">
|
||||
<Box flexDirection="column" justifyContent="flex-end" display="flex"
|
||||
<Box
|
||||
style={{
|
||||
position: "fixed",
|
||||
right: "16px"
|
||||
position: "sticky",
|
||||
top: "16px",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
}}>
|
||||
<Button variant="outlined" onClick={(e) => {
|
||||
navigator.clipboard.writeText(formattedJson);
|
||||
showInfoMessage("Formatted JSON has been copied");
|
||||
e.preventDefault(); // needed to avoid snackbar immediate disappearing
|
||||
}}>Copy JSON</Button>
|
||||
<Button variant="outlined"
|
||||
fullWidth={false}
|
||||
onClick={(e) => {
|
||||
navigator.clipboard.writeText(formattedJson);
|
||||
showInfoMessage("Formatted JSON has been copied");
|
||||
e.preventDefault(); // needed to avoid snackbar immediate disappearing
|
||||
}}>
|
||||
Copy JSON
|
||||
</Button>
|
||||
</Box>
|
||||
<pre>{formattedJson}</pre>
|
||||
<pre style={{margin: 0}}>{formattedJson}</pre>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React, {FC, useMemo} from "preact/compat";
|
||||
import {InstantMetricResult} from "../../../api/types";
|
||||
import {InstantDataSeries} from "../../../types";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
|
@ -37,7 +36,7 @@ const TableView: FC<GraphViewProps> = ({data}) => {
|
|||
return (
|
||||
<>
|
||||
{(rows.length > 0)
|
||||
? <TableContainer component={Paper}>
|
||||
? <TableContainer>
|
||||
<Table aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
|
@ -48,7 +47,7 @@ const TableView: FC<GraphViewProps> = ({data}) => {
|
|||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableRow key={index} hover>
|
||||
{row.metadata.map((rowMeta, index2) => {
|
||||
const prevRowValue = rows[index - 1] && rows[index - 1].metadata[index2];
|
||||
return (
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import Box from "@mui/material/Box";
|
||||
import CircularProgress, {CircularProgressProps} from "@mui/material/CircularProgress";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import React, {FC} from "preact/compat";
|
||||
|
||||
const CircularProgressWithLabel: FC<CircularProgressProps & { label: number }> = (props) => {
|
||||
return (
|
||||
<Box position="relative" display="inline-flex">
|
||||
<CircularProgress variant="determinate" {...props} />
|
||||
<Box
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
position="absolute"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Typography variant="caption" component="div">{`${props.label}s`}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CircularProgressWithLabel;
|
|
@ -1,19 +0,0 @@
|
|||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import React from "preact/compat";
|
||||
import Link from "@mui/material/Link";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
inlineBtn: {
|
||||
"&:hover": {
|
||||
cursor: "pointer"
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export const InlineBtn: React.FC<{handler: () => void; text: string}> = ({handler, text}) => {
|
||||
const classes = useStyles();
|
||||
return <Link component="span" className={classes.inlineBtn}
|
||||
onClick={handler}>
|
||||
{text}
|
||||
</Link>;
|
||||
};
|
|
@ -1,5 +1,26 @@
|
|||
import { useState, useEffect } from "preact/compat";
|
||||
|
||||
const getScrollbarWidth = () => {
|
||||
// Creating invisible container
|
||||
const outer = document.createElement("div");
|
||||
outer.style.visibility = "hidden";
|
||||
outer.style.overflow = "scroll"; // forcing scrollbar to appear
|
||||
document.body.appendChild(outer);
|
||||
|
||||
// Creating inner element and placing it in the container
|
||||
const inner = document.createElement("div");
|
||||
outer.appendChild(inner);
|
||||
|
||||
// Calculating difference between container's full width and the child width
|
||||
const scrollbarWidth = (outer.offsetWidth - inner.offsetWidth);
|
||||
|
||||
// Removing temporary elements from the DOM
|
||||
inner.remove();
|
||||
outer.remove();
|
||||
|
||||
return scrollbarWidth;
|
||||
};
|
||||
|
||||
const useResize = (node: HTMLElement | null): {width: number, height: number} => {
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
width: 0,
|
||||
|
@ -9,7 +30,7 @@ const useResize = (node: HTMLElement | null): {width: number, height: number} =>
|
|||
if (!node) return;
|
||||
const handleResize = () => {
|
||||
setWindowSize({
|
||||
width: node.offsetWidth,
|
||||
width: node.offsetWidth - getScrollbarWidth(),
|
||||
height: node.offsetHeight,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -59,7 +59,7 @@ const query = getQueryArray();
|
|||
|
||||
export const initialState: AppState = {
|
||||
serverUrl: getDefaultServer(),
|
||||
displayType: getQueryStringValue("tab", "chart") as DisplayType,
|
||||
displayType: getQueryStringValue("g0.tab", "chart") as DisplayType,
|
||||
query: query, // demo_memory_usage_bytes
|
||||
queryHistory: query.map(q => ({index: 0, values: [q]})),
|
||||
time: {
|
||||
|
|
|
@ -49,17 +49,27 @@ const THEME = createTheme({
|
|||
MuiAccordion: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
boxShadow: "rgba(0, 0, 0, 0.16) 0px 1px 4px;"
|
||||
boxShadow: "rgba(0, 0, 0, 0.16) 0px 1px 4px"
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
elevation3: {
|
||||
boxShadow: "rgba(0, 0, 0, 0.2) 0px 3px 8px;"
|
||||
root: {
|
||||
boxShadow: "rgba(0, 0, 0, 0.2) 0px 3px 8px"
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
contained: {
|
||||
boxShadow: "rgba(17, 17, 26, 0.1) 0px 0px 16px",
|
||||
"&:hover": {
|
||||
boxShadow: "rgba(0, 0, 0, 0.1) 0px 4px 12px",
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiIconButton: {
|
||||
defaultProps: {
|
||||
size: "large",
|
||||
|
@ -77,6 +87,20 @@ const THEME = createTheme({
|
|||
borderRadius: "20%",
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiTooltip: {
|
||||
styleOverrides: {
|
||||
tooltip: {
|
||||
fontSize: "10px"
|
||||
}
|
||||
}
|
||||
},
|
||||
MuiAlert: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
boxShadow: "rgba(0, 0, 0, 0.08) 0px 4px 12px"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
typography: {
|
||||
|
|
Loading…
Reference in a new issue