vmui: improve mobile ui (#3848)

* feat: improve mobile ui

* feat: improve mobile ui

* fix: change style server url

* fix: improve ExploreMetrics mobile

* fix: display global settings on all pages
This commit is contained in:
Yury Molodov 2023-02-24 04:18:49 +01:00 committed by GitHub
parent a02cf92fd1
commit d4fc0ed874
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
106 changed files with 1554 additions and 445 deletions

View file

@ -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 <div className="vm-additional-settings">
return (
<div
className={classNames({
"vm-additional-settings": true,
"vm-additional-settings_mobile": isMobile
})}
>
<Switch
label={"Autocomplete"}
value={autocomplete}
onChange={onChangeAutocomplete}
fullWidth={isMobile}
/>
<Switch
label={"Disable cache"}
value={nocache}
onChange={onChangeCache}
fullWidth={isMobile}
/>
<Switch
label={"Trace query"}
value={isTracingEnabled}
onChange={onChangeQueryTracing}
fullWidth={isMobile}
/>
</div>;
</div>
);
};
const AdditionalSettings: FC = () => {
const { isMobile } = useDeviceDetect();
const [openList, setOpenList] = useState(false);
const targetRef = useRef<HTMLDivElement>(null);
const handleToggleList = () => {
setOpenList(prev => !prev);
};
const handleCloseList = () => {
setOpenList(false);
};
if (isMobile) {
return (
<>
<div ref={targetRef}>
<Button
variant="outlined"
startIcon={<TuneIcon/>}
onClick={handleToggleList}
/>
</div>
<Popper
open={openList}
buttonRef={targetRef}
placement="bottom-left"
onClose={handleCloseList}
title={"Query settings"}
>
<AdditionalSettingsControls isMobile={isMobile}/>
</Popper>
</>
);
}
return <AdditionalSettingsControls/>;
};
export default AdditionalSettings;

View file

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

View file

@ -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<HTMLDivElement>(null);
@ -24,6 +26,16 @@ const CardinalityDatePicker: FC = () => {
return (
<div>
<div ref={buttonRef}>
{isMobile ? (
<div className="vm-mobile-option">
<span className="vm-mobile-option__icon"><CalendarIcon/></span>
<div className="vm-mobile-option-text">
<span className="vm-mobile-option-text__label">Date control</span>
<span className="vm-mobile-option-text__value">{dateFormatted}</span>
</div>
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
</div>
) : (
<Tooltip title="Date control">
<Button
className={appModeEnable ? "" : "vm-header-button"}
@ -34,8 +46,10 @@ const CardinalityDatePicker: FC = () => {
{dateFormatted}
</Button>
</Tooltip>
)}
</div>
<DatePicker
label="Date control"
date={date || ""}
format={DATE_FORMAT}
onChange={handleChangeDate}

View file

@ -1,7 +1,7 @@
import React, { FC, useEffect, useState } from "preact/compat";
import ServerConfigurator from "./ServerConfigurator/ServerConfigurator";
import { useAppDispatch, useAppState } from "../../../state/common/StateContext";
import { SettingsIcon } from "../../Main/Icons";
import { ArrowDownIcon, SettingsIcon } from "../../Main/Icons";
import Button from "../../Main/Button/Button";
import Modal from "../../Main/Modal/Modal";
import "./style.scss";
@ -18,7 +18,7 @@ import useDeviceDetect from "../../../hooks/useDeviceDetect";
const title = "Settings";
const GlobalSettings: FC<{showTitle?: boolean}> = ({ 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,10 +50,19 @@ const GlobalSettings: FC<{showTitle?: boolean}> = ({ showTitle }) => {
}, [stateServerUrl]);
return <>
<Tooltip
open={showTitle === true ? false : undefined}
title={title}
{isMobile ? (
<div
className="vm-mobile-option"
onClick={handleOpen}
>
<span className="vm-mobile-option__icon"><SettingsIcon/></span>
<div className="vm-mobile-option-text">
<span className="vm-mobile-option-text__label">{title}</span>
</div>
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
</div>
) : (
<Tooltip title={title}>
<Button
className={classNames({
"vm-header-button": !appModeEnable
@ -63,10 +71,9 @@ const GlobalSettings: FC<{showTitle?: boolean}> = ({ showTitle }) => {
color="primary"
startIcon={<SettingsIcon/>}
onClick={handleOpen}
>
{showTitle && title}
</Button>
/>
</Tooltip>
)}
{open && (
<Modal
title={title}
@ -84,6 +91,7 @@ const GlobalSettings: FC<{showTitle?: boolean}> = ({ showTitle }) => {
serverUrl={serverUrl}
onChange={setServerUrl}
onEnter={handlerApply}
onBlur={handlerApply}
/>
</div>
)}
@ -105,21 +113,6 @@ const GlobalSettings: FC<{showTitle?: boolean}> = ({ showTitle }) => {
<ThemeControl/>
</div>
)}
<div className="vm-server-configurator__footer">
<Button
variant="outlined"
color="error"
onClick={handleClose}
>
Cancel
</Button>
<Button
variant="contained"
onClick={handlerApply}
>
apply
</Button>
</div>
</div>
</Modal>
)}

View file

@ -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<ServerConfiguratorProps> = ({ limits, onChange , onEnter }) => {
const { isMobile } = useDeviceDetect();
const [error, setError] = useState({
table: "",
@ -68,7 +71,12 @@ const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , on
</Button>
</div>
</div>
<div className="vm-limits-configurator__inputs">
<div
className={classNames({
"vm-limits-configurator__inputs": true,
"vm-limits-configurator__inputs_mobile": isMobile
})}
>
{fields.map(f => (
<div key={f.type}>
<TextField

View file

@ -18,6 +18,10 @@
justify-content: space-between;
gap: $padding-global;
&_mobile {
gap: $padding-small;
}
div {
flex-grow: 1;
}

View file

@ -7,9 +7,10 @@ export interface ServerConfiguratorProps {
serverUrl: string
onChange: (url: string) => void
onEnter: () => void
onBlur: () => void
}
const ServerConfigurator: FC<ServerConfiguratorProps> = ({ serverUrl, onChange , onEnter }) => {
const ServerConfigurator: FC<ServerConfiguratorProps> = ({ serverUrl, onChange , onEnter, onBlur }) => {
const [error, setError] = useState("");
@ -29,6 +30,8 @@ const ServerConfigurator: FC<ServerConfiguratorProps> = ({ serverUrl, onChange ,
error={error}
onChange={onChangeServer}
onEnter={onEnter}
onBlur={onBlur}
inputmode="url"
/>
);
};

View file

@ -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,13 +81,26 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
<div className="vm-tenant-input">
<Tooltip title="Define Tenant ID if you need request to another storage">
<div ref={optionsButtonRef}>
{isMobile ? (
<div
className="vm-mobile-option"
onClick={toggleOpenOptions}
>
<span className="vm-mobile-option__icon"><StorageIcon/></span>
<div className="vm-mobile-option-text">
<span className="vm-mobile-option-text__label">Tenant ID</span>
<span className="vm-mobile-option-text__value">{tenantIdState}</span>
</div>
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
</div>
) : (
<Button
className={appModeEnable ? "" : "vm-header-button"}
variant="contained"
color="primary"
fullWidth
startIcon={<StorageIcon/>}
endIcon={!isMobile ? (
endIcon={(
<div
className={classNames({
"vm-execution-controls-buttons__arrow": true,
@ -96,11 +109,12 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
>
<ArrowDownIcon/>
</div>
) : undefined}
)}
onClick={toggleOpenOptions}
>
{!isMobile && tenantIdState}
{tenantIdState}
</Button>
)}
</div>
</Tooltip>
<Popper
@ -108,20 +122,28 @@ const TenantsConfiguration: FC<{accountIds: string[]}> = ({ accountIds }) => {
placement="bottom-right"
onClose={handleCloseOptions}
buttonRef={optionsButtonRef}
title={isMobile ? "Define Tenant ID" : undefined}
>
<div
className={classNames({
"vm-list vm-tenant-input-list": true,
"vm-list vm-tenant-input-list_mobile": isMobile,
})}
>
<div className="vm-list vm-tenant-input-list">
<div className="vm-tenant-input-list__search">
<TextField
autofocus
label="Search"
value={search}
onChange={setSearch}
type="search"
/>
</div>
{accountIdsFiltered.map(id => (
<div
className={classNames({
"vm-list-item": true,
"vm-list-item_mobile": isMobile,
"vm-list-item_active": id === tenantIdState
})}
key={id}

View file

@ -9,10 +9,18 @@
overscroll-behavior: none;
border-radius: $border-radius-medium;
&_mobile {
max-height: calc(($vh * 100) - 70px);
}
&_mobile &__search {
padding: 0 $padding-global $padding-small;
}
&__search {
position: sticky;
top: 0;
padding: $padding-small;
padding: $padding-small $padding-global;
background-color: $color-background-block;
}
}

View file

@ -8,6 +8,7 @@ import dayjs from "dayjs";
import TextField from "../../../Main/TextField/TextField";
import { Timezone } from "../../../../types";
import "./style.scss";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
interface TimezonesProps {
timezoneState: string
@ -15,7 +16,7 @@ interface TimezonesProps {
}
const Timezones: FC<TimezonesProps> = ({ timezoneState, onChange }) => {
const { isMobile } = useDeviceDetect();
const timezones = getTimezoneList();
const [openList, setOpenList] = useState(false);
@ -92,8 +93,14 @@ const Timezones: FC<TimezonesProps> = ({ timezoneState, onChange }) => {
placement="bottom-left"
onClose={handleCloseList}
fullWidth
title={isMobile ? "Time zone" : undefined}
>
<div
className={classNames({
"vm-timezones-list": true,
"vm-timezones-list_mobile": isMobile,
})}
>
<div className="vm-timezones-list">
<div className="vm-timezones-list-header">
<div className="vm-timezones-list-header__search">
<TextField

View file

@ -51,6 +51,14 @@
border-radius: $border-radius-medium;
overflow: auto;
&_mobile {
max-height: calc(($vh * 100) - 70px);
}
&_mobile &-header__search {
padding: 0 $padding-global 0;
}
&-header {
position: sticky;
top: 0;

View file

@ -6,6 +6,7 @@
align-items: center;
gap: $padding-medium;
width: 600px;
padding-bottom: $padding-medium;
&_mobile {
grid-auto-rows: min-content;
@ -20,12 +21,6 @@
&__input {
width: 100%;
&_server {
display: grid;
grid-template-columns: 1fr auto;
gap: 0 $padding-small;
}
}
&__title {
@ -37,20 +32,4 @@
font-weight: bold;
margin-bottom: $padding-global;
}
&__footer {
display: inline-grid;
grid-template-columns: repeat(2, 1fr);
align-items: center;
justify-content: flex-end;
gap: $padding-small;
margin-left: auto;
margin-right: 0;
}
&_mobile &__footer {
align-items: flex-end;
flex-grow: 1;
width: 100%;
}
}

View file

@ -4,6 +4,8 @@ import { AxisRange, YaxisState } from "../../../../state/graph/reducer";
import "./style.scss";
import TextField from "../../../Main/TextField/TextField";
import Switch from "../../../Main/Switch/Switch";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import classNames from "classnames";
interface AxesLimitsConfiguratorProps {
yaxis: YaxisState,
@ -12,6 +14,7 @@ interface AxesLimitsConfiguratorProps {
}
const AxesLimitsConfigurator: FC<AxesLimitsConfiguratorProps> = ({ yaxis, setYaxisLimits, toggleEnableLimits }) => {
const { isMobile } = useDeviceDetect();
const axes = useMemo(() => Object.keys(yaxis.limits.range), [yaxis.limits.range]);
@ -27,11 +30,17 @@ const AxesLimitsConfigurator: FC<AxesLimitsConfiguratorProps> = ({ yaxis, setYax
debouncedOnChangeLimit(val, axis, index);
};
return <div className="vm-axes-limits">
return <div
className={classNames({
"vm-axes-limits": true,
"vm-axes-limits_mobile": isMobile
})}
>
<Switch
value={yaxis.limits.enable}
onChange={toggleEnableLimits}
label="Fix the limits for y-axis"
fullWidth={isMobile}
/>
<div className="vm-axes-limits-list">
{axes.map(axis => (

View file

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

View file

@ -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<GraphSettingsProps> = ({ yaxis, setYaxisLimits, toggleEn
const popperRef = useRef<HTMLDivElement>(null);
const [openPopper, setOpenPopper] = useState(false);
const buttonRef = useRef<HTMLDivElement>(null);
useClickOutside(popperRef, () => setOpenPopper(false), buttonRef);
const toggleOpen = () => {
setOpenPopper(prev => !prev);
@ -46,22 +44,12 @@ const GraphSettings: FC<GraphSettingsProps> = ({ yaxis, setYaxisLimits, toggleEn
buttonRef={buttonRef}
placement="bottom-right"
onClose={handleClose}
title={title}
>
<div
className="vm-graph-settings-popper"
ref={popperRef}
>
<div className="vm-popper-header">
<h3 className="vm-popper-header__title">
{title}
</h3>
<Button
size="small"
variant="text"
startIcon={<CloseIcon/>}
onClick={handleClose}
/>
</div>
<div className="vm-graph-settings-popper__body">
<AxesLimitsConfigurator
yaxis={yaxis}

View file

@ -78,9 +78,11 @@ const QueryEditor: FC<QueryEditorProps> = ({
onKeyDown={handleKeyDown}
onChange={onChange}
disabled={disabled}
inputmode={"search"}
/>
{autocomplete && (
<Autocomplete
disabledFullScreen
value={value}
options={options}
anchor={autocompleteAnchorEl}

View file

@ -1,5 +1,5 @@
import React, { FC, useEffect, useRef, useState } from "preact/compat";
import { RestartIcon, TimelineIcon } from "../../Main/Icons";
import { ArrowDownIcon, RestartIcon, TimelineIcon } from "../../Main/Icons";
import TextField from "../../Main/TextField/TextField";
import Button from "../../Main/Button/Button";
import Tooltip from "../../Main/Tooltip/Tooltip";
@ -11,9 +11,12 @@ import usePrevious from "../../../hooks/usePrevious";
import "./style.scss";
import { getAppModeEnable } from "../../../utils/app-mode";
import Popper from "../../Main/Popper/Popper";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import classNames from "classnames";
const StepConfigurator: FC = () => {
const appModeEnable = getAppModeEnable();
const { isMobile } = useDeviceDetect();
const { customStep: value } = useGraphState();
const { period: { step: defaultStep } } = useTimeState();
@ -103,6 +106,19 @@ const StepConfigurator: FC = () => {
className="vm-step-control"
ref={buttonRef}
>
{isMobile ? (
<div
className="vm-mobile-option"
onClick={toggleOpenOptions}
>
<span className="vm-mobile-option__icon"><TimelineIcon/></span>
<div className="vm-mobile-option-text">
<span className="vm-mobile-option-text__label">Step</span>
<span className="vm-mobile-option-text__value">{customStep}</span>
</div>
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
</div>
) : (
<Tooltip title="Query resolution step width">
<Button
className={appModeEnable ? "" : "vm-header-button"}
@ -119,13 +135,20 @@ const StepConfigurator: FC = () => {
</p>
</Button>
</Tooltip>
)}
<Popper
open={openOptions}
placement="bottom-right"
onClose={handleCloseOptions}
buttonRef={buttonRef}
title={isMobile ? "Query resolution step width" : undefined}
>
<div
className={classNames({
"vm-step-control-popper": true,
"vm-step-control-popper_mobile": isMobile,
})}
>
<div className="vm-step-control-popper">
<TextField
autofocus
label="Step value"

View file

@ -11,10 +11,6 @@
&__value {
display: inline;
margin-left: 3px;
@media (max-width: 500px) {
display: none;
}
}
&-popper {
@ -26,6 +22,16 @@
padding: $padding-global;
font-size: $font-size;
&_mobile {
padding: 0 $padding-global $padding-small;
max-width: 100%;
max-height: calc(($vh * 100) - 70px);
}
&_mobile &-info {
font-size: $font-size;
}
&-info {
font-size: $font-size-small;
line-height: 1.6;

View file

@ -10,5 +10,6 @@
&_mobile &__toggle {
display: flex;
min-width: 100%;
}
}

View file

@ -2,12 +2,12 @@ import React, { FC, useEffect, useRef, useState } from "preact/compat";
import { useTimeDispatch } from "../../../../state/time/TimeStateContext";
import { getAppModeEnable } from "../../../../utils/app-mode";
import Button from "../../../Main/Button/Button";
import { ArrowDownIcon, RefreshIcon } from "../../../Main/Icons";
import { ArrowDownIcon, RefreshIcon, RestartIcon } from "../../../Main/Icons";
import Popper from "../../../Main/Popper/Popper";
import "./style.scss";
import classNames from "classnames";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import useResize from "../../../../hooks/useResize";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
interface AutoRefreshOption {
seconds: number
@ -30,7 +30,7 @@ const delayOptions: AutoRefreshOption[] = [
];
export const ExecutionControls: FC = () => {
const windowSize = useResize(document.body);
const { isMobile } = useDeviceDetect();
const dispatch = useTimeDispatch();
const appModeEnable = getAppModeEnable();
@ -85,11 +85,11 @@ export const ExecutionControls: FC = () => {
<div
className={classNames({
"vm-execution-controls-buttons": true,
"vm-execution-controls-buttons_mobile": isMobile,
"vm-header-button": !appModeEnable,
"vm-execution-controls-buttons_short": windowSize.width <= 360
})}
>
{windowSize.width > 360 && (
{!isMobile && (
<Tooltip title="Refresh dashboard">
<Button
variant="contained"
@ -99,6 +99,19 @@ export const ExecutionControls: FC = () => {
/>
</Tooltip>
)}
{isMobile ? (
<div
className="vm-mobile-option"
onClick={toggleOpenOptions}
>
<span className="vm-mobile-option__icon"><RestartIcon/></span>
<div className="vm-mobile-option-text">
<span className="vm-mobile-option-text__label">Auto-refresh</span>
<span className="vm-mobile-option-text__value">{selectedDelay.title}</span>
</div>
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
</div>
) : (
<Tooltip title="Auto-refresh control">
<div ref={optionsButtonRef}>
<Button
@ -121,6 +134,7 @@ export const ExecutionControls: FC = () => {
</Button>
</div>
</Tooltip>
)}
</div>
</div>
<Popper
@ -128,12 +142,19 @@ export const ExecutionControls: FC = () => {
placement="bottom-right"
onClose={handleCloseOptions}
buttonRef={optionsButtonRef}
title={isMobile ? "Auto-refresh duration" : undefined}
>
<div
className={classNames({
"vm-execution-controls-list": true,
"vm-execution-controls-list_mobile": isMobile,
})}
>
<div className="vm-execution-controls-list">
{delayOptions.map(d => (
<div
className={classNames({
"vm-list-item": true,
"vm-list-item_mobile": isMobile,
"vm-list-item_active": d.seconds === selectedDelay.seconds
})}
key={d.seconds}

View file

@ -9,8 +9,9 @@
border-radius: calc($button-radius + 1px);
min-width: 107px;
&_short {
min-width: auto;
&_mobile {
flex-direction: column;
gap: $padding-medium;
}
&__arrow {
@ -32,5 +33,11 @@
overflow: auto;
padding: $padding-small 0;
font-size: $font-size;
&_mobile {
width: 100%;
max-height: calc(($vh * 100) - 70px);
padding: 0;
}
}
}

View file

@ -2,6 +2,7 @@ import React, { FC } from "preact/compat";
import { relativeTimeOptions } from "../../../../utils/time";
import "./style.scss";
import classNames from "classnames";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
interface TimeDurationSelector {
setDuration: ({ duration, until, id }: {duration: string, until: Date, id: string}) => void;
@ -9,17 +10,24 @@ interface TimeDurationSelector {
}
const TimeDurationSelector: FC<TimeDurationSelector> = ({ relativeTime, setDuration }) => {
const { isMobile } = useDeviceDetect();
const createHandlerClick = (value: { duration: string, until: Date, id: string }) => () => {
setDuration(value);
};
return (
<div className="vm-time-duration">
<div
className={classNames({
"vm-time-duration": true,
"vm-time-duration_mobile": isMobile,
})}
>
{relativeTimeOptions.map(({ id, duration, until, title }) => (
<div
className={classNames({
"vm-list-item": true,
"vm-list-item_mobile": isMobile,
"vm-list-item_active": id === relativeTime
})}
key={id}

View file

@ -4,4 +4,8 @@
max-height: 200px;
overflow: auto;
font-size: $font-size;
&_mobile {
max-height: 100%
}
}

View file

@ -4,7 +4,7 @@ import TimeDurationSelector from "../TimeDurationSelector/TimeDurationSelector";
import dayjs from "dayjs";
import { getAppModeEnable } from "../../../../utils/app-mode";
import { useTimeDispatch, useTimeState } from "../../../../state/time/TimeStateContext";
import { AlarmIcon, CalendarIcon, ClockIcon } from "../../../Main/Icons";
import { AlarmIcon, ArrowDownIcon, CalendarIcon, ClockIcon } from "../../../Main/Icons";
import Button from "../../../Main/Button/Button";
import Popper from "../../../Main/Popper/Popper";
import Tooltip from "../../../Main/Tooltip/Tooltip";
@ -15,8 +15,10 @@ import "./style.scss";
import useClickOutside from "../../../../hooks/useClickOutside";
import classNames from "classnames";
import { useAppState } from "../../../../state/common/StateContext";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
export const TimeSelector: FC = () => {
const { isMobile } = useDeviceDetect();
const { isDarkTheme } = useAppState();
const wrapperRef = useRef<HTMLDivElement>(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,6 +126,19 @@ export const TimeSelector: FC = () => {
return <>
<div ref={buttonRef}>
{isMobile ? (
<div
className="vm-mobile-option"
onClick={toggleOpenOptions}
>
<span className="vm-mobile-option__icon"><ClockIcon/></span>
<div className="vm-mobile-option-text">
<span className="vm-mobile-option-text__label">Time range</span>
<span className="vm-mobile-option-text__value">{dateTitle}</span>
</div>
<span className="vm-mobile-option__arrow"><ArrowDownIcon/></span>
</div>
) : (
<Tooltip title={displayFullDate ? "Time range controls" : dateTitle}>
<Button
className={appModeEnable ? "" : "vm-header-button"}
@ -134,6 +150,7 @@ export const TimeSelector: FC = () => {
{displayFullDate && <span>{dateTitle}</span>}
</Button>
</Tooltip>
)}
</div>
<Popper
open={openOptions}
@ -141,9 +158,13 @@ export const TimeSelector: FC = () => {
placement="bottom-right"
onClose={handleCloseOptions}
clickOutside={false}
title={isMobile ? "Time range controls" : ""}
>
<div
className="vm-time-selector"
className={classNames({
"vm-time-selector": true,
"vm-time-selector_mobile": isMobile
})}
ref={wrapperRef}
>
<div className="vm-time-selector-left">
@ -161,6 +182,7 @@ export const TimeSelector: FC = () => {
<span>{formFormat}</span>
<CalendarIcon/>
<DatePicker
label={"Date From"}
ref={fromPickerRef}
date={from || ""}
onChange={handleFromChange}
@ -176,6 +198,7 @@ export const TimeSelector: FC = () => {
<span>{untilFormat}</span>
<CalendarIcon/>
<DatePicker
label={"Date To"}
ref={untilPickerRef}
date={until || ""}
onChange={handleUntilChange}

View file

@ -5,9 +5,18 @@
grid-template-columns: repeat(2, 230px);
padding: $padding-global 0;
@media (max-width: 500px) {
&_mobile {
grid-template-columns: 1fr;
min-width: 250px;
width: 100%;
max-height: calc(($vh * 100) - 70px);
overflow: auto;
}
&_mobile &-left {
border-right: none;
border-bottom: $border-divider;
padding-bottom: $padding-global;
}
&-left {
@ -17,12 +26,6 @@
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;
@ -62,6 +65,7 @@
grid-column: 1/3;
font-size: $font-size-small;
color: $color-text-secondary;
user-select: none;
}
svg {

View file

@ -8,6 +8,8 @@ import Spinner from "../../Main/Spinner/Spinner";
import Alert from "../../Main/Alert/Alert";
import Button from "../../Main/Button/Button";
import "./style.scss";
import classNames from "classnames";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
interface ExploreMetricItemGraphProps {
name: string,
@ -26,6 +28,7 @@ const ExploreMetricItem: FC<ExploreMetricItemGraphProps> = ({
isBucket,
height
}) => {
const { isMobile } = useDeviceDetect();
const { customStep, yaxis } = useGraphState();
const { period } = useTimeState();
@ -92,7 +95,12 @@ with (q = ${queryBase}) (
};
return (
<div className="vm-explore-metrics-graph">
<div
className={classNames({
"vm-explore-metrics-graph": true,
"vm-explore-metrics-graph_mobile": isMobile
})}
>
{isLoading && <Spinner />}
{error && <Alert variant="error">{error}</Alert>}
{warning && <Alert variant="warning">

View file

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

View file

@ -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<ExploreMetricItemProps> = ({
job,
instance,
index,
length,
size,
onRemoveItem,
onChangeOrder,
@ -42,6 +44,7 @@ const ExploreMetricItem: FC<ExploreMetricItemProps> = ({
<ExploreMetricItemHeader
name={name}
index={index}
length={length}
isBucket={isBucket}
rateEnabled={rateEnabled}
size={size.id}

View file

@ -1,13 +1,16 @@
import React, { FC } from "preact/compat";
import React, { FC, useState } from "preact/compat";
import "./style.scss";
import Switch from "../../Main/Switch/Switch";
import Tooltip from "../../Main/Tooltip/Tooltip";
import Button from "../../Main/Button/Button";
import { ArrowDownIcon, CloseIcon } from "../../Main/Icons";
import { ArrowDownIcon, CloseIcon, MinusIcon, MoreIcon, PlusIcon } from "../../Main/Icons";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import Modal from "../../Main/Modal/Modal";
interface ExploreMetricItemControlsProps {
name: string
index: number
length: number
isBucket: boolean
rateEnabled: boolean
size: string
@ -19,12 +22,15 @@ interface ExploreMetricItemControlsProps {
const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
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<ExploreMetricItemControlsProps> = ({
onChangeOrder(name, index, index - 1);
};
const handleOpenOptions = () => {
setOpenOptions(true);
};
const handleCloseOptions = () => {
setOpenOptions(false);
};
if (isMobile) {
return (
<div className="vm-explore-metrics-item-header vm-explore-metrics-item-header_mobile">
<div className="vm-explore-metrics-item-header__name">{name}</div>
<Button
variant="text"
size="small"
startIcon={<MoreIcon/>}
onClick={handleOpenOptions}
/>
{openOptions && (
<Modal
title={name}
onClose={handleCloseOptions}
>
<div className="vm-explore-metrics-item-header-modal">
<div className="vm-explore-metrics-item-header-modal-order">
<Button
startIcon={<MinusIcon/>}
variant="outlined"
onClick={handleOrderUp}
disabled={index === 0}
/>
<p>position:
<span className="vm-explore-metrics-item-header-modal-order__index">#{index + 1}</span>
</p>
<Button
endIcon={<PlusIcon/>}
variant="outlined"
onClick={handleOrderDown}
disabled={index === length - 1}
/>
</div>
{!isBucket && (
<div className="vm-explore-metrics-item-header-modal__rate">
<Switch
label={<span>enable <code>rate()</code></span>}
value={rateEnabled}
onChange={onChangeRate}
fullWidth
/>
<p>
calculates the average per-second speed of metrics change
</p>
</div>
)}
<Button
startIcon={<CloseIcon/>}
color="error"
variant="outlined"
onClick={handleClickRemove}
fullWidth
>
Remove graph
</Button>
</div>
</Modal>
)}
</div>
);
}
return (
<div className="vm-explore-metrics-item-header">
<div className="vm-explore-metrics-item-header-order">
@ -65,6 +141,7 @@ const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
</div>
<div className="vm-explore-metrics-item-header__name">{name}</div>
{!isBucket && (
<div className="vm-explore-metrics-item-header__rate">
<Tooltip title="calculates the average per-second speed of metric's change">
<Switch
label={<span>enable <code>rate()</code></span>}
@ -72,8 +149,9 @@ const ExploreMetricItemHeader: FC<ExploreMetricItemControlsProps> = ({
onChange={onChangeRate}
/>
</Tooltip>
</div>
)}
<div className="vm-explore-metrics-item-header__layout">
<div className="vm-explore-metrics-item-header__close">
<Tooltip title="close graph">
<Button
startIcon={<CloseIcon/>}

View file

@ -1,14 +1,19 @@
@use "src/styles/variables" as *;
.vm-explore-metrics-item-header {
display: flex;
flex-wrap: wrap;
display: grid;
grid-template-columns: auto 1fr auto auto;
align-items: center;
justify-content: flex-start;
padding: $padding-global;
border-bottom: $border-divider;
gap: $padding-global;
&_mobile {
grid-template-columns: 1fr auto;
padding: $padding-small $padding-global;
}
&__index {
color: $color-text-secondary;
font-size: $font-size-small;
@ -17,9 +22,14 @@
&__name {
flex-grow: 1;
font-weight: bold;
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
line-height: 130%;
}
&-order {
grid-column: 1;
display: grid;
grid-template-columns: auto 20px auto;
align-items: center;
@ -31,7 +41,13 @@
}
}
&__layout {
&__rate {
grid-column: 3;
}
&__close {
grid-row: 1;
grid-column: 4;
display: grid;
align-items: center;
}
@ -42,4 +58,35 @@
background-color: $color-hover-black;
border-radius: 6px;
}
&-modal {
display: grid;
align-items: flex-start;
gap: $padding-medium;
&-order {
display: flex;
align-items: center;
justify-content: space-between;
gap: $padding-medium;
p {
display: flex;
align-items: center;
}
&__index {
margin-left: 4px;
}
}
&__rate {
display: grid;
gap: $padding-small;
p {
color: $color-text-secondary;
}
}
}
}

View file

@ -2,6 +2,8 @@ import React, { FC, useMemo } from "preact/compat";
import Select from "../../Main/Select/Select";
import "./style.scss";
import { GRAPH_SIZES } from "../../../constants/graph";
import classNames from "classnames";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
interface ExploreMetricsHeaderProps {
jobs: string[]
@ -34,9 +36,17 @@ const ExploreMetricsHeader: FC<ExploreMetricsHeaderProps> = ({
}) => {
const noInstanceText = useMemo(() => job ? "" : "No instances. Please select job", [job]);
const noMetricsText = useMemo(() => job ? "" : "No metric names. Please select job", [job]);
const { isMobile } = useDeviceDetect();
return (
<div className="vm-explore-metrics-header vm-block">
<div
className={classNames({
"vm-explore-metrics-header": true,
"vm-explore-metrics-header_mobile": isMobile,
"vm-block": true,
"vm-block_mobile": isMobile,
})}
>
<div className="vm-explore-metrics-header__job">
<Select
value={job}
@ -45,6 +55,7 @@ const ExploreMetricsHeader: FC<ExploreMetricsHeaderProps> = ({
placeholder="Please select job"
onChange={onChangeJob}
autofocus={!job}
searchable
/>
</div>
<div className="vm-explore-metrics-header__instance">
@ -56,6 +67,7 @@ const ExploreMetricsHeader: FC<ExploreMetricsHeaderProps> = ({
onChange={onChangeInstance}
noOptionsText={noInstanceText}
clearable
searchable
/>
</div>
<div className="vm-explore-metrics-header__size">
@ -68,12 +80,14 @@ const ExploreMetricsHeader: FC<ExploreMetricsHeaderProps> = ({
</div>
<div className="vm-explore-metrics-header-metrics">
<Select
label={"Metrics"}
value={selectedMetrics}
list={names}
placeholder="Search metric name"
onChange={onToggleMetric}
noOptionsText={noMetricsText}
clearable
searchable
/>
</div>
</div>

View file

@ -6,17 +6,26 @@
align-items: center;
justify-content: flex-start;
gap: $padding-global calc($padding-small + 10px);
max-width: calc(100vw - var(--scrollbar-width));
&_mobile {
flex-direction: column;
align-items: stretch;
}
&__job {
flex-grow: 1;
min-width: 150px;
}
&__instance {
flex-grow: 2;
min-width: 150px;
}
&__size {
flex-grow: 1;
min-width: 150px;
}
&-metrics {
@ -35,5 +44,4 @@
opacity: 0.7
}
}
}

View file

@ -2,8 +2,10 @@ import React, { FC } from "preact/compat";
import dayjs from "dayjs";
import "./style.scss";
import { IssueIcon, LogoIcon, WikiIcon } from "../../Main/Icons";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
const Footer: FC = () => {
const { isMobile } = useDeviceDetect();
const copyrightYears = `2019-${dayjs().format("YYYY")}`;
return <footer className="vm-footer">
@ -23,7 +25,7 @@ const Footer: FC = () => {
rel="help noreferrer"
>
<WikiIcon/>
Documentation
{isMobile ? "Docs" : "Documentation"}
</a>
<a
className="vm-link vm-footer__link"
@ -32,7 +34,7 @@ const Footer: FC = () => {
rel="noreferrer"
>
<IssueIcon/>
Create an issue
{isMobile ? "New issue" : "Create an issue"}
</a>
<div className="vm-footer__copyright">
&copy; {copyrightYears} VictoriaMetrics

View file

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

View file

@ -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 = () => {
<SidebarHeader
background={background}
color={color}
onClickLogo={onClickLogo}
/>
) : (
<>
@ -84,15 +74,19 @@ const Header: FC = () => {
/>
</>
)}
<div className="vm-header__settings">
{headerSetup?.tenant && <TenantsConfiguration accountIds={accountIds}/>}
{headerSetup?.stepControl && <StepConfigurator/>}
{headerSetup?.timeSelector && <TimeSelector/>}
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
{headerSetup?.executionControls && <ExecutionControls/>}
{!displaySidebar && <GlobalSettings/>}
{!displaySidebar && <ShortcutKeys/>}
{isMobile && (
<div
className="vm-header-logo vm-header-logo_mobile"
onClick={onClickLogo}
style={{ color }}
>
<LogoFullIcon/>
</div>
)}
<HeaderControls
displaySidebar={displaySidebar}
isMobile={isMobile}
/>
</header>;
};

View file

@ -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<HeaderControlsProp> = ({
displaySidebar,
isMobile,
headerSetup,
accountIds
}) => {
return (
<div
className={classNames({
"vm-header-controls": true,
"vm-header-controls_mobile": isMobile,
})}
>
{headerSetup?.tenant && <TenantsConfiguration accountIds={accountIds || []}/>}
{headerSetup?.stepControl && <StepConfigurator/>}
{headerSetup?.timeSelector && <TimeSelector/>}
{headerSetup?.cardinalityDatePicker && <CardinalityDatePicker/>}
{headerSetup?.executionControls && <ExecutionControls/>}
<GlobalSettings/>
{!displaySidebar && <ShortcutKeys/>}
</div>
);
};
const HeaderControls: FC<HeaderControlsProp> = (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 (
<>
<div>
<Button
className={classNames({
"vm-header-button": !appModeEnable
})}
startIcon={<MoreIcon/>}
onClick={handleToggleList}
/>
</div>
<Modal
title={"Controls"}
onClose={handleCloseList}
isOpen={openList}
className={classNames({
"vm-header-controls-modal": true,
"vm-header-controls-modal_open": openList,
})}
>
<Controls
{...props}
accountIds={accountIds}
headerSetup={headerSetup}
/>
</Modal>
</>
);
}
return <Controls
{...props}
accountIds={accountIds}
headerSetup={headerSetup}
/>;
};
export default HeaderControls;

View file

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

View file

@ -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<SidebarHeaderProps> = ({
background,
color,
onClickLogo,
}) => {
const { pathname } = useLocation();
const { isMobile } = useDeviceDetect();
@ -48,11 +44,9 @@ const SidebarHeader: FC<SidebarHeaderProps> = ({
"vm-header-sidebar-button": true,
"vm-header-sidebar-button_open": openMenu
})}
>
<MenuBurger
open={openMenu}
onClick={handleToggleMenu}
/>
>
<MenuBurger open={openMenu}/>
</div>
<div
className={classNames({
@ -60,13 +54,6 @@ const SidebarHeader: FC<SidebarHeaderProps> = ({
"vm-header-sidebar-menu_open": openMenu
})}
>
<div
className="vm-header-sidebar-menu__logo"
onClick={onClickLogo}
style={{ color }}
>
<LogoFullIcon/>
</div>
<div>
<HeaderNav
color={color}
@ -75,7 +62,6 @@ const SidebarHeader: FC<SidebarHeaderProps> = ({
/>
</div>
<div className="vm-header-sidebar-menu-settings">
<GlobalSettings showTitle={true}/>
{!isMobile && <ShortcutKeys showTitle={true}/>}
</div>
</div>

View file

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

View file

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

View file

@ -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 = () => {
<div
className={classNames({
"vm-container-body": true,
"vm-container-body_mobile": isMobile,
"vm-container-body_app": appModeEnable
})}
>

View file

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

View file

@ -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<AlertProps> = ({
variant,
children }) => {
const { isDarkTheme } = useAppState();
const { isMobile } = useDeviceDetect();
return (
<div
className={classNames({
"vm-alert": true,
[`vm-alert_${variant}`]: variant,
"vm-alert_dark": isDarkTheme
"vm-alert_dark": isDarkTheme,
"vm-alert_mobile": isMobile
})}
>
<div className="vm-alert__icon">{icons[variant || "info"]}</div>

View file

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

View file

@ -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<AutocompleteProps> = ({
fullWidth,
selected,
noOptionsText,
label,
disabledFullScreen,
onSelect,
onOpenAutocomplete
}) => {
const { isMobile } = useDeviceDetect();
const wrapperEl = useRef<HTMLDivElement>(null);
const [openAutocomplete, setOpenAutocomplete] = useState(false);
@ -118,8 +123,6 @@ const Autocomplete: FC<AutocompleteProps> = ({
onOpenAutocomplete && onOpenAutocomplete(openAutocomplete);
}, [openAutocomplete]);
useClickOutside(wrapperEl, handleCloseAutocomplete, anchor);
return (
<Popper
open={openAutocomplete}
@ -127,9 +130,14 @@ const Autocomplete: FC<AutocompleteProps> = ({
placement="bottom-left"
onClose={handleCloseAutocomplete}
fullWidth={fullWidth}
title={isMobile ? label : undefined}
disabledFullScreen={disabledFullScreen}
>
<div
className="vm-autocomplete"
className={classNames({
"vm-autocomplete": true,
"vm-autocomplete_mobile": isMobile && !disabledFullScreen,
})}
ref={wrapperEl}
>
{displayNoOptionsText && <div className="vm-autocomplete__no-options">{noOptionsText}</div>}
@ -137,6 +145,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
<div
className={classNames({
"vm-list-item": true,
"vm-list-item_mobile": isMobile,
"vm-list-item_active": i === focusOption,
"vm-list-item_multiselect": selected,
"vm-list-item_multiselect_selected": selected?.includes(option)

View file

@ -3,6 +3,11 @@
.vm-autocomplete {
max-height: 300px;
overflow: auto;
overscroll-behavior: none;
&_mobile {
max-height: calc(($vh * 100) - 70px);
}
&__no-options {
padding: $padding-global;

View file

@ -93,6 +93,7 @@ $button-radius: 6px;
/* variant CONTAINED */
&_contained_primary {
color: $color-primary-text;
background-color: $color-primary;
&:before {
background-color: $color-primary;

View file

@ -8,6 +8,8 @@ import { DATE_TIME_FORMAT } from "../../../../constants/date";
import "./style.scss";
import { CalendarIcon, ClockIcon } from "../../Icons";
import Tabs from "../../Tabs/Tabs";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
import classNames from "classnames";
interface DatePickerProps {
date: Date | Dayjs
@ -33,6 +35,7 @@ const Calendar: FC<DatePickerProps> = ({
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<DatePickerProps> = ({
}, [selectDate]);
return (
<div className="vm-calendar">
<div
className={classNames({
"vm-calendar": true,
"vm-calendar_mobile": isMobile,
})}
>
{tab === "date" && (
<CalendarHeader
viewDate={viewDate}

View file

@ -9,6 +9,10 @@
background-color: $color-background-block;
border-radius: $border-radius-medium;
&_mobile {
padding: 0 $padding-global;
}
&__tabs {
margin: 0 0-$padding-global 0-$padding-global;
border-top: $border-divider;
@ -61,6 +65,8 @@
&__prev,
&__next {
margin: -8px;
padding: 8px;
cursor: pointer;
transition: opacity 200ms ease-in-out;
@ -87,6 +93,11 @@
justify-content: center;
gap: 2px;
@media (max-width: 500px) {
grid-template-columns: repeat(7, calc((100vw - ($padding-global * 2) - (6 * 2px))/7));
grid-template-rows: repeat(6, calc((100vw - ($padding-global * 2) - (5 * 2px))/7));
}
&-cell {
display: flex;
align-items: center;

View file

@ -3,12 +3,14 @@ import Calendar from "../../Main/DatePicker/Calendar/Calendar";
import dayjs, { Dayjs } from "dayjs";
import Popper from "../../Main/Popper/Popper";
import { DATE_TIME_FORMAT } from "../../../constants/date";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
interface DatePickerProps {
date: string | Date | Dayjs,
targetRef: Ref<HTMLElement>
format?: string
timepicker?: boolean
label?: string
onChange: (val: string) => void
}
@ -18,9 +20,11 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
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<HTMLDivElement, DatePickerProps>(({
buttonRef={targetRef}
placement="bottom-right"
onClose={handleCloseCalendar}
title={isMobile ? label : undefined}
>
<div ref={ref}>
<Calendar

View file

@ -125,17 +125,6 @@ export const ArrowDropDownIcon = () => (
</svg>
);
export const PlusCircleFillIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"
></path>
</svg>
);
export const ClockIcon = () => (
<svg
viewBox="0 0 24 24"
@ -181,15 +170,6 @@ export const KeyboardIcon = () => (
</svg>
);
export const RemoveCircleIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11H7v-2h10v2z"></path>
</svg>
);
export const PlayIcon = () => (
<svg
viewBox="0 0 24 24"
@ -257,6 +237,15 @@ export const PlusIcon = () => (
</svg>
);
export const MinusIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M19 13H5v-2h14v2z"></path>
</svg>
);
export const DoneIcon = () => (
<svg
viewBox="0 0 24 24"
@ -310,30 +299,6 @@ export const DragIcon = () => (
</svg>
);
export const SearchIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
></path>
</svg>
);
export const ResizeIcon = () => (
<svg
className="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiBox-root css-1om0hkc"
focusable="false"
aria-hidden="true"
viewBox="0 0 24 24"
data-testid="OpenInFullIcon"
fill="currentColor"
>
<path d="M21 11V3h-8l3.29 3.29-10 10L3 13v8h8l-3.29-3.29 10-10z"></path>
</svg>
);
export const TimelineIcon = () => (
<svg
viewBox="0 0 24 24"
@ -401,13 +366,24 @@ export const StorageIcon = () => (
</svg>
);
export const MenuIcon = () => (
export const MoreIcon = () => (
<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"
d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
></path>
</svg>
);
export const TuneIcon = () => (
<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M3 17v2h6v-2H3zM3 5v2h10V5H3zm10 16v-2h8v-2h-8v-2h-2v6h2zM7 9v2H3v2h4v2h2V9H7zm14 4v-2H11v2h10zm-6-4h2V7h4V5h-4V3h-2v6z"
></path>
</svg>
);

View file

@ -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}) => (
<button
className={classNames({
"vm-menu-burger": true,
"vm-menu-burger_opened": open
})}
onClick={onClick}
>
<span></span>
</button>

View file

@ -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<ModalProps> = ({ title, children, onClose }) => {
const Modal: FC<ModalProps> = ({
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<ModalProps> = ({ 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<ModalProps> = ({ title, children, onClose }) => {
document.body.style.overflow = "auto";
window.removeEventListener("keyup", handleKeyUp);
};
}, []);
};
useEffect(handleDisplayModal, [isOpen]);
return ReactDOM.createPortal((
<div
className={classNames({
"vm-modal": true,
"vm-modal_mobile": isMobile
"vm-modal_mobile": isMobile,
[`${className}`]: className
})}
onMouseDown={onClose}
>
<div className="vm-modal-content">
<div className="vm-modal-content-header">
<div
className="vm-modal-content-header"
onMouseDown={handleMouseDown}
>
{title && (
<div className="vm-modal-content-header__title">
{title}

View file

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

View file

@ -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<PopperProps> = ({
@ -24,10 +30,14 @@ const Popper: FC<PopperProps> = ({
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<HTMLDivElement>(null);
@ -50,6 +60,13 @@ const Popper: FC<PopperProps> = ({
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<PopperProps> = ({
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<PopperProps> = ({
return position;
},[buttonRef, placement, isOpen, children, fullWidth]);
const handleClickClose = (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
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((
<div
className={popperClasses}
ref={popperRef}
style={popperStyle}
style={(isMobile && !disabledFullScreen) ? {} : popperStyle}
>
{(title || (isMobile && !disabledFullScreen)) && (
<div className="vm-popper-header">
<p className="vm-popper-header__title">{title}</p>
<Button
variant="text"
size="small"
onClick={handleClickClose}
>
<CloseIcon/>
</Button>
</div>
)}
{children}
</div>), document.body)}
</>

View file

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

View file

@ -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<SelectProps> = ({
placeholder,
noOptionsText,
clearable = false,
searchable = false,
autofocus,
onChange
}) => {
const { isDarkTheme } = useAppState();
const { isMobile } = useDeviceDetect();
const [search, setSearch] = useState("");
const autocompleteAnchorEl = useRef<HTMLDivElement>(null);
@ -95,7 +99,7 @@ const Select: FC<SelectProps> = ({
}, [openList, inputRef]);
useEffect(() => {
if (!autofocus || !inputRef.current) return;
if (!autofocus || !inputRef.current || isMobile) return;
inputRef.current.focus();
}, [autofocus, inputRef]);
@ -120,17 +124,23 @@ const Select: FC<SelectProps> = ({
ref={autocompleteAnchorEl}
>
<div className="vm-select-input-content">
{selectedValues && selectedValues.map(item => (
{!isMobile && selectedValues && selectedValues.map(item => (
<div
className="vm-select-input-content__selected"
key={item}
>
{item}
<span>{item}</span>
<div onClick={createHandleClick(item)}>
<CloseIcon/>
</div>
</div>
))}
{isMobile && !!selectedValues?.length && (
<span className="vm-select-input-content__counter">
selected {selectedValues.length}
</span>
)}
{!isMobile || (isMobile && (!selectedValues || !selectedValues?.length)) && (
<input
value={textFieldValue}
type="text"
@ -138,7 +148,9 @@ const Select: FC<SelectProps> = ({
onInput={handleChange}
onFocus={handleFocus}
ref={inputRef}
readOnly={isMobile || !searchable}
/>
)}
</div>
{label && <span className="vm-text-field__label">{label}</span>}
{clearable && value && (
@ -159,6 +171,7 @@ const Select: FC<SelectProps> = ({
</div>
</div>
<Autocomplete
label={label}
value={autocompleteValue}
options={list}
anchor={autocompleteAnchorEl}

View file

@ -19,6 +19,16 @@
flex-wrap: wrap;
gap: $padding-small;
width: 100%;
max-width: calc(100% - ($padding-global + 61px));
&_mobile {
flex-wrap: nowrap;
}
&__counter {
font-size: $font-size;
line-height: $font-size;
}
&__selected {
display: inline-flex;
@ -29,6 +39,13 @@
border-radius: $border-radius-small;
font-size: $font-size;
line-height: $font-size;
max-width: 100%;
span {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
svg {
width: 20px;
@ -95,4 +112,20 @@
}
}
}
&-options {
display: grid;
gap: $padding-small;
max-width: 300px;
max-height: 208px;
overflow: auto;
padding: $padding-global;
font-size: $font-size;
&_mobile {
padding: 0 $padding-global $padding-small;
max-width: 100%;
max-height: calc(($vh * 100) - 70px);
}
}
}

View file

@ -8,11 +8,17 @@ interface SwitchProps {
color?: "primary" | "secondary" | "error"
disabled?: boolean
label?: string | ReactNode
fullWidth?: boolean
onChange: (value: boolean) => void
}
const Switch: FC<SwitchProps> = ({
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<SwitchProps> = ({
const switchClasses = classNames({
"vm-switch": true,
"vm-switch_full-width": fullWidth,
"vm-switch_disabled": disabled,
"vm-switch_active": value,
[`vm-switch_${color}_active`]: value,

View file

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

View file

@ -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<TextFieldProps> = ({
disabled = false,
autofocus = false,
helperText,
inputmode = "text",
onChange,
onEnter,
onKeyDown,
@ -40,6 +43,7 @@ const TextField: FC<TextFieldProps> = ({
onBlur
}) => {
const { isDarkTheme } = useAppState();
const { isMobile } = useDeviceDetect();
const inputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
@ -67,7 +71,7 @@ const TextField: FC<TextFieldProps> = ({
};
useEffect(() => {
if (!autofocus) return;
if (!autofocus || isMobile) return;
fieldRef?.current?.focus && fieldRef.current.focus();
}, [fieldRef, autofocus]);
@ -97,7 +101,9 @@ const TextField: FC<TextFieldProps> = ({
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<TextFieldProps> = ({
value={value}
type={type}
placeholder={placeholder}
inputMode={inputmode}
autoCapitalize={"none"}
onInput={handleChange}
onKeyDown={handleKeyDown}
onFocus={handleFocus}

View file

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

View file

@ -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<ThemeProviderProps> = ({ 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<ThemeProviderProps> = ({ onLoaded }) => {
setTheme();
}, [palette]);
useEffect(setScrollbarSize, [windowSize]);
useEffect(updatePalette, [theme, isDarkTheme]);
useEffect(() => {

View file

@ -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<RecursiveProps> = ({ 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<RecursiveProps> = ({ trace, totalMsec }) => {
className={classNames({
"vm-nested-nav": true,
"vm-nested-nav_dark": isDarkTheme,
"vm-nested-nav_mobile": isMobile,
})}
>
<div

View file

@ -5,6 +5,10 @@
border-radius: $border-radius-small;
background-color: rgba($color-tropical-blue, 0.4);
&_mobile {
margin-left: $padding-small;
}
&_dark {
background-color: rgba($color-black, 0.1);
}

View file

@ -8,6 +8,8 @@ import Alert from "../Main/Alert/Alert";
import Tooltip from "../Main/Tooltip/Tooltip";
import Modal from "../Main/Modal/Modal";
import JsonForm from "../../pages/TracePage/JsonForm/JsonForm";
import classNames from "classnames";
import useDeviceDetect from "../../hooks/useDeviceDetect";
interface TraceViewProps {
traces: Trace[];
@ -16,6 +18,7 @@ interface TraceViewProps {
}
const TracingsView: FC<TraceViewProps> = ({ traces, jsonEditor = false, onDeleteClick }) => {
const { isMobile } = useDeviceDetect();
const [openTrace, setOpenTrace] = useState<Trace | null>(null);
const handleCloseJson = () => {
@ -77,7 +80,12 @@ const TracingsView: FC<TraceViewProps> = ({ traces, jsonEditor = false, onDelete
/>
</Tooltip>
</div>
<nav className="vm-tracings-view-trace__nav">
<nav
className={classNames({
"vm-tracings-view-trace__nav": true,
"vm-tracings-view-trace__nav_mobile": isMobile
})}
>
<NestedNav
trace={trace}
totalMsec={trace.duration}

View file

@ -25,6 +25,10 @@
&__nav {
padding: $padding-medium $padding-medium $padding-medium 0;
&_mobile {
padding: $padding-small $padding-small $padding-small 0;
}
}
}
}

View file

@ -13,6 +13,7 @@ import classNames from "classnames";
import { useTimeState } from "../../../state/time/TimeStateContext";
import "./style.scss";
import { promValueToNumber } from "../../../utils/metric";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
export interface GraphViewProps {
data?: MetricResult[];
@ -43,6 +44,7 @@ const GraphView: FC<GraphViewProps> = ({
fullWidth = true,
height
}) => {
const { isMobile } = useDeviceDetect();
const { timezone } = useTimeState();
const currentStep = useMemo(() => customStep || period.step || "1s", [period.step, customStep]);
const getSeriesItem = useCallback(getSeriesItemContext(), [data]);
@ -132,7 +134,8 @@ const GraphView: FC<GraphViewProps> = ({
<div
className={classNames({
"vm-graph-view": true,
"vm-graph-view_full-width": fullWidth
"vm-graph-view_full-width": fullWidth,
"vm-graph-view_full-width_mobile": fullWidth && isMobile
})}
ref={containerRef}
>

View file

@ -9,5 +9,9 @@
@media (max-width: 768px) {
width: calc(100vw - ($padding-medium * 2) - var(--scrollbar-width));
}
&_mobile {
width: calc(100vw - ($padding-global * 2) - var(--scrollbar-width));
}
}
}

View file

@ -4,7 +4,7 @@
&__copy {
position: sticky;
top: $padding-medium;
top: 0;
display: flex;
justify-content: flex-end;
z-index: 2;

View file

@ -12,6 +12,7 @@ import { getNameForMetric } from "../../../utils/metric";
import { useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
import "./style.scss";
import useResize from "../../../hooks/useResize";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
export interface GraphViewProps {
data: InstantMetricResult[];
@ -20,6 +21,7 @@ export interface GraphViewProps {
const TableView: FC<GraphViewProps> = ({ data, displayColumns }) => {
const { showInfoMessage } = useSnack();
const { isMobile } = useDeviceDetect();
const { tableCompact } = useCustomPanelState();
const windowSize = useResize(document.body);
@ -108,7 +110,12 @@ const TableView: FC<GraphViewProps> = ({ data, displayColumns }) => {
if (!rows.length) return <Alert variant="warning">No data to show</Alert>;
return (
<div className="vm-table-view">
<div
className={classNames({
"vm-table-view": true,
"vm-table-view_mobile": isMobile,
})}
>
<table
className="vm-table"
ref={tableRef}

View file

@ -5,6 +5,10 @@
max-width: 100%;
overflow: auto;
&_mobile {
margin-top: -$padding-global;
}
table {
margin-top: 0;
}

View file

@ -1,5 +1,8 @@
import React, { createContext, FC, useContext, useEffect, useState } from "preact/compat";
import Alert from "../components/Main/Alert/Alert";
import useDeviceDetect from "../hooks/useDeviceDetect";
import classNames from "classnames";
import { CloseIcon } from "../components/Main/Icons";
export interface SnackModel {
message?: string;
@ -26,6 +29,8 @@ export const SnackbarContext = createContext<SnackbarContextType>({
export const useSnack = (): SnackbarContextType => useContext(SnackbarContext);
export const SnackbarProvider: FC = ({ children }) => {
const { isMobile } = useDeviceDetect();
const [snack, setSnack] = useState<SnackModel>({});
const [open, setOpen] = useState(false);
@ -44,17 +49,28 @@ export const SnackbarProvider: FC = ({ children }) => {
return () => clearTimeout(timeout);
}, [infoMessage]);
const handleClose = (e: unknown, reason: string): void => {
if (reason !== "clickaway") {
const handleClose = () => {
setInfoMessage(undefined);
setOpen(false);
}
};
return <SnackbarContext.Provider value={{ showInfoMessage: setInfoMessage }}>
{open && <div className="vm-snackbar">
{open && <div
className={classNames({
"vm-snackbar": true,
"vm-snackbar_mobile": isMobile,
})}
>
<Alert variant={snack.variant}>
{snack.message}
<div className="vm-snackbar-content">
<span>{snack.message}</span>
<div
className="vm-snackbar-content__close"
onClick={handleClose}
>
<CloseIcon/>
</div>
</div>
</Alert>
</div>}
{children}

View file

@ -4,12 +4,17 @@ import useResize from "./useResize";
export default function useDeviceDetect() {
const windowSize = useResize(document.body);
const [isMobile, setMobile] = useState(false);
useEffect(() => {
const getIsMobile = () => {
const mobileAgent = isMobileAgent();
const smallWidth = window.innerWidth < 500;
setMobile(mobileAgent || smallWidth);
return mobileAgent || smallWidth;
};
const [isMobile, setMobile] = useState(getIsMobile());
useEffect(() => {
setMobile(getIsMobile());
}, [windowSize]);
return { isMobile };

View file

@ -9,6 +9,8 @@ import Button from "../../../components/Main/Button/Button";
import TextField from "../../../components/Main/TextField/TextField";
import "./style.scss";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import classNames from "classnames";
export interface CardinalityConfiguratorProps {
onSetHistory: (step: number) => void;
@ -43,6 +45,7 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
}) => {
const { autocomplete } = useQueryState();
const queryDispatch = useQueryDispatch();
const { isMobile } = useDeviceDetect();
const { queryOptions } = useFetchQueryOptions();
@ -60,7 +63,13 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
onSetHistory(1);
};
return <div className="vm-cardinality-configurator vm-block">
return <div
className={classNames({
"vm-cardinality-configurator": true,
"vm-block": true,
"vm-block_mobile": isMobile,
})}
>
<div className="vm-cardinality-configurator-controls">
<div className="vm-cardinality-configurator-controls__query">
<QueryEditor
@ -112,7 +121,12 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
onChange={onChangeAutocomplete}
/>
</div>
<div className="vm-cardinality-configurator-bottom">
<div
className={classNames({
"vm-cardinality-configurator-bottom": true,
"vm-cardinality-configurator-bottom_mobile": isMobile,
})}
>
<div className="vm-cardinality-configurator-bottom__info">
Analyzed <b>{totalSeries}</b> series with <b>{totalLabelValuePairs}</b> &quot;label=value&quot; pairs
at <b>{date}</b>{match && <span> for series selector <b>{match}</b></span>}.
@ -141,6 +155,7 @@ const CardinalityConfigurator: FC<CardinalityConfiguratorProps> = ({
<Button
startIcon={<PlayIcon/>}
onClick={onRunQuery}
fullWidth
>
Execute Query
</Button>

View file

@ -38,6 +38,10 @@
gap: $padding-global;
}
&_mobile &__docs {
justify-content: space-between;
}
&__info {
flex-grow: 1;
font-size: $font-size;
@ -50,5 +54,14 @@
button {
margin: 0 0 0 auto;
}
&_mobile {
display: grid;
grid-template-columns: 1fr;
button {
margin: 0;
}
}
}
}

View file

@ -9,6 +9,8 @@ import Tabs from "../../../components/Main/Tabs/Tabs";
import { useMemo } from "preact/compat";
import { ChartIcon, TableIcon } from "../../../components/Main/Icons";
import "./style.scss";
import classNames from "classnames";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
interface MetricsProperties {
rows: Data[];
@ -35,6 +37,8 @@ const MetricsContent: FC<MetricsProperties> = ({
sectionTitle,
tableHeaderCells,
}) => {
const { isMobile } = useDeviceDetect();
const tableCells = (row: Data) => (
<TableCells
row={row}
@ -54,9 +58,21 @@ const MetricsContent: FC<MetricsProperties> = ({
};
return (
<div className="vm-metrics-content vm-block">
<div
className={classNames({
"vm-metrics-content": true,
"vm-metrics-content_mobile": isMobile,
"vm-block": true,
"vm-block_mobile": isMobile,
})}
>
<div className="vm-metrics-content-header vm-section-header">
<h5 className="vm-section-header__title">{sectionTitle}</h5>
<h5
className={classNames({
"vm-section-header__title": true,
"vm-section-header__title_mobile": isMobile,
})}
>{sectionTitle}</h5>
<div className="vm-section-header__tabs">
<Tabs
activeItem={String(activeTab)}
@ -67,7 +83,10 @@ const MetricsContent: FC<MetricsProperties> = ({
</div>
<div
ref={chartContainer}
className="vm-metrics-content__table"
className={classNames({
"vm-metrics-content__table": true,
"vm-metrics-content__table_mobile": isMobile
})}
>
{activeTab === 0 && (
<EnhancedTable

View file

@ -5,6 +5,10 @@
margin: -$padding-medium 0-$padding-medium 0;
}
&_mobile &-header {
margin: -$padding-global 0-$padding-global 0;
}
&__table {
padding-top: $padding-medium;
width: calc(100vw - ($padding-medium * 4) - var(--scrollbar-width));
@ -14,8 +18,16 @@
width: calc(100vw - ($padding-medium * 2) - var(--scrollbar-width));
}
&_mobile {
width: calc(100vw - ($padding-global * 2) - var(--scrollbar-width));
}
.vm-table-cell_header {
white-space: nowrap;
}
}
&_mobile &__table {
width: calc(100vw - ($padding-global * 2) - var(--scrollbar-width));
}
}

View file

@ -2,7 +2,7 @@ import React, { FC, useEffect, useState, useRef, useMemo } from "preact/compat";
import { useSortedCategories } from "../../../../hooks/useSortedCategories";
import { InstantMetricResult } from "../../../../api/types";
import Button from "../../../../components/Main/Button/Button";
import { CloseIcon, RestartIcon, SettingsIcon } from "../../../../components/Main/Icons";
import { RestartIcon, SettingsIcon } from "../../../../components/Main/Icons";
import Popper from "../../../../components/Main/Popper/Popper";
import "./style.scss";
import Checkbox from "../../../../components/Main/Checkbox/Checkbox";
@ -10,6 +10,8 @@ import Tooltip from "../../../../components/Main/Tooltip/Tooltip";
import { useCustomPanelDispatch, useCustomPanelState } from "../../../../state/customPanel/CustomPanelStateContext";
import Switch from "../../../../components/Main/Switch/Switch";
import { arrayEquals } from "../../../../utils/array";
import classNames from "classnames";
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
const title = "Table settings";
@ -20,6 +22,7 @@ interface TableSettingsProps {
}
const TableSettings: FC<TableSettingsProps> = ({ data, defaultColumns = [], onChange }) => {
const { isMobile } = useDeviceDetect();
const { tableCompact } = useCustomPanelState();
const customPanelDispatch = useCustomPanelDispatch();
@ -79,20 +82,15 @@ const TableSettings: FC<TableSettingsProps> = ({ data, defaultColumns = [], onCh
onClose={handleClose}
placement="bottom-right"
buttonRef={buttonRef}
title={title}
>
<div className="vm-table-settings-popper">
<div className="vm-popper-header">
<h3 className="vm-popper-header__title">
{title}
</h3>
<Button
onClick={handleClose}
startIcon={<CloseIcon/>}
size="small"
variant="text"
/>
</div>
<div className="vm-table-settings-popper-list">
<div
className={classNames({
"vm-table-settings-popper": true,
"vm-table-settings-popper_mobile": isMobile
})}
>
<div className="vm-table-settings-popper-list vm-table-settings-popper-list_first">
<Switch
label={"Compact view"}
value={tableCompact}

View file

@ -4,6 +4,14 @@
display: grid;
min-width: 250px;
&_mobile &-list {
gap: $padding-global;
&:first-child {
padding-top: 0;
}
}
&-list {
display: grid;
gap: $padding-small;
@ -12,6 +20,10 @@
overflow: auto;
border-bottom: $border-divider;
&_first {
padding-top: 0;
}
&-header {
display: grid;
align-items: center;

View file

@ -10,11 +10,14 @@ import { DefaultActiveTab, Tabs, TSDBStatus, Containers } from "./types";
import { useSetQueryParams } from "./hooks/useSetQueryParams";
import Alert from "../../components/Main/Alert/Alert";
import "./style.scss";
import classNames from "classnames";
import useDeviceDetect from "../../hooks/useDeviceDetect";
const spinnerMessage = `Please wait while cardinality stats is calculated.
This may take some time if the db contains big number of time series.`;
const Index: FC = () => {
const { isMobile } = useDeviceDetect();
const { topN, match, date, focusLabel } = useCardinalityState();
const cardinalityDispatch = useCardinalityDispatch();
useSetQueryParams();
@ -70,7 +73,12 @@ const Index: FC = () => {
};
return (
<div className="vm-cardinality-panel">
<div
className={classNames({
"vm-cardinality-panel": true,
"vm-cardinality-panel_mobile": isMobile
})}
>
{isLoading && <Spinner message={spinnerMessage}/>}
<CardinalityConfigurator
error={configError}

View file

@ -4,4 +4,8 @@
display: grid;
align-items: flex-start;
gap: $padding-medium;
&_mobile {
gap: $padding-small;
}
}

View file

@ -13,6 +13,7 @@ import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import classNames from "classnames";
import { MouseEvent as ReactMouseEvent } from "react";
import { arrayEquals } from "../../../utils/array";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
export interface QueryConfiguratorProps {
error?: ErrorTypes | string;
@ -21,6 +22,7 @@ export interface QueryConfiguratorProps {
}
const QueryConfigurator: FC<QueryConfiguratorProps> = ({ error, queryOptions, onHideQuery }) => {
const { isMobile } = useDeviceDetect();
const { query, queryHistory, autocomplete } = useQueryState();
const queryDispatch = useQueryDispatch();
@ -111,13 +113,20 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({ error, queryOptions, on
onHideQuery(hideQuery);
}, [hideQuery]);
return <div className="vm-query-configurator vm-block">
return <div
className={classNames({
"vm-query-configurator": true,
"vm-block": true,
"vm-block_mobile": isMobile
})}
>
<div className="vm-query-configurator-list">
{stateQuery.map((q, i) => (
<div
className={classNames({
"vm-query-configurator-list-row": true,
"vm-query-configurator-list-row_disabled": hideQuery.includes(i)
"vm-query-configurator-list-row_disabled": hideQuery.includes(i),
"vm-query-configurator-list-row_mobile": isMobile
})}
key={i}
>
@ -175,7 +184,7 @@ const QueryConfigurator: FC<QueryConfiguratorProps> = ({ error, queryOptions, on
onClick={onRunQuery}
startIcon={<PlayIcon/>}
>
Execute Query
{isMobile ? "Execute" : "Execute Query"}
</Button>
</div>
</div>

View file

@ -2,7 +2,7 @@
.vm-query-configurator {
display: grid;
gap: $padding-small;
gap: $padding-global;
&-list {
display: grid;
@ -13,6 +13,10 @@
align-items: center;
gap: $padding-small;
&_mobile {
gap: 4px;
}
&_disabled {
filter: grayscale(100%);
opacity: 0.5;
@ -33,22 +37,12 @@
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

@ -20,12 +20,15 @@ import "./style.scss";
import Alert from "../../components/Main/Alert/Alert";
import TableView from "../../components/Views/TableView/TableView";
import Button from "../../components/Main/Button/Button";
import classNames from "classnames";
import useDeviceDetect from "../../hooks/useDeviceDetect";
const CustomPanel: FC = () => {
const { displayType, isTracingEnabled } = useCustomPanelState();
const { query } = useQueryState();
const { period } = useTimeState();
const timeDispatch = useTimeDispatch();
const { isMobile } = useDeviceDetect();
useSetQueryParams();
const [displayColumns, setDisplayColumns] = useState<string[]>();
@ -84,7 +87,12 @@ const CustomPanel: FC = () => {
}, [query]);
return (
<div className="vm-custom-panel">
<div
className={classNames({
"vm-custom-panel": true,
"vm-custom-panel_mobile": isMobile,
})}
>
<QueryConfigurator
error={error}
queryOptions={queryOptions}
@ -101,7 +109,12 @@ const CustomPanel: FC = () => {
{isLoading && <Spinner />}
{error && <Alert variant="error">{error}</Alert>}
{warning && <Alert variant="warning">
<div className="vm-custom-panel__warning">
<div
className={classNames({
"vm-custom-panel__warning": true,
"vm-custom-panel__warning_mobile": isMobile
})}
>
<p>{warning}</p>
<Button
color="warning"
@ -112,7 +125,14 @@ const CustomPanel: FC = () => {
</Button>
</div>
</Alert>}
<div className="vm-custom-panel-body vm-block">
<div
className={classNames({
"vm-custom-panel-body": true,
"vm-custom-panel-body_mobile": isMobile,
"vm-block": true,
"vm-block_mobile": isMobile,
})}
>
<div className="vm-custom-panel-body-header">
<DisplayTypeSwitch/>
{displayType === "chart" && (
@ -139,6 +159,7 @@ const CustomPanel: FC = () => {
yaxis={yaxis}
setYaxisLimits={setYaxisLimits}
setPeriod={setPeriod}
height={isMobile ? window.innerHeight * 0.5 : 500}
/>
)}
{liveData && (displayType === "code") && (

View file

@ -7,11 +7,20 @@
gap: $padding-medium;
height: 100%;
&_mobile {
gap: $padding-small;
}
&__warning {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
justify-content: space-between;
gap: $padding-small;
&_mobile {
grid-template-columns: 1fr;
}
}
&__trace {
@ -31,5 +40,10 @@
border-bottom: $border-divider;
z-index: 1;
}
&_mobile &-header {
margin: -$padding-global 0-$padding-global $padding-global;
padding: 0 $padding-global;
}
}
}

View file

@ -96,6 +96,7 @@ const ExploreMetrics: FC = () => {
job={job}
instance={instance}
index={i}
length={metrics.length}
size={size}
onRemoveItem={handleToggleMetric}
onChangeOrder={handleChangeOrder}

View file

@ -5,9 +5,17 @@
align-items: flex-start;
gap: $padding-medium;
@media (max-width: 500px) {
gap: $padding-small;
}
&-body {
display: grid;
align-items: flex-start;
gap: $padding-medium;
@media (max-width: 500px) {
gap: $padding-small;
}
}
}

View file

@ -12,6 +12,7 @@ import "./style.scss";
import Alert from "../../../components/Main/Alert/Alert";
import Tooltip from "../../../components/Main/Tooltip/Tooltip";
import { useGraphState } from "../../../state/graph/GraphStateContext";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
export interface PredefinedPanelsProps extends PanelSettings {
filename: string;
@ -26,7 +27,7 @@ const PredefinedPanel: FC<PredefinedPanelsProps> = ({
filename,
alias
}) => {
const { isMobile } = useDeviceDetect();
const { period } = useTimeState();
const { customStep } = useGraphState();
const dispatch = useTimeDispatch();
@ -138,6 +139,7 @@ const PredefinedPanel: FC<PredefinedPanelsProps> = ({
setYaxisLimits={setYaxisLimits}
setPeriod={setPeriod}
fullWidth={false}
height={isMobile ? window.innerHeight * 0.5 : 500}
/>
}
</div>

View file

@ -38,5 +38,9 @@
&-body {
padding: $padding-small $padding-small*2;
min-height: 500px;
@media (max-width: 500px) {
padding: 0;
}
}
}

View file

@ -6,9 +6,11 @@ import classNames from "classnames";
import "./style.scss";
import { useDashboardsState } from "../../state/dashboards/DashboardsStateContext";
import Spinner from "../../components/Main/Spinner/Spinner";
import useDeviceDetect from "../../hooks/useDeviceDetect";
const DashboardsLayout: FC = () => {
useSetQueryParams();
const { isMobile } = useDeviceDetect();
const { dashboardsSettings, dashboardsLoading, dashboardsError } = useDashboardsState();
const [dashboard, setDashboard] = useState(0);
@ -35,7 +37,13 @@ const DashboardsLayout: FC = () => {
{dashboardsError && <Alert variant="error">{dashboardsError}</Alert>}
{!dashboardsSettings.length && <Alert variant="info">Dashboards not found</Alert>}
{dashboards.length > 1 && (
<div className="vm-predefined-panels-tabs vm-block">
<div
className={classNames({
"vm-predefined-panels-tabs": true,
"vm-block": true,
"vm-block_mobile": isMobile,
})}
>
{dashboards.map(tab => (
<div
key={tab.value}

View file

@ -9,6 +9,10 @@
padding: $padding-medium 0;
}
@media (max-width: 500px) {
padding: $padding-small 0;
}
&-tabs.vm-block {
padding: $padding-global;
}

View file

@ -5,6 +5,8 @@ import { CodeIcon, TableIcon } from "../../../components/Main/Icons";
import Tabs from "../../../components/Main/Tabs/Tabs";
import TopQueryTable from "../TopQueryTable/TopQueryTable";
import "./style.scss";
import classNames from "classnames";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
export interface TopQueryPanelProps {
rows: TopQuery[],
@ -19,7 +21,7 @@ const tabs = ["table", "JSON"].map((t, i) => ({
}));
const TopQueryPanel: FC<TopQueryPanelProps> = ({ rows, title, columns, defaultOrderBy }) => {
const { isMobile } = useDeviceDetect();
const [activeTab, setActiveTab] = useState(0);
const handleChangeTab = (val: string) => {
@ -27,10 +29,26 @@ const TopQueryPanel: FC<TopQueryPanelProps> = ({ rows, title, columns, defaultOr
};
return (
<div className="vm-top-queries-panel vm-block">
<div className="vm-top-queries-panel-header vm-section-header">
<h5 className="vm-section-header__title">{title}</h5>
<div
className={classNames({
"vm-top-queries-panel": true,
"vm-block": true,
"vm-block_mobile": isMobile,
})}
>
<div
className={classNames({
"vm-top-queries-panel-header": true,
"vm-section-header": true,
"vm-top-queries-panel-header_mobile": isMobile,
})}
>
<h5
className={classNames({
"vm-section-header__title": true,
"vm-section-header__title_mobile": isMobile,
})}
>{title}</h5>
<div className="vm-section-header__tabs">
<Tabs
activeItem={String(activeTab)}
@ -40,7 +58,12 @@ const TopQueryPanel: FC<TopQueryPanelProps> = ({ rows, title, columns, defaultOr
</div>
</div>
<div className="vm-top-queries-panel__table">
<div
className={classNames({
"vm-top-queries-panel__table": true,
"vm-top-queries-panel__table_mobile": isMobile,
})}
>
{activeTab === 0 && (
<TopQueryTable
rows={rows}

View file

@ -3,6 +3,10 @@
.vm-top-queries-panel {
&-header {
margin: -$padding-medium 0-$padding-medium 0;
&_mobile {
margin: -$padding-global 0-$padding-global 0;
}
}
&__table {
@ -14,6 +18,10 @@
width: calc(100vw - ($padding-medium * 2) - var(--scrollbar-width));
}
&_mobile {
width: calc(100vw - ($padding-global * 2) - var(--scrollbar-width));
}
.vm-table-cell_header {
white-space: nowrap;
}

View file

@ -14,10 +14,14 @@ import TextField from "../../components/Main/TextField/TextField";
import Alert from "../../components/Main/Alert/Alert";
import Tooltip from "../../components/Main/Tooltip/Tooltip";
import "./style.scss";
import useDeviceDetect from "../../hooks/useDeviceDetect";
import classNames from "classnames";
const exampleDuration = "30ms, 15s, 3d4h, 1y2w";
const Index: FC = () => {
const { isMobile } = useDeviceDetect();
const { data, error, loading } = useFetchTopQueries();
const { topN, maxLifetime } = useTopQueriesState();
const topQueriesDispatch = useTopQueriesDispatch();
@ -67,10 +71,21 @@ const Index: FC = () => {
}, [data]);
return (
<div className="vm-top-queries">
<div
className={classNames({
"vm-top-queries": true,
"vm-top-queries_mobile": isMobile,
})}
>
{loading && <Spinner containerStyles={{ height: "500px" }}/>}
<div className="vm-top-queries-controls vm-block">
<div
className={classNames({
"vm-top-queries-controls": true,
"vm-block": true,
"vm-block_mobile": isMobile,
})}
>
<div className="vm-top-queries-controls-fields">
<div className="vm-top-queries-controls-fields__item">
<TextField
@ -93,7 +108,12 @@ const Index: FC = () => {
/>
</div>
</div>
<div className="vm-top-queries-controls-bottom">
<div
className={classNames({
"vm-top-queries-controls-bottom": true,
"vm-top-queries-controls-bottom_mobile": isMobile,
})}
>
<div className="vm-top-queries-controls-bottom__info">
VictoriaMetrics tracks the last&nbsp;
<Tooltip title="search.queryStats.lastQueriesCount">

View file

@ -5,6 +5,10 @@
align-items: flex-start;
gap: $padding-medium;
&_mobile {
gap: $padding-small;
}
&-controls {
display: grid;
gap: $padding-small;
@ -28,6 +32,11 @@
justify-content: space-between;
gap: $padding-medium;
&_mobile {
grid-template-columns: 1fr;
gap: $padding-small;
}
&__info {
}

View file

@ -80,6 +80,7 @@ const JsonForm: FC<JsonFormProps> = ({
className={classNames({
"vm-json-form": true,
"vm-json-form_one-field": !displayTitle,
"vm-json-form_one-field_mobile": !displayTitle && isMobile,
"vm-json-form_mobile": isMobile
})}
>

View file

@ -12,14 +12,15 @@
&_mobile {
width: 100%;
min-height: 100%;
grid-template-rows: auto 1fr auto;
grid-template-rows: auto calc(($vh * 100) - 200px - ($padding-global*3)) auto;
}
&_one-field {
grid-template-rows: calc(($vh * 70) - 78px - ($padding-medium*3)) auto;
}
.vm-text-field_textarea {
&_mobile {
grid-template-rows: calc(($vh * 100) - 160px - ($padding-global*2)) auto;
}
}
textarea {

View file

@ -6,6 +6,11 @@
border-radius: $border-radius-medium;
box-shadow: $box-shadow;
&_mobile {
padding: $padding-global;
border-radius: 0;
}
&_empty-padding {
padding: 0;
}

View file

@ -7,6 +7,10 @@
background-color: transparent;
transition: background-color 200ms ease;
&_mobile {
padding: $padding-global $padding-global;
}
&:hover,
&_active {
background-color: $color-hover-black;

Some files were not shown because too many files have changed in this diff Show more