mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-01-20 07:19:17 +01:00
vmui: timezone select (#3414)
* feat: add timezone selection * vmui: provide feature timezone select * fix: correct timezone with relative time Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
parent
08a68a2829
commit
daaea87931
@ -49,7 +49,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||
const value = useMemo(() => get(u, ["data", seriesIdx, dataIdx], 0), [u, seriesIdx, dataIdx]);
|
||||
const valueFormat = useMemo(() => formatPrettyNumber(value), [value]);
|
||||
const dataTime = useMemo(() => u.data[0][dataIdx], [u, dataIdx]);
|
||||
const date = useMemo(() => dayjs(new Date(dataTime * 1000)).format(DATE_FULL_TIMEZONE_FORMAT), [dataTime]);
|
||||
const date = useMemo(() => dayjs(dataTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT), [dataTime]);
|
||||
|
||||
const color = useMemo(() => getColorLine(series[seriesIdx]?.label || ""), [series, seriesIdx]);
|
||||
|
||||
|
@ -11,7 +11,7 @@ import { defaultOptions } from "../../../utils/uplot/helpers";
|
||||
import { dragChart } from "../../../utils/uplot/events";
|
||||
import { getAxes, getMinMaxBuffer } from "../../../utils/uplot/axes";
|
||||
import { MetricResult } from "../../../api/types";
|
||||
import { limitsDurations } from "../../../utils/time";
|
||||
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../utils/time";
|
||||
import throttle from "lodash.throttle";
|
||||
import useResize from "../../../hooks/useResize";
|
||||
import { TimeParams } from "../../../types";
|
||||
@ -20,6 +20,7 @@ import "uplot/dist/uPlot.min.css";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export interface LineChartProps {
|
||||
metrics: MetricResult[];
|
||||
@ -57,7 +58,10 @@ const LineChart: FC<LineChartProps> = ({
|
||||
const tooltipId = useMemo(() => `${tooltipIdx.seriesIdx}_${tooltipIdx.dataIdx}`, [tooltipIdx]);
|
||||
|
||||
const setScale = ({ min, max }: { min: number, max: number }): void => {
|
||||
setPeriod({ from: new Date(min * 1000), to: new Date(max * 1000) });
|
||||
setPeriod({
|
||||
from: dayjs(min * 1000).toDate(),
|
||||
to: dayjs(max * 1000).toDate()
|
||||
});
|
||||
};
|
||||
const throttledSetScale = useCallback(throttle(setScale, 500), []);
|
||||
const setPlotScale = ({ u, min, max }: { u: uPlot, min: number, max: number }) => {
|
||||
@ -163,6 +167,7 @@ const LineChart: FC<LineChartProps> = ({
|
||||
|
||||
const options: uPlotOptions = {
|
||||
...defaultOptions,
|
||||
tzDate: ts => dayjs(formatDateForNativeInput(dateFromSeconds(ts))).local().toDate(),
|
||||
series,
|
||||
axes: getAxes( [{}, { scale: "1" }], unit),
|
||||
scales: { ...getScales() },
|
||||
|
@ -15,7 +15,7 @@ const CardinalityDatePicker: FC = () => {
|
||||
const { date } = useCardinalityState();
|
||||
const cardinalityDispatch = useCardinalityDispatch();
|
||||
|
||||
const dateFormatted = useMemo(() => dayjs(date).format(DATE_FORMAT), [date]);
|
||||
const dateFormatted = useMemo(() => dayjs.tz(date).format(DATE_FORMAT), [date]);
|
||||
|
||||
const handleChangeDate = (val: string) => {
|
||||
cardinalityDispatch({ type: "SET_DATE", payload: val });
|
||||
|
@ -11,6 +11,8 @@ import { SeriesLimits } from "../../../types";
|
||||
import { useCustomPanelDispatch, useCustomPanelState } from "../../../state/customPanel/CustomPanelStateContext";
|
||||
import { getAppModeEnable } from "../../../utils/app-mode";
|
||||
import classNames from "classnames";
|
||||
import Timezones from "./Timezones/Timezones";
|
||||
import { useTimeDispatch, useTimeState } from "../../../state/time/TimeStateContext";
|
||||
|
||||
const title = "Settings";
|
||||
|
||||
@ -18,13 +20,16 @@ const GlobalSettings: FC = () => {
|
||||
|
||||
const appModeEnable = getAppModeEnable();
|
||||
const { serverUrl: stateServerUrl } = useAppState();
|
||||
const { timezone: stateTimezone } = useTimeState();
|
||||
const { seriesLimits } = useCustomPanelState();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const timeDispatch = useTimeDispatch();
|
||||
const customPanelDispatch = useCustomPanelDispatch();
|
||||
|
||||
const [serverUrl, setServerUrl] = useState(stateServerUrl);
|
||||
const [limits, setLimits] = useState<SeriesLimits>(seriesLimits);
|
||||
const [timezone, setTimezone] = useState(stateTimezone);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleOpen = () => setOpen(true);
|
||||
@ -32,6 +37,7 @@ const GlobalSettings: FC = () => {
|
||||
|
||||
const handlerApply = () => {
|
||||
dispatch({ type: "SET_SERVER", payload: serverUrl });
|
||||
timeDispatch({ type: "SET_TIMEZONE", payload: timezone });
|
||||
customPanelDispatch({ type: "SET_SERIES_LIMITS", payload: limits });
|
||||
handleClose();
|
||||
};
|
||||
@ -70,6 +76,12 @@ const GlobalSettings: FC = () => {
|
||||
onEnter={handlerApply}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-server-configurator__input">
|
||||
<Timezones
|
||||
timezoneState={timezone}
|
||||
onChange={setTimezone}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-server-configurator__footer">
|
||||
<Button
|
||||
variant="outlined"
|
||||
|
@ -46,7 +46,7 @@ const LimitsConfigurator: FC<ServerConfiguratorProps> = ({ limits, onChange , on
|
||||
|
||||
return (
|
||||
<div className="vm-limits-configurator">
|
||||
<div className="vm-limits-configurator-title">
|
||||
<div className="vm-server-configurator__title">
|
||||
Series limits by tabs
|
||||
<Tooltip title="To disable limits set to 0">
|
||||
<Button
|
||||
|
@ -3,13 +3,6 @@
|
||||
.vm-limits-configurator {
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
font-size: $font-size;
|
||||
font-weight: bold;
|
||||
margin-bottom: $padding-global;
|
||||
|
||||
&__reset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -22,14 +22,18 @@ const ServerConfigurator: FC<ServerConfiguratorProps> = ({ serverUrl, onChange ,
|
||||
};
|
||||
|
||||
return (
|
||||
<TextField
|
||||
autofocus
|
||||
label="Server URL"
|
||||
value={serverUrl}
|
||||
error={error}
|
||||
onChange={onChangeServer}
|
||||
onEnter={onEnter}
|
||||
/>
|
||||
<div>
|
||||
<div className="vm-server-configurator__title">
|
||||
Server URL
|
||||
</div>
|
||||
<TextField
|
||||
autofocus
|
||||
value={serverUrl}
|
||||
error={error}
|
||||
onChange={onChangeServer}
|
||||
onEnter={onEnter}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,143 @@
|
||||
import React, { FC, useMemo, useRef, useState } from "preact/compat";
|
||||
import { getTimezoneList, getUTCByTimezone } from "../../../../utils/time";
|
||||
import { ArrowDropDownIcon } from "../../../Main/Icons";
|
||||
import classNames from "classnames";
|
||||
import Popper from "../../../Main/Popper/Popper";
|
||||
import Accordion from "../../../Main/Accordion/Accordion";
|
||||
import dayjs from "dayjs";
|
||||
import TextField from "../../../Main/TextField/TextField";
|
||||
import { Timezone } from "../../../../types";
|
||||
import "./style.scss";
|
||||
|
||||
interface TimezonesProps {
|
||||
timezoneState: string
|
||||
onChange: (val: string) => void
|
||||
}
|
||||
|
||||
const Timezones: FC<TimezonesProps> = ({ timezoneState, onChange }) => {
|
||||
|
||||
const timezones = getTimezoneList();
|
||||
|
||||
const [openList, setOpenList] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
const targetRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const searchTimezones = useMemo(() => {
|
||||
if (!search) return timezones;
|
||||
try {
|
||||
return getTimezoneList(search);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}, [search, timezones]);
|
||||
|
||||
const timezonesGroups = useMemo(() => Object.keys(searchTimezones), [searchTimezones]);
|
||||
|
||||
const localTimezone = useMemo(() => ({
|
||||
region: dayjs.tz.guess(),
|
||||
utc: getUTCByTimezone(dayjs.tz.guess())
|
||||
}), []);
|
||||
|
||||
const activeTimezone = useMemo(() => ({
|
||||
region: timezoneState,
|
||||
utc: getUTCByTimezone(timezoneState)
|
||||
}), [timezoneState]);
|
||||
|
||||
const toggleOpenList = () => {
|
||||
setOpenList(prev => !prev);
|
||||
};
|
||||
|
||||
const handleCloseList = () => {
|
||||
setOpenList(false);
|
||||
};
|
||||
|
||||
const handleChangeSearch = (val: string) => {
|
||||
setSearch(val);
|
||||
};
|
||||
|
||||
const handleSetTimezone = (val: Timezone) => {
|
||||
onChange(val.region);
|
||||
setSearch("");
|
||||
handleCloseList();
|
||||
};
|
||||
|
||||
const createHandlerSetTimezone = (val: Timezone) => () => {
|
||||
handleSetTimezone(val);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-timezones">
|
||||
<div className="vm-server-configurator__title">
|
||||
Time zone
|
||||
</div>
|
||||
<div
|
||||
className="vm-timezones-item vm-timezones-item_selected"
|
||||
onClick={toggleOpenList}
|
||||
ref={targetRef}
|
||||
>
|
||||
<div className="vm-timezones-item__title">{activeTimezone.region}</div>
|
||||
<div className="vm-timezones-item__utc">{activeTimezone.utc}</div>
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-timezones-item__icon": true,
|
||||
"vm-timezones-item__icon_open": openList
|
||||
})}
|
||||
>
|
||||
<ArrowDropDownIcon/>
|
||||
</div>
|
||||
</div>
|
||||
<Popper
|
||||
open={openList}
|
||||
buttonRef={targetRef}
|
||||
placement="bottom-left"
|
||||
onClose={handleCloseList}
|
||||
>
|
||||
<div className="vm-timezones-list">
|
||||
<div className="vm-timezones-list-header">
|
||||
<div className="vm-timezones-list-header__search">
|
||||
<TextField
|
||||
autofocus
|
||||
label="Search"
|
||||
value={search}
|
||||
onChange={handleChangeSearch}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="vm-timezones-item vm-timezones-list-group-options__item"
|
||||
onClick={createHandlerSetTimezone(localTimezone)}
|
||||
>
|
||||
<div className="vm-timezones-item__title">Browser Time ({localTimezone.region})</div>
|
||||
<div className="vm-timezones-item__utc">{localTimezone.utc}</div>
|
||||
</div>
|
||||
</div>
|
||||
{timezonesGroups.map(t => (
|
||||
<div
|
||||
className="vm-timezones-list-group"
|
||||
key={t}
|
||||
>
|
||||
<Accordion
|
||||
defaultExpanded={true}
|
||||
title={<div className="vm-timezones-list-group__title">{t}</div>}
|
||||
>
|
||||
<div className="vm-timezones-list-group-options">
|
||||
{searchTimezones[t] && searchTimezones[t].map(item => (
|
||||
<div
|
||||
className="vm-timezones-item vm-timezones-list-group-options__item"
|
||||
onClick={createHandlerSetTimezone(item)}
|
||||
key={item.search}
|
||||
>
|
||||
<div className="vm-timezones-item__title">{item.region}</div>
|
||||
<div className="vm-timezones-item__utc">{item.utc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Popper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Timezones;
|
@ -0,0 +1,96 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-timezones {
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $padding-small;
|
||||
cursor: pointer;
|
||||
|
||||
&_selected {
|
||||
border: $border-divider;
|
||||
padding: $padding-small $padding-global;
|
||||
border-radius: $border-radius-small;
|
||||
}
|
||||
|
||||
&__title {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
&__utc {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba($color-black, 0.06);
|
||||
padding: calc($padding-small/2);
|
||||
border-radius: $border-radius-small;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin: 0 0 0 auto;
|
||||
transition: transform 200ms ease-in;
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
&_open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-list {
|
||||
min-width: 600px;
|
||||
max-height: 300px;
|
||||
background-color: $color-background-block;
|
||||
border-radius: $border-radius-medium;
|
||||
overflow: auto;
|
||||
|
||||
&-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: $color-background-block;
|
||||
z-index: 2;
|
||||
border-bottom: $border-divider;
|
||||
|
||||
&__search {
|
||||
padding: $padding-small;
|
||||
}
|
||||
}
|
||||
|
||||
&-group {
|
||||
padding: $padding-small 0;
|
||||
border-bottom: $border-divider;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: bold;
|
||||
color: $color-text-secondary;
|
||||
padding: $padding-small $padding-global;
|
||||
}
|
||||
|
||||
&-options {
|
||||
display: grid;
|
||||
align-items: flex-start;
|
||||
|
||||
&__item {
|
||||
padding: $padding-small $padding-global;
|
||||
transition: background-color 200ms ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($color-black, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -10,6 +10,15 @@
|
||||
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
font-size: $font-size;
|
||||
font-weight: bold;
|
||||
margin-bottom: $padding-global;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: inline-grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
@ -1,7 +1,7 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-time-duration {
|
||||
max-height: 168px;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
font-size: $font-size;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { FC, useEffect, useState, useMemo, useRef } from "preact/compat";
|
||||
import { dateFromSeconds, formatDateForNativeInput } from "../../../../utils/time";
|
||||
import { dateFromSeconds, formatDateForNativeInput, getRelativeTime, getUTCByTimezone } from "../../../../utils/time";
|
||||
import TimeDurationSelector from "../TimeDurationSelector/TimeDurationSelector";
|
||||
import dayjs from "dayjs";
|
||||
import { getAppModeEnable } from "../../../../utils/app-mode";
|
||||
@ -22,20 +22,25 @@ export const TimeSelector: FC = () => {
|
||||
const [until, setUntil] = useState<string>();
|
||||
const [from, setFrom] = useState<string>();
|
||||
|
||||
const formFormat = useMemo(() => dayjs(from).format(DATE_TIME_FORMAT), [from]);
|
||||
const untilFormat = useMemo(() => dayjs(until).format(DATE_TIME_FORMAT), [until]);
|
||||
const formFormat = useMemo(() => dayjs.tz(from).format(DATE_TIME_FORMAT), [from]);
|
||||
const untilFormat = useMemo(() => dayjs.tz(until).format(DATE_TIME_FORMAT), [until]);
|
||||
|
||||
const { period: { end, start }, relativeTime } = useTimeState();
|
||||
const { period: { end, start }, relativeTime, timezone, duration } = useTimeState();
|
||||
const dispatch = useTimeDispatch();
|
||||
const appModeEnable = getAppModeEnable();
|
||||
|
||||
const activeTimezone = useMemo(() => ({
|
||||
region: timezone,
|
||||
utc: getUTCByTimezone(timezone)
|
||||
}), [timezone]);
|
||||
|
||||
useEffect(() => {
|
||||
setUntil(formatDateForNativeInput(dateFromSeconds(end)));
|
||||
}, [end]);
|
||||
}, [timezone, end]);
|
||||
|
||||
useEffect(() => {
|
||||
setFrom(formatDateForNativeInput(dateFromSeconds(start)));
|
||||
}, [start]);
|
||||
}, [timezone, start]);
|
||||
|
||||
const setDuration = ({ duration, until, id }: {duration: string, until: Date, id: string}) => {
|
||||
dispatch({ type: "SET_RELATIVE_TIME", payload: { duration, until, id } });
|
||||
@ -43,13 +48,13 @@ export const TimeSelector: FC = () => {
|
||||
};
|
||||
|
||||
const formatRange = useMemo(() => {
|
||||
const startFormat = dayjs(dateFromSeconds(start)).format(DATE_TIME_FORMAT);
|
||||
const endFormat = dayjs(dateFromSeconds(end)).format(DATE_TIME_FORMAT);
|
||||
const startFormat = dayjs.tz(dateFromSeconds(start)).format(DATE_TIME_FORMAT);
|
||||
const endFormat = dayjs.tz(dateFromSeconds(end)).format(DATE_TIME_FORMAT);
|
||||
return {
|
||||
start: startFormat,
|
||||
end: endFormat
|
||||
};
|
||||
}, [start, end]);
|
||||
}, [start, end, timezone]);
|
||||
|
||||
const dateTitle = useMemo(() => {
|
||||
const isRelativeTime = relativeTime && relativeTime !== "none";
|
||||
@ -65,7 +70,10 @@ export const TimeSelector: FC = () => {
|
||||
|
||||
const setTimeAndClosePicker = () => {
|
||||
if (from && until) {
|
||||
dispatch({ type: "SET_PERIOD", payload: { from: new Date(from), to: new Date(until) } });
|
||||
dispatch({ type: "SET_PERIOD", payload: {
|
||||
from: dayjs(from).toDate(),
|
||||
to: dayjs(until).toDate()
|
||||
} });
|
||||
}
|
||||
setOpenOptions(false);
|
||||
};
|
||||
@ -91,6 +99,15 @@ export const TimeSelector: FC = () => {
|
||||
setOpenOptions(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const value = getRelativeTime({
|
||||
relativeTimeId: relativeTime,
|
||||
defaultDuration: duration,
|
||||
defaultEndInput: dateFromSeconds(end),
|
||||
});
|
||||
setDuration({ id: value.relativeTimeId, duration: value.duration, until: value.endInput });
|
||||
}, [timezone]);
|
||||
|
||||
useClickOutside(wrapperRef, (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const isFromButton = fromRef?.current && fromRef.current.contains(target);
|
||||
@ -159,6 +176,10 @@ export const TimeSelector: FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="vm-time-selector-left-timezone">
|
||||
<div className="vm-time-selector-left-timezone__title">{activeTimezone.region}</div>
|
||||
<div className="vm-time-selector-left-timezone__utc">{activeTimezone.utc}</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={<AlarmIcon />}
|
||||
|
@ -30,6 +30,10 @@
|
||||
cursor: pointer;
|
||||
transition: color 200ms ease-in-out, border-bottom-color 300ms ease;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-bottom-color: $color-primary;
|
||||
}
|
||||
@ -52,6 +56,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
&-timezone {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $padding-small;
|
||||
font-size: $font-size-small;
|
||||
margin-bottom: $padding-small;
|
||||
|
||||
&__title {}
|
||||
|
||||
&__utc {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba($color-black, 0.06);
|
||||
padding: calc($padding-small/2);
|
||||
border-radius: $border-radius-small;
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
@ -30,8 +30,8 @@ const Calendar: FC<DatePickerProps> = ({
|
||||
onClose
|
||||
}) => {
|
||||
const [displayYears, setDisplayYears] = useState(false);
|
||||
const [viewDate, setViewDate] = useState(dayjs(date));
|
||||
const [selectDate, setSelectDate] = useState(dayjs(date));
|
||||
const [viewDate, setViewDate] = useState(dayjs.tz(date));
|
||||
const [selectDate, setSelectDate] = useState(dayjs.tz(date));
|
||||
const [tab, setTab] = useState(tabs[0].value);
|
||||
|
||||
const toggleDisplayYears = () => {
|
||||
@ -62,7 +62,7 @@ const Calendar: FC<DatePickerProps> = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectDate.format() === dayjs(date).format()) return;
|
||||
if (selectDate.format() === dayjs.tz(date).format()) return;
|
||||
onChange(selectDate.format(format));
|
||||
}, [selectDate]);
|
||||
|
||||
|
@ -11,7 +11,7 @@ interface CalendarBodyProps {
|
||||
const weekday = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
|
||||
const CalendarBody: FC<CalendarBodyProps> = ({ viewDate, selectDate, onChangeSelectDate }) => {
|
||||
const today = dayjs().startOf("day");
|
||||
const today = dayjs().tz().startOf("day");
|
||||
|
||||
const days: (Dayjs|null)[] = useMemo(() => {
|
||||
const result = new Array(42).fill(null);
|
||||
|
@ -135,6 +135,7 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: $padding-small;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
|
||||
&__year {
|
||||
|
@ -20,7 +20,7 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
|
||||
onChange,
|
||||
}, ref) => {
|
||||
const [openCalendar, setOpenCalendar] = useState(false);
|
||||
const dateDayjs = useMemo(() => date ? dayjs(date) : dayjs(), [date]);
|
||||
const dateDayjs = useMemo(() => date ? dayjs.tz(date) : dayjs().tz(), [date]);
|
||||
|
||||
const toggleOpenCalendar = () => {
|
||||
setOpenCalendar(prev => !prev);
|
||||
|
@ -10,6 +10,7 @@ import { TimeParams } from "../../../types";
|
||||
import { AxisRange, YaxisState } from "../../../state/graph/reducer";
|
||||
import { getAvgFromArray, getMaxFromArray, getMinFromArray } from "../../../utils/math";
|
||||
import classNames from "classnames";
|
||||
import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||
import "./style.scss";
|
||||
|
||||
export interface GraphViewProps {
|
||||
@ -54,6 +55,7 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
alias = [],
|
||||
fullWidth = true
|
||||
}) => {
|
||||
const { timezone } = useTimeState();
|
||||
const currentStep = useMemo(() => customStep || period.step || 1, [period.step, customStep]);
|
||||
|
||||
const [dataChart, setDataChart] = useState<uPlotData>([[]]);
|
||||
@ -121,7 +123,7 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
setDataChart(timeDataSeries as uPlotData);
|
||||
setSeries(tempSeries);
|
||||
setLegend(tempLegend);
|
||||
}, [data]);
|
||||
}, [data, timezone]);
|
||||
|
||||
useEffect(() => {
|
||||
const tempLegend: LegendItemType[] = [];
|
||||
|
8
app/vmui/packages/vmui/src/constants/dayjsPlugins.ts
Normal file
8
app/vmui/packages/vmui/src/constants/dayjsPlugins.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(utc);
|
@ -36,7 +36,7 @@ export const SnackbarProvider: FC = ({ children }) => {
|
||||
setSnack({
|
||||
message: infoMessage.text,
|
||||
variant: infoMessage.type,
|
||||
key: new Date().getTime()
|
||||
key: Date.now()
|
||||
});
|
||||
setOpen(true);
|
||||
const timeout = setTimeout(handleClose, 4000);
|
||||
|
@ -8,9 +8,8 @@ const useClickOutside = <T extends HTMLElement = HTMLElement>(
|
||||
preventRef?: RefObject<T>
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const el = ref?.current;
|
||||
|
||||
const listener = (event: Event) => {
|
||||
const el = ref?.current;
|
||||
const target = event.target as HTMLElement;
|
||||
const isPreventRef = preventRef?.current && preventRef.current.contains(target);
|
||||
if (!el || el.contains((event?.target as Node) || null) || isPreventRef) {
|
||||
@ -23,13 +22,10 @@ const useClickOutside = <T extends HTMLElement = HTMLElement>(
|
||||
document.addEventListener("mousedown", listener);
|
||||
document.addEventListener("touchstart", listener);
|
||||
|
||||
const removeListeners = () => {
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", listener);
|
||||
document.removeEventListener("touchstart", listener);
|
||||
};
|
||||
|
||||
if (!el) removeListeners();
|
||||
return removeListeners;
|
||||
}, [ref, handler]); // Reload only if ref or handler changes
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { render } from "preact/compat";
|
||||
import "./constants/dayjsPlugins";
|
||||
import App from "./App";
|
||||
import reportWebVitals from "./reportWebVitals";
|
||||
import "./styles/style.scss";
|
||||
|
@ -23,7 +23,7 @@ export type Action =
|
||||
export const initialState: CardinalityState = {
|
||||
runQuery: 0,
|
||||
topN: getQueryStringValue("topN", 10) as number,
|
||||
date: getQueryStringValue("date", dayjs(new Date()).format(DATE_FORMAT)) as string,
|
||||
date: getQueryStringValue("date", dayjs().tz().format(DATE_FORMAT)) as string,
|
||||
focusLabel: getQueryStringValue("focusLabel", "") as string,
|
||||
match: getQueryStringValue("match", "") as string,
|
||||
extraLabel: getQueryStringValue("extra_label", "") as string,
|
||||
|
@ -5,14 +5,18 @@ import {
|
||||
getDateNowUTC,
|
||||
getDurationFromPeriod,
|
||||
getTimeperiodForDuration,
|
||||
getRelativeTime
|
||||
getRelativeTime,
|
||||
setTimezone
|
||||
} from "../../utils/time";
|
||||
import { getQueryStringValue } from "../../utils/query-string";
|
||||
import dayjs from "dayjs";
|
||||
import { getFromStorage, saveToStorage } from "../../utils/storage";
|
||||
|
||||
export interface TimeState {
|
||||
duration: string;
|
||||
period: TimeParams;
|
||||
relativeTime?: string;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export type TimeAction =
|
||||
@ -21,12 +25,16 @@ export type TimeAction =
|
||||
| { type: "SET_PERIOD", payload: TimePeriod }
|
||||
| { type: "RUN_QUERY"}
|
||||
| { type: "RUN_QUERY_TO_NOW"}
|
||||
| { type: "SET_TIMEZONE", payload: string }
|
||||
|
||||
const timezone = getFromStorage("TIMEZONE") as string || dayjs.tz.guess();
|
||||
setTimezone(timezone);
|
||||
|
||||
const defaultDuration = getQueryStringValue("g0.range_input") as string;
|
||||
|
||||
const { duration, endInput, relativeTimeId } = getRelativeTime({
|
||||
defaultDuration: defaultDuration || "1h",
|
||||
defaultEndInput: new Date(formatDateToLocal(getQueryStringValue("g0.end_input", getDateNowUTC()) as Date)),
|
||||
defaultEndInput: formatDateToLocal(getQueryStringValue("g0.end_input", getDateNowUTC()) as string),
|
||||
relativeTimeId: defaultDuration ? getQueryStringValue("g0.relative_time", "none") as string : undefined
|
||||
});
|
||||
|
||||
@ -34,8 +42,10 @@ export const initialTimeState: TimeState = {
|
||||
duration,
|
||||
period: getTimeperiodForDuration(duration, endInput),
|
||||
relativeTime: relativeTimeId,
|
||||
timezone,
|
||||
};
|
||||
|
||||
|
||||
export function reducer(state: TimeState, action: TimeAction): TimeState {
|
||||
switch (action.type) {
|
||||
case "SET_DURATION":
|
||||
@ -49,7 +59,7 @@ export function reducer(state: TimeState, action: TimeAction): TimeState {
|
||||
return {
|
||||
...state,
|
||||
duration: action.payload.duration,
|
||||
period: getTimeperiodForDuration(action.payload.duration, new Date(action.payload.until)),
|
||||
period: getTimeperiodForDuration(action.payload.duration, action.payload.until),
|
||||
relativeTime: action.payload.id,
|
||||
};
|
||||
case "SET_PERIOD":
|
||||
@ -77,6 +87,13 @@ export function reducer(state: TimeState, action: TimeAction): TimeState {
|
||||
...state,
|
||||
period: getTimeperiodForDuration(state.duration)
|
||||
};
|
||||
case "SET_TIMEZONE":
|
||||
setTimezone(action.payload);
|
||||
saveToStorage("TIMEZONE", action.payload);
|
||||
return {
|
||||
...state,
|
||||
timezone: action.payload
|
||||
};
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
|
@ -105,3 +105,9 @@ export interface SeriesLimits {
|
||||
chart: number,
|
||||
code: number,
|
||||
}
|
||||
|
||||
export interface Timezone {
|
||||
region: string,
|
||||
utc: string,
|
||||
search?: string
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ export type StorageKeys = "BASIC_AUTH_DATA"
|
||||
| "QUERY_TRACING"
|
||||
| "SERIES_LIMITS"
|
||||
| "TABLE_COMPACT"
|
||||
| "TIMEZONE"
|
||||
|
||||
export const saveToStorage = (key: StorageKeys, value: string | boolean | Record<string, unknown>): void => {
|
||||
if (value) {
|
||||
|
@ -1,17 +1,16 @@
|
||||
import { RelativeTimeOption, TimeParams, TimePeriod } from "../types";
|
||||
import { RelativeTimeOption, TimeParams, TimePeriod, Timezone } from "../types";
|
||||
import dayjs, { UnitTypeShort } from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { getQueryStringValue } from "./query-string";
|
||||
import { DATE_ISO_FORMAT } from "../constants/date";
|
||||
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(utc);
|
||||
|
||||
const MAX_ITEMS_PER_CHART = window.innerWidth / 4;
|
||||
|
||||
export const limitsDurations = { min: 1, max: 1.578e+11 }; // min: 1 ms, max: 5 years
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
export const supportedTimezones = Intl.supportedValuesOf("timeZone") as string[];
|
||||
|
||||
export const supportedDurations = [
|
||||
{ long: "days", short: "d", possible: "day" },
|
||||
{ long: "weeks", short: "w", possible: "week" },
|
||||
@ -38,7 +37,7 @@ export const isSupportedDuration = (str: string): Partial<Record<UnitTypeShort,
|
||||
};
|
||||
|
||||
export const getTimeperiodForDuration = (dur: string, date?: Date): TimeParams => {
|
||||
const n = (date || new Date()).valueOf() / 1000;
|
||||
const n = (date || dayjs().toDate()).valueOf() / 1000;
|
||||
|
||||
const durItems = dur.trim().split(" ");
|
||||
|
||||
@ -64,24 +63,24 @@ export const getTimeperiodForDuration = (dur: string, date?: Date): TimeParams =
|
||||
start: n - delta,
|
||||
end: n,
|
||||
step: step,
|
||||
date: formatDateToUTC(date || new Date())
|
||||
date: formatDateToUTC(date || dayjs().toDate())
|
||||
};
|
||||
};
|
||||
|
||||
export const formatDateToLocal = (date: Date): string => {
|
||||
return dayjs(date).utcOffset(0, true).local().format(DATE_ISO_FORMAT);
|
||||
export const formatDateToLocal = (date: string): Date => {
|
||||
return dayjs(date).utcOffset(0, true).toDate();
|
||||
};
|
||||
|
||||
export const formatDateToUTC = (date: Date): string => {
|
||||
return dayjs(date).utc().format(DATE_ISO_FORMAT);
|
||||
return dayjs.tz(date).utc().format(DATE_ISO_FORMAT);
|
||||
};
|
||||
|
||||
export const formatDateForNativeInput = (date: Date): string => {
|
||||
return dayjs(date).format(DATE_ISO_FORMAT);
|
||||
return dayjs.tz(date).format(DATE_ISO_FORMAT);
|
||||
};
|
||||
|
||||
export const getDateNowUTC = (): Date => {
|
||||
return new Date(dayjs().utc().format(DATE_ISO_FORMAT));
|
||||
export const getDateNowUTC = (): string => {
|
||||
return dayjs().utc().format(DATE_ISO_FORMAT);
|
||||
};
|
||||
|
||||
export const getDurationFromMilliseconds = (ms: number): string => {
|
||||
@ -115,7 +114,10 @@ export const checkDurationLimit = (dur: string): string => {
|
||||
return dur;
|
||||
};
|
||||
|
||||
export const dateFromSeconds = (epochTimeInSeconds: number): Date => new Date(epochTimeInSeconds * 1000);
|
||||
export const dateFromSeconds = (epochTimeInSeconds: number): Date => dayjs(epochTimeInSeconds * 1000).toDate();
|
||||
|
||||
const getYesterday = () => dayjs().tz().subtract(1, "day").endOf("day").toDate();
|
||||
const getToday = () => dayjs().tz().endOf("day").toDate();
|
||||
|
||||
export const relativeTimeOptions: RelativeTimeOption[] = [
|
||||
{ title: "Last 5 minutes", duration: "5m" },
|
||||
@ -132,11 +134,11 @@ export const relativeTimeOptions: RelativeTimeOption[] = [
|
||||
{ title: "Last 90 days", duration: "90d" },
|
||||
{ title: "Last 180 days", duration: "180d" },
|
||||
{ title: "Last 1 year", duration: "1y" },
|
||||
{ title: "Yesterday", duration: "1d", until: () => dayjs().subtract(1, "day").endOf("day").toDate() },
|
||||
{ title: "Today", duration: "1d", until: () => dayjs().endOf("day").toDate() },
|
||||
{ title: "Yesterday", duration: "1d", until: getYesterday },
|
||||
{ title: "Today", duration: "1d", until: getToday },
|
||||
].map(o => ({
|
||||
id: o.title.replace(/\s/g, "_").toLocaleLowerCase(),
|
||||
until: o.until ? o.until : () => dayjs().toDate(),
|
||||
until: o.until ? o.until : () => dayjs().tz().toDate(),
|
||||
...o
|
||||
}));
|
||||
|
||||
@ -151,3 +153,35 @@ export const getRelativeTime = ({ relativeTimeId, defaultDuration, defaultEndInp
|
||||
endInput: target ? target.until() : defaultEndInput
|
||||
};
|
||||
};
|
||||
|
||||
export const getUTCByTimezone = (timezone: string) => {
|
||||
const date = dayjs().tz(timezone);
|
||||
return `UTC${date.format("Z")}`;
|
||||
};
|
||||
|
||||
export const getTimezoneList = (search = "") => {
|
||||
const regexp = new RegExp(search, "i");
|
||||
|
||||
return supportedTimezones.reduce((acc: {[key: string]: Timezone[]}, region) => {
|
||||
const zone = (region.match(/^(.*?)\//) || [])[1] || "unknown";
|
||||
const utc = getUTCByTimezone(region);
|
||||
const item = {
|
||||
region,
|
||||
utc,
|
||||
search: `${region} ${utc} ${region.replace(/[/_]/gmi, " ")}`
|
||||
};
|
||||
const includeZone = !search || (search && regexp.test(item.search));
|
||||
|
||||
if (includeZone && acc[zone]) {
|
||||
acc[zone].push(item);
|
||||
} else if (includeZone) {
|
||||
acc[zone] = [item];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const setTimezone = (timezone: string) => {
|
||||
dayjs.tz.setDefault(timezone);
|
||||
};
|
||||
|
@ -5,6 +5,18 @@ import { AxisRange } from "../../state/graph/reducer";
|
||||
import { formatTicks, sizeAxis } from "./helpers";
|
||||
import { TimeParams } from "../../types";
|
||||
|
||||
// see https://github.com/leeoniya/uPlot/tree/master/docs#axis--grid-opts
|
||||
const timeValues = [
|
||||
// tick incr default year month day hour min sec mode
|
||||
[3600 * 24 * 365, "{YYYY}", null, null, null, null, null, null, 1],
|
||||
[3600 * 24 * 28, "{MMM}", "\n{YYYY}", null, null, null, null, null, 1],
|
||||
[3600 * 24, "{MM}-{DD}", "\n{YYYY}", null, null, null, null, null, 1],
|
||||
[3600, "{HH}:{mm}", "\n{YYYY}-{MM}-{DD}", null, "\n{MM}-{DD}", null, null, null, 1],
|
||||
[60, "{HH}:{mm}", "\n{YYYY}-{MM}-{DD}", null, "\n{MM}-{DD}", null, null, null, 1],
|
||||
[1, "{HH}:{mm}:{ss}", "\n{YYYY}-{MM}-{DD}", null, "\n{MM}-{DD} {HH}:{mm}", null, null, null, 1],
|
||||
[0.001, ":{ss}.{fff}", "\n{YYYY}-{MM}-{DD} {HH}:{mm}", null, "\n{MM}-{DD} {HH}:{mm}", null, "\n{HH}:{mm}", null, 1],
|
||||
];
|
||||
|
||||
export const getAxes = (series: Series[], unit?: string): Axis[] => Array.from(new Set(series.map(s => s.scale))).map(a => {
|
||||
const axis = {
|
||||
scale: a,
|
||||
@ -13,7 +25,7 @@ export const getAxes = (series: Series[], unit?: string): Axis[] => Array.from(n
|
||||
font: "10px Arial",
|
||||
values: (u: uPlot, ticks: number[]) => formatTicks(u, ticks, unit)
|
||||
};
|
||||
if (!a) return { space: 80 };
|
||||
if (!a) return { space: 80, values: timeValues };
|
||||
if (!(Number(a) % 2)) return { ...axis, side: 1 };
|
||||
return axis;
|
||||
});
|
||||
|
@ -51,6 +51,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): add `-remoteWrite.sendTimeout` command-line flag, which allows configuring timeout for sending data to `-remoteWrite.url`. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3408).
|
||||
* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): add ability to migrate data between VictoriaMetrics clusters with automatic tenants discovery. See [these docs](https://docs.victoriametrics.com/vmctl.html#cluster-to-cluster-migration-mode) and [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2930).
|
||||
* FEATURE: [vmctl](https://docs.victoriametrics.com/vmctl.html): add ability to copy data from sources via Prometheus `remote_read` protocol. See [these docs](https://docs.victoriametrics.com/vmctl.html#migrating-data-by-remote-read-protocol). The related issues: [one](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3132) and [two](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1101).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): allow changing timezones for the requested data. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3075).
|
||||
* FEATURE: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): add `range_trim_spikes(phi, q)` function for trimming `phi` percent of the largest spikes per each time series returned by `q`. See [these docs](https://docs.victoriametrics.com/MetricsQL.html#range_trim_spikes).
|
||||
|
||||
* BUGFIX: [vmalert](https://docs.victoriametrics.com/vmalert.html): properly pass HTTP headers during the alert state restore procedure. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3418).
|
||||
|
Loading…
Reference in New Issue
Block a user