diff --git a/app/vmui/packages/vmui/src/components/Configurators/AdditionalSettings/AdditionalSettings.tsx b/app/vmui/packages/vmui/src/components/Configurators/AdditionalSettings/AdditionalSettings.tsx index dd71c0901..1ea20aeaa 100644 --- a/app/vmui/packages/vmui/src/components/Configurators/AdditionalSettings/AdditionalSettings.tsx +++ b/app/vmui/packages/vmui/src/components/Configurators/AdditionalSettings/AdditionalSettings.tsx @@ -1,11 +1,15 @@ -import React, { FC } from "preact/compat"; +import React, { FC, useRef, useState } from "preact/compat"; import { useCustomPanelDispatch, useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext"; import { useQueryDispatch, useQueryState } from "../../../state/query/QueryStateContext"; import "./style.scss"; import Switch from "../../Main/Switch/Switch"; +import useDeviceDetect from "../../../hooks/useDeviceDetect"; +import Popper from "../../Main/Popper/Popper"; +import { TuneIcon } from "../../Main/Icons"; +import Button from "../../Main/Button/Button"; +import classNames from "classnames"; -const AdditionalSettings: FC = () => { - +const AdditionalSettingsControls: FC<{isMobile?: boolean}> = ({ isMobile }) => { const { autocomplete } = useQueryState(); const queryDispatch = useQueryDispatch(); @@ -24,23 +28,72 @@ const AdditionalSettings: FC = () => { queryDispatch({ type: "TOGGLE_AUTOCOMPLETE" }); }; - return
- - - -
; + return ( +
+ + + +
+ ); +}; + +const AdditionalSettings: FC = () => { + const { isMobile } = useDeviceDetect(); + const [openList, setOpenList] = useState(false); + const targetRef = useRef(null); + + const handleToggleList = () => { + setOpenList(prev => !prev); + }; + + const handleCloseList = () => { + setOpenList(false); + }; + + if (isMobile) { + return ( + <> +
+
+ + + + + ); + } + + return ; }; export default AdditionalSettings; diff --git a/app/vmui/packages/vmui/src/components/Configurators/AdditionalSettings/style.scss b/app/vmui/packages/vmui/src/components/Configurators/AdditionalSettings/style.scss index 6ba16cbac..82433db55 100644 --- a/app/vmui/packages/vmui/src/components/Configurators/AdditionalSettings/style.scss +++ b/app/vmui/packages/vmui/src/components/Configurators/AdditionalSettings/style.scss @@ -5,10 +5,19 @@ align-items: center; justify-content: flex-start; flex-wrap: wrap; - gap: 24px; + gap: $padding-global; &__input { flex-basis: 160px; margin-bottom: -6px; } + + &_mobile { + display: grid; + grid-template-columns: 1fr; + align-items: flex-start; + padding: 0 $padding-global; + gap: $padding-medium; + width: 100%; + } } diff --git a/app/vmui/packages/vmui/src/components/Configurators/CardinalityDatePicker/CardinalityDatePicker.tsx b/app/vmui/packages/vmui/src/components/Configurators/CardinalityDatePicker/CardinalityDatePicker.tsx index 04fa6c3c8..ad88363bf 100644 --- a/app/vmui/packages/vmui/src/components/Configurators/CardinalityDatePicker/CardinalityDatePicker.tsx +++ b/app/vmui/packages/vmui/src/components/Configurators/CardinalityDatePicker/CardinalityDatePicker.tsx @@ -2,13 +2,15 @@ import React, { FC, useMemo, useRef } from "preact/compat"; import { useCardinalityState, useCardinalityDispatch } from "../../../state/cardinality/CardinalityStateContext"; import dayjs from "dayjs"; import Button from "../../Main/Button/Button"; -import { CalendarIcon } from "../../Main/Icons"; +import { ArrowDownIcon, CalendarIcon } from "../../Main/Icons"; import Tooltip from "../../Main/Tooltip/Tooltip"; import { getAppModeEnable } from "../../../utils/app-mode"; import { DATE_FORMAT } from "../../../constants/date"; import DatePicker from "../../Main/DatePicker/DatePicker"; +import useDeviceDetect from "../../../hooks/useDeviceDetect"; const CardinalityDatePicker: FC = () => { + const { isMobile } = useDeviceDetect(); const appModeEnable = getAppModeEnable(); const buttonRef = useRef(null); @@ -24,18 +26,30 @@ const CardinalityDatePicker: FC = () => { return (
- - - + {isMobile ? ( +
+ +
+ Date control + {dateFormatted} +
+ +
+ ) : ( + + + + )}
= ({ showTitle }) => { +const GlobalSettings: FC = () => { const { isMobile } = useDeviceDetect(); const appModeEnable = getAppModeEnable(); @@ -42,7 +42,6 @@ const GlobalSettings: FC<{showTitle?: boolean}> = ({ showTitle }) => { dispatch({ type: "SET_SERVER", payload: serverUrl }); timeDispatch({ type: "SET_TIMEZONE", payload: timezone }); customPanelDispatch({ type: "SET_SERIES_LIMITS", payload: limits }); - handleClose(); }; useEffect(() => { @@ -51,22 +50,30 @@ const GlobalSettings: FC<{showTitle?: boolean}> = ({ showTitle }) => { }, [stateServerUrl]); return <> - - - + +
+ {title} +
+ +
+ ) : ( + + - - )} diff --git a/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/LimitsConfigurator/LimitsConfigurator.tsx b/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/LimitsConfigurator/LimitsConfigurator.tsx index 907b5db75..d4ce7e0f7 100644 --- a/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/LimitsConfigurator/LimitsConfigurator.tsx +++ b/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/LimitsConfigurator/LimitsConfigurator.tsx @@ -6,6 +6,8 @@ import { InfoIcon, RestartIcon } from "../../../Main/Icons"; import Button from "../../../Main/Button/Button"; import { DEFAULT_MAX_SERIES } from "../../../../constants/graph"; import "./style.scss"; +import classNames from "classnames"; +import useDeviceDetect from "../../../../hooks/useDeviceDetect"; export interface ServerConfiguratorProps { limits: SeriesLimits @@ -20,6 +22,7 @@ const fields: {label: string, type: DisplayType}[] = [ ]; const LimitsConfigurator: FC = ({ limits, onChange , onEnter }) => { + const { isMobile } = useDeviceDetect(); const [error, setError] = useState({ table: "", @@ -68,7 +71,12 @@ const LimitsConfigurator: FC = ({ limits, onChange , on -
+
{fields.map(f => (
void onEnter: () => void + onBlur: () => void } -const ServerConfigurator: FC = ({ serverUrl, onChange , onEnter }) => { +const ServerConfigurator: FC = ({ serverUrl, onChange , onEnter, onBlur }) => { const [error, setError] = useState(""); @@ -29,6 +30,8 @@ const ServerConfigurator: FC = ({ serverUrl, onChange , error={error} onChange={onChangeServer} onEnter={onEnter} + onBlur={onBlur} + inputmode="url" /> ); }; diff --git a/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration.tsx b/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration.tsx index 8627328bc..ec57de90e 100644 --- a/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration.tsx +++ b/app/vmui/packages/vmui/src/components/Configurators/GlobalSettings/TenantsConfiguration/TenantsConfiguration.tsx @@ -41,7 +41,7 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => { }; const showTenantSelector = useMemo(() => { - const id = getTenantIdFromUrl(serverUrl); + const id = true; //getTenantIdFromUrl(serverUrl); return accountIds.length > 1 && id; }, [accountIds, serverUrl]); @@ -81,26 +81,40 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
- + +
+ ) : ( + + )}
= ({ accountIds }) => { placement="bottom-right" onClose={handleCloseOptions} buttonRef={optionsButtonRef} + title={isMobile ? "Define Tenant ID" : undefined} > -
+
{accountIdsFiltered.map(id => (
= ({ timezoneState, onChange }) => { - + const { isMobile } = useDeviceDetect(); const timezones = getTimezoneList(); const [openList, setOpenList] = useState(false); @@ -92,8 +93,14 @@ const Timezones: FC = ({ timezoneState, onChange }) => { placement="bottom-left" onClose={handleCloseList} fullWidth + title={isMobile ? "Time zone" : undefined} > -
+
= ({ yaxis, setYaxisLimits, toggleEnableLimits }) => { + const { isMobile } = useDeviceDetect(); const axes = useMemo(() => Object.keys(yaxis.limits.range), [yaxis.limits.range]); @@ -27,11 +30,17 @@ const AxesLimitsConfigurator: FC = ({ yaxis, setYax debouncedOnChangeLimit(val, axis, index); }; - return
+ return
{axes.map(axis => ( diff --git a/app/vmui/packages/vmui/src/components/Configurators/GraphSettings/AxesLimitsConfigurator/style.scss b/app/vmui/packages/vmui/src/components/Configurators/GraphSettings/AxesLimitsConfigurator/style.scss index 68246f675..c76bb78c2 100644 --- a/app/vmui/packages/vmui/src/components/Configurators/GraphSettings/AxesLimitsConfigurator/style.scss +++ b/app/vmui/packages/vmui/src/components/Configurators/GraphSettings/AxesLimitsConfigurator/style.scss @@ -6,6 +6,16 @@ gap: $padding-global; max-width: 300px; + &_mobile { + width: 100%; + max-width: 100%; + gap: $padding-medium; + } + + &_mobile &-list__inputs { + grid-template-columns: repeat(2, 1fr); + } + &-list { display: grid; align-items: center; diff --git a/app/vmui/packages/vmui/src/components/Configurators/GraphSettings/GraphSettings.tsx b/app/vmui/packages/vmui/src/components/Configurators/GraphSettings/GraphSettings.tsx index c7bdf3008..d46de83fd 100644 --- a/app/vmui/packages/vmui/src/components/Configurators/GraphSettings/GraphSettings.tsx +++ b/app/vmui/packages/vmui/src/components/Configurators/GraphSettings/GraphSettings.tsx @@ -1,9 +1,8 @@ import React, { FC, useRef, useState } from "preact/compat"; import AxesLimitsConfigurator from "./AxesLimitsConfigurator/AxesLimitsConfigurator"; import { AxisRange, YaxisState } from "../../../state/graph/reducer"; -import { CloseIcon, SettingsIcon } from "../../Main/Icons"; +import { SettingsIcon } from "../../Main/Icons"; import Button from "../../Main/Button/Button"; -import useClickOutside from "../../../hooks/useClickOutside"; import Popper from "../../Main/Popper/Popper"; import "./style.scss"; import Tooltip from "../../Main/Tooltip/Tooltip"; @@ -20,7 +19,6 @@ const GraphSettings: FC = ({ yaxis, setYaxisLimits, toggleEn const popperRef = useRef(null); const [openPopper, setOpenPopper] = useState(false); const buttonRef = useRef(null); - useClickOutside(popperRef, () => setOpenPopper(false), buttonRef); const toggleOpen = () => { setOpenPopper(prev => !prev); @@ -46,22 +44,12 @@ const GraphSettings: FC = ({ yaxis, setYaxisLimits, toggleEn buttonRef={buttonRef} placement="bottom-right" onClose={handleClose} + title={title} >
-
-

- {title} -

-
= ({ onKeyDown={handleKeyDown} onChange={onChange} disabled={disabled} + inputmode={"search"} /> {autocomplete && ( { const appModeEnable = getAppModeEnable(); + const { isMobile } = useDeviceDetect(); const { customStep: value } = useGraphState(); const { period: { step: defaultStep } } = useTimeState(); @@ -103,29 +106,49 @@ const StepConfigurator: FC = () => { className="vm-step-control" ref={buttonRef} > - - - + + + )} -
+
{ - const windowSize = useResize(document.body); + const { isMobile } = useDeviceDetect(); const dispatch = useTimeDispatch(); const appModeEnable = getAppModeEnable(); @@ -85,11 +85,11 @@ export const ExecutionControls: FC = () => {
- {windowSize.width > 360 && ( + {!isMobile && (
- )} - onClick={toggleOpenOptions} - > - {selectedDelay.title} - + {isMobile ? ( +
+ +
+ Auto-refresh + {selectedDelay.title} +
+
- + ) : ( + +
+
+ )} + onClick={toggleOpenOptions} + > + {selectedDelay.title} + +
+ + )}
{ placement="bottom-right" onClose={handleCloseOptions} buttonRef={optionsButtonRef} + title={isMobile ? "Auto-refresh duration" : undefined} > -
+
{delayOptions.map(d => (
void; @@ -9,17 +10,24 @@ interface TimeDurationSelector { } const TimeDurationSelector: FC = ({ relativeTime, setDuration }) => { + const { isMobile } = useDeviceDetect(); const createHandlerClick = (value: { duration: string, until: Date, id: string }) => () => { setDuration(value); }; return ( -
+
{relativeTimeOptions.map(({ id, duration, until, title }) => (
{ + const { isMobile } = useDeviceDetect(); const { isDarkTheme } = useAppState(); const wrapperRef = useRef(null); const documentSize = useResize(document.body); @@ -112,6 +114,7 @@ export const TimeSelector: FC = () => { }, [timezone]); useClickOutside(wrapperRef, (e) => { + if (isMobile) return; const target = e.target as HTMLElement; const isFromButton = fromRef?.current && fromRef.current.contains(target); const isUntilButton = untilRef?.current && untilRef.current.contains(target); @@ -123,17 +126,31 @@ export const TimeSelector: FC = () => { return <>
- - - + +
+ Time range + {dateTitle} +
+ +
+ ) : ( + + + + )}
{ placement="bottom-right" onClose={handleCloseOptions} clickOutside={false} + title={isMobile ? "Time range controls" : ""} >
@@ -161,6 +182,7 @@ export const TimeSelector: FC = () => { {formFormat} { {untilFormat} = ({ isBucket, height }) => { + const { isMobile } = useDeviceDetect(); const { customStep, yaxis } = useGraphState(); const { period } = useTimeState(); @@ -92,7 +95,12 @@ with (q = ${queryBase}) ( }; return ( -
+
{isLoading && } {error && {error}} {warning && diff --git a/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricGraph/style.scss b/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricGraph/style.scss index 9fe6dbcc0..614bc922a 100644 --- a/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricGraph/style.scss +++ b/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricGraph/style.scss @@ -3,6 +3,10 @@ .vm-explore-metrics-graph { padding: 0 $padding-global $padding-global; + &_mobile { + padding: 0 $padding-global $padding-global; + } + &__warning { display: grid; grid-template-columns: 1fr auto; diff --git a/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricItem/ExploreMetricItem.tsx b/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricItem/ExploreMetricItem.tsx index af462fd28..4f07c68d1 100644 --- a/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricItem/ExploreMetricItem.tsx +++ b/app/vmui/packages/vmui/src/components/ExploreMetrics/ExploreMetricItem/ExploreMetricItem.tsx @@ -10,6 +10,7 @@ interface ExploreMetricItemProps { job: string instance: string index: number + length: number size: GraphSize onRemoveItem: (name: string) => void onChangeOrder: (name: string, oldIndex: number, newIndex: number) => void @@ -20,6 +21,7 @@ const ExploreMetricItem: FC = ({ job, instance, index, + length, size, onRemoveItem, onChangeOrder, @@ -42,6 +44,7 @@ const ExploreMetricItem: FC = ({ = ({ name, index, + length, isBucket, rateEnabled, onChangeRate, onRemoveItem, onChangeOrder, }) => { + const { isMobile } = useDeviceDetect(); + const [openOptions, setOpenOptions] = useState(false); const handleClickRemove = () => { onRemoveItem(name); @@ -38,6 +44,76 @@ const ExploreMetricItemHeader: FC = ({ onChangeOrder(name, index, index - 1); }; + const handleOpenOptions = () => { + setOpenOptions(true); + }; + + const handleCloseOptions = () => { + setOpenOptions(false); + }; + + if (isMobile) { + return ( +
+
{name}
+
+ {!isBucket && ( +
+ enable rate()} + value={rateEnabled} + onChange={onChangeRate} + fullWidth + /> +

+ calculates the average per-second speed of metrics change +

+
+ )} + +
+ + )} +
+ ); + } + return (
@@ -65,15 +141,17 @@ const ExploreMetricItemHeader: FC = ({
{name}
{!isBucket && ( - - enable rate()} - value={rateEnabled} - onChange={onChangeRate} - /> - +
+ + enable rate()} + value={rateEnabled} + onChange={onChangeRate} + /> + +
)} -
+
+ + + + + ); + } + + 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} > - +
= ({ "vm-header-sidebar-menu_open": openMenu })} > -
- -
= ({ />
- {!isMobile && }
diff --git a/app/vmui/packages/vmui/src/components/Layout/Header/SidebarNav/style.scss b/app/vmui/packages/vmui/src/components/Layout/Header/SidebarNav/style.scss index aa6ef2285..e251106c3 100644 --- a/app/vmui/packages/vmui/src/components/Layout/Header/SidebarNav/style.scss +++ b/app/vmui/packages/vmui/src/components/Layout/Header/SidebarNav/style.scss @@ -1,5 +1,7 @@ @use "src/styles/variables" as *; +$sidebar-transition: cubic-bezier(0.280, 0.840, 0.420, 1); + .vm-header-sidebar { width: 24px; height: 24px; @@ -7,14 +9,19 @@ background-color: inherit; &-button { + display: flex; + align-items: center; + justify-content: center; position: absolute; - left: $padding-global; - top: $padding-global; - transition: left 300ms cubic-bezier(0.280, 0.840, 0.420, 1); + left: 0; + top: 0; + height: 51px; + width: 51px; + transition: left 350ms $sidebar-transition; &_open { position: fixed; - left: calc(182px - $padding-global); + left: 149px; z-index: 102; } } @@ -26,14 +33,14 @@ display: grid; gap: $padding-global; padding: $padding-global; - grid-template-rows: auto 1fr auto; + grid-template-rows: 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); + transition: transform 300ms $sidebar-transition; box-shadow: $box-shadow-popper; &_open { diff --git a/app/vmui/packages/vmui/src/components/Layout/Header/style.scss b/app/vmui/packages/vmui/src/components/Layout/Header/style.scss index 76cef773d..361cfff76 100644 --- a/app/vmui/packages/vmui/src/components/Layout/Header/style.scss +++ b/app/vmui/packages/vmui/src/components/Layout/Header/style.scss @@ -21,6 +21,12 @@ padding: $padding-small; } + &_mobile { + display: grid; + grid-template-columns: 33px 1fr 33px; + justify-content: space-between; + } + &_dark { .vm-header-button, button:before, @@ -50,18 +56,16 @@ max-width: 65px; min-width: 65px; } + + &_mobile { + max-width: 65px; + min-width: 65px; + margin: 0 auto; + } } &-nav { font-size: $font-size-small; font-weight: bold; } - - &__settings { - display: flex; - align-items: center; - justify-content: flex-end; - gap: $padding-small; - flex-grow: 1; - } } diff --git a/app/vmui/packages/vmui/src/components/Layout/Layout.tsx b/app/vmui/packages/vmui/src/components/Layout/Layout.tsx index 831612caf..1d89a2984 100644 --- a/app/vmui/packages/vmui/src/components/Layout/Layout.tsx +++ b/app/vmui/packages/vmui/src/components/Layout/Layout.tsx @@ -7,9 +7,11 @@ import classNames from "classnames"; import Footer from "./Footer/Footer"; import { routerOptions } from "../../router"; import { useFetchDashboards } from "../../pages/PredefinedPanels/hooks/useFetchDashboards"; +import useDeviceDetect from "../../hooks/useDeviceDetect"; const Layout: FC = () => { const appModeEnable = getAppModeEnable(); + const { isMobile } = useDeviceDetect(); useFetchDashboards(); const { pathname } = useLocation(); @@ -24,6 +26,7 @@ const Layout: FC = () => {
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)) && ( +
+

{title}

+ +
+ )} {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} >
- {selectedValues && selectedValues.map(item => ( + {!isMobile && selectedValues && selectedValues.map(item => (
- {item} + {item}
))} - + {isMobile && !!selectedValues?.length && ( + + selected {selectedValues.length} + + )} + {!isMobile || (isMobile && (!selectedValues || !selectedValues?.length)) && ( + + )}
{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 />
-