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>
|
||||
<meta charset="utf-8" />
|
||||
<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="description"
|
||||
|
|
|
@ -100,10 +100,15 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
|||
const overflowX = leftOnChart + tooltipWidth >= width ? tooltipWidth + (2 * margin) : 0;
|
||||
const overflowY = topOnChart + tooltipHeight >= height ? tooltipHeight + (2 * margin) : 0;
|
||||
|
||||
setPosition({
|
||||
const position = {
|
||||
top: topOnChart + tooltipOffset.top + margin - overflowY,
|
||||
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]);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
grid-gap: $padding-small;
|
||||
align-items: start;
|
||||
justify-content: start;
|
||||
padding: $padding-small $padding-large $padding-small $padding-small;
|
||||
padding: $padding-small;
|
||||
background-color: $color-background-block;
|
||||
cursor: pointer;
|
||||
transition: 0.2s ease;
|
||||
|
@ -30,9 +30,9 @@
|
|||
|
||||
&-info {
|
||||
font-weight: normal;
|
||||
word-break: break-all;
|
||||
|
||||
&__label {
|
||||
|
||||
}
|
||||
|
||||
&__free-fields {
|
||||
|
|
|
@ -55,6 +55,7 @@ const LineChart: FC<LineChartProps> = ({
|
|||
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
|
||||
const [yRange, setYRange] = useState([0, 1]);
|
||||
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
||||
const [startTouchDistance, setStartTouchDistance] = useState(0);
|
||||
const layoutSize = useResize(container);
|
||||
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
@ -84,6 +85,7 @@ const LineChart: FC<LineChartProps> = ({
|
|||
left: parseFloat(u.over.style.left),
|
||||
top: parseFloat(u.over.style.top)
|
||||
});
|
||||
|
||||
u.over.addEventListener("mousedown", e => {
|
||||
const { ctrlKey, metaKey, button } = e;
|
||||
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 => {
|
||||
if (!e.ctrlKey && !e.metaKey) return;
|
||||
e.preventDefault();
|
||||
|
@ -235,6 +241,47 @@ const LineChart: FC<LineChartProps> = ({
|
|||
};
|
||||
}, [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.xRange), [xRange]);
|
||||
useEffect(() => updateChart(typeChartUpdate.yRange), [yaxis]);
|
||||
|
@ -256,6 +303,10 @@ const LineChart: FC<LineChartProps> = ({
|
|||
"vm-line-chart": true,
|
||||
"vm-line-chart_panning": isPanning
|
||||
})}
|
||||
style={{
|
||||
minWidth: `${layoutSize.width || 400}px`,
|
||||
minHeight: `${height || 500}px`
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="vm-line-chart__u-plot"
|
||||
|
|
|
@ -14,10 +14,12 @@ import classNames from "classnames";
|
|||
import Timezones from "./Timezones/Timezones";
|
||||
import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import ThemeControl from "../ThemeControl/ThemeControl";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
|
||||
const title = "Settings";
|
||||
|
||||
const GlobalSettings: FC = () => {
|
||||
const GlobalSettings: FC<{showTitle?: boolean}> = ({ showTitle }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { serverUrl: stateServerUrl } = useAppState();
|
||||
|
@ -49,7 +51,10 @@ const GlobalSettings: FC = () => {
|
|||
}, [stateServerUrl]);
|
||||
|
||||
return <>
|
||||
<Tooltip title={title}>
|
||||
<Tooltip
|
||||
open={showTitle === true ? false : undefined}
|
||||
title={title}
|
||||
>
|
||||
<Button
|
||||
className={classNames({
|
||||
"vm-header-button": !appModeEnable
|
||||
|
@ -58,14 +63,21 @@ const GlobalSettings: FC = () => {
|
|||
color="primary"
|
||||
startIcon={<SettingsIcon/>}
|
||||
onClick={handleOpen}
|
||||
/>
|
||||
>
|
||||
{showTitle && title}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{open && (
|
||||
<Modal
|
||||
title={title}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<div className="vm-server-configurator">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-server-configurator": true,
|
||||
"vm-server-configurator_mobile": isMobile
|
||||
})}
|
||||
>
|
||||
{!appModeEnable && (
|
||||
<div className="vm-server-configurator__input">
|
||||
<ServerConfigurator
|
||||
|
|
|
@ -70,15 +70,16 @@ const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , on
|
|||
</div>
|
||||
<div className="vm-limits-configurator__inputs">
|
||||
{fields.map(f => (
|
||||
<TextField
|
||||
key={f.type}
|
||||
label={f.label}
|
||||
value={limits[f.type]}
|
||||
error={error[f.type]}
|
||||
onChange={createChangeHandler(f.type)}
|
||||
onEnter={onEnter}
|
||||
type="number"
|
||||
/>
|
||||
<div key={f.type}>
|
||||
<TextField
|
||||
label={f.label}
|
||||
value={limits[f.type]}
|
||||
error={error[f.type]}
|
||||
onChange={createChangeHandler(f.type)}
|
||||
onEnter={onEnter}
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,10 +12,14 @@
|
|||
}
|
||||
|
||||
&__inputs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $padding-global;
|
||||
|
||||
div {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC, useState, useRef, useEffect, useMemo } from "preact/compat";
|
||||
import { useAppDispatch, useAppState } from "../../../../state/common/StateContext";
|
||||
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 "./style.scss";
|
||||
import { replaceTenantId } from "../../../../utils/default-server-url";
|
||||
|
@ -9,9 +9,11 @@ import classNames from "classnames";
|
|||
import Popper from "../../../Main/Popper/Popper";
|
||||
import { getAppModeEnable } from "../../../../utils/app-mode";
|
||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||
|
||||
const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const { tenantId: tenantIdState, serverUrl } = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
|
@ -71,8 +73,8 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
|
|||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
startIcon={<StorageIcons/>}
|
||||
endIcon={(
|
||||
startIcon={<StorageIcon/>}
|
||||
endIcon={!isMobile ? (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons__arrow": true,
|
||||
|
@ -81,10 +83,10 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
|
|||
>
|
||||
<ArrowDownIcon/>
|
||||
</div>
|
||||
)}
|
||||
) : undefined}
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
{tenantIdState}
|
||||
{!isMobile && tenantIdState}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
|
|
@ -91,6 +91,7 @@ const Timezones: FC<TimezonesProps> = ({ timezoneState, onChange }) => {
|
|||
buttonRef={targetRef}
|
||||
placement="bottom-left"
|
||||
onClose={handleCloseList}
|
||||
fullWidth
|
||||
>
|
||||
<div className="vm-timezones-list">
|
||||
<div className="vm-timezones-list-header">
|
||||
|
|
|
@ -46,7 +46,6 @@
|
|||
}
|
||||
|
||||
&-list {
|
||||
min-width: 600px;
|
||||
max-height: 200px;
|
||||
background-color: $color-background-block;
|
||||
border-radius: $border-radius-medium;
|
||||
|
|
|
@ -1,12 +1,25 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-server-configurator {
|
||||
display: grid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $padding-medium;
|
||||
width: 600px;
|
||||
|
||||
&_mobile {
|
||||
grid-auto-rows: min-content;
|
||||
align-items: flex-start;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 100%;
|
||||
|
||||
&_server {
|
||||
display: grid;
|
||||
|
@ -34,4 +47,10 @@
|
|||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&_mobile &__footer {
|
||||
align-items: flex-end;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,7 +111,12 @@ const StepConfigurator: FC = () => {
|
|||
startIcon={<TimelineIcon/>}
|
||||
onClick={toggleOpenOptions}
|
||||
>
|
||||
STEP {customStep}
|
||||
<p>
|
||||
STEP
|
||||
<p className="vm-step-control__value">
|
||||
{customStep}
|
||||
</p>
|
||||
</p>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Popper
|
||||
|
|
|
@ -8,6 +8,15 @@
|
|||
text-transform: none;
|
||||
}
|
||||
|
||||
&__value {
|
||||
display: inline;
|
||||
margin-left: 3px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-popper {
|
||||
display: grid;
|
||||
gap: $padding-small;
|
||||
|
|
|
@ -3,9 +3,12 @@ import "./style.scss";
|
|||
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
|
||||
import { Theme } from "../../../types";
|
||||
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 ThemeControl = () => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
const { theme } = useAppState();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
@ -14,11 +17,19 @@ const ThemeControl = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="vm-theme-control">
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-theme-control": true,
|
||||
"vm-theme-control_mobile": isMobile
|
||||
})}
|
||||
>
|
||||
<div className="vm-server-configurator__title">
|
||||
Theme preferences
|
||||
</div>
|
||||
<div className="vm-theme-control__toggle">
|
||||
<div
|
||||
className="vm-theme-control__toggle"
|
||||
key={`${isMobile}`}
|
||||
>
|
||||
<Toggle
|
||||
options={options}
|
||||
value={theme}
|
||||
|
|
|
@ -7,4 +7,8 @@
|
|||
min-width: 300px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
&_mobile &__toggle {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import Popper from "../../../Main/Popper/Popper";
|
|||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||
import useResize from "../../../../hooks/useResize";
|
||||
|
||||
interface AutoRefreshOption {
|
||||
seconds: number
|
||||
|
@ -29,6 +30,7 @@ const delayOptions: AutoRefreshOption[] = [
|
|||
];
|
||||
|
||||
export const ExecutionControls: FC = () => {
|
||||
const windowSize = useResize(document.body);
|
||||
|
||||
const dispatch = useTimeDispatch();
|
||||
const appModeEnable = getAppModeEnable();
|
||||
|
@ -83,17 +85,20 @@ export const ExecutionControls: FC = () => {
|
|||
<div
|
||||
className={classNames({
|
||||
"vm-execution-controls-buttons": true,
|
||||
"vm-header-button": !appModeEnable
|
||||
"vm-header-button": !appModeEnable,
|
||||
"vm-execution-controls-buttons_short": windowSize.width <= 360
|
||||
})}
|
||||
>
|
||||
<Tooltip title="Refresh dashboard">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleUpdate}
|
||||
startIcon={<RefreshIcon/>}
|
||||
/>
|
||||
</Tooltip>
|
||||
{windowSize.width > 360 && (
|
||||
<Tooltip title="Refresh dashboard">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleUpdate}
|
||||
startIcon={<RefreshIcon/>}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="Auto-refresh control">
|
||||
<div ref={optionsButtonRef}>
|
||||
<Button
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
border-radius: calc($button-radius + 1px);
|
||||
min-width: 107px;
|
||||
|
||||
&_short {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -5,6 +5,11 @@
|
|||
grid-template-columns: repeat(2, 230px);
|
||||
padding: $padding-global 0;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
grid-template-columns: 1fr;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -12,6 +17,12 @@
|
|||
border-right: $border-divider;
|
||||
padding: 0 $padding-global;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
border-right: none;
|
||||
border-bottom: $border-divider;
|
||||
padding-bottom: $padding-global;
|
||||
}
|
||||
|
||||
&-inputs {
|
||||
flex-grow: 1;
|
||||
display: grid;
|
||||
|
|
|
@ -5,16 +5,18 @@
|
|||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: $padding-small calc($padding-small + 10px);
|
||||
gap: $padding-global calc($padding-small + 10px);
|
||||
|
||||
&__job {
|
||||
flex-grow: 0.5;
|
||||
min-width: 200px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__instance {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
&__size {
|
||||
flex-grow: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
&-metrics {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
.vm-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $padding-medium;
|
||||
|
@ -21,6 +22,11 @@
|
|||
|
||||
&__website {
|
||||
margin-right: $padding-global;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__link {
|
||||
|
@ -30,5 +36,10 @@
|
|||
&__copyright {
|
||||
text-align: right;
|
||||
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 TenantsConfiguration from "../../Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration";
|
||||
import { useFetchAccountIds } from "../../Configurators/GlobalSettings/TenantsConfiguration/hooks/useFetchAccountIds";
|
||||
import useResize from "../../../hooks/useResize";
|
||||
import SidebarHeader from "./SidebarNav/SidebarHeader";
|
||||
|
||||
const Header: FC = () => {
|
||||
const windowSize = useResize(document.body);
|
||||
const displaySidebar = useMemo(() => window.innerWidth < 1000, [windowSize]);
|
||||
|
||||
const { isDarkTheme } = useAppState();
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { accountIds } = useFetchAccountIds();
|
||||
|
@ -58,27 +63,37 @@ const Header: FC = () => {
|
|||
})}
|
||||
style={{ background, color }}
|
||||
>
|
||||
{!appModeEnable && (
|
||||
<div
|
||||
className="vm-header-logo"
|
||||
onClick={onClickLogo}
|
||||
style={{ color }}
|
||||
>
|
||||
<LogoFullIcon/>
|
||||
</div>
|
||||
{displaySidebar ? (
|
||||
<SidebarHeader
|
||||
background={background}
|
||||
color={color}
|
||||
onClickLogo={onClickLogo}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{!appModeEnable && (
|
||||
<div
|
||||
className="vm-header-logo"
|
||||
onClick={onClickLogo}
|
||||
style={{ color }}
|
||||
>
|
||||
<LogoFullIcon/>
|
||||
</div>
|
||||
)}
|
||||
<HeaderNav
|
||||
color={color}
|
||||
background={background}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<HeaderNav
|
||||
color={color}
|
||||
background={background}
|
||||
/>
|
||||
<div className="vm-header__settings">
|
||||
{headerSetup?.tenant && <TenantsConfiguration accountIds={accountIds}/>}
|
||||
{headerSetup?.stepControl && <StepConfigurator/>}
|
||||
{headerSetup?.timeSelector && <TimeSelector/>}
|
||||
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
|
||||
{headerSetup?.executionControls && <ExecutionControls/>}
|
||||
<GlobalSettings/>
|
||||
<ShortcutKeys/>
|
||||
{!displaySidebar && <GlobalSettings/>}
|
||||
{!displaySidebar && <ShortcutKeys/>}
|
||||
</div>
|
||||
</header>;
|
||||
};
|
||||
|
|
|
@ -7,13 +7,15 @@ import { useEffect } from "react";
|
|||
import "./style.scss";
|
||||
import NavItem from "./NavItem";
|
||||
import NavSubItem from "./NavSubItem";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface HeaderNavProps {
|
||||
color: string
|
||||
background: string
|
||||
direction?: "row" | "column"
|
||||
}
|
||||
|
||||
const HeaderNav: FC<HeaderNavProps> = ({ color, background }) => {
|
||||
const HeaderNav: FC<HeaderNavProps> = ({ color, background, direction }) => {
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { dashboardsSettings } = useDashboardsState();
|
||||
const { pathname } = useLocation();
|
||||
|
@ -59,7 +61,12 @@ const HeaderNav: FC<HeaderNavProps> = ({ color, background }) => {
|
|||
|
||||
|
||||
return (
|
||||
<nav className="vm-header-nav">
|
||||
<nav
|
||||
className={classNames({
|
||||
"vm-header-nav": true,
|
||||
[`vm-header-nav_${direction}`]: direction
|
||||
})}
|
||||
>
|
||||
{menu.map(m => (
|
||||
m.submenu
|
||||
? (
|
||||
|
@ -70,6 +77,7 @@ const HeaderNav: FC<HeaderNavProps> = ({ color, background }) => {
|
|||
submenu={m.submenu}
|
||||
color={color}
|
||||
background={background}
|
||||
direction={direction}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
|
|
|
@ -12,6 +12,7 @@ interface NavItemProps {
|
|||
submenu: {label: string | undefined, value: string}[],
|
||||
color?: string
|
||||
background?: string
|
||||
direction?: "row" | "column"
|
||||
}
|
||||
|
||||
const NavSubItem: FC<NavItemProps> = ({
|
||||
|
@ -19,7 +20,8 @@ const NavSubItem: FC<NavItemProps> = ({
|
|||
label,
|
||||
color,
|
||||
background,
|
||||
submenu
|
||||
submenu,
|
||||
direction
|
||||
}) => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
|
@ -50,6 +52,21 @@ const NavSubItem: FC<NavItemProps> = ({
|
|||
handleCloseSubmenu();
|
||||
}, [pathname]);
|
||||
|
||||
if (direction === "column") {
|
||||
return (
|
||||
<>
|
||||
{submenu.map(sm => (
|
||||
<NavItem
|
||||
key={sm.value}
|
||||
activeMenu={activeMenu}
|
||||
value={sm.value}
|
||||
label={sm.label || ""}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
|
|
|
@ -8,6 +8,20 @@
|
|||
font-size: $font-size-small;
|
||||
font-weight: bold;
|
||||
|
||||
&_column {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: $padding-small;
|
||||
}
|
||||
|
||||
&_column &-item {
|
||||
padding: $padding-global 0;
|
||||
|
||||
&_sub {
|
||||
justify-content: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
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;
|
||||
padding: $padding-small $padding-medium;
|
||||
gap: 0 $padding-large;
|
||||
min-height: 51px;
|
||||
z-index: 99;
|
||||
|
||||
&_app {
|
||||
padding: $padding-small 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
gap: $padding-small;
|
||||
padding: $padding-small;
|
||||
}
|
||||
|
||||
&_dark {
|
||||
.vm-header-button,
|
||||
button:before,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
.vm-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - var(--scrollbar-height));
|
||||
min-height: calc(($vh * 100) - var(--scrollbar-height));
|
||||
|
||||
&-body {
|
||||
flex-grow: 1;
|
||||
|
@ -11,6 +11,10 @@
|
|||
padding: $padding-medium;
|
||||
background-color: $color-background-body;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&_app {
|
||||
padding: $padding-small 0;
|
||||
background-color: transparent;
|
||||
|
|
|
@ -390,7 +390,7 @@ export const QuestionIcon = () => (
|
|||
|
||||
);
|
||||
|
||||
export const StorageIcons = () => (
|
||||
export const StorageIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
|
@ -400,3 +400,14 @@ export const StorageIcons = () => (
|
|||
></path>
|
||||
</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 { ReactNode, MouseEvent } from "react";
|
||||
import "./style.scss";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface ModalProps {
|
||||
title?: string
|
||||
|
@ -12,6 +14,7 @@ interface ModalProps {
|
|||
}
|
||||
|
||||
const Modal: FC<ModalProps> = ({ title, children, onClose }) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
|
@ -22,16 +25,21 @@ const Modal: FC<ModalProps> = ({ title, children, onClose }) => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = "hidden";
|
||||
window.addEventListener("keyup", handleKeyUp);
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = "auto";
|
||||
window.removeEventListener("keyup", handleKeyUp);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return ReactDOM.createPortal((
|
||||
<div
|
||||
className="vm-modal"
|
||||
className={classNames({
|
||||
"vm-modal": true,
|
||||
"vm-modal_mobile": isMobile
|
||||
})}
|
||||
onMouseDown={onClose}
|
||||
>
|
||||
<div className="vm-modal-content">
|
||||
|
|
|
@ -14,12 +14,28 @@ $padding-modal: 22px;
|
|||
justify-content: center;
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
align-items: flex-start;
|
||||
padding: $padding-modal;
|
||||
background: $color-background-block;
|
||||
box-shadow: 0 0 24px rgba($color-black, 0.07);
|
||||
border-radius: $border-radius-small;
|
||||
max-height: 90vh;
|
||||
max-height: calc($vh * 90);
|
||||
overflow: auto;
|
||||
|
||||
&-header {
|
||||
|
@ -44,6 +60,8 @@ $padding-modal: 22px;
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&-body {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -99,12 +99,20 @@ const Popper: FC<PopperProps> = ({
|
|||
if (isOverflowLeft) position.left = buttonPos.left + offsetLeft;
|
||||
|
||||
if (fullWidth) position.width = `${buttonPos.width}px`;
|
||||
if (position.top < 0) position.top = 20;
|
||||
|
||||
return position;
|
||||
},[buttonRef, placement, isOpen, children, fullWidth]);
|
||||
|
||||
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({
|
||||
"vm-popper": true,
|
||||
"vm-popper_open": isOpen,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 Button from "../Button/Button";
|
||||
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 appModeEnable = getAppModeEnable();
|
||||
|
||||
|
@ -83,7 +85,8 @@ const ShortcutKeys: FC = () => {
|
|||
|
||||
return <>
|
||||
<Tooltip
|
||||
title="Shortcut keys"
|
||||
open={showTitle === true ? false : undefined}
|
||||
title={title}
|
||||
placement="bottom-center"
|
||||
>
|
||||
<Button
|
||||
|
@ -92,7 +95,9 @@ const ShortcutKeys: FC = () => {
|
|||
color="primary"
|
||||
startIcon={<KeyboardIcon/>}
|
||||
onClick={handleOpen}
|
||||
/>
|
||||
>
|
||||
{showTitle && title}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{openList && (
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
.vm-shortcuts {
|
||||
min-width: 400px;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
&-section {
|
||||
margin-bottom: $padding-medium;
|
||||
|
||||
|
@ -17,12 +21,20 @@
|
|||
display: grid;
|
||||
gap: $padding-global;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
gap: $padding-medium;
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: grid;
|
||||
grid-template-columns: 210px 1fr;
|
||||
align-items: center;
|
||||
gap: $padding-small;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
&__key {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -39,6 +39,7 @@ export const ThemeProvider: FC<ThemeProviderProps> = ({ onLoaded }) => {
|
|||
const { clientWidth, clientHeight } = document.documentElement;
|
||||
setCssVariable("scrollbar-width", `${innerWidth - clientWidth}px`);
|
||||
setCssVariable("scrollbar-height", `${innerHeight - clientHeight}px`);
|
||||
setCssVariable("vh", `${innerHeight * 0.01}px`);
|
||||
};
|
||||
|
||||
const setContrastText = () => {
|
||||
|
|
|
@ -3,6 +3,7 @@ import ReactDOM from "react-dom";
|
|||
import "./style.scss";
|
||||
import { ReactNode } from "react";
|
||||
import { ExoticComponent } from "react";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
|
||||
interface TooltipProps {
|
||||
children: ReactNode
|
||||
|
@ -19,6 +20,7 @@ const Tooltip: FC<TooltipProps> = ({
|
|||
placement = "bottom-center",
|
||||
offset = { top: 6, left: 0 }
|
||||
}) => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [popperSize, setPopperSize] = useState({ width: 0, height: 0 });
|
||||
|
@ -121,7 +123,7 @@ const Tooltip: FC<TooltipProps> = ({
|
|||
{children}
|
||||
</Fragment>
|
||||
|
||||
{isOpen && ReactDOM.createPortal((
|
||||
{!isMobile && isOpen && ReactDOM.createPortal((
|
||||
<div
|
||||
className="vm-tooltip"
|
||||
ref={popperRef}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
@use "src/styles/variables" as *;
|
||||
$all-paddings: $padding-medium * 4;
|
||||
|
||||
.vm-graph-view {
|
||||
width: 100%;
|
||||
|
||||
&_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 () => {
|
||||
if (node) observer.unobserve(node);
|
||||
};
|
||||
}, []);
|
||||
}, [node]);
|
||||
return windowSize;
|
||||
};
|
||||
|
||||
|
|
|
@ -118,24 +118,26 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
|
|||
at <b>{date}</b>{match && <span> for series selector <b>{match}</b></span>}.
|
||||
Show top {topN} entries per table.
|
||||
</div>
|
||||
<a
|
||||
className="vm-link vm-link_with-icon"
|
||||
target="_blank"
|
||||
href="https://docs.victoriametrics.com/#cardinality-explorer"
|
||||
rel="help noreferrer"
|
||||
>
|
||||
<WikiIcon/>
|
||||
Documentation
|
||||
</a>
|
||||
<a
|
||||
className="vm-link vm-link_with-icon"
|
||||
target="_blank"
|
||||
href="https://victoriametrics.com/blog/cardinality-explorer/"
|
||||
rel="help noreferrer"
|
||||
>
|
||||
<QuestionIcon/>
|
||||
Example of using
|
||||
</a>
|
||||
<div className="vm-cardinality-configurator-bottom__docs">
|
||||
<a
|
||||
className="vm-link vm-link_with-icon"
|
||||
target="_blank"
|
||||
href="https://docs.victoriametrics.com/#cardinality-explorer"
|
||||
rel="help noreferrer"
|
||||
>
|
||||
<WikiIcon/>
|
||||
Documentation
|
||||
</a>
|
||||
<a
|
||||
className="vm-link vm-link_with-icon"
|
||||
target="_blank"
|
||||
href="https://victoriametrics.com/blog/cardinality-explorer/"
|
||||
rel="help noreferrer"
|
||||
>
|
||||
<QuestionIcon/>
|
||||
Example of using
|
||||
</a>
|
||||
</div>
|
||||
<Button
|
||||
startIcon={<PlayIcon/>}
|
||||
onClick={onRunQuery}
|
||||
|
|
|
@ -12,6 +12,10 @@
|
|||
gap: 0 $padding-medium;
|
||||
|
||||
&__query {
|
||||
flex-grow: 8;
|
||||
}
|
||||
|
||||
&__item {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
@ -19,13 +23,21 @@
|
|||
&-additional {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: $padding-small;
|
||||
}
|
||||
|
||||
&-bottom {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: $padding-global;
|
||||
|
||||
&__docs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $padding-global;
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex-grow: 1;
|
||||
font-size: $font-size;
|
||||
|
@ -34,5 +46,9 @@
|
|||
a {
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0 0 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,7 +65,10 @@ const MetricsContent: FC<MetricsProperties> = ({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref={chartContainer}>
|
||||
<div
|
||||
ref={chartContainer}
|
||||
className="vm-metrics-content__table"
|
||||
>
|
||||
{activeTab === 0 && (
|
||||
<EnhancedTable
|
||||
rows={rows}
|
||||
|
|
|
@ -2,6 +2,20 @@
|
|||
|
||||
.vm-metrics-content {
|
||||
&-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 {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $padding-medium;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
flex-grow: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, auto);
|
||||
gap: $padding-small;
|
||||
justify-content: flex-end;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,10 @@
|
|||
gap: $padding-global;
|
||||
padding: 0;
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
&-panel {
|
||||
position: relative;
|
||||
border-radius: $border-radius-medium;
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
gap: $padding-global;
|
||||
align-items: flex-start;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: $padding-medium 0;
|
||||
}
|
||||
|
||||
&-tabs.vm-block {
|
||||
padding: $padding-global;
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ const TopQueryPanel: FC<TopQueryPanelProps> = ({ rows, title, columns, defaultOr
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="vm-top-queries-panel__table">
|
||||
{activeTab === 0 && (
|
||||
<TopQueryTable
|
||||
rows={rows}
|
||||
|
|
|
@ -2,6 +2,20 @@
|
|||
|
||||
.vm-top-queries-panel {
|
||||
&-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,23 +71,27 @@ const Index: FC = () => {
|
|||
{loading && <Spinner containerStyles={{ height: "500px" }}/>}
|
||||
|
||||
<div className="vm-top-queries-controls vm-block">
|
||||
<div className="vm-top-queries-controls__fields">
|
||||
<TextField
|
||||
label="Max lifetime"
|
||||
value={maxLifetime}
|
||||
error={errorMaxLife}
|
||||
helperText={`For example ${exampleDuration}`}
|
||||
onChange={onMaxLifetimeChange}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<TextField
|
||||
label="Number of returned queries"
|
||||
type="number"
|
||||
value={topN || ""}
|
||||
error={errorTopN}
|
||||
onChange={onTopNChange}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<div className="vm-top-queries-controls-fields">
|
||||
<div className="vm-top-queries-controls-fields__item">
|
||||
<TextField
|
||||
label="Max lifetime"
|
||||
value={maxLifetime}
|
||||
error={errorMaxLife}
|
||||
helperText={`For example ${exampleDuration}`}
|
||||
onChange={onMaxLifetimeChange}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-top-queries-controls-fields__item">
|
||||
<TextField
|
||||
label="Number of returned queries"
|
||||
type="number"
|
||||
value={topN || ""}
|
||||
error={errorTopN}
|
||||
onChange={onTopNChange}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="vm-top-queries-controls-bottom">
|
||||
<div className="vm-top-queries-controls-bottom__info">
|
||||
|
|
|
@ -9,10 +9,16 @@
|
|||
display: grid;
|
||||
gap: $padding-small;
|
||||
|
||||
&__fields {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
&-fields {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: $padding-medium;
|
||||
|
||||
&__item {
|
||||
flex-grow: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
&-bottom {
|
||||
|
|
|
@ -7,6 +7,7 @@ import { ErrorTypes } from "../../../types";
|
|||
import classNames from "classnames";
|
||||
import { useSnack } from "../../../contexts/Snackbar";
|
||||
import { CopyIcon, RestartIcon } from "../../../components/Main/Icons";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
|
||||
interface JsonFormProps {
|
||||
defaultJson?: string
|
||||
|
@ -28,6 +29,7 @@ const JsonForm: FC<JsonFormProps> = ({
|
|||
onUpload,
|
||||
}) => {
|
||||
const { showInfoMessage } = useSnack();
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const [json, setJson] = useState(defaultJson);
|
||||
const [title, setTitle] = useState(defaultTile);
|
||||
|
@ -77,7 +79,8 @@ const JsonForm: FC<JsonFormProps> = ({
|
|||
<div
|
||||
className={classNames({
|
||||
"vm-json-form": true,
|
||||
"vm-json-form_one-field": !displayTitle
|
||||
"vm-json-form_one-field": !displayTitle,
|
||||
"vm-json-form_mobile": isMobile
|
||||
})}
|
||||
>
|
||||
{displayTitle && (
|
||||
|
|
|
@ -2,15 +2,21 @@
|
|||
|
||||
.vm-json-form {
|
||||
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;
|
||||
width: 70vw;
|
||||
max-width: 1000px;
|
||||
max-height: 900px;
|
||||
overflow: hidden;
|
||||
|
||||
&_mobile {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
&_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 {
|
||||
|
@ -29,6 +35,14 @@
|
|||
justify-content: space-between;
|
||||
gap: $padding-small;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
flex-direction: column;
|
||||
|
||||
button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
|
@ -36,10 +50,22 @@
|
|||
justify-content: flex-start;
|
||||
gap: $padding-small;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&_right {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 90px);
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: $padding-global;
|
||||
min-height: 100%;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: $padding-medium 0;
|
||||
}
|
||||
|
||||
&-controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
@ -21,6 +24,11 @@
|
|||
gap: $padding-global;
|
||||
margin-bottom: $padding-medium;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0 $padding-medium;
|
||||
}
|
||||
|
||||
&-errors {
|
||||
display: grid;
|
||||
align-items: flex-start;
|
||||
|
@ -28,6 +36,10 @@
|
|||
grid-template-columns: 1fr;
|
||||
gap: $padding-medium;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
display: grid;
|
||||
|
|
|
@ -16,9 +16,6 @@
|
|||
}
|
||||
|
||||
&_header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&_selected {
|
||||
|
|
|
@ -13,12 +13,13 @@ html, body, #root {
|
|||
}
|
||||
|
||||
body {
|
||||
overflow: scroll;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
* {
|
||||
font: inherit;
|
||||
cursor: inherit;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
|
||||
code {
|
||||
|
|
|
@ -61,3 +61,4 @@ $box-shadow: var(--box-shadow);
|
|||
$box-shadow-popper: var(--box-shadow-popper);
|
||||
|
||||
$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;
|
||||
};
|
||||
|
||||
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 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 => {
|
||||
e.preventDefault();
|
||||
const isMouseEvent = e instanceof MouseEvent;
|
||||
|
||||
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 scXMin = u.scales.x.min || 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();
|
||||
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 });
|
||||
};
|
||||
const mouseUp = () => {
|
||||
setPanning(false);
|
||||
document.removeEventListener("mousemove", mouseMove);
|
||||
document.removeEventListener("mouseup", mouseUp);
|
||||
document.removeEventListener("touchmove", mouseMove);
|
||||
document.removeEventListener("touchend", mouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", mouseMove);
|
||||
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);
|
||||
|
||||
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);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* eslint-disable */
|
||||
import uPlot from "uplot";
|
||||
import {getCssVariable} from "../theme";
|
||||
import {sizeAxis} from "./helpers";
|
||||
|
||||
export const seriesBarsPlugin = (opts) => {
|
||||
let pxRatio;
|
||||
|
@ -262,7 +263,9 @@ export const seriesBarsPlugin = (opts) => {
|
|||
},
|
||||
values: u => u.data[0],
|
||||
gap: 15,
|
||||
size: ori === 0 ? 40 : 150,
|
||||
size: sizeAxis,
|
||||
stroke: getCssVariable("color-text"),
|
||||
font: "10px Arial",
|
||||
labelSize: 20,
|
||||
grid: {show: false},
|
||||
ticks: {show: false},
|
||||
|
|
|
@ -8,7 +8,7 @@ export interface HideSeriesArgs {
|
|||
}
|
||||
|
||||
export interface DragArgs {
|
||||
e: MouseEvent,
|
||||
e: MouseEvent | TouchEvent,
|
||||
u: uPlot,
|
||||
factor: number,
|
||||
setPanning: (enable: boolean) => void,
|
||||
|
|
Loading…
Reference in a new issue