© {copyrightYears} VictoriaMetrics
diff --git a/app/vmui/packages/vmui/src/components/Layout/Footer/style.scss b/app/vmui/packages/vmui/src/components/Layout/Footer/style.scss
index 172724ce6..490565533 100644
--- a/app/vmui/packages/vmui/src/components/Layout/Footer/style.scss
+++ b/app/vmui/packages/vmui/src/components/Layout/Footer/style.scss
@@ -11,6 +11,11 @@
color: $color-text-secondary;
background: $color-background-body;
+ @media (max-width: 768px) {
+ padding: $padding-global;
+ gap: $padding-global;
+ }
+
&__link,
&__website {
display: grid;
@@ -25,7 +30,6 @@
@media (max-width: 768px) {
margin-right: 0;
- width: 100%;
}
}
@@ -39,6 +43,7 @@
@media (max-width: 768px) {
width: 100%;
+ font-size: $font-size-small;
text-align: center;
}
}
diff --git a/app/vmui/packages/vmui/src/components/Layout/Header/Header.tsx b/app/vmui/packages/vmui/src/components/Layout/Header/Header.tsx
index 24e6a72bf..b64be06a0 100644
--- a/app/vmui/packages/vmui/src/components/Layout/Header/Header.tsx
+++ b/app/vmui/packages/vmui/src/components/Layout/Header/Header.tsx
@@ -1,31 +1,26 @@
import React, { FC, useMemo } from "preact/compat";
-import { ExecutionControls } from "../../Configurators/TimeRangeSettings/ExecutionControls/ExecutionControls";
-import { TimeSelector } from "../../Configurators/TimeRangeSettings/TimeSelector/TimeSelector";
-import GlobalSettings from "../../Configurators/GlobalSettings/GlobalSettings";
-import { useLocation, useNavigate } from "react-router-dom";
-import router, { RouterOptions, routerOptions } from "../../../router";
-import ShortcutKeys from "../../Main/ShortcutKeys/ShortcutKeys";
+import { useNavigate } from "react-router-dom";
+import router from "../../../router";
import { getAppModeEnable, getAppModeParams } from "../../../utils/app-mode";
-import CardinalityDatePicker from "../../Configurators/CardinalityDatePicker/CardinalityDatePicker";
import { LogoFullIcon } from "../../Main/Icons";
import { getCssVariable } from "../../../utils/theme";
import "./style.scss";
import classNames from "classnames";
-import StepConfigurator from "../../Configurators/StepConfigurator/StepConfigurator";
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";
+import HeaderControls from "./HeaderControls/HeaderControls";
+import useDeviceDetect from "../../../hooks/useDeviceDetect";
const Header: FC = () => {
+ const { isMobile } = useDeviceDetect();
+
const windowSize = useResize(document.body);
const displaySidebar = useMemo(() => window.innerWidth < 1000, [windowSize]);
const { isDarkTheme } = useAppState();
const appModeEnable = getAppModeEnable();
- const { accountIds } = useFetchAccountIds();
const primaryColor = useMemo(() => {
const variable = isDarkTheme ? "color-background-block" : "color-primary";
@@ -42,11 +37,6 @@ const Header: FC = () => {
}, [primaryColor]);
const navigate = useNavigate();
- const { pathname } = useLocation();
-
- const headerSetup = useMemo(() => {
- return ((routerOptions[pathname] || {}) as RouterOptions).header || {};
- }, [pathname]);
const onClickLogo = () => {
navigate({ pathname: router.home });
@@ -57,7 +47,8 @@ const Header: FC = () => {
className={classNames({
"vm-header": true,
"vm-header_app": appModeEnable,
- "vm-header_dark": isDarkTheme
+ "vm-header_dark": isDarkTheme,
+ "vm-header_mobile": isMobile
})}
style={{ background, color }}
>
@@ -65,7 +56,6 @@ const Header: FC = () => {
) : (
<>
@@ -84,15 +74,19 @@ const Header: FC = () => {
/>
>
)}
-
- {headerSetup?.tenant && }
- {headerSetup?.stepControl && }
- {headerSetup?.timeSelector && }
- {headerSetup?.cardinalityDatePicker && }
- {headerSetup?.executionControls && }
- {!displaySidebar && }
- {!displaySidebar && }
-
+ {isMobile && (
+
+
+
+ )}
+
;
};
diff --git a/app/vmui/packages/vmui/src/components/Layout/Header/HeaderControls/HeaderControls.tsx b/app/vmui/packages/vmui/src/components/Layout/Header/HeaderControls/HeaderControls.tsx
new file mode 100644
index 000000000..51f746a63
--- /dev/null
+++ b/app/vmui/packages/vmui/src/components/Layout/Header/HeaderControls/HeaderControls.tsx
@@ -0,0 +1,107 @@
+import React, { FC, useMemo, useState } from "preact/compat";
+import { RouterOptions, routerOptions, RouterOptionsHeader } from "../../../../router";
+import TenantsConfiguration from "../../../Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration";
+import StepConfigurator from "../../../Configurators/StepConfigurator/StepConfigurator";
+import { TimeSelector } from "../../../Configurators/TimeRangeSettings/TimeSelector/TimeSelector";
+import CardinalityDatePicker from "../../../Configurators/CardinalityDatePicker/CardinalityDatePicker";
+import { ExecutionControls } from "../../../Configurators/TimeRangeSettings/ExecutionControls/ExecutionControls";
+import GlobalSettings from "../../../Configurators/GlobalSettings/GlobalSettings";
+import ShortcutKeys from "../../../Main/ShortcutKeys/ShortcutKeys";
+import { useLocation } from "react-router-dom";
+import { useFetchAccountIds } from "../../../Configurators/GlobalSettings/TenantsConfiguration/hooks/useFetchAccountIds";
+import Button from "../../../Main/Button/Button";
+import { MoreIcon } from "../../../Main/Icons";
+import "./style.scss";
+import classNames from "classnames";
+import { getAppModeEnable } from "../../../../utils/app-mode";
+import Modal from "../../../Main/Modal/Modal";
+
+interface HeaderControlsProp {
+ displaySidebar: boolean
+ isMobile?: boolean
+ headerSetup?: RouterOptionsHeader
+ accountIds?: string[]
+}
+
+const Controls: FC
= ({
+ displaySidebar,
+ isMobile,
+ headerSetup,
+ accountIds
+}) => {
+
+ return (
+
+ {headerSetup?.tenant && }
+ {headerSetup?.stepControl && }
+ {headerSetup?.timeSelector && }
+ {headerSetup?.cardinalityDatePicker && }
+ {headerSetup?.executionControls && }
+
+ {!displaySidebar && }
+
+ );
+};
+
+const HeaderControls: FC = (props) => {
+ const appModeEnable = getAppModeEnable();
+ const [openList, setOpenList] = useState(false);
+ const { pathname } = useLocation();
+ const { accountIds } = useFetchAccountIds();
+
+ const headerSetup = useMemo(() => {
+ return ((routerOptions[pathname] || {}) as RouterOptions).header || {};
+ }, [pathname]);
+
+ const handleToggleList = () => {
+ setOpenList(prev => !prev);
+ };
+
+ const handleCloseList = () => {
+ setOpenList(false);
+ };
+
+ if (props.isMobile) {
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+ }
+
+ return ;
+};
+
+export default HeaderControls;
diff --git a/app/vmui/packages/vmui/src/components/Layout/Header/HeaderControls/style.scss b/app/vmui/packages/vmui/src/components/Layout/Header/HeaderControls/style.scss
new file mode 100644
index 000000000..3e71f7842
--- /dev/null
+++ b/app/vmui/packages/vmui/src/components/Layout/Header/HeaderControls/style.scss
@@ -0,0 +1,27 @@
+@use "src/styles/variables" as *;
+
+.vm-header-controls {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: $padding-small;
+ flex-grow: 1;
+
+ &_mobile {
+ display: grid;
+ grid-template-columns: 1fr;
+ padding: 0;
+
+ .vm-header-button {
+ border: none;
+ }
+ }
+
+ &-modal {
+ transform: scale(0);
+
+ &_open {
+ transform: scale(1);
+ }
+ }
+}
diff --git a/app/vmui/packages/vmui/src/components/Layout/Header/SidebarNav/SidebarHeader.tsx b/app/vmui/packages/vmui/src/components/Layout/Header/SidebarNav/SidebarHeader.tsx
index cf2a791b7..fc845ca03 100644
--- a/app/vmui/packages/vmui/src/components/Layout/Header/SidebarNav/SidebarHeader.tsx
+++ b/app/vmui/packages/vmui/src/components/Layout/Header/SidebarNav/SidebarHeader.tsx
@@ -1,8 +1,6 @@
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";
@@ -13,13 +11,11 @@ import "./style.scss";
interface SidebarHeaderProps {
background: string
color: string
- onClickLogo: () => void
}
const SidebarHeader: FC = ({
background,
color,
- onClickLogo,
}) => {
const { pathname } = useLocation();
const { isMobile } = useDeviceDetect();
@@ -48,11 +44,9 @@ const SidebarHeader: FC = ({
"vm-header-sidebar-button": true,
"vm-header-sidebar-button_open": openMenu
})}
+ onClick={handleToggleMenu}
>
-
+
diff --git a/app/vmui/packages/vmui/src/components/Layout/style.scss b/app/vmui/packages/vmui/src/components/Layout/style.scss
index ad28fbeec..32e8ccf90 100644
--- a/app/vmui/packages/vmui/src/components/Layout/style.scss
+++ b/app/vmui/packages/vmui/src/components/Layout/style.scss
@@ -11,8 +11,12 @@
padding: $padding-medium;
background-color: $color-background-body;
+ &_mobile {
+ padding: $padding-small 0 0;
+ }
+
@media (max-width: 768px) {
- padding: 0;
+ padding: $padding-small 0 0;
}
&_app {
diff --git a/app/vmui/packages/vmui/src/components/Main/Alert/Alert.tsx b/app/vmui/packages/vmui/src/components/Main/Alert/Alert.tsx
index cdf8db49c..d4b57c012 100644
--- a/app/vmui/packages/vmui/src/components/Main/Alert/Alert.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Alert/Alert.tsx
@@ -4,6 +4,7 @@ import classNames from "classnames";
import { ErrorIcon, InfoIcon, SuccessIcon, WarningIcon } from "../Icons";
import "./style.scss";
import { useAppState } from "../../../state/common/StateContext";
+import useDeviceDetect from "../../../hooks/useDeviceDetect";
interface AlertProps {
variant?: "success" | "error" | "info" | "warning"
@@ -21,13 +22,15 @@ const Alert: FC
= ({
variant,
children }) => {
const { isDarkTheme } = useAppState();
+ const { isMobile } = useDeviceDetect();
return (
{icons[variant || "info"]}
diff --git a/app/vmui/packages/vmui/src/components/Main/Alert/style.scss b/app/vmui/packages/vmui/src/components/Main/Alert/style.scss
index a55e38824..74e4c4e9e 100644
--- a/app/vmui/packages/vmui/src/components/Main/Alert/style.scss
+++ b/app/vmui/packages/vmui/src/components/Main/Alert/style.scss
@@ -15,6 +15,11 @@
color: $color-text;
line-height: 20px;
+ &_mobile {
+ align-items: flex-start;
+ border-radius: 0;
+ }
+
&:after {
position: absolute;
content: '';
@@ -27,6 +32,10 @@
opacity: 0.1;
}
+ &_mobile:after {
+ border-radius: 0;
+ }
+
&__icon,
&__content {
position: relative;
@@ -48,8 +57,8 @@
color: $color-success;
&:after {
- background-color: $color-success;
- }
+ background-color: $color-success;
+ }
}
&_error {
diff --git a/app/vmui/packages/vmui/src/components/Main/Autocomplete/Autocomplete.tsx b/app/vmui/packages/vmui/src/components/Main/Autocomplete/Autocomplete.tsx
index 8509c5e08..e65a51bea 100644
--- a/app/vmui/packages/vmui/src/components/Main/Autocomplete/Autocomplete.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Autocomplete/Autocomplete.tsx
@@ -1,9 +1,9 @@
import React, { FC, Ref, useEffect, useMemo, useRef, useState } from "preact/compat";
import classNames from "classnames";
-import useClickOutside from "../../../hooks/useClickOutside";
import Popper from "../Popper/Popper";
import "./style.scss";
import { DoneIcon } from "../Icons";
+import useDeviceDetect from "../../../hooks/useDeviceDetect";
interface AutocompleteProps {
value: string
@@ -15,7 +15,9 @@ interface AutocompleteProps {
fullWidth?: boolean
noOptionsText?: string
selected?: string[]
- onSelect: (val: string) => void,
+ label?: string
+ disabledFullScreen?: boolean
+ onSelect: (val: string) => void
onOpenAutocomplete?: (val: boolean) => void
}
@@ -29,9 +31,12 @@ const Autocomplete: FC
= ({
fullWidth,
selected,
noOptionsText,
+ label,
+ disabledFullScreen,
onSelect,
onOpenAutocomplete
}) => {
+ const { isMobile } = useDeviceDetect();
const wrapperEl = useRef(null);
const [openAutocomplete, setOpenAutocomplete] = useState(false);
@@ -118,8 +123,6 @@ const Autocomplete: FC = ({
onOpenAutocomplete && onOpenAutocomplete(openAutocomplete);
}, [openAutocomplete]);
- useClickOutside(wrapperEl, handleCloseAutocomplete, anchor);
-
return (
= ({
placement="bottom-left"
onClose={handleCloseAutocomplete}
fullWidth={fullWidth}
+ title={isMobile ? label : undefined}
+ disabledFullScreen={disabledFullScreen}
>
{displayNoOptionsText &&
{noOptionsText}
}
@@ -137,6 +145,7 @@ const Autocomplete: FC
= ({
= ({
const [viewDate, setViewDate] = useState(dayjs.tz(date));
const [selectDate, setSelectDate] = useState(dayjs.tz(date));
const [tab, setTab] = useState(tabs[0].value);
+ const { isMobile } = useDeviceDetect();
const toggleDisplayYears = () => {
setDisplayYears(prev => !prev);
@@ -67,7 +70,12 @@ const Calendar: FC
= ({
}, [selectDate]);
return (
-
+
{tab === "date" && (
format?: string
timepicker?: boolean
+ label?: string
onChange: (val: string) => void
}
@@ -18,9 +20,11 @@ const DatePicker = forwardRef(({
format = DATE_TIME_FORMAT,
timepicker,
onChange,
+ label
}, ref) => {
const [openCalendar, setOpenCalendar] = useState(false);
const dateDayjs = useMemo(() => date ? dayjs.tz(date) : dayjs().tz(), [date]);
+ const { isMobile } = useDeviceDetect();
const toggleOpenCalendar = () => {
setOpenCalendar(prev => !prev);
@@ -61,6 +65,7 @@ const DatePicker = forwardRef(({
buttonRef={targetRef}
placement="bottom-right"
onClose={handleCloseCalendar}
+ title={isMobile ? label : undefined}
>
(
);
-export const PlusCircleFillIcon = () => (
-
-);
-
export const ClockIcon = () => (
);
-export const RemoveCircleIcon = () => (
-
-);
-
export const PlayIcon = () => (
);
+export const MinusIcon = () => (
+
+);
+
export const DoneIcon = () => (
);
-export const SearchIcon = () => (
-
-);
-
-export const ResizeIcon = () => (
-
-);
-
export const TimelineIcon = () => (
);
-export const MenuIcon = () => (
+export const MoreIcon = () => (
+);
+
+export const TuneIcon = () => (
+
);
diff --git a/app/vmui/packages/vmui/src/components/Main/MenuBurger/MenuBurger.tsx b/app/vmui/packages/vmui/src/components/Main/MenuBurger/MenuBurger.tsx
index faa05d18b..a336607fe 100644
--- a/app/vmui/packages/vmui/src/components/Main/MenuBurger/MenuBurger.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/MenuBurger/MenuBurger.tsx
@@ -2,13 +2,12 @@ import React from "preact/compat";
import classNames from "classnames";
import "./style.scss";
-const MenuBurger = ({ open, onClick }: {open: boolean, onClick: () => void}) => (
+const MenuBurger = ({ open }: {open: boolean}) => (
diff --git a/app/vmui/packages/vmui/src/components/Main/Modal/Modal.tsx b/app/vmui/packages/vmui/src/components/Main/Modal/Modal.tsx
index 4799c233b..6fbbaeb13 100644
--- a/app/vmui/packages/vmui/src/components/Main/Modal/Modal.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Modal/Modal.tsx
@@ -6,15 +6,26 @@ import { ReactNode, MouseEvent } from "react";
import "./style.scss";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import classNames from "classnames";
+import { useLocation, useNavigate } from "react-router-dom";
interface ModalProps {
title?: string
children: ReactNode
onClose: () => void
+ className?: string
+ isOpen?: boolean
}
-const Modal: FC = ({ title, children, onClose }) => {
+const Modal: FC = ({
+ title,
+ children,
+ onClose,
+ className,
+ isOpen= true
+}) => {
const { isMobile } = useDeviceDetect();
+ const navigate = useNavigate();
+ const location = useLocation();
const handleKeyUp = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
@@ -24,7 +35,23 @@ const Modal: FC = ({ title, children, onClose }) => {
e.stopPropagation();
};
+ const handlePopstate = () => {
+ if (isOpen) {
+ navigate(location, { replace: true });
+ onClose();
+ }
+ };
+
useEffect(() => {
+ window.addEventListener("popstate", handlePopstate);
+
+ return () => {
+ window.removeEventListener("popstate", handlePopstate);
+ };
+ }, [isOpen, location]);
+
+ const handleDisplayModal = () => {
+ if (!isOpen) return;
document.body.style.overflow = "hidden";
window.addEventListener("keyup", handleKeyUp);
@@ -32,18 +59,24 @@ const Modal: FC = ({ title, children, onClose }) => {
document.body.style.overflow = "auto";
window.removeEventListener("keyup", handleKeyUp);
};
- }, []);
+ };
+
+ useEffect(handleDisplayModal, [isOpen]);
return ReactDOM.createPortal((
-
+
{title && (
{title}
diff --git a/app/vmui/packages/vmui/src/components/Main/Modal/style.scss b/app/vmui/packages/vmui/src/components/Main/Modal/style.scss
index 7e89e845d..163574e42 100644
--- a/app/vmui/packages/vmui/src/components/Main/Modal/style.scss
+++ b/app/vmui/packages/vmui/src/components/Main/Modal/style.scss
@@ -14,16 +14,31 @@ $padding-modal: 22px;
justify-content: center;
background: rgba($color-black, 0.55);
- &_mobile &-content {
+ &_mobile {
+ align-items: flex-start;
min-height: calc($vh * 100);
max-height: calc($vh * 100);
+ overflow: auto;
+ }
+
+ &_mobile &-content {
width: 100vw;
border-radius: 0;
+ overflow: visible;
+ min-height: 100%;
+ max-height: max-content;
+ grid-template-rows: 70px max-content;
+
+ &-header {
+ padding: $padding-small $padding-small $padding-small $padding-global;
+ margin-bottom: $padding-global;
+ }
&-body {
display: grid;
align-items: flex-start;
min-height: 100%;
+ padding: 0 $padding-global $padding-modal;
}
}
@@ -31,7 +46,6 @@ $padding-modal: 22px;
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;
@@ -39,14 +53,25 @@ $padding-modal: 22px;
overflow: auto;
&-header {
+ position: sticky;
+ top: 0;
display: grid;
grid-template-columns: 1fr auto;
+ gap: $padding-small;
align-items: center;
- margin-bottom: $padding-modal ;
+ justify-content: space-between;
+ background-color: $color-background-block;
+ padding: $padding-global $padding-modal;
+ border-radius: $border-radius-small $border-radius-small 0 0;
+ color: $color-text;
+ border-bottom: $border-divider;
+ margin-bottom: $padding-modal;
+ min-height: 51px;
+ z-index: 3;
&__title {
font-weight: bold;
- font-size: $font-size-medium;
+ user-select: none;
}
&__close {
@@ -61,7 +86,9 @@ $padding-modal: 22px;
}
}
- &-body {}
+ &-body {
+ padding: 0 $padding-modal $padding-modal;
+ }
}
}
diff --git a/app/vmui/packages/vmui/src/components/Main/Popper/Popper.tsx b/app/vmui/packages/vmui/src/components/Main/Popper/Popper.tsx
index ea7ea8d5a..3e7f79de3 100644
--- a/app/vmui/packages/vmui/src/components/Main/Popper/Popper.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Popper/Popper.tsx
@@ -1,8 +1,12 @@
-import React, { FC, ReactNode, useEffect, useMemo, useRef, useState } from "react";
+import React, { FC, MouseEvent as ReactMouseEvent, ReactNode, useEffect, useMemo, useRef, useState } from "react";
import classNames from "classnames";
import ReactDOM from "react-dom";
import "./style.scss";
import useClickOutside from "../../../hooks/useClickOutside";
+import useDeviceDetect from "../../../hooks/useDeviceDetect";
+import Button from "../Button/Button";
+import { CloseIcon } from "../Icons";
+import { useLocation, useNavigate } from "react-router-dom";
interface PopperProps {
children: ReactNode
@@ -14,6 +18,8 @@ interface PopperProps {
offset?: {top: number, left: number}
clickOutside?: boolean,
fullWidth?: boolean
+ title?: string
+ disabledFullScreen?: boolean
}
const Popper: FC
= ({
@@ -24,10 +30,14 @@ const Popper: FC = ({
onClose,
offset = { top: 6, left: 0 },
clickOutside = true,
- fullWidth
+ fullWidth,
+ title,
+ disabledFullScreen
}) => {
-
- const [isOpen, setIsOpen] = useState(true);
+ const { isMobile } = useDeviceDetect();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const [isOpen, setIsOpen] = useState(false);
const [popperSize, setPopperSize] = useState({ width: 0, height: 0 });
const popperRef = useRef(null);
@@ -50,6 +60,13 @@ const Popper: FC = ({
useEffect(() => {
if (!isOpen && onClose) onClose();
+ if (isOpen && isMobile && !disabledFullScreen) {
+ document.body.style.overflow = "hidden";
+ }
+
+ return () => {
+ document.body.style.overflow = "auto";
+ };
}, [isOpen]);
useEffect(() => {
@@ -63,7 +80,7 @@ const Popper: FC = ({
const popperStyle = useMemo(() => {
const buttonEl = buttonRef.current;
- if (!buttonEl|| !isOpen) return {};
+ if (!buttonEl || !isOpen) return {};
const buttonPos = buttonEl.getBoundingClientRect();
@@ -104,28 +121,63 @@ const Popper: FC = ({
return position;
},[buttonRef, placement, isOpen, children, fullWidth]);
+ const handleClickClose = (e: ReactMouseEvent) => {
+ e.stopPropagation();
+ onClose();
+ };
+
if (clickOutside) useClickOutside(popperRef, () => setIsOpen(false), buttonRef);
useEffect(() => {
- if (!popperRef.current || !isOpen) return;
+ if (!popperRef.current || !isOpen || (isMobile && !disabledFullScreen)) return;
const { right, width } = popperRef.current.getBoundingClientRect();
- if (right > window.innerWidth) popperRef.current.style.left = `${window.innerWidth - 20 -width}px`;
+ if (right > window.innerWidth) {
+ const left = window.innerWidth - 20 - width;
+ popperRef.current.style.left = left < window.innerWidth ? "0" : `${left}px`;
+ }
}, [isOpen, popperRef]);
+ const handlePopstate = () => {
+ if (isOpen && isMobile && !disabledFullScreen) {
+ navigate(location, { replace: true });
+ onClose();
+ }
+ };
+
+ useEffect(() => {
+ window.addEventListener("popstate", handlePopstate);
+
+ return () => {
+ window.removeEventListener("popstate", handlePopstate);
+ };
+ }, [isOpen, isMobile, disabledFullScreen, location]);
const popperClasses = classNames({
"vm-popper": true,
- "vm-popper_open": isOpen,
+ "vm-popper_mobile": isMobile && !disabledFullScreen,
+ "vm-popper_open": (isMobile || Object.keys(popperStyle).length) && isOpen,
});
return (
<>
- {isOpen && ReactDOM.createPortal((
+ {(isOpen || !popperSize.width) && ReactDOM.createPortal((
+ {(title || (isMobile && !disabledFullScreen)) && (
+
+ )}
{children}
), document.body)}
>
diff --git a/app/vmui/packages/vmui/src/components/Main/Popper/style.scss b/app/vmui/packages/vmui/src/components/Main/Popper/style.scss
index 9c8169e15..67c039484 100644
--- a/app/vmui/packages/vmui/src/components/Main/Popper/style.scss
+++ b/app/vmui/packages/vmui/src/components/Main/Popper/style.scss
@@ -17,6 +17,38 @@
animation: vm-slider 150ms cubic-bezier(0.280, 0.840, 0.420, 1.1);
pointer-events: auto;
}
+
+ &_mobile {
+ position: fixed;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ width: 100%;
+ border-radius: 0;
+ overflow: auto;
+ animation: none;
+ }
+
+ &-header {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ gap: $padding-small;
+ align-items: center;
+ justify-content: space-between;
+ background-color: $color-background-block;
+ padding: $padding-small $padding-small $padding-small $padding-global;
+ border-radius: $border-radius-small $border-radius-small 0 0;
+ color: $color-text;
+ border-bottom: $border-divider;
+ margin-bottom: $padding-global;
+ min-height: 51px;
+
+ &__title {
+ font-weight: bold;
+ user-select: none;
+ }
+ }
}
@keyframes vm-slider {
diff --git a/app/vmui/packages/vmui/src/components/Main/Select/Select.tsx b/app/vmui/packages/vmui/src/components/Main/Select/Select.tsx
index 9182dc875..f0c45cacd 100644
--- a/app/vmui/packages/vmui/src/components/Main/Select/Select.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/Select/Select.tsx
@@ -5,6 +5,7 @@ import { FormEvent, MouseEvent } from "react";
import Autocomplete from "../Autocomplete/Autocomplete";
import { useAppState } from "../../../state/common/StateContext";
import "./style.scss";
+import useDeviceDetect from "../../../hooks/useDeviceDetect";
interface SelectProps {
value: string | string[]
@@ -13,6 +14,7 @@ interface SelectProps {
placeholder?: string
noOptionsText?: string
clearable?: boolean
+ searchable?: boolean
autofocus?: boolean
onChange: (value: string) => void
}
@@ -24,10 +26,12 @@ const Select: FC = ({
placeholder,
noOptionsText,
clearable = false,
+ searchable = false,
autofocus,
onChange
}) => {
const { isDarkTheme } = useAppState();
+ const { isMobile } = useDeviceDetect();
const [search, setSearch] = useState("");
const autocompleteAnchorEl = useRef(null);
@@ -95,7 +99,7 @@ const Select: FC = ({
}, [openList, inputRef]);
useEffect(() => {
- if (!autofocus || !inputRef.current) return;
+ if (!autofocus || !inputRef.current || isMobile) return;
inputRef.current.focus();
}, [autofocus, inputRef]);
@@ -120,25 +124,33 @@ const Select: FC = ({
ref={autocompleteAnchorEl}
>
{label && {label}}
{clearable && value && (
@@ -159,6 +171,7 @@ const Select: FC = ({
void
}
const Switch: FC = ({
- value = false, disabled = false, label, color = "secondary", onChange
+ value = false,
+ disabled = false,
+ label,
+ color = "secondary",
+ fullWidth,
+ onChange
}) => {
const toggleSwitch = () => {
if (disabled) return;
@@ -21,6 +27,7 @@ const Switch: FC = ({
const switchClasses = classNames({
"vm-switch": true,
+ "vm-switch_full-width": fullWidth,
"vm-switch_disabled": disabled,
"vm-switch_active": value,
[`vm-switch_${color}_active`]: value,
diff --git a/app/vmui/packages/vmui/src/components/Main/Switch/style.scss b/app/vmui/packages/vmui/src/components/Main/Switch/style.scss
index c1f924f29..b83863be4 100644
--- a/app/vmui/packages/vmui/src/components/Main/Switch/style.scss
+++ b/app/vmui/packages/vmui/src/components/Main/Switch/style.scss
@@ -11,6 +11,16 @@ $switch-border-radius: $switch-handle-size + ($switch-padding * 2);
align-items: center;
justify-content: flex-start;
cursor: pointer;
+ user-select: none;
+
+ &_full-width {
+ justify-content: space-between;
+ flex-direction: row-reverse;
+ }
+
+ &_full-width &__label {
+ margin-left: 0;
+ }
&_disabled {
opacity: 0.6;
diff --git a/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx b/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx
index cbfccacbf..e2ccdaf30 100644
--- a/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx
+++ b/app/vmui/packages/vmui/src/components/Main/TextField/TextField.tsx
@@ -2,6 +2,7 @@ import React, { FC, KeyboardEvent, useEffect, useRef, HTMLInputTypeAttribute, Re
import classNames from "classnames";
import { useMemo } from "preact/compat";
import { useAppState } from "../../../state/common/StateContext";
+import useDeviceDetect from "../../../hooks/useDeviceDetect";
import "./style.scss";
interface TextFieldProps {
@@ -15,6 +16,7 @@ interface TextFieldProps {
disabled?: boolean
autofocus?: boolean
helperText?: string
+ inputmode?: "search" | "text" | "email" | "tel" | "url" | "none" | "numeric" | "decimal"
onChange?: (value: string) => void
onEnter?: () => void
onKeyDown?: (e: KeyboardEvent) => void
@@ -33,6 +35,7 @@ const TextField: FC = ({
disabled = false,
autofocus = false,
helperText,
+ inputmode = "text",
onChange,
onEnter,
onKeyDown,
@@ -40,6 +43,7 @@ const TextField: FC = ({
onBlur
}) => {
const { isDarkTheme } = useAppState();
+ const { isMobile } = useDeviceDetect();
const inputRef = useRef(null);
const textareaRef = useRef(null);
@@ -67,7 +71,7 @@ const TextField: FC = ({
};
useEffect(() => {
- if (!autofocus) return;
+ if (!autofocus || isMobile) return;
fieldRef?.current?.focus && fieldRef.current.focus();
}, [fieldRef, autofocus]);
@@ -97,7 +101,9 @@ const TextField: FC = ({
ref={textareaRef}
value={value}
rows={1}
+ inputMode={inputmode}
placeholder={placeholder}
+ autoCapitalize={"none"}
onInput={handleChange}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
@@ -112,6 +118,8 @@ const TextField: FC = ({
value={value}
type={type}
placeholder={placeholder}
+ inputMode={inputmode}
+ autoCapitalize={"none"}
onInput={handleChange}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
diff --git a/app/vmui/packages/vmui/src/components/Main/TextField/style.scss b/app/vmui/packages/vmui/src/components/Main/TextField/style.scss
index 9fd651dc5..9e70bcc7c 100644
--- a/app/vmui/packages/vmui/src/components/Main/TextField/style.scss
+++ b/app/vmui/packages/vmui/src/components/Main/TextField/style.scss
@@ -43,6 +43,11 @@
-webkit-line-clamp: 2; /* number of lines to show */
line-clamp: 2;
-webkit-box-orient: vertical;
+
+ @media (max-width: 500px) {
+ -webkit-line-clamp: 1; /* number of lines to show */
+ line-clamp: 1;
+ }
}
&__label {
diff --git a/app/vmui/packages/vmui/src/components/Main/ThemeProvider/ThemeProvider.ts b/app/vmui/packages/vmui/src/components/Main/ThemeProvider/ThemeProvider.ts
index 75d8be6ca..c0905fe0e 100644
--- a/app/vmui/packages/vmui/src/components/Main/ThemeProvider/ThemeProvider.ts
+++ b/app/vmui/packages/vmui/src/components/Main/ThemeProvider/ThemeProvider.ts
@@ -7,6 +7,7 @@ import { darkPalette, lightPalette } from "../../../constants/palette";
import { Theme } from "../../../types";
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
import useSystemTheme from "../../../hooks/useSystemTheme";
+import useResize from "../../../hooks/useResize";
interface ThemeProviderProps {
onLoaded: (val: boolean) => void
@@ -28,6 +29,7 @@ export const ThemeProvider: FC = ({ onLoaded }) => {
const { theme } = useAppState();
const isDarkTheme = useSystemTheme();
const dispatch = useAppDispatch();
+ const windowSize = useResize(document.body);
const [palette, setPalette] = useState({
[Theme.dark]: darkPalette,
@@ -93,6 +95,7 @@ export const ThemeProvider: FC = ({ onLoaded }) => {
setTheme();
}, [palette]);
+ useEffect(setScrollbarSize, [windowSize]);
useEffect(updatePalette, [theme, isDarkTheme]);
useEffect(() => {
diff --git a/app/vmui/packages/vmui/src/components/TraceQuery/NestedNav/NestedNav.tsx b/app/vmui/packages/vmui/src/components/TraceQuery/NestedNav/NestedNav.tsx
index 909142ec5..73d081ed6 100644
--- a/app/vmui/packages/vmui/src/components/TraceQuery/NestedNav/NestedNav.tsx
+++ b/app/vmui/packages/vmui/src/components/TraceQuery/NestedNav/NestedNav.tsx
@@ -5,6 +5,7 @@ import { ArrowDownIcon } from "../../Main/Icons";
import "./style.scss";
import classNames from "classnames";
import { useAppState } from "../../../state/common/StateContext";
+import useDeviceDetect from "../../../hooks/useDeviceDetect";
interface RecursiveProps {
trace: Trace;
@@ -17,6 +18,7 @@ interface OpenLevels {
const NestedNav: FC = ({ trace, totalMsec }) => {
const { isDarkTheme } = useAppState();
+ const { isMobile } = useDeviceDetect();
const [openLevels, setOpenLevels] = useState({} as OpenLevels);
const handleListClick = (level: number) => () => {
@@ -32,6 +34,7 @@ const NestedNav: FC = ({ trace, totalMsec }) => {
className={classNames({
"vm-nested-nav": true,
"vm-nested-nav_dark": isDarkTheme,
+ "vm-nested-nav_mobile": isMobile,
})}
>
= ({ traces, jsonEditor = false, onDeleteClick }) => {
+ const { isMobile } = useDeviceDetect();
const [openTrace, setOpenTrace] = useState(null);
const handleCloseJson = () => {
@@ -77,7 +80,12 @@ const TracingsView: FC = ({ traces, jsonEditor = false, onDelete
/>
-