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:
Yury Molodov 2022-12-06 07:44:31 +01:00 committed by Aliaksandr Valialkin
parent 08a68a2829
commit daaea87931
No known key found for this signature in database
GPG Key ID: A72BEC6CD3D0DED1
29 changed files with 453 additions and 67 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
@use "src/styles/variables" as *;
.vm-time-duration {
max-height: 168px;
max-height: 200px;
overflow: auto;
font-size: $font-size;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -135,6 +135,7 @@
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $padding-small;
max-height: 400px;
overflow: auto;
&__year {

View File

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

View File

@ -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[] = [];

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -105,3 +105,9 @@ export interface SeriesLimits {
chart: number,
code: number,
}
export interface Timezone {
region: string,
utc: string,
search?: string
}

View File

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

View File

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

View File

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

View File

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