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:
Yury Molodov 2022-01-18 13:44:22 +03:00 committed by GitHub
parent c2a3911bb5
commit fcd33fc409
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 511 additions and 527 deletions

View file

@ -1,12 +1,12 @@
{ {
"files": { "files": {
"main.css": "./static/css/main.79ff1ad2.css", "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", "static/js/27.cc1b69f7.chunk.js": "./static/js/27.cc1b69f7.chunk.js",
"index.html": "./index.html" "index.html": "./index.html"
}, },
"entrypoints": [ "entrypoints": [
"static/css/main.79ff1ad2.css", "static/css/main.79ff1ad2.css",
"static/js/main.31aed9a0.js" "static/js/main.2473acb3.js"
] ]
} }

View file

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -5,10 +5,11 @@ import Link from "@mui/material/Link";
import Toolbar from "@mui/material/Toolbar"; import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import {ExecutionControls} from "../Home/Configurator/Time/ExecutionControls"; import {ExecutionControls} from "../Home/Configurator/Time/ExecutionControls";
import {DisplayTypeSwitch} from "../Home/Configurator/DisplayTypeSwitch";
import Logo from "../common/Logo"; import Logo from "../common/Logo";
import makeStyles from "@mui/styles/makeStyles"; import makeStyles from "@mui/styles/makeStyles";
import {setQueryStringWithoutPageReload} from "../../utils/query-string"; import {setQueryStringWithoutPageReload} from "../../utils/query-string";
import {TimeSelector} from "../Home/Configurator/Time/TimeSelector";
import GlobalSettings from "../Home/Configurator/Settings/GlobalSettings";
const useStyles = makeStyles({ const useStyles = makeStyles({
logo: { logo: {
@ -22,8 +23,6 @@ const useStyles = makeStyles({
} }
}, },
issueLink: { issueLink: {
position: "absolute",
bottom: "6px",
textAlign: "center", textAlign: "center",
fontSize: "10px", fontSize: "10px",
opacity: ".4", opacity: ".4",
@ -45,7 +44,7 @@ const Header: FC = () => {
window.location.reload(); window.location.reload();
}; };
return <AppBar position="static"> return <AppBar position="static" sx={{px: 1, boxShadow: "none"}}>
<Toolbar> <Toolbar>
<Box display="grid" alignItems="center" justifyContent="center"> <Box display="grid" alignItems="center" justifyContent="center">
<Box onClick={onClickLogo} className={classes.logo}> <Box onClick={onClickLogo} className={classes.logo}>
@ -60,10 +59,11 @@ const Header: FC = () => {
create an issue create an issue
</Link> </Link>
</Box> </Box>
<Box ml={4} flexGrow={1}> <Box display="grid" gridTemplateColumns="repeat(3, auto)" gap={1} alignItems="center" ml="auto" mr={0}>
<TimeSelector/>
<ExecutionControls/> <ExecutionControls/>
<GlobalSettings/>
</Box> </Box>
<DisplayTypeSwitch/>
</Toolbar> </Toolbar>
</AppBar>; </AppBar>;
}; };

View file

@ -2,48 +2,39 @@ import React, {FC} from "preact/compat";
import TableChartIcon from "@mui/icons-material/TableChart"; import TableChartIcon from "@mui/icons-material/TableChart";
import ShowChartIcon from "@mui/icons-material/ShowChart"; import ShowChartIcon from "@mui/icons-material/ShowChart";
import CodeIcon from "@mui/icons-material/Code"; import CodeIcon from "@mui/icons-material/Code";
import ToggleButton from "@mui/material/ToggleButton"; import Tabs from "@mui/material/Tabs";
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; import Tab from "@mui/material/Tab";
import {useAppDispatch, useAppState} from "../../../state/common/StateContext"; import {useAppDispatch, useAppState} from "../../../state/common/StateContext";
import withStyles from "@mui/styles/withStyles"; import {SyntheticEvent} from "react";
export type DisplayType = "table" | "chart" | "code"; export type DisplayType = "table" | "chart" | "code";
const StylizedToggleButton = withStyles({ const tabs = [
root: { {value: "chart", icon: <ShowChartIcon/>, label: "Graph"},
display: "grid", {value: "code", icon: <CodeIcon/>, label: "JSON"},
gridTemplateColumns: "18px auto", {value: "table", icon: <TableChartIcon/>, label: "Table"}
gridGap: 6, ];
padding: "8px 12px",
color: "white",
lineHeight: "19px",
"&.Mui-selected": {
color: "white"
}
}
})(ToggleButton);
export const DisplayTypeSwitch: FC = () => { export const DisplayTypeSwitch: FC = () => {
const {displayType} = useAppState(); const {displayType} = useAppState();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
return <ToggleButtonGroup const handleChange = (event: SyntheticEvent, newValue: DisplayType) => {
dispatch({type: "SET_DISPLAY_TYPE", payload: newValue ?? displayType});
};
return <Tabs
value={displayType} value={displayType}
exclusive onChange={handleChange}
onChange={ sx={{minHeight: "0", marginBottom: "-1px"}}
(e, val) => >
// Toggle Button Group returns null in case of click on selected element, avoiding it {tabs.map(t =>
dispatch({type: "SET_DISPLAY_TYPE", payload: val ?? displayType}) <Tab key={t.value}
}> icon={t.icon}
<StylizedToggleButton value="chart" aria-label="display as chart"> iconPosition="start"
<ShowChartIcon/><span>Query Range as Chart</span> label={t.label} value={t.value}
</StylizedToggleButton> sx={{minHeight: "41px"}}
<StylizedToggleButton value="code" aria-label="display as code"> />)}
<CodeIcon/><span>Instant Query as JSON</span> </Tabs>;
</StylizedToggleButton>
<StylizedToggleButton value="table" aria-label="display as table">
<TableChartIcon/><span>Instant Query as Table</span>
</StylizedToggleButton>
</ToggleButtonGroup>;
}; };

View file

@ -2,13 +2,14 @@ import SettingsIcon from "@mui/icons-material/Settings";
import React, {FC, useState} from "preact/compat"; import React, {FC, useState} from "preact/compat";
import AxesLimitsConfigurator from "./AxesLimitsConfigurator"; import AxesLimitsConfigurator from "./AxesLimitsConfigurator";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import Paper from "@mui/material/Paper"; 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 Typography from "@mui/material/Typography";
import makeStyles from "@mui/styles/makeStyles"; import makeStyles from "@mui/styles/makeStyles";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import ClickAwayListener from "@mui/material/ClickAwayListener";
const useStyles = makeStyles({ const useStyles = makeStyles({
popover: { popover: {
@ -32,31 +33,29 @@ const useStyles = makeStyles({
} }
}); });
const title = "Axes Settings";
const GraphSettings: FC = () => { const GraphSettings: FC = () => {
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null); const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const classes = useStyles(); const classes = useStyles();
return <Box display="flex" px={2}> return <Box>
<Button variant="outlined" aria-describedby="settings-popover" <Tooltip title={title}>
onClick={(e) => setAnchorEl(e.currentTarget)} > <IconButton onClick={(e) => setAnchorEl(e.currentTarget)}>
<SettingsIcon sx={{fontSize: 16, marginRight: "4px"}}/> <SettingsIcon/>
<span style={{lineHeight: 1, paddingTop: "1px"}}>{open ? "Hide" : "Show"} graph settings</span> </IconButton>
</Button> </Tooltip>
<Popover <Popper
id="settings-popover"
open={open} open={open}
anchorEl={anchorEl} anchorEl={anchorEl}
onClose={() => setAnchorEl(null)} placement="left-start"
anchorOrigin={{ modifiers={[{name: "offset", options: {offset: [0, 6]}}]}>
vertical: "top", <ClickAwayListener onClickAway={() => setAnchorEl(null)}>
horizontal: anchorEl ? anchorEl.offsetWidth + 15 : 200
}}
>
<Paper elevation={3} className={classes.popover}> <Paper elevation={3} className={classes.popover}>
<div id="handle" className={classes.popoverHeader}> <div id="handle" className={classes.popoverHeader}>
<Typography variant="body1"><b>Graph Settings</b></Typography> <Typography variant="body1"><b>{title}</b></Typography>
<IconButton size="small" onClick={() => setAnchorEl(null)}> <IconButton size="small" onClick={() => setAnchorEl(null)}>
<CloseIcon style={{color: "white"}}/> <CloseIcon style={{color: "white"}}/>
</IconButton> </IconButton>
@ -65,7 +64,8 @@ const GraphSettings: FC = () => {
<AxesLimitsConfigurator/> <AxesLimitsConfigurator/>
</Box> </Box>
</Paper> </Paper>
</Popover> </ClickAwayListener>
</Popper>
</Box>; </Box>;
}; };

View file

@ -1,21 +1,12 @@
import React, {FC, useEffect, useRef, useState} from "preact/compat"; import React, {FC, useEffect, useRef} from "preact/compat";
import Accordion from "@mui/material/Accordion";
import AccordionDetails from "@mui/material/AccordionDetails";
import AccordionSummary from "@mui/material/AccordionSummary";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import Button from "@mui/material/Button";
import Portal from "@mui/material/Portal";
import QueryEditor from "./QueryEditor"; import QueryEditor from "./QueryEditor";
import {TimeSelector} from "../Time/TimeSelector";
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext"; import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import HighlightOffIcon from "@mui/icons-material/HighlightOff"; 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 PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline";
import ServerConfigurator from "./ServerConfigurator";
import AdditionalSettings from "./AdditionalSettings"; import AdditionalSettings from "./AdditionalSettings";
import {ErrorTypes} from "../../../../types"; import {ErrorTypes} from "../../../../types";
@ -28,15 +19,11 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({error, queryOptions}) =>
const {query, queryHistory, queryControls: {autocomplete}} = useAppState(); const {query, queryHistory, queryControls: {autocomplete}} = useAppState();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [expanded, setExpanded] = useState(true);
const queryContainer = useRef<HTMLDivElement>(null);
const queryRef = useRef(query); const queryRef = useRef(query);
useEffect(() => { useEffect(() => {
queryRef.current = query; queryRef.current = query;
}, [query]); }, [query]);
const onSetDuration = (dur: string) => dispatch({type: "SET_DURATION", payload: dur});
const updateHistory = () => { const updateHistory = () => {
dispatch({ dispatch({
type: "SET_QUERY_HISTORY", payload: query.map((q, i) => { 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} payload: {value: {values, index: newIndexHistory}, queryNumber: indexQuery}
}); });
}; };
return <Box boxShadow="rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;" p={4} pb={2} m={-4} mb={2}>
return <> <Box>
<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) => {query.map((q, i) =>
<Box key={i} display="grid" gridTemplateColumns="1fr auto" gap="4px" width="100%" <Box key={i} display="grid" gridTemplateColumns="1fr auto auto" gap="4px" width="100%"
mb={i === query.length - 1 ? 0 : 2}> mb={i === query.length - 1 ? 0 : 2.5}>
<QueryEditor query={query[i]} index={i} autocomplete={autocomplete} queryOptions={queryOptions} <QueryEditor query={query[i]} index={i} autocomplete={autocomplete} queryOptions={queryOptions}
error={error} setHistoryIndex={setHistoryIndex} runQuery={onRunQuery} setQuery={onSetQuery}/> error={error} setHistoryIndex={setHistoryIndex} runQuery={onRunQuery} setQuery={onSetQuery}/>
{i === 0 && <Tooltip title="Execute Query"> {i === 0 && <Tooltip title="Execute Query">
<IconButton onClick={onRunQuery}> <IconButton onClick={onRunQuery} sx={{height: "49px", width: "49px"}}>
<PlayCircleOutlineIcon/> <PlayCircleOutlineIcon/>
</IconButton> </IconButton>
</Tooltip>} </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"> {i > 0 && <Tooltip title="Remove Query">
<IconButton onClick={() => onRemoveQuery(i)}> <IconButton onClick={() => onRemoveQuery(i)} sx={{height: "49px", width: "49px"}}>
<HighlightOffIcon/> <HighlightOffIcon/>
</IconButton> </IconButton>
</Tooltip>} </Tooltip>}
</Box>)} </Box>)}
</Portal>
</Box> </Box>
</AccordionSummary> <Box mt={3}>
<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/> <AdditionalSettings/>
</Box> </Box>
</Box> </Box>;
</AccordionDetails>
</Accordion>
</>;
}; };
export default QueryConfigurator; export default QueryConfigurator;

View file

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

View file

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

View file

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

View file

@ -1,92 +1,100 @@
import React, {FC, useEffect, useState} from "preact/compat"; 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 Tooltip from "@mui/material/Tooltip";
import EqualizerIcon from "@mui/icons-material/Equalizer";
import {useAppDispatch, useAppState} from "../../../../state/common/StateContext"; import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import CircularProgressWithLabel from "../../../common/CircularProgressWithLabel"; import Button from "@mui/material/Button";
import makeStyles from "@mui/styles/makeStyles"; import Popper from "@mui/material/Popper";
import BasicSwitch from "../../../../theme/switch"; 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({ interface AutoRefreshOption {
colorizing: { seconds: number
color: "white" 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 = () => { export const ExecutionControls: FC = () => {
const classes = useStyles();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const {queryControls: {autoRefresh}} = useAppState(); const {queryControls: {autoRefresh}} = useAppState();
const [delay, setDelay] = useState<(1|2|5)>(5); const [selectedDelay, setSelectedDelay] = useState<AutoRefreshOption>(delayOptions[0]);
const [lastUpdate, setLastUpdate] = useState<number|undefined>();
const [progress, setProgress] = React.useState(100);
const handleChange = () => { const handleChange = (d: AutoRefreshOption) => {
if ((autoRefresh && !d.seconds) || (!autoRefresh && d.seconds)) {
dispatch({type: "TOGGLE_AUTOREFRESH"}); dispatch({type: "TOGGLE_AUTOREFRESH"});
}
setSelectedDelay(d);
setAnchorEl(null);
}; };
useEffect(() => { useEffect(() => {
const delay = selectedDelay.seconds;
let timer: number; let timer: number;
if (autoRefresh) { if (autoRefresh) {
setLastUpdate(new Date().valueOf());
timer = setInterval(() => { timer = setInterval(() => {
setLastUpdate(new Date().valueOf());
dispatch({type: "RUN_QUERY_TO_NOW"}); dispatch({type: "RUN_QUERY_TO_NOW"});
}, delay * 1000) as unknown as number; }, delay * 1000) as unknown as number;
} else {
setSelectedDelay(delayOptions[0]);
} }
return () => { return () => {
timer && clearInterval(timer); timer && clearInterval(timer);
}; };
}, [delay, autoRefresh]); }, [selectedDelay, autoRefresh]);
useEffect(() => { const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const timer = setInterval(() => { const open = Boolean(anchorEl);
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 iterateDelays = () => { return <>
setDelay(prev => { <Tooltip title="Auto-refresh control">
switch (prev) { <Button variant="contained" color="primary"
case 1: sx={{
return 2; minWidth: "110px",
case 2: color: "white",
return 5; border: "1px solid rgba(0, 0, 0, 0.2)",
case 5: justifyContent: "space-between",
return 1; boxShadow: "none",
default: }}
return 5; startIcon={<AutorenewIcon/>}
} endIcon={<KeyboardArrowDownIcon sx={{transform: open ? "rotate(180deg)" : "none"}}/>}
}); onClick={(e) => setAnchorEl(e.currentTarget)}
}; >
{selectedDelay.title}
return <Box display="flex" alignItems="center"> </Button>
{<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> </Tooltip>
</>} <Popper
</Box>; 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>
</>;
}; };

View file

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

View file

@ -1,103 +1,47 @@
import React, {FC, useEffect, useState} from "preact/compat"; import React, {FC} from "preact/compat";
import {ChangeEvent, MouseEvent, KeyboardEvent} from "react"; import List from "@mui/material/List";
import Box from "@mui/material/Box"; import ListItem from "@mui/material/ListItem";
import Popover from "@mui/material/Popover"; import ListItemText from "@mui/material/ListItemText";
import TextField from "@mui/material/TextField"; import dayjs from "dayjs";
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";
interface TimeDurationSelector { 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 TimeDurationSelector: FC<TimeDurationSelector> = ({setDuration}) => {
const {time: {duration}} = useAppState(); // setDurationString("5m"))
const [anchorEl, setAnchorEl] = React.useState<Element | null>(null); return <List style={{maxHeight: "168px", overflow: "auto", paddingRight: "15px"}}>
const [durationString, setDurationString] = useState<string>(duration); {durationOptions.map(d =>
const [durationStringFocused, setFocused] = useState(false); <ListItem key={d.duration} button onClick={() => setDuration(d.duration, d.from ? d.from() : new Date())}>
const open = Boolean(anchorEl); <ListItemText primary={d.title || d.duration}/>
</ListItem>)}
const handleDurationChange = (event: ChangeEvent<HTMLInputElement>) => { </List>;
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:&nbsp;
</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"/>,&nbsp;
<InlineBtn handler={() => setDurationString("1h")} text="1h"/>,&nbsp;
<InlineBtn handler={() => setDurationString("1h 30m")} text="1h 30m"/>
</Typography>
</Box>
</>;
}; };
export default TimeDurationSelector; export default TimeDurationSelector;

View file

@ -1,40 +1,32 @@
import React, {FC, useEffect, useState} from "preact/compat"; import React, {FC, useEffect, useState, useMemo} 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 {useAppDispatch, useAppState} from "../../../../state/common/StateContext"; import {useAppDispatch, useAppState} from "../../../../state/common/StateContext";
import {dateFromSeconds, formatDateForNativeInput} from "../../../../utils/time"; import {dateFromSeconds, formatDateForNativeInput} from "../../../../utils/time";
import {InlineBtn} from "../../../common/InlineBtn";
import makeStyles from "@mui/styles/makeStyles"; import makeStyles from "@mui/styles/makeStyles";
import TimeDurationSelector from "./TimeDurationSelector"; import TimeDurationSelector from "./TimeDurationSelector";
import dayjs from "dayjs"; 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 { const formatDate = "YYYY-MM-DD HH:mm:ss";
setDuration: (str: string) => void;
}
const useStyles = makeStyles({ const useStyles = makeStyles({
container: { container: {
display: "grid", display: "grid",
gridTemplateColumns: "200px 1fr", gridTemplateColumns: "200px auto 200px",
gridGap: "20px", gridGap: "10px",
height: "100%",
padding: "20px", padding: "20px",
borderRadius: "4px",
borderColor: "#b9b9b9",
borderStyle: "solid",
borderWidth: "1px"
}, },
timeControls: { timeControls: {
display: "grid", display: "grid",
gridTemplateColumns: "1fr", gridTemplateRows: "auto 1fr auto",
gridTemplateRows: "auto 1fr",
gridGap: "16px 0",
},
datePickers: {
display: "grid",
gridTemplateColumns: "repeat(auto-fit, 200px)",
gridGap: "16px 0", gridGap: "16px 0",
}, },
datePickerItem: { datePickerItem: {
@ -42,7 +34,7 @@ const useStyles = makeStyles({
}, },
}); });
export const TimeSelector: FC<TimeSelectorProps> = ({setDuration}) => { export const TimeSelector: FC = () => {
const classes = useStyles(); const classes = useStyles();
@ -60,14 +52,46 @@ export const TimeSelector: FC<TimeSelectorProps> = ({setDuration}) => {
setFrom(formatDateForNativeInput(dateFromSeconds(start))); setFrom(formatDateForNativeInput(dateFromSeconds(start)));
}, [start]); }, [start]);
return <Box className={classes.container}> const setDuration = (dur: string, from: Date) => {
{/*setup duration*/} dispatch({type: "SET_UNTIL", payload: from});
<Box> setAnchorEl(null);
<TimeDurationSelector setDuration={setDuration}/> dispatch({type: "SET_DURATION", payload: dur});
</Box> };
{/*setup end time*/}
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.timeControls}>
<Box className={classes.datePickers}>
<Box className={classes.datePickerItem}> <Box className={classes.datePickerItem}>
<DateTimePicker <DateTimePicker
label="From" label="From"
@ -75,31 +99,41 @@ export const TimeSelector: FC<TimeSelectorProps> = ({setDuration}) => {
value={from} value={from}
onChange={date => dispatch({type: "SET_FROM", payload: date as unknown as Date})} onChange={date => dispatch({type: "SET_FROM", payload: date as unknown as Date})}
onError={console.log} onError={console.log}
inputFormat="DD/MM/YYYY HH:mm:ss" inputFormat={formatDate}
mask="__/__/____ __:__:__" mask="____-__-__ __:__:__"
renderInput={(params) => <TextField {...params} variant="standard"/>} renderInput={(params) => <TextField {...params} variant="standard"/>}
maxDate={dayjs(until)} maxDate={dayjs(until)}
/> PopperProps={{disablePortal: true}}/>
</Box> </Box>
<Box className={classes.datePickerItem}> <Box className={classes.datePickerItem}>
<DateTimePicker <DateTimePicker
label="Until" label="To"
ampm={false} ampm={false}
value={until} value={until}
onChange={date => dispatch({type: "SET_UNTIL", payload: date as unknown as Date})} onChange={date => dispatch({type: "SET_UNTIL", payload: date as unknown as Date})}
onError={console.log} onError={console.log}
inputFormat="DD/MM/YYYY HH:mm:ss" inputFormat={formatDate}
mask="__/__/____ __:__:__" mask="____-__-__ __:__:__"
renderInput={(params) => <TextField {...params} variant="standard"/>} 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>
</Box> </Box>
{/*setup duration*/}
<Divider orientation="vertical" flexItem />
<Box> <Box>
<Typography variant="body2"> <TimeDurationSelector setDuration={setDuration}/>
Will be changed to current time for auto-refresh mode.&nbsp;
<InlineBtn handler={() => dispatch({type: "RUN_QUERY_TO_NOW"})} text="Switch to now"/>
</Typography>
</Box> </Box>
</Box> </Box>
</Box>; </Paper>
</ClickAwayListener>
</Popper>
</>;
}; };

View file

@ -10,6 +10,8 @@ import QueryConfigurator from "./Configurator/Query/QueryConfigurator";
import {useFetchQuery} from "./Configurator/Query/useFetchQuery"; import {useFetchQuery} from "./Configurator/Query/useFetchQuery";
import JsonView from "./Views/JsonView"; import JsonView from "./Views/JsonView";
import Header from "../Header/Header"; import Header from "../Header/Header";
import {DisplayTypeSwitch} from "./Configurator/DisplayTypeSwitch";
import GraphSettings from "./Configurator/Graph/GraphSettings";
const HomeLayout: FC = () => { const HomeLayout: FC = () => {
@ -20,18 +22,16 @@ const HomeLayout: FC = () => {
return ( return (
<Box id="homeLayout"> <Box id="homeLayout">
<Header/> <Header/>
<Box p={4} display="grid" gridTemplateRows="auto 1fr" gap={"20px"} style={{minHeight: "calc(100vh - 64px)"}}> <Box p={4} display="grid" gridTemplateRows="auto 1fr" style={{minHeight: "calc(100vh - 64px)"}}>
<Box>
<QueryConfigurator error={error} queryOptions={queryOptions}/> <QueryConfigurator error={error} queryOptions={queryOptions}/>
</Box> <Box height="100%">
<Box height={"100%"}>
{isLoading && <Fade in={isLoading} style={{ {isLoading && <Fade in={isLoading} style={{
transitionDelay: isLoading ? "300ms" : "0ms", transitionDelay: isLoading ? "300ms" : "0ms",
}}> }}>
<Box alignItems="center" justifyContent="center" flexDirection="column" display="flex" <Box alignItems="center" justifyContent="center" flexDirection="column" display="flex"
style={{ style={{
width: "100%", width: "100%",
maxWidth: "calc(100vw - 32px)", maxWidth: "calc(100vw - 64px)",
position: "absolute", position: "absolute",
height: "50%", height: "50%",
background: "linear-gradient(rgba(255,255,255,.7), rgba(255,255,255,.7), rgba(255,255,255,0))" 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> </Box>
</Fade>} </Fade>}
{<Box height={"100%"} bgcolor={"#fff"}> {<Box height={"100%"} bgcolor={"#fff"}>
{error && <Box display="grid" gridTemplateColumns="1fr auto" alignItems="center" mx={-4} px={4} mb={2}
<Alert color="error" severity="error" style={{fontSize: "14px", whiteSpace: "pre-wrap"}}> borderBottom={1} borderColor="divider">
<DisplayTypeSwitch/>
{displayType === "chart" && <GraphSettings/>}
</Box>
{error && <Alert color="error" severity="error"
style={{fontSize: "14px", whiteSpace: "pre-wrap", marginTop: "20px"}}>
{error} {error}
</Alert>} </Alert>}
{graphData && period && (displayType === "chart") && {graphData && period && (displayType === "chart") && <GraphView data={graphData}/>}
<GraphView data={graphData}/>}
{liveData && (displayType === "code") && <JsonView data={liveData}/>} {liveData && (displayType === "code") && <JsonView data={liveData}/>}
{liveData && (displayType === "table") && <TableView data={liveData}/>} {liveData && (displayType === "table") && <TableView data={liveData}/>}
</Box>} </Box>}

View file

@ -7,7 +7,6 @@ import {useGraphDispatch, useGraphState} from "../../../state/graph/GraphStateCo
import {getHideSeries, getLegendItem, getSeriesItem} from "../../../utils/uplot/series"; import {getHideSeries, getLegendItem, getSeriesItem} from "../../../utils/uplot/series";
import {getLimitsYAxis, getTimeSeries} from "../../../utils/uplot/axes"; import {getLimitsYAxis, getTimeSeries} from "../../../utils/uplot/axes";
import {LegendItem} from "../../../utils/uplot/types"; import {LegendItem} from "../../../utils/uplot/types";
import GraphSettings from "../Configurator/Graph/GraphSettings";
import {useAppState} from "../../../state/common/StateContext"; import {useAppState} from "../../../state/common/StateContext";
export interface GraphViewProps { export interface GraphViewProps {
@ -82,7 +81,6 @@ const GraphView: FC<GraphViewProps> = ({data = []}) => {
return <> return <>
{(data.length > 0) {(data.length > 0)
? <div> ? <div>
<GraphSettings/>
<LineChart data={dataChart} series={series} metrics={data}/> <LineChart data={dataChart} series={series} metrics={data}/>
<Legend labels={legend} onChange={onChangeLegend}/> <Legend labels={legend} onChange={onChangeLegend}/>
</div> </div>

View file

@ -15,18 +15,24 @@ const JsonView: FC<JsonViewProps> = ({data}) => {
return ( return (
<Box position="relative"> <Box position="relative">
<Box flexDirection="column" justifyContent="flex-end" display="flex" <Box
style={{ style={{
position: "fixed", position: "sticky",
right: "16px" top: "16px",
display: "flex",
justifyContent: "flex-end",
}}> }}>
<Button variant="outlined" onClick={(e) => { <Button variant="outlined"
fullWidth={false}
onClick={(e) => {
navigator.clipboard.writeText(formattedJson); navigator.clipboard.writeText(formattedJson);
showInfoMessage("Formatted JSON has been copied"); showInfoMessage("Formatted JSON has been copied");
e.preventDefault(); // needed to avoid snackbar immediate disappearing e.preventDefault(); // needed to avoid snackbar immediate disappearing
}}>Copy JSON</Button> }}>
Copy JSON
</Button>
</Box> </Box>
<pre>{formattedJson}</pre> <pre style={{margin: 0}}>{formattedJson}</pre>
</Box> </Box>
); );
}; };

View file

@ -1,7 +1,6 @@
import React, {FC, useMemo} from "preact/compat"; import React, {FC, useMemo} from "preact/compat";
import {InstantMetricResult} from "../../../api/types"; import {InstantMetricResult} from "../../../api/types";
import {InstantDataSeries} from "../../../types"; import {InstantDataSeries} from "../../../types";
import Paper from "@mui/material/Paper";
import Table from "@mui/material/Table"; import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody"; import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell"; import TableCell from "@mui/material/TableCell";
@ -37,7 +36,7 @@ const TableView: FC<GraphViewProps> = ({data}) => {
return ( return (
<> <>
{(rows.length > 0) {(rows.length > 0)
? <TableContainer component={Paper}> ? <TableContainer>
<Table aria-label="simple table"> <Table aria-label="simple table">
<TableHead> <TableHead>
<TableRow> <TableRow>
@ -48,7 +47,7 @@ const TableView: FC<GraphViewProps> = ({data}) => {
</TableHead> </TableHead>
<TableBody> <TableBody>
{rows.map((row, index) => ( {rows.map((row, index) => (
<TableRow key={index}> <TableRow key={index} hover>
{row.metadata.map((rowMeta, index2) => { {row.metadata.map((rowMeta, index2) => {
const prevRowValue = rows[index - 1] && rows[index - 1].metadata[index2]; const prevRowValue = rows[index - 1] && rows[index - 1].metadata[index2];
return ( return (

View file

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

View file

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

View file

@ -1,5 +1,26 @@
import { useState, useEffect } from "preact/compat"; 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 useResize = (node: HTMLElement | null): {width: number, height: number} => {
const [windowSize, setWindowSize] = useState({ const [windowSize, setWindowSize] = useState({
width: 0, width: 0,
@ -9,7 +30,7 @@ const useResize = (node: HTMLElement | null): {width: number, height: number} =>
if (!node) return; if (!node) return;
const handleResize = () => { const handleResize = () => {
setWindowSize({ setWindowSize({
width: node.offsetWidth, width: node.offsetWidth - getScrollbarWidth(),
height: node.offsetHeight, height: node.offsetHeight,
}); });
}; };

View file

@ -59,7 +59,7 @@ const query = getQueryArray();
export const initialState: AppState = { export const initialState: AppState = {
serverUrl: getDefaultServer(), serverUrl: getDefaultServer(),
displayType: getQueryStringValue("tab", "chart") as DisplayType, displayType: getQueryStringValue("g0.tab", "chart") as DisplayType,
query: query, // demo_memory_usage_bytes query: query, // demo_memory_usage_bytes
queryHistory: query.map(q => ({index: 0, values: [q]})), queryHistory: query.map(q => ({index: 0, values: [q]})),
time: { time: {

View file

@ -49,17 +49,27 @@ const THEME = createTheme({
MuiAccordion: { MuiAccordion: {
styleOverrides: { styleOverrides: {
root: { root: {
boxShadow: "rgba(0, 0, 0, 0.16) 0px 1px 4px;" boxShadow: "rgba(0, 0, 0, 0.16) 0px 1px 4px"
}, },
}, },
}, },
MuiPaper: { MuiPaper: {
styleOverrides: { styleOverrides: {
elevation3: { root: {
boxShadow: "rgba(0, 0, 0, 0.2) 0px 3px 8px;" 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: { MuiIconButton: {
defaultProps: { defaultProps: {
size: "large", size: "large",
@ -77,6 +87,20 @@ const THEME = createTheme({
borderRadius: "20%", borderRadius: "20%",
} }
} }
},
MuiTooltip: {
styleOverrides: {
tooltip: {
fontSize: "10px"
}
}
},
MuiAlert: {
styleOverrides: {
root: {
boxShadow: "rgba(0, 0, 0, 0.08) 0px 4px 12px"
}
}
} }
}, },
typography: { typography: {