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:
Yury Molodov 2023-02-04 04:27:57 +01:00 committed by GitHub
parent 88fed0232c
commit f63f487787
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 895 additions and 144 deletions

View file

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

View file

@ -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]);
@ -170,7 +175,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
style={{ background: color }}
/>
<p>
{metricName}:
{metricName}:
<b className="vm-chart-tooltip-data__value">{valueFormat}</b>
{unit}
</p>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -46,7 +46,6 @@
}
&-list {
min-width: 600px;
max-height: 200px;
background-color: $color-background-block;
border-radius: $border-radius-medium;

View file

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

View file

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

View file

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

View file

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

View file

@ -7,4 +7,8 @@
min-width: 300px;
text-transform: capitalize;
}
&_mobile &__toggle {
display: flex;
}
}

View file

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

View file

@ -9,6 +9,10 @@
border-radius: calc($button-radius + 1px);
min-width: 107px;
&_short {
min-width: auto;
}
&__arrow {
display: flex;
align-items: center;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -14,7 +14,7 @@ const useResize = (node: HTMLElement | null): {width: number, height: number} =>
return () => {
if (node) observer.unobserve(node);
};
}, []);
}, [node]);
return windowSize;
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,9 +16,6 @@
}
&_header {
position: sticky;
top: 0;
z-index: 2;
}
&_selected {

View file

@ -13,12 +13,13 @@ html, body, #root {
}
body {
overflow: scroll;
overflow: auto;
}
* {
font: inherit;
cursor: inherit;
touch-action: pan-x pan-y;
}
code {

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ export interface HideSeriesArgs {
}
export interface DragArgs {
e: MouseEvent,
e: MouseEvent | TouchEvent,
u: uPlot,
factor: number,
setPanning: (enable: boolean) => void,