mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmui: mobile view (#3742)
* feat: add detect the system theme * fix: change logic fetch tenants * feat: add docs and info to cardinality page * feat: add mobile view #3707
This commit is contained in:
parent
88fed0232c
commit
f63f487787
65 changed files with 895 additions and 144 deletions
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
|
|
|
@ -100,10 +100,15 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||||
const overflowX = leftOnChart + tooltipWidth >= width ? tooltipWidth + (2 * margin) : 0;
|
const overflowX = leftOnChart + tooltipWidth >= width ? tooltipWidth + (2 * margin) : 0;
|
||||||
const overflowY = topOnChart + tooltipHeight >= height ? tooltipHeight + (2 * margin) : 0;
|
const overflowY = topOnChart + tooltipHeight >= height ? tooltipHeight + (2 * margin) : 0;
|
||||||
|
|
||||||
setPosition({
|
const position = {
|
||||||
top: topOnChart + tooltipOffset.top + margin - overflowY,
|
top: topOnChart + tooltipOffset.top + margin - overflowY,
|
||||||
left: leftOnChart + tooltipOffset.left + margin - overflowX
|
left: leftOnChart + tooltipOffset.left + margin - overflowX
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (position.left < 0) position.left = 20;
|
||||||
|
if (position.top < 0) position.top = 20;
|
||||||
|
|
||||||
|
setPosition(position);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(calcPosition, [u, value, dataTime, seriesIdx, tooltipOffset, tooltipRef]);
|
useEffect(calcPosition, [u, value, dataTime, seriesIdx, tooltipOffset, tooltipRef]);
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
grid-gap: $padding-small;
|
grid-gap: $padding-small;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
padding: $padding-small $padding-large $padding-small $padding-small;
|
padding: $padding-small;
|
||||||
background-color: $color-background-block;
|
background-color: $color-background-block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: 0.2s ease;
|
transition: 0.2s ease;
|
||||||
|
@ -30,9 +30,9 @@
|
||||||
|
|
||||||
&-info {
|
&-info {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
&__label {
|
&__label {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__free-fields {
|
&__free-fields {
|
||||||
|
|
|
@ -55,6 +55,7 @@ const LineChart: FC<LineChartProps> = ({
|
||||||
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
|
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
|
||||||
const [yRange, setYRange] = useState([0, 1]);
|
const [yRange, setYRange] = useState([0, 1]);
|
||||||
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
||||||
|
const [startTouchDistance, setStartTouchDistance] = useState(0);
|
||||||
const layoutSize = useResize(container);
|
const layoutSize = useResize(container);
|
||||||
|
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
@ -84,6 +85,7 @@ const LineChart: FC<LineChartProps> = ({
|
||||||
left: parseFloat(u.over.style.left),
|
left: parseFloat(u.over.style.left),
|
||||||
top: parseFloat(u.over.style.top)
|
top: parseFloat(u.over.style.top)
|
||||||
});
|
});
|
||||||
|
|
||||||
u.over.addEventListener("mousedown", e => {
|
u.over.addEventListener("mousedown", e => {
|
||||||
const { ctrlKey, metaKey, button } = e;
|
const { ctrlKey, metaKey, button } = e;
|
||||||
const leftClick = button === 0;
|
const leftClick = button === 0;
|
||||||
|
@ -94,6 +96,10 @@ const LineChart: FC<LineChartProps> = ({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
u.over.addEventListener("touchstart", e => {
|
||||||
|
dragChart({ u, e, setPanning, setPlotScale, factor });
|
||||||
|
});
|
||||||
|
|
||||||
u.over.addEventListener("wheel", e => {
|
u.over.addEventListener("wheel", e => {
|
||||||
if (!e.ctrlKey && !e.metaKey) return;
|
if (!e.ctrlKey && !e.metaKey) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -235,6 +241,47 @@ const LineChart: FC<LineChartProps> = ({
|
||||||
};
|
};
|
||||||
}, [xRange]);
|
}, [xRange]);
|
||||||
|
|
||||||
|
const handleTouchStart = (e: TouchEvent) => {
|
||||||
|
if (e.touches.length !== 2) return;
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||||
|
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||||
|
setStartTouchDistance(Math.sqrt(dx * dx + dy * dy));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchMove = (e: TouchEvent) => {
|
||||||
|
if (e.touches.length !== 2 || !uPlotInst) return;
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||||
|
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||||
|
const endTouchDistance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
const diffDistance = startTouchDistance - endTouchDistance;
|
||||||
|
|
||||||
|
const max = (uPlotInst.scales.x.max || xRange.max);
|
||||||
|
const min = (uPlotInst.scales.x.min || xRange.min);
|
||||||
|
const dur = max - min;
|
||||||
|
const dir = (diffDistance > 0 ? -1 : 1);
|
||||||
|
|
||||||
|
const zoomFactor = dur / 50 * dir;
|
||||||
|
uPlotInst.batch(() => setPlotScale({
|
||||||
|
u: uPlotInst,
|
||||||
|
min: min + zoomFactor,
|
||||||
|
max: max - zoomFactor
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("touchmove", handleTouchMove);
|
||||||
|
window.addEventListener("touchstart", handleTouchStart);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("touchmove", handleTouchMove);
|
||||||
|
window.removeEventListener("touchstart", handleTouchStart);
|
||||||
|
};
|
||||||
|
}, [uPlotInst, startTouchDistance]);
|
||||||
|
|
||||||
useEffect(() => updateChart(typeChartUpdate.data), [data]);
|
useEffect(() => updateChart(typeChartUpdate.data), [data]);
|
||||||
useEffect(() => updateChart(typeChartUpdate.xRange), [xRange]);
|
useEffect(() => updateChart(typeChartUpdate.xRange), [xRange]);
|
||||||
useEffect(() => updateChart(typeChartUpdate.yRange), [yaxis]);
|
useEffect(() => updateChart(typeChartUpdate.yRange), [yaxis]);
|
||||||
|
@ -256,6 +303,10 @@ const LineChart: FC<LineChartProps> = ({
|
||||||
"vm-line-chart": true,
|
"vm-line-chart": true,
|
||||||
"vm-line-chart_panning": isPanning
|
"vm-line-chart_panning": isPanning
|
||||||
})}
|
})}
|
||||||
|
style={{
|
||||||
|
minWidth: `${layoutSize.width || 400}px`,
|
||||||
|
minHeight: `${height || 500}px`
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="vm-line-chart__u-plot"
|
className="vm-line-chart__u-plot"
|
||||||
|
|
|
@ -14,10 +14,12 @@ import classNames from "classnames";
|
||||||
import Timezones from "./Timezones/Timezones";
|
import Timezones from "./Timezones/Timezones";
|
||||||
import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext";
|
import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext";
|
||||||
import ThemeControl from "../ThemeControl/ThemeControl";
|
import ThemeControl from "../ThemeControl/ThemeControl";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
const title = "Settings";
|
const title = "Settings";
|
||||||
|
|
||||||
const GlobalSettings: FC = () => {
|
const GlobalSettings: FC<{showTitle?: boolean}> = ({ showTitle }) => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
const { serverUrl: stateServerUrl } = useAppState();
|
const { serverUrl: stateServerUrl } = useAppState();
|
||||||
|
@ -49,7 +51,10 @@ const GlobalSettings: FC = () => {
|
||||||
}, [stateServerUrl]);
|
}, [stateServerUrl]);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Tooltip title={title}>
|
<Tooltip
|
||||||
|
open={showTitle === true ? false : undefined}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-header-button": !appModeEnable
|
"vm-header-button": !appModeEnable
|
||||||
|
@ -58,14 +63,21 @@ const GlobalSettings: FC = () => {
|
||||||
color="primary"
|
color="primary"
|
||||||
startIcon={<SettingsIcon/>}
|
startIcon={<SettingsIcon/>}
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
/>
|
>
|
||||||
|
{showTitle && title}
|
||||||
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{open && (
|
{open && (
|
||||||
<Modal
|
<Modal
|
||||||
title={title}
|
title={title}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
>
|
>
|
||||||
<div className="vm-server-configurator">
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-server-configurator": true,
|
||||||
|
"vm-server-configurator_mobile": isMobile
|
||||||
|
})}
|
||||||
|
>
|
||||||
{!appModeEnable && (
|
{!appModeEnable && (
|
||||||
<div className="vm-server-configurator__input">
|
<div className="vm-server-configurator__input">
|
||||||
<ServerConfigurator
|
<ServerConfigurator
|
||||||
|
|
|
@ -70,8 +70,8 @@ const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , on
|
||||||
</div>
|
</div>
|
||||||
<div className="vm-limits-configurator__inputs">
|
<div className="vm-limits-configurator__inputs">
|
||||||
{fields.map(f => (
|
{fields.map(f => (
|
||||||
|
<div key={f.type}>
|
||||||
<TextField
|
<TextField
|
||||||
key={f.type}
|
|
||||||
label={f.label}
|
label={f.label}
|
||||||
value={limits[f.type]}
|
value={limits[f.type]}
|
||||||
error={error[f.type]}
|
error={error[f.type]}
|
||||||
|
@ -79,6 +79,7 @@ const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , on
|
||||||
onEnter={onEnter}
|
onEnter={onEnter}
|
||||||
type="number"
|
type="number"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,10 +12,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__inputs {
|
&__inputs {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: $padding-global;
|
gap: $padding-global;
|
||||||
|
|
||||||
|
div {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { FC, useState, useRef, useEffect, useMemo } from "preact/compat";
|
import React, { FC, useState, useRef, useEffect, useMemo } from "preact/compat";
|
||||||
import { useAppDispatch, useAppState } from "../../../../state/common/StateContext";
|
import { useAppDispatch, useAppState } from "../../../../state/common/StateContext";
|
||||||
import { useTimeDispatch } from "../../../../state/time/TimeStateContext";
|
import { useTimeDispatch } from "../../../../state/time/TimeStateContext";
|
||||||
import { ArrowDownIcon, StorageIcons } from "../../../Main/Icons";
|
import { ArrowDownIcon, StorageIcon } from "../../../Main/Icons";
|
||||||
import Button from "../../../Main/Button/Button";
|
import Button from "../../../Main/Button/Button";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import { replaceTenantId } from "../../../../utils/default-server-url";
|
import { replaceTenantId } from "../../../../utils/default-server-url";
|
||||||
|
@ -9,9 +9,11 @@ import classNames from "classnames";
|
||||||
import Popper from "../../../Main/Popper/Popper";
|
import Popper from "../../../Main/Popper/Popper";
|
||||||
import { getAppModeEnable } from "../../../../utils/app-mode";
|
import { getAppModeEnable } from "../../../../utils/app-mode";
|
||||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||||
|
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
|
const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const { tenantId: tenantIdState, serverUrl } = useAppState();
|
const { tenantId: tenantIdState, serverUrl } = useAppState();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
@ -71,8 +73,8 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
fullWidth
|
fullWidth
|
||||||
startIcon={<StorageIcons/>}
|
startIcon={<StorageIcon/>}
|
||||||
endIcon={(
|
endIcon={!isMobile ? (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-execution-controls-buttons__arrow": true,
|
"vm-execution-controls-buttons__arrow": true,
|
||||||
|
@ -81,10 +83,10 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
|
||||||
>
|
>
|
||||||
<ArrowDownIcon/>
|
<ArrowDownIcon/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : undefined}
|
||||||
onClick={toggleOpenOptions}
|
onClick={toggleOpenOptions}
|
||||||
>
|
>
|
||||||
{tenantIdState}
|
{!isMobile && tenantIdState}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -91,6 +91,7 @@ const Timezones: FC<TimezonesProps> = ({ timezoneState, onChange }) => {
|
||||||
buttonRef={targetRef}
|
buttonRef={targetRef}
|
||||||
placement="bottom-left"
|
placement="bottom-left"
|
||||||
onClose={handleCloseList}
|
onClose={handleCloseList}
|
||||||
|
fullWidth
|
||||||
>
|
>
|
||||||
<div className="vm-timezones-list">
|
<div className="vm-timezones-list">
|
||||||
<div className="vm-timezones-list-header">
|
<div className="vm-timezones-list-header">
|
||||||
|
|
|
@ -46,7 +46,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&-list {
|
&-list {
|
||||||
min-width: 600px;
|
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
background-color: $color-background-block;
|
background-color: $color-background-block;
|
||||||
border-radius: $border-radius-medium;
|
border-radius: $border-radius-medium;
|
||||||
|
|
|
@ -1,12 +1,25 @@
|
||||||
@use "src/styles/variables" as *;
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
.vm-server-configurator {
|
.vm-server-configurator {
|
||||||
display: grid;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $padding-medium;
|
gap: $padding-medium;
|
||||||
width: 600px;
|
width: 600px;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
grid-auto-rows: min-content;
|
||||||
|
align-items: flex-start;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
&__input {
|
&__input {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
&_server {
|
&_server {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -34,4 +47,10 @@
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&_mobile &__footer {
|
||||||
|
align-items: flex-end;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,7 +111,12 @@ const StepConfigurator: FC = () => {
|
||||||
startIcon={<TimelineIcon/>}
|
startIcon={<TimelineIcon/>}
|
||||||
onClick={toggleOpenOptions}
|
onClick={toggleOpenOptions}
|
||||||
>
|
>
|
||||||
STEP {customStep}
|
<p>
|
||||||
|
STEP
|
||||||
|
<p className="vm-step-control__value">
|
||||||
|
{customStep}
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Popper
|
<Popper
|
||||||
|
|
|
@ -8,6 +8,15 @@
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
display: inline;
|
||||||
|
margin-left: 3px;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-popper {
|
&-popper {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: $padding-small;
|
gap: $padding-small;
|
||||||
|
|
|
@ -3,9 +3,12 @@ import "./style.scss";
|
||||||
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
|
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
|
||||||
import { Theme } from "../../../types";
|
import { Theme } from "../../../types";
|
||||||
import Toggle from "../../Main/Toggle/Toggle";
|
import Toggle from "../../Main/Toggle/Toggle";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
const options = Object.values(Theme).map(value => ({ title: value, value }));
|
const options = Object.values(Theme).map(value => ({ title: value, value }));
|
||||||
const ThemeControl = () => {
|
const ThemeControl = () => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
const { theme } = useAppState();
|
const { theme } = useAppState();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
@ -14,11 +17,19 @@ const ThemeControl = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vm-theme-control">
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-theme-control": true,
|
||||||
|
"vm-theme-control_mobile": isMobile
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div className="vm-server-configurator__title">
|
<div className="vm-server-configurator__title">
|
||||||
Theme preferences
|
Theme preferences
|
||||||
</div>
|
</div>
|
||||||
<div className="vm-theme-control__toggle">
|
<div
|
||||||
|
className="vm-theme-control__toggle"
|
||||||
|
key={`${isMobile}`}
|
||||||
|
>
|
||||||
<Toggle
|
<Toggle
|
||||||
options={options}
|
options={options}
|
||||||
value={theme}
|
value={theme}
|
||||||
|
|
|
@ -7,4 +7,8 @@
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&_mobile &__toggle {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import Popper from "../../../Main/Popper/Popper";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||||
|
import useResize from "../../../../hooks/useResize";
|
||||||
|
|
||||||
interface AutoRefreshOption {
|
interface AutoRefreshOption {
|
||||||
seconds: number
|
seconds: number
|
||||||
|
@ -29,6 +30,7 @@ const delayOptions: AutoRefreshOption[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ExecutionControls: FC = () => {
|
export const ExecutionControls: FC = () => {
|
||||||
|
const windowSize = useResize(document.body);
|
||||||
|
|
||||||
const dispatch = useTimeDispatch();
|
const dispatch = useTimeDispatch();
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
|
@ -83,9 +85,11 @@ export const ExecutionControls: FC = () => {
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-execution-controls-buttons": true,
|
"vm-execution-controls-buttons": true,
|
||||||
"vm-header-button": !appModeEnable
|
"vm-header-button": !appModeEnable,
|
||||||
|
"vm-execution-controls-buttons_short": windowSize.width <= 360
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
{windowSize.width > 360 && (
|
||||||
<Tooltip title="Refresh dashboard">
|
<Tooltip title="Refresh dashboard">
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@ -94,6 +98,7 @@ export const ExecutionControls: FC = () => {
|
||||||
startIcon={<RefreshIcon/>}
|
startIcon={<RefreshIcon/>}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
)}
|
||||||
<Tooltip title="Auto-refresh control">
|
<Tooltip title="Auto-refresh control">
|
||||||
<div ref={optionsButtonRef}>
|
<div ref={optionsButtonRef}>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -9,6 +9,10 @@
|
||||||
border-radius: calc($button-radius + 1px);
|
border-radius: calc($button-radius + 1px);
|
||||||
min-width: 107px;
|
min-width: 107px;
|
||||||
|
|
||||||
|
&_short {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
&__arrow {
|
&__arrow {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -5,6 +5,11 @@
|
||||||
grid-template-columns: repeat(2, 230px);
|
grid-template-columns: repeat(2, 230px);
|
||||||
padding: $padding-global 0;
|
padding: $padding-global 0;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
&-left {
|
&-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -12,6 +17,12 @@
|
||||||
border-right: $border-divider;
|
border-right: $border-divider;
|
||||||
padding: 0 $padding-global;
|
padding: 0 $padding-global;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: $border-divider;
|
||||||
|
padding-bottom: $padding-global;
|
||||||
|
}
|
||||||
|
|
||||||
&-inputs {
|
&-inputs {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
@ -5,16 +5,18 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: $padding-small calc($padding-small + 10px);
|
gap: $padding-global calc($padding-small + 10px);
|
||||||
|
|
||||||
&__job {
|
&__job {
|
||||||
flex-grow: 0.5;
|
flex-grow: 1;
|
||||||
min-width: 200px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__instance {
|
&__instance {
|
||||||
|
flex-grow: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__size {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
min-width: 300px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-metrics {
|
&-metrics {
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
.vm-footer {
|
.vm-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: $padding-medium;
|
padding: $padding-medium;
|
||||||
|
@ -21,6 +22,11 @@
|
||||||
|
|
||||||
&__website {
|
&__website {
|
||||||
margin-right: $padding-global;
|
margin-right: $padding-global;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
margin-right: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__link {
|
&__link {
|
||||||
|
@ -30,5 +36,10 @@
|
||||||
&__copyright {
|
&__copyright {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,13 @@ import { useAppState } from "../../../state/common/StateContext";
|
||||||
import HeaderNav from "./HeaderNav/HeaderNav";
|
import HeaderNav from "./HeaderNav/HeaderNav";
|
||||||
import TenantsConfiguration from "../../Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration";
|
import TenantsConfiguration from "../../Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration";
|
||||||
import { useFetchAccountIds } from "../../Configurators/GlobalSettings/TenantsConfiguration/hooks/useFetchAccountIds";
|
import { useFetchAccountIds } from "../../Configurators/GlobalSettings/TenantsConfiguration/hooks/useFetchAccountIds";
|
||||||
|
import useResize from "../../../hooks/useResize";
|
||||||
|
import SidebarHeader from "./SidebarNav/SidebarHeader";
|
||||||
|
|
||||||
const Header: FC = () => {
|
const Header: FC = () => {
|
||||||
|
const windowSize = useResize(document.body);
|
||||||
|
const displaySidebar = useMemo(() => window.innerWidth < 1000, [windowSize]);
|
||||||
|
|
||||||
const { isDarkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
const { accountIds } = useFetchAccountIds();
|
const { accountIds } = useFetchAccountIds();
|
||||||
|
@ -58,6 +63,14 @@ const Header: FC = () => {
|
||||||
})}
|
})}
|
||||||
style={{ background, color }}
|
style={{ background, color }}
|
||||||
>
|
>
|
||||||
|
{displaySidebar ? (
|
||||||
|
<SidebarHeader
|
||||||
|
background={background}
|
||||||
|
color={color}
|
||||||
|
onClickLogo={onClickLogo}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
{!appModeEnable && (
|
{!appModeEnable && (
|
||||||
<div
|
<div
|
||||||
className="vm-header-logo"
|
className="vm-header-logo"
|
||||||
|
@ -71,14 +84,16 @@ const Header: FC = () => {
|
||||||
color={color}
|
color={color}
|
||||||
background={background}
|
background={background}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className="vm-header__settings">
|
<div className="vm-header__settings">
|
||||||
{headerSetup?.tenant && <TenantsConfiguration accountIds={accountIds}/>}
|
{headerSetup?.tenant && <TenantsConfiguration accountIds={accountIds}/>}
|
||||||
{headerSetup?.stepControl && <StepConfigurator/>}
|
{headerSetup?.stepControl && <StepConfigurator/>}
|
||||||
{headerSetup?.timeSelector && <TimeSelector/>}
|
{headerSetup?.timeSelector && <TimeSelector/>}
|
||||||
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
|
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
|
||||||
{headerSetup?.executionControls && <ExecutionControls/>}
|
{headerSetup?.executionControls && <ExecutionControls/>}
|
||||||
<GlobalSettings/>
|
{!displaySidebar && <GlobalSettings/>}
|
||||||
<ShortcutKeys/>
|
{!displaySidebar && <ShortcutKeys/>}
|
||||||
</div>
|
</div>
|
||||||
</header>;
|
</header>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,13 +7,15 @@ import { useEffect } from "react";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import NavItem from "./NavItem";
|
import NavItem from "./NavItem";
|
||||||
import NavSubItem from "./NavSubItem";
|
import NavSubItem from "./NavSubItem";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
interface HeaderNavProps {
|
interface HeaderNavProps {
|
||||||
color: string
|
color: string
|
||||||
background: string
|
background: string
|
||||||
|
direction?: "row" | "column"
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderNav: FC<HeaderNavProps> = ({ color, background }) => {
|
const HeaderNav: FC<HeaderNavProps> = ({ color, background, direction }) => {
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
const { dashboardsSettings } = useDashboardsState();
|
const { dashboardsSettings } = useDashboardsState();
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
@ -59,7 +61,12 @@ const HeaderNav: FC<HeaderNavProps> = ({ color, background }) => {
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="vm-header-nav">
|
<nav
|
||||||
|
className={classNames({
|
||||||
|
"vm-header-nav": true,
|
||||||
|
[`vm-header-nav_${direction}`]: direction
|
||||||
|
})}
|
||||||
|
>
|
||||||
{menu.map(m => (
|
{menu.map(m => (
|
||||||
m.submenu
|
m.submenu
|
||||||
? (
|
? (
|
||||||
|
@ -70,6 +77,7 @@ const HeaderNav: FC<HeaderNavProps> = ({ color, background }) => {
|
||||||
submenu={m.submenu}
|
submenu={m.submenu}
|
||||||
color={color}
|
color={color}
|
||||||
background={background}
|
background={background}
|
||||||
|
direction={direction}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
|
|
|
@ -12,6 +12,7 @@ interface NavItemProps {
|
||||||
submenu: {label: string | undefined, value: string}[],
|
submenu: {label: string | undefined, value: string}[],
|
||||||
color?: string
|
color?: string
|
||||||
background?: string
|
background?: string
|
||||||
|
direction?: "row" | "column"
|
||||||
}
|
}
|
||||||
|
|
||||||
const NavSubItem: FC<NavItemProps> = ({
|
const NavSubItem: FC<NavItemProps> = ({
|
||||||
|
@ -19,7 +20,8 @@ const NavSubItem: FC<NavItemProps> = ({
|
||||||
label,
|
label,
|
||||||
color,
|
color,
|
||||||
background,
|
background,
|
||||||
submenu
|
submenu,
|
||||||
|
direction
|
||||||
}) => {
|
}) => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
@ -50,6 +52,21 @@ const NavSubItem: FC<NavItemProps> = ({
|
||||||
handleCloseSubmenu();
|
handleCloseSubmenu();
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
|
if (direction === "column") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{submenu.map(sm => (
|
||||||
|
<NavItem
|
||||||
|
key={sm.value}
|
||||||
|
activeMenu={activeMenu}
|
||||||
|
value={sm.value}
|
||||||
|
label={sm.label || ""}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
|
|
|
@ -8,6 +8,20 @@
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
|
&_column {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: $padding-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_column &-item {
|
||||||
|
padding: $padding-global 0;
|
||||||
|
|
||||||
|
&_sub {
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: $padding-global $padding-small;
|
padding: $padding-global $padding-small;
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
import React, { FC, useEffect, useRef, useState } from "preact/compat";
|
||||||
|
import GlobalSettings from "../../../Configurators/GlobalSettings/GlobalSettings";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import ShortcutKeys from "../../../Main/ShortcutKeys/ShortcutKeys";
|
||||||
|
import { LogoFullIcon } from "../../../Main/Icons";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import HeaderNav from "../HeaderNav/HeaderNav";
|
||||||
|
import useClickOutside from "../../../../hooks/useClickOutside";
|
||||||
|
import MenuBurger from "../../../Main/MenuBurger/MenuBurger";
|
||||||
|
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||||
|
import "./style.scss";
|
||||||
|
|
||||||
|
interface SidebarHeaderProps {
|
||||||
|
background: string
|
||||||
|
color: string
|
||||||
|
onClickLogo: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarHeader: FC<SidebarHeaderProps> = ({
|
||||||
|
background,
|
||||||
|
color,
|
||||||
|
onClickLogo,
|
||||||
|
}) => {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
|
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [openMenu, setOpenMenu] = useState(false);
|
||||||
|
|
||||||
|
const handleToggleMenu = () => {
|
||||||
|
setOpenMenu(prev => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseMenu = () => {
|
||||||
|
setOpenMenu(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(handleCloseMenu, [pathname]);
|
||||||
|
|
||||||
|
useClickOutside(sidebarRef, handleCloseMenu);
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className="vm-header-sidebar"
|
||||||
|
ref={sidebarRef}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-header-sidebar-button": true,
|
||||||
|
"vm-header-sidebar-button_open": openMenu
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<MenuBurger
|
||||||
|
open={openMenu}
|
||||||
|
onClick={handleToggleMenu}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
"vm-header-sidebar-menu": true,
|
||||||
|
"vm-header-sidebar-menu_open": openMenu
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="vm-header-sidebar-menu__logo"
|
||||||
|
onClick={onClickLogo}
|
||||||
|
style={{ color }}
|
||||||
|
>
|
||||||
|
<LogoFullIcon/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<HeaderNav
|
||||||
|
color={color}
|
||||||
|
background={background}
|
||||||
|
direction="column"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="vm-header-sidebar-menu-settings">
|
||||||
|
<GlobalSettings showTitle={true}/>
|
||||||
|
{!isMobile && <ShortcutKeys showTitle={true}/>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SidebarHeader;
|
|
@ -0,0 +1,58 @@
|
||||||
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
|
.vm-header-sidebar {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
color: inherit;
|
||||||
|
background-color: inherit;
|
||||||
|
|
||||||
|
&-button {
|
||||||
|
position: absolute;
|
||||||
|
left: $padding-global;
|
||||||
|
top: $padding-global;
|
||||||
|
transition: left 300ms cubic-bezier(0.280, 0.840, 0.420, 1);
|
||||||
|
|
||||||
|
&_open {
|
||||||
|
position: fixed;
|
||||||
|
left: calc(182px - $padding-global);
|
||||||
|
z-index: 102;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-menu {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: $padding-global;
|
||||||
|
padding: $padding-global;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
width: 200px;
|
||||||
|
height: 100%;
|
||||||
|
background-color: inherit;
|
||||||
|
z-index: 101;
|
||||||
|
transform-origin: left;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 300ms cubic-bezier(0.280, 0.840, 0.420, 1);
|
||||||
|
box-shadow: $box-shadow-popper;
|
||||||
|
|
||||||
|
&_open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__logo {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-settings {
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
gap: $padding-small;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,12 +7,20 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding: $padding-small $padding-medium;
|
padding: $padding-small $padding-medium;
|
||||||
gap: 0 $padding-large;
|
gap: 0 $padding-large;
|
||||||
|
min-height: 51px;
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
|
|
||||||
&_app {
|
&_app {
|
||||||
padding: $padding-small 0;
|
padding: $padding-small 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
gap: $padding-small;
|
||||||
|
padding: $padding-small;
|
||||||
|
}
|
||||||
|
|
||||||
&_dark {
|
&_dark {
|
||||||
.vm-header-button,
|
.vm-header-button,
|
||||||
button:before,
|
button:before,
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
.vm-container {
|
.vm-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: calc(100vh - var(--scrollbar-height));
|
min-height: calc(($vh * 100) - var(--scrollbar-height));
|
||||||
|
|
||||||
&-body {
|
&-body {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -11,6 +11,10 @@
|
||||||
padding: $padding-medium;
|
padding: $padding-medium;
|
||||||
background-color: $color-background-body;
|
background-color: $color-background-body;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&_app {
|
&_app {
|
||||||
padding: $padding-small 0;
|
padding: $padding-small 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
|
@ -390,7 +390,7 @@ export const QuestionIcon = () => (
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const StorageIcons = () => (
|
export const StorageIcon = () => (
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
@ -400,3 +400,14 @@ export const StorageIcons = () => (
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const MenuIcon = () => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4 18h16c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1s.45 1 1 1zm0-5h16c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1s.45 1 1 1zM3 7c0 .55.45 1 1 1h16c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React from "preact/compat";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import "./style.scss";
|
||||||
|
|
||||||
|
const MenuBurger = ({ open, onClick }: {open: boolean, onClick: () => void}) => (
|
||||||
|
<button
|
||||||
|
className={classNames({
|
||||||
|
"vm-menu-burger": true,
|
||||||
|
"vm-menu-burger_opened": open
|
||||||
|
})}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MenuBurger;
|
133
app/vmui/packages/vmui/src/components/Main/MenuBurger/style.scss
Normal file
133
app/vmui/packages/vmui/src/components/Main/MenuBurger/style.scss
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
|
$width-line: 2px;
|
||||||
|
|
||||||
|
.vm-menu-burger {
|
||||||
|
position: relative;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -6px;
|
||||||
|
top: -6px;
|
||||||
|
width: calc(100% + 12px);
|
||||||
|
height: calc(100% + 12px);
|
||||||
|
background-color: rgba($color-black, 0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: scale(0) translateZ(-2px);
|
||||||
|
transition: transform 140ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&:after {
|
||||||
|
transform: scale(1) translateZ(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: block;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
border-top: $width-line solid #fff;
|
||||||
|
transition: transform 0.3s ease, border-color 0.3s ease;
|
||||||
|
|
||||||
|
&,
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: $width-line;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
top: 0;
|
||||||
|
background: $color-white;
|
||||||
|
animation-duration: 0.6s;
|
||||||
|
animation-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
animation-name: topLineBurger;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
animation-name: bottomLineBurger;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&_opened span {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_opened span:before {
|
||||||
|
animation-name: topLineCross;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_opened span:after {
|
||||||
|
animation-name: bottomLineCross;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes topLineCross {
|
||||||
|
0% {
|
||||||
|
transform: translateY(-7px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
width: 60%;
|
||||||
|
transform: translateY(-2px) translateX(30%) rotate(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bottomLineCross {
|
||||||
|
0% {
|
||||||
|
transform: translateY(3px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
width: 60%;
|
||||||
|
transform: translateY(-2px) translateX(30%) rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes topLineBurger {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0px) rotate(45deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(-7px) rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bottomLineBurger {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0px) rotate(-45deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(3px) rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ import { CloseIcon } from "../Icons";
|
||||||
import Button from "../Button/Button";
|
import Button from "../Button/Button";
|
||||||
import { ReactNode, MouseEvent } from "react";
|
import { ReactNode, MouseEvent } from "react";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
title?: string
|
title?: string
|
||||||
|
@ -12,6 +14,7 @@ interface ModalProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Modal: FC<ModalProps> = ({ title, children, onClose }) => {
|
const Modal: FC<ModalProps> = ({ title, children, onClose }) => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const handleKeyUp = (e: KeyboardEvent) => {
|
const handleKeyUp = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape") onClose();
|
if (e.key === "Escape") onClose();
|
||||||
|
@ -22,16 +25,21 @@ const Modal: FC<ModalProps> = ({ title, children, onClose }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
window.addEventListener("keyup", handleKeyUp);
|
window.addEventListener("keyup", handleKeyUp);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
window.removeEventListener("keyup", handleKeyUp);
|
window.removeEventListener("keyup", handleKeyUp);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return ReactDOM.createPortal((
|
return ReactDOM.createPortal((
|
||||||
<div
|
<div
|
||||||
className="vm-modal"
|
className={classNames({
|
||||||
|
"vm-modal": true,
|
||||||
|
"vm-modal_mobile": isMobile
|
||||||
|
})}
|
||||||
onMouseDown={onClose}
|
onMouseDown={onClose}
|
||||||
>
|
>
|
||||||
<div className="vm-modal-content">
|
<div className="vm-modal-content">
|
||||||
|
|
|
@ -14,12 +14,28 @@ $padding-modal: 22px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgba($color-black, 0.55);
|
background: rgba($color-black, 0.55);
|
||||||
|
|
||||||
|
&_mobile &-content {
|
||||||
|
min-height: calc($vh * 100);
|
||||||
|
max-height: calc($vh * 100);
|
||||||
|
width: 100vw;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
&-body {
|
||||||
|
display: grid;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-content {
|
&-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
align-items: flex-start;
|
||||||
padding: $padding-modal;
|
padding: $padding-modal;
|
||||||
background: $color-background-block;
|
background: $color-background-block;
|
||||||
box-shadow: 0 0 24px rgba($color-black, 0.07);
|
box-shadow: 0 0 24px rgba($color-black, 0.07);
|
||||||
border-radius: $border-radius-small;
|
border-radius: $border-radius-small;
|
||||||
max-height: 90vh;
|
max-height: calc($vh * 90);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
&-header {
|
&-header {
|
||||||
|
@ -44,6 +60,8 @@ $padding-modal: 22px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-body {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,12 +99,20 @@ const Popper: FC<PopperProps> = ({
|
||||||
if (isOverflowLeft) position.left = buttonPos.left + offsetLeft;
|
if (isOverflowLeft) position.left = buttonPos.left + offsetLeft;
|
||||||
|
|
||||||
if (fullWidth) position.width = `${buttonPos.width}px`;
|
if (fullWidth) position.width = `${buttonPos.width}px`;
|
||||||
|
if (position.top < 0) position.top = 20;
|
||||||
|
|
||||||
return position;
|
return position;
|
||||||
},[buttonRef, placement, isOpen, children, fullWidth]);
|
},[buttonRef, placement, isOpen, children, fullWidth]);
|
||||||
|
|
||||||
if (clickOutside) useClickOutside(popperRef, () => setIsOpen(false), buttonRef);
|
if (clickOutside) useClickOutside(popperRef, () => setIsOpen(false), buttonRef);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!popperRef.current || !isOpen) return;
|
||||||
|
const { right, width } = popperRef.current.getBoundingClientRect();
|
||||||
|
if (right > window.innerWidth) popperRef.current.style.left = `${window.innerWidth - 20 -width}px`;
|
||||||
|
}, [isOpen, popperRef]);
|
||||||
|
|
||||||
|
|
||||||
const popperClasses = classNames({
|
const popperClasses = classNames({
|
||||||
"vm-popper": true,
|
"vm-popper": true,
|
||||||
"vm-popper_open": isOpen,
|
"vm-popper_open": isOpen,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { FC, useState } from "preact/compat";
|
import React, { FC, useState } from "preact/compat";
|
||||||
import { isMacOs } from "../../../utils/detect-os";
|
import { isMacOs } from "../../../utils/detect-device";
|
||||||
import { getAppModeEnable } from "../../../utils/app-mode";
|
import { getAppModeEnable } from "../../../utils/app-mode";
|
||||||
import Button from "../Button/Button";
|
import Button from "../Button/Button";
|
||||||
import { KeyboardIcon } from "../Icons";
|
import { KeyboardIcon } from "../Icons";
|
||||||
|
@ -69,7 +69,9 @@ const keyList = [
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const ShortcutKeys: FC = () => {
|
const title = "Shortcut keys";
|
||||||
|
|
||||||
|
const ShortcutKeys: FC<{showTitle?: boolean}> = ({ showTitle }) => {
|
||||||
const [openList, setOpenList] = useState(false);
|
const [openList, setOpenList] = useState(false);
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
|
|
||||||
|
@ -83,7 +85,8 @@ const ShortcutKeys: FC = () => {
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title="Shortcut keys"
|
open={showTitle === true ? false : undefined}
|
||||||
|
title={title}
|
||||||
placement="bottom-center"
|
placement="bottom-center"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
@ -92,7 +95,9 @@ const ShortcutKeys: FC = () => {
|
||||||
color="primary"
|
color="primary"
|
||||||
startIcon={<KeyboardIcon/>}
|
startIcon={<KeyboardIcon/>}
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
/>
|
>
|
||||||
|
{showTitle && title}
|
||||||
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{openList && (
|
{openList && (
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
.vm-shortcuts {
|
.vm-shortcuts {
|
||||||
min-width: 400px;
|
min-width: 400px;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
&-section {
|
&-section {
|
||||||
margin-bottom: $padding-medium;
|
margin-bottom: $padding-medium;
|
||||||
|
|
||||||
|
@ -17,12 +21,20 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: $padding-global;
|
gap: $padding-global;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
gap: $padding-medium;
|
||||||
|
}
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 210px 1fr;
|
grid-template-columns: 210px 1fr;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $padding-small;
|
gap: $padding-small;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
&__key {
|
&__key {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -39,6 +39,7 @@ export const ThemeProvider: FC<ThemeProviderProps> = ({ onLoaded }) => {
|
||||||
const { clientWidth, clientHeight } = document.documentElement;
|
const { clientWidth, clientHeight } = document.documentElement;
|
||||||
setCssVariable("scrollbar-width", `${innerWidth - clientWidth}px`);
|
setCssVariable("scrollbar-width", `${innerWidth - clientWidth}px`);
|
||||||
setCssVariable("scrollbar-height", `${innerHeight - clientHeight}px`);
|
setCssVariable("scrollbar-height", `${innerHeight - clientHeight}px`);
|
||||||
|
setCssVariable("vh", `${innerHeight * 0.01}px`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setContrastText = () => {
|
const setContrastText = () => {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import ReactDOM from "react-dom";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { ExoticComponent } from "react";
|
import { ExoticComponent } from "react";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
interface TooltipProps {
|
interface TooltipProps {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
|
@ -19,6 +20,7 @@ const Tooltip: FC<TooltipProps> = ({
|
||||||
placement = "bottom-center",
|
placement = "bottom-center",
|
||||||
offset = { top: 6, left: 0 }
|
offset = { top: 6, left: 0 }
|
||||||
}) => {
|
}) => {
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [popperSize, setPopperSize] = useState({ width: 0, height: 0 });
|
const [popperSize, setPopperSize] = useState({ width: 0, height: 0 });
|
||||||
|
@ -121,7 +123,7 @@ const Tooltip: FC<TooltipProps> = ({
|
||||||
{children}
|
{children}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|
||||||
{isOpen && ReactDOM.createPortal((
|
{!isMobile && isOpen && ReactDOM.createPortal((
|
||||||
<div
|
<div
|
||||||
className="vm-tooltip"
|
className="vm-tooltip"
|
||||||
ref={popperRef}
|
ref={popperRef}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
@use "src/styles/variables" as *;
|
@use "src/styles/variables" as *;
|
||||||
$all-paddings: $padding-medium * 4;
|
|
||||||
|
|
||||||
.vm-graph-view {
|
.vm-graph-view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&_full-width {
|
&_full-width {
|
||||||
width: calc(100vw - $all-paddings - var(--scrollbar-width));
|
width: calc(100vw - ($padding-medium * 4) - var(--scrollbar-width));
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: calc(100vw - ($padding-medium * 2) - var(--scrollbar-width));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
16
app/vmui/packages/vmui/src/hooks/useDeviceDetect.ts
Normal file
16
app/vmui/packages/vmui/src/hooks/useDeviceDetect.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { isMobileAgent } from "../utils/detect-device";
|
||||||
|
import useResize from "./useResize";
|
||||||
|
|
||||||
|
export default function useDeviceDetect() {
|
||||||
|
const windowSize = useResize(document.body);
|
||||||
|
const [isMobile, setMobile] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mobileAgent = isMobileAgent();
|
||||||
|
const smallWidth = window.innerWidth < 500;
|
||||||
|
setMobile(mobileAgent || smallWidth);
|
||||||
|
}, [windowSize]);
|
||||||
|
|
||||||
|
return { isMobile };
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ const useResize = (node: HTMLElement | null): {width: number, height: number} =>
|
||||||
return () => {
|
return () => {
|
||||||
if (node) observer.unobserve(node);
|
if (node) observer.unobserve(node);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [node]);
|
||||||
return windowSize;
|
return windowSize;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -118,6 +118,7 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
|
||||||
at <b>{date}</b>{match && <span> for series selector <b>{match}</b></span>}.
|
at <b>{date}</b>{match && <span> for series selector <b>{match}</b></span>}.
|
||||||
Show top {topN} entries per table.
|
Show top {topN} entries per table.
|
||||||
</div>
|
</div>
|
||||||
|
<div className="vm-cardinality-configurator-bottom__docs">
|
||||||
<a
|
<a
|
||||||
className="vm-link vm-link_with-icon"
|
className="vm-link vm-link_with-icon"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -136,6 +137,7 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
|
||||||
<QuestionIcon/>
|
<QuestionIcon/>
|
||||||
Example of using
|
Example of using
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<PlayIcon/>}
|
startIcon={<PlayIcon/>}
|
||||||
onClick={onRunQuery}
|
onClick={onRunQuery}
|
||||||
|
|
|
@ -12,6 +12,10 @@
|
||||||
gap: 0 $padding-medium;
|
gap: 0 $padding-medium;
|
||||||
|
|
||||||
&__query {
|
&__query {
|
||||||
|
flex-grow: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,13 +23,21 @@
|
||||||
&-additional {
|
&-additional {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-bottom: $padding-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-bottom {
|
&-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $padding-global;
|
gap: $padding-global;
|
||||||
|
|
||||||
|
&__docs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $padding-global;
|
||||||
|
}
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
font-size: $font-size;
|
font-size: $font-size;
|
||||||
|
@ -34,5 +46,9 @@
|
||||||
a {
|
a {
|
||||||
color: $color-text-secondary;
|
color: $color-text-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 0 0 0 auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,10 @@ const MetricsContent: FC<MetricsProperties> = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref={chartContainer}>
|
<div
|
||||||
|
ref={chartContainer}
|
||||||
|
className="vm-metrics-content__table"
|
||||||
|
>
|
||||||
{activeTab === 0 && (
|
{activeTab === 0 && (
|
||||||
<EnhancedTable
|
<EnhancedTable
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
|
|
@ -2,6 +2,20 @@
|
||||||
|
|
||||||
.vm-metrics-content {
|
.vm-metrics-content {
|
||||||
&-header {
|
&-header {
|
||||||
margin: -$padding-medium 0-$padding-medium $padding-medium;
|
margin: -$padding-medium 0-$padding-medium 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__table {
|
||||||
|
padding-top: $padding-medium;
|
||||||
|
width: calc(100vw - ($padding-medium * 4) - var(--scrollbar-width));
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: calc(100vw - ($padding-medium * 2) - var(--scrollbar-width));
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-table-cell_header {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,14 +28,27 @@
|
||||||
|
|
||||||
&-settings {
|
&-settings {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: $padding-medium;
|
gap: $padding-medium;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
&__buttons {
|
&__buttons {
|
||||||
|
flex-grow: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, auto);
|
grid-template-columns: repeat(2, auto);
|
||||||
gap: $padding-small;
|
gap: $padding-small;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,10 @@
|
||||||
gap: $padding-global;
|
gap: $padding-global;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
&-panel {
|
&-panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: $border-radius-medium;
|
border-radius: $border-radius-medium;
|
||||||
|
|
|
@ -5,6 +5,10 @@
|
||||||
gap: $padding-global;
|
gap: $padding-global;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: $padding-medium 0;
|
||||||
|
}
|
||||||
|
|
||||||
&-tabs.vm-block {
|
&-tabs.vm-block {
|
||||||
padding: $padding-global;
|
padding: $padding-global;
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ const TopQueryPanel: FC<TopQueryPanelProps> = ({ rows, title, columns, defaultOr
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="vm-top-queries-panel__table">
|
||||||
{activeTab === 0 && (
|
{activeTab === 0 && (
|
||||||
<TopQueryTable
|
<TopQueryTable
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
|
|
@ -2,6 +2,20 @@
|
||||||
|
|
||||||
.vm-top-queries-panel {
|
.vm-top-queries-panel {
|
||||||
&-header {
|
&-header {
|
||||||
margin: -$padding-medium 0-$padding-medium $padding-medium;
|
margin: -$padding-medium 0-$padding-medium 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__table {
|
||||||
|
padding-top: $padding-medium;
|
||||||
|
width: calc(100vw - ($padding-medium * 4) - var(--scrollbar-width));
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: calc(100vw - ($padding-medium * 2) - var(--scrollbar-width));
|
||||||
|
}
|
||||||
|
|
||||||
|
.vm-table-cell_header {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,8 @@ const Index: FC = () => {
|
||||||
{loading && <Spinner containerStyles={{ height: "500px" }}/>}
|
{loading && <Spinner containerStyles={{ height: "500px" }}/>}
|
||||||
|
|
||||||
<div className="vm-top-queries-controls vm-block">
|
<div className="vm-top-queries-controls vm-block">
|
||||||
<div className="vm-top-queries-controls__fields">
|
<div className="vm-top-queries-controls-fields">
|
||||||
|
<div className="vm-top-queries-controls-fields__item">
|
||||||
<TextField
|
<TextField
|
||||||
label="Max lifetime"
|
label="Max lifetime"
|
||||||
value={maxLifetime}
|
value={maxLifetime}
|
||||||
|
@ -80,6 +81,8 @@ const Index: FC = () => {
|
||||||
onChange={onMaxLifetimeChange}
|
onChange={onMaxLifetimeChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="vm-top-queries-controls-fields__item">
|
||||||
<TextField
|
<TextField
|
||||||
label="Number of returned queries"
|
label="Number of returned queries"
|
||||||
type="number"
|
type="number"
|
||||||
|
@ -89,6 +92,7 @@ const Index: FC = () => {
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="vm-top-queries-controls-bottom">
|
<div className="vm-top-queries-controls-bottom">
|
||||||
<div className="vm-top-queries-controls-bottom__info">
|
<div className="vm-top-queries-controls-bottom__info">
|
||||||
VictoriaMetrics tracks the last
|
VictoriaMetrics tracks the last
|
||||||
|
|
|
@ -9,10 +9,16 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: $padding-small;
|
gap: $padding-small;
|
||||||
|
|
||||||
&__fields {
|
&-fields {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr auto;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: $padding-medium;
|
gap: $padding-medium;
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-bottom {
|
&-bottom {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { ErrorTypes } from "../../../types";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useSnack } from "../../../contexts/Snackbar";
|
import { useSnack } from "../../../contexts/Snackbar";
|
||||||
import { CopyIcon, RestartIcon } from "../../../components/Main/Icons";
|
import { CopyIcon, RestartIcon } from "../../../components/Main/Icons";
|
||||||
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
|
|
||||||
interface JsonFormProps {
|
interface JsonFormProps {
|
||||||
defaultJson?: string
|
defaultJson?: string
|
||||||
|
@ -28,6 +29,7 @@ const JsonForm: FC<JsonFormProps> = ({
|
||||||
onUpload,
|
onUpload,
|
||||||
}) => {
|
}) => {
|
||||||
const { showInfoMessage } = useSnack();
|
const { showInfoMessage } = useSnack();
|
||||||
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const [json, setJson] = useState(defaultJson);
|
const [json, setJson] = useState(defaultJson);
|
||||||
const [title, setTitle] = useState(defaultTile);
|
const [title, setTitle] = useState(defaultTile);
|
||||||
|
@ -77,7 +79,8 @@ const JsonForm: FC<JsonFormProps> = ({
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-json-form": true,
|
"vm-json-form": true,
|
||||||
"vm-json-form_one-field": !displayTitle
|
"vm-json-form_one-field": !displayTitle,
|
||||||
|
"vm-json-form_mobile": isMobile
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{displayTitle && (
|
{displayTitle && (
|
||||||
|
|
|
@ -2,15 +2,21 @@
|
||||||
|
|
||||||
.vm-json-form {
|
.vm-json-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto calc(70vh - 78px - ($padding-medium*3)) auto;
|
grid-template-rows: auto calc(($vh * 70) - 78px - ($padding-medium*3)) auto;
|
||||||
gap: $padding-global;
|
gap: $padding-global;
|
||||||
width: 70vw;
|
width: 70vw;
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
max-height: 900px;
|
max-height: 900px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
&_mobile {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
&_one-field {
|
&_one-field {
|
||||||
grid-template-rows: calc(70vh - 78px - ($padding-medium*3)) auto;
|
grid-template-rows: calc(($vh * 70) - 78px - ($padding-medium*3)) auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm-text-field_textarea {
|
.vm-text-field_textarea {
|
||||||
|
@ -29,6 +35,14 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: $padding-small;
|
gap: $padding-small;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__controls {
|
&__controls {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -36,10 +50,22 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: $padding-small;
|
gap: $padding-small;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
&_right {
|
&_right {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 90px);
|
grid-template-columns: repeat(2, 90px);
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,12 @@
|
||||||
.vm-trace-page {
|
.vm-trace-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: $padding-global;
|
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: $padding-medium 0;
|
||||||
|
}
|
||||||
|
|
||||||
&-controls {
|
&-controls {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
|
@ -21,6 +24,11 @@
|
||||||
gap: $padding-global;
|
gap: $padding-global;
|
||||||
margin-bottom: $padding-medium;
|
margin-bottom: $padding-medium;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding: 0 $padding-medium;
|
||||||
|
}
|
||||||
|
|
||||||
&-errors {
|
&-errors {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
@ -28,6 +36,10 @@
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: $padding-medium;
|
gap: $padding-medium;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
@ -16,9 +16,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&_header {
|
&_header {
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&_selected {
|
&_selected {
|
||||||
|
|
|
@ -13,12 +13,13 @@ html, body, #root {
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
overflow: scroll;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
cursor: inherit;
|
cursor: inherit;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
|
|
@ -61,3 +61,4 @@ $box-shadow: var(--box-shadow);
|
||||||
$box-shadow-popper: var(--box-shadow-popper);
|
$box-shadow-popper: var(--box-shadow-popper);
|
||||||
|
|
||||||
$color-hover-black: var(--color-hover-black);
|
$color-hover-black: var(--color-hover-black);
|
||||||
|
$vh: var(--vh);
|
||||||
|
|
29
app/vmui/packages/vmui/src/utils/detect-device.ts
Normal file
29
app/vmui/packages/vmui/src/utils/detect-device.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
const desktopOs = {
|
||||||
|
windows: "Windows",
|
||||||
|
mac: "Mac OS",
|
||||||
|
linux: "Linux"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOs = () => {
|
||||||
|
return Object.values(desktopOs).find(os => navigator.userAgent.indexOf(os) >= 0) || "unknown";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isMacOs = () => {
|
||||||
|
return getOs() === desktopOs.mac;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isMobileAgent = () => {
|
||||||
|
const mobileUserAgents = [
|
||||||
|
"Android",
|
||||||
|
"webOS",
|
||||||
|
"iPhone",
|
||||||
|
"iPad",
|
||||||
|
"iPod",
|
||||||
|
"BlackBerry",
|
||||||
|
"Windows Phone",
|
||||||
|
];
|
||||||
|
|
||||||
|
// check for common mobile user agents
|
||||||
|
const matches = mobileUserAgents.map(m => navigator.userAgent.match(new RegExp(m, "i")));
|
||||||
|
return matches.some(m => m);
|
||||||
|
};
|
|
@ -1,13 +0,0 @@
|
||||||
const desktopOs = {
|
|
||||||
windows: "Windows",
|
|
||||||
mac: "Mac OS",
|
|
||||||
linux: "Linux"
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getOs = () : string => {
|
|
||||||
return Object.values(desktopOs).find(os => navigator.userAgent.indexOf(os) >= 0) || "unknown";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isMacOs = (): boolean => {
|
|
||||||
return getOs() === desktopOs.mac;
|
|
||||||
};
|
|
|
@ -145,7 +145,10 @@ export const checkDurationLimit = (dur: string): string => {
|
||||||
return dur;
|
return dur;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dateFromSeconds = (epochTimeInSeconds: number): Date => dayjs(epochTimeInSeconds * 1000).toDate();
|
export const dateFromSeconds = (epochTimeInSeconds: number): Date => {
|
||||||
|
const date = dayjs(epochTimeInSeconds * 1000);
|
||||||
|
return date.isValid() ? date.toDate() : new Date();
|
||||||
|
};
|
||||||
|
|
||||||
const getYesterday = () => dayjs().tz().subtract(1, "day").endOf("day").toDate();
|
const getYesterday = () => dayjs().tz().subtract(1, "day").endOf("day").toDate();
|
||||||
const getToday = () => dayjs().tz().endOf("day").toDate();
|
const getToday = () => dayjs().tz().endOf("day").toDate();
|
||||||
|
|
|
@ -2,23 +2,33 @@ import { DragArgs } from "./types";
|
||||||
|
|
||||||
export const dragChart = ({ e, factor = 0.85, u, setPanning, setPlotScale }: DragArgs): void => {
|
export const dragChart = ({ e, factor = 0.85, u, setPanning, setPlotScale }: DragArgs): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
const isMouseEvent = e instanceof MouseEvent;
|
||||||
|
|
||||||
setPanning(true);
|
setPanning(true);
|
||||||
const leftStart = e.clientX;
|
const leftStart = isMouseEvent ? e.clientX : e.touches[0].clientX;
|
||||||
const xUnitsPerPx = u.posToVal(1, "x") - u.posToVal(0, "x");
|
const xUnitsPerPx = u.posToVal(1, "x") - u.posToVal(0, "x");
|
||||||
const scXMin = u.scales.x.min || 0;
|
const scXMin = u.scales.x.min || 0;
|
||||||
const scXMax = u.scales.x.max || 0;
|
const scXMax = u.scales.x.max || 0;
|
||||||
|
|
||||||
const mouseMove = (e: MouseEvent) => {
|
const mouseMove = (e: MouseEvent | TouchEvent) => {
|
||||||
|
const isMouseEvent = e instanceof MouseEvent;
|
||||||
|
if (!isMouseEvent && e.touches.length > 1) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const dx = xUnitsPerPx * ((e.clientX - leftStart) * factor);
|
|
||||||
|
const clientX = isMouseEvent ? e.clientX : e.touches[0].clientX;
|
||||||
|
const dx = xUnitsPerPx * ((clientX - leftStart) * factor);
|
||||||
setPlotScale({ u, min: scXMin - dx, max: scXMax - dx });
|
setPlotScale({ u, min: scXMin - dx, max: scXMax - dx });
|
||||||
};
|
};
|
||||||
const mouseUp = () => {
|
const mouseUp = () => {
|
||||||
setPanning(false);
|
setPanning(false);
|
||||||
document.removeEventListener("mousemove", mouseMove);
|
document.removeEventListener("mousemove", mouseMove);
|
||||||
document.removeEventListener("mouseup", mouseUp);
|
document.removeEventListener("mouseup", mouseUp);
|
||||||
|
document.removeEventListener("touchmove", mouseMove);
|
||||||
|
document.removeEventListener("touchend", mouseUp);
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("mousemove", mouseMove);
|
document.addEventListener("mousemove", mouseMove);
|
||||||
document.addEventListener("mouseup", mouseUp);
|
document.addEventListener("mouseup", mouseUp);
|
||||||
|
document.addEventListener("touchmove", mouseMove);
|
||||||
|
document.addEventListener("touchend", mouseUp);
|
||||||
};
|
};
|
||||||
|
|
|
@ -79,7 +79,7 @@ export const sizeAxis = (u: uPlot, values: string[], axisIdx: number, cycleNum:
|
||||||
let axisSize = 6 + (axis?.ticks?.size || 0) + (axis.gap || 0);
|
let axisSize = 6 + (axis?.ticks?.size || 0) + (axis.gap || 0);
|
||||||
|
|
||||||
const longestVal = (values ?? []).reduce((acc, val) => val.length > acc.length ? val : acc, "");
|
const longestVal = (values ?? []).reduce((acc, val) => val.length > acc.length ? val : acc, "");
|
||||||
if (longestVal != "") axisSize += getTextWidth(longestVal, u.ctx.font);
|
if (longestVal != "") axisSize += getTextWidth(longestVal, "10px Arial");
|
||||||
|
|
||||||
return Math.ceil(axisSize);
|
return Math.ceil(axisSize);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import uPlot from "uplot";
|
import uPlot from "uplot";
|
||||||
import {getCssVariable} from "../theme";
|
import {getCssVariable} from "../theme";
|
||||||
|
import {sizeAxis} from "./helpers";
|
||||||
|
|
||||||
export const seriesBarsPlugin = (opts) => {
|
export const seriesBarsPlugin = (opts) => {
|
||||||
let pxRatio;
|
let pxRatio;
|
||||||
|
@ -262,7 +263,9 @@ export const seriesBarsPlugin = (opts) => {
|
||||||
},
|
},
|
||||||
values: u => u.data[0],
|
values: u => u.data[0],
|
||||||
gap: 15,
|
gap: 15,
|
||||||
size: ori === 0 ? 40 : 150,
|
size: sizeAxis,
|
||||||
|
stroke: getCssVariable("color-text"),
|
||||||
|
font: "10px Arial",
|
||||||
labelSize: 20,
|
labelSize: 20,
|
||||||
grid: {show: false},
|
grid: {show: false},
|
||||||
ticks: {show: false},
|
ticks: {show: false},
|
||||||
|
|
|
@ -8,7 +8,7 @@ export interface HideSeriesArgs {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DragArgs {
|
export interface DragArgs {
|
||||||
e: MouseEvent,
|
e: MouseEvent | TouchEvent,
|
||||||
u: uPlot,
|
u: uPlot,
|
||||||
factor: number,
|
factor: number,
|
||||||
setPanning: (enable: boolean) => void,
|
setPanning: (enable: boolean) => void,
|
||||||
|
|
Loading…
Reference in a new issue