mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmui: improve usability of date/time picker (#3968)
* vmui: allow manually set input date and time * vmui/docs: improve usability of date/time picker
This commit is contained in:
parent
693a3de0a6
commit
b66953d8e1
14 changed files with 352 additions and 320 deletions
45
app/vmui/packages/vmui/package-lock.json
generated
45
app/vmui/packages/vmui/package-lock.json
generated
|
@ -18,6 +18,7 @@
|
|||
"@types/marked": "^4.0.2",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/react-input-mask": "^3.0.2",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/webpack-env": "^1.16.3",
|
||||
"classnames": "^2.3.2",
|
||||
|
@ -28,6 +29,7 @@
|
|||
"marked": "^4.0.14",
|
||||
"preact": "^10.7.1",
|
||||
"qs": "^6.10.3",
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"sass": "^1.56.0",
|
||||
"typescript": "~4.6.2",
|
||||
|
@ -4392,6 +4394,14 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-input-mask": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-input-mask/-/react-input-mask-3.0.2.tgz",
|
||||
"integrity": "sha512-WTli3kUyvUqqaOLYG/so2pLqUvRb+n4qnx2He5klfqZDiQmRyD07jVIt/bco/1BrcErkPMtpOm+bHii4Oed6cQ==",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-router": {
|
||||
"version": "5.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
|
||||
|
@ -10236,6 +10246,14 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz",
|
||||
|
@ -16403,6 +16421,19 @@
|
|||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-input-mask": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-input-mask/-/react-input-mask-2.0.4.tgz",
|
||||
"integrity": "sha512-1hwzMr/aO9tXfiroiVCx5EtKohKwLk/NT8QlJXHQ4N+yJJFyUuMT+zfTpLBwX/lK3PkuMlievIffncpMZ3HGRQ==",
|
||||
"dependencies": {
|
||||
"invariant": "^2.2.4",
|
||||
"warning": "^4.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=0.14.0",
|
||||
"react-dom": ">=0.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
|
@ -18811,6 +18842,14 @@
|
|||
"makeerror": "1.0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||
|
@ -18851,9 +18890,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.75.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz",
|
||||
"integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==",
|
||||
"version": "5.76.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.2.tgz",
|
||||
"integrity": "sha512-Th05ggRm23rVzEOlX8y67NkYCHa9nTNcwHPBhdg+lKG+mtiW7XgggjAeeLnADAe7mLjJ6LUNfgHAuRRh+Z6J7w==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"@types/marked": "^4.0.2",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/react-input-mask": "^3.0.2",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/webpack-env": "^1.16.3",
|
||||
"classnames": "^2.3.2",
|
||||
|
@ -24,6 +25,7 @@
|
|||
"marked": "^4.0.14",
|
||||
"preact": "^10.7.1",
|
||||
"qs": "^6.10.3",
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"sass": "^1.56.0",
|
||||
"typescript": "~4.6.2",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-time-duration {
|
||||
max-height: 200px;
|
||||
max-height: 227px;
|
||||
overflow: auto;
|
||||
font-size: $font-size;
|
||||
|
||||
|
|
|
@ -4,18 +4,18 @@ import TimeDurationSelector from "../TimeDurationSelector/TimeDurationSelector";
|
|||
import dayjs from "dayjs";
|
||||
import { getAppModeEnable } from "../../../../utils/app-mode";
|
||||
import { useTimeDispatch, useTimeState } from "../../../../state/time/TimeStateContext";
|
||||
import { AlarmIcon, ArrowDownIcon, CalendarIcon, ClockIcon } from "../../../Main/Icons";
|
||||
import { AlarmIcon, ArrowDownIcon, ClockIcon } from "../../../Main/Icons";
|
||||
import Button from "../../../Main/Button/Button";
|
||||
import Popper from "../../../Main/Popper/Popper";
|
||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||
import { DATE_TIME_FORMAT } from "../../../../constants/date";
|
||||
import useResize from "../../../../hooks/useResize";
|
||||
import DatePicker from "../../../Main/DatePicker/DatePicker";
|
||||
import "./style.scss";
|
||||
import useClickOutside from "../../../../hooks/useClickOutside";
|
||||
import classNames from "classnames";
|
||||
import { useAppState } from "../../../../state/common/StateContext";
|
||||
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||
import DateTimeInput from "../../../Main/DatePicker/DateTimeInput/DateTimeInput";
|
||||
|
||||
export const TimeSelector: FC = () => {
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
@ -27,9 +27,6 @@ export const TimeSelector: FC = () => {
|
|||
const [until, setUntil] = useState<string>();
|
||||
const [from, setFrom] = useState<string>();
|
||||
|
||||
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, timezone, duration } = useTimeState();
|
||||
const dispatch = useTimeDispatch();
|
||||
const appModeEnable = getAppModeEnable();
|
||||
|
@ -55,10 +52,7 @@ export const TimeSelector: FC = () => {
|
|||
const formatRange = useMemo(() => {
|
||||
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
|
||||
};
|
||||
return { start: startFormat, end: endFormat };
|
||||
}, [start, end, timezone]);
|
||||
|
||||
const dateTitle = useMemo(() => {
|
||||
|
@ -66,8 +60,6 @@ export const TimeSelector: FC = () => {
|
|||
return isRelativeTime ? relativeTime.replace(/_/g, " ") : `${formatRange.start} - ${formatRange.end}`;
|
||||
}, [relativeTime, formatRange]);
|
||||
|
||||
const fromRef = useRef<HTMLDivElement>(null);
|
||||
const untilRef = useRef<HTMLDivElement>(null);
|
||||
const fromPickerRef = useRef<HTMLDivElement>(null);
|
||||
const untilPickerRef = useRef<HTMLDivElement>(null);
|
||||
const [openOptions, setOpenOptions] = useState(false);
|
||||
|
@ -82,11 +74,6 @@ export const TimeSelector: FC = () => {
|
|||
}
|
||||
setOpenOptions(false);
|
||||
};
|
||||
const handleFromChange = (from: string) => setFrom(from);
|
||||
|
||||
const handleUntilChange = (until: string) => setUntil(until);
|
||||
|
||||
const onApplyClick = () => setTimeAndClosePicker();
|
||||
|
||||
const onSwitchToNow = () => dispatch({ type: "RUN_QUERY_TO_NOW" });
|
||||
|
||||
|
@ -116,11 +103,9 @@ export const TimeSelector: FC = () => {
|
|||
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);
|
||||
const isFromPicker = fromPickerRef?.current && fromPickerRef?.current?.contains(target);
|
||||
const isUntilPicker = untilPickerRef?.current && untilPickerRef?.current?.contains(target);
|
||||
if (isFromButton || isUntilButton || isFromPicker || isUntilPicker) return;
|
||||
if (isFromPicker || isUntilPicker) return;
|
||||
handleCloseOptions();
|
||||
});
|
||||
|
||||
|
@ -174,38 +159,22 @@ export const TimeSelector: FC = () => {
|
|||
"vm-time-selector-left-inputs_dark": isDarkTheme
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className="vm-time-selector-left-inputs__date"
|
||||
ref={fromRef}
|
||||
>
|
||||
<label>From:</label>
|
||||
<span>{formFormat}</span>
|
||||
<CalendarIcon/>
|
||||
<DatePicker
|
||||
label={"Date From"}
|
||||
ref={fromPickerRef}
|
||||
date={from || ""}
|
||||
onChange={handleFromChange}
|
||||
targetRef={fromRef}
|
||||
timepicker={true}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="vm-time-selector-left-inputs__date"
|
||||
ref={untilRef}
|
||||
>
|
||||
<label>To:</label>
|
||||
<span>{untilFormat}</span>
|
||||
<CalendarIcon/>
|
||||
<DatePicker
|
||||
label={"Date To"}
|
||||
ref={untilPickerRef}
|
||||
date={until || ""}
|
||||
onChange={handleUntilChange}
|
||||
targetRef={untilRef}
|
||||
timepicker={true}
|
||||
/>
|
||||
</div>
|
||||
<DateTimeInput
|
||||
value={from}
|
||||
label="From:"
|
||||
pickerLabel="Date From"
|
||||
pickerRef={fromPickerRef}
|
||||
onChange={setFrom}
|
||||
onEnter={setTimeAndClosePicker}
|
||||
/>
|
||||
<DateTimeInput
|
||||
value={until}
|
||||
label="To:"
|
||||
pickerLabel="Date To"
|
||||
pickerRef={untilPickerRef}
|
||||
onChange={setUntil}
|
||||
onEnter={setTimeAndClosePicker}
|
||||
/>
|
||||
</div>
|
||||
<div className="vm-time-selector-left-timezone">
|
||||
<div className="vm-time-selector-left-timezone__title">{activeTimezone.region}</div>
|
||||
|
@ -228,7 +197,7 @@ export const TimeSelector: FC = () => {
|
|||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={onApplyClick}
|
||||
onClick={setTimeAndClosePicker}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
|
|
|
@ -31,48 +31,6 @@
|
|||
display: grid;
|
||||
align-items: flex-start;
|
||||
justify-content: stretch;
|
||||
|
||||
&_dark &__date {
|
||||
border-color: $color-text-disabled;
|
||||
}
|
||||
|
||||
&__date {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 14px;
|
||||
gap: $padding-small;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: $padding-small;
|
||||
margin-bottom: $padding-global;
|
||||
border-bottom: $border-divider;
|
||||
cursor: pointer;
|
||||
transition: color 200ms ease-in-out, border-bottom-color 300ms ease;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-bottom-color: $color-primary;
|
||||
}
|
||||
|
||||
&:hover svg,
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
label {
|
||||
grid-column: 1/3;
|
||||
font-size: $font-size-small;
|
||||
color: $color-text-secondary;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: $color-text-secondary;
|
||||
transition: color 200ms ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-timezone {
|
||||
|
|
|
@ -3,65 +3,45 @@ import dayjs, { Dayjs } from "dayjs";
|
|||
import CalendarHeader from "./CalendarHeader/CalendarHeader";
|
||||
import CalendarBody from "./CalendarBody/CalendarBody";
|
||||
import YearsList from "./YearsList/YearsList";
|
||||
import TimePicker from "../TImePicker/TimePicker";
|
||||
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";
|
||||
import MonthsList from "./MonthsList/MonthsList";
|
||||
|
||||
interface DatePickerProps {
|
||||
date: Date | Dayjs
|
||||
format?: string
|
||||
timepicker?: boolean,
|
||||
onChange: (date: string) => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ value: "date", icon: <CalendarIcon/> },
|
||||
{ value: "time", icon: <ClockIcon/> }
|
||||
];
|
||||
enum CalendarTypeView {
|
||||
"days",
|
||||
"months",
|
||||
"years"
|
||||
}
|
||||
|
||||
const Calendar: FC<DatePickerProps> = ({
|
||||
date,
|
||||
timepicker = false,
|
||||
format = DATE_TIME_FORMAT,
|
||||
onChange,
|
||||
onClose
|
||||
}) => {
|
||||
const [displayYears, setDisplayYears] = useState(false);
|
||||
const [viewType, setViewType] = useState<CalendarTypeView>(CalendarTypeView.days);
|
||||
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);
|
||||
setViewType(prev => prev === CalendarTypeView.years ? CalendarTypeView.days : CalendarTypeView.years);
|
||||
};
|
||||
|
||||
const handleChangeViewDate = (date: Dayjs) => {
|
||||
setViewDate(date);
|
||||
setDisplayYears(false);
|
||||
setViewType(prev => prev === CalendarTypeView.years ? CalendarTypeView.months : CalendarTypeView.days);
|
||||
};
|
||||
|
||||
const handleChangeSelectDate = (date: Dayjs) => {
|
||||
setSelectDate(date);
|
||||
if (timepicker) setTab("time");
|
||||
};
|
||||
|
||||
const handleChangeTime = (time: string) => {
|
||||
const [hour, minute, second] = time.split(":");
|
||||
setSelectDate(prev => prev.set("hour", +hour).set("minute", +minute).set("second", +second));
|
||||
};
|
||||
|
||||
const handleChangeTab = (value: string) => {
|
||||
setTab(value);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -69,6 +49,12 @@ const Calendar: FC<DatePickerProps> = ({
|
|||
onChange(selectDate.format(format));
|
||||
}, [selectDate]);
|
||||
|
||||
useEffect(() => {
|
||||
const value = dayjs.tz(date);
|
||||
setViewDate(value);
|
||||
setSelectDate(value);
|
||||
}, [date]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
|
@ -76,51 +62,32 @@ const Calendar: FC<DatePickerProps> = ({
|
|||
"vm-calendar_mobile": isMobile,
|
||||
})}
|
||||
>
|
||||
{tab === "date" && (
|
||||
<CalendarHeader
|
||||
<CalendarHeader
|
||||
viewDate={viewDate}
|
||||
onChangeViewDate={handleChangeViewDate}
|
||||
toggleDisplayYears={toggleDisplayYears}
|
||||
showArrowNav={viewType === CalendarTypeView.days}
|
||||
/>
|
||||
{viewType === CalendarTypeView.days && (
|
||||
<CalendarBody
|
||||
viewDate={viewDate}
|
||||
selectDate={selectDate}
|
||||
onChangeSelectDate={handleChangeSelectDate}
|
||||
/>
|
||||
)}
|
||||
{viewType === CalendarTypeView.years && (
|
||||
<YearsList
|
||||
viewDate={viewDate}
|
||||
onChangeViewDate={handleChangeViewDate}
|
||||
toggleDisplayYears={toggleDisplayYears}
|
||||
displayYears={displayYears}
|
||||
/>
|
||||
)}
|
||||
|
||||
{tab === "date" && (
|
||||
<>
|
||||
{!displayYears && (
|
||||
<CalendarBody
|
||||
viewDate={viewDate}
|
||||
selectDate={selectDate}
|
||||
onChangeSelectDate={handleChangeSelectDate}
|
||||
/>
|
||||
)}
|
||||
{displayYears && (
|
||||
<YearsList
|
||||
viewDate={viewDate}
|
||||
onChangeViewDate={handleChangeViewDate}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{tab === "time" && (
|
||||
<TimePicker
|
||||
{viewType === CalendarTypeView.months && (
|
||||
<MonthsList
|
||||
selectDate={selectDate}
|
||||
onChangeTime={handleChangeTime}
|
||||
onClose={handleClose}
|
||||
viewDate={viewDate}
|
||||
onChangeViewDate={handleChangeViewDate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{timepicker && (
|
||||
<div className="vm-calendar__tabs">
|
||||
<Tabs
|
||||
activeItem={tab}
|
||||
items={tabs}
|
||||
onChange={handleChangeTab}
|
||||
indicatorPlacement="top"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,11 +5,11 @@ import { ArrowDownIcon, ArrowDropDownIcon } from "../../../Icons";
|
|||
interface CalendarHeaderProps {
|
||||
viewDate: Dayjs
|
||||
onChangeViewDate: (date: Dayjs) => void
|
||||
displayYears: boolean
|
||||
showArrowNav: boolean
|
||||
toggleDisplayYears: () => void
|
||||
}
|
||||
|
||||
const CalendarHeader: FC<CalendarHeaderProps> = ({ viewDate, displayYears, onChangeViewDate, toggleDisplayYears }) => {
|
||||
const CalendarHeader: FC<CalendarHeaderProps> = ({ viewDate, showArrowNav, onChangeViewDate, toggleDisplayYears }) => {
|
||||
|
||||
const setPrevMonth = () => {
|
||||
onChangeViewDate(viewDate.subtract(1, "month"));
|
||||
|
@ -32,7 +32,7 @@ const CalendarHeader: FC<CalendarHeaderProps> = ({ viewDate, displayYears, onCha
|
|||
<ArrowDropDownIcon/>
|
||||
</div>
|
||||
</div>
|
||||
{!displayYears && (
|
||||
{showArrowNav && (
|
||||
<div className="vm-calendar-header-right">
|
||||
<div
|
||||
className="vm-calendar-header-right__prev"
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import React, { FC, useEffect, useMemo } from "preact/compat";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface CalendarMonthsProps {
|
||||
viewDate: Dayjs,
|
||||
selectDate: Dayjs
|
||||
|
||||
onChangeViewDate: (date: Dayjs) => void
|
||||
}
|
||||
|
||||
const MonthsList: FC<CalendarMonthsProps> = ({ viewDate, selectDate, onChangeViewDate }) => {
|
||||
|
||||
const today = dayjs().format("MM");
|
||||
const currentMonths = useMemo(() => selectDate.format("MM"), [selectDate]);
|
||||
const months: Dayjs[] = useMemo(() => {
|
||||
return new Array(12).fill("").map((d, i) => dayjs(viewDate).month(i));
|
||||
}, [viewDate]);
|
||||
|
||||
useEffect(() => {
|
||||
const selectedEl = document.getElementById(`vm-calendar-year-${currentMonths}`);
|
||||
if (!selectedEl) return;
|
||||
selectedEl.scrollIntoView({ block: "center" });
|
||||
}, []);
|
||||
|
||||
const createHandlerClick = (date: Dayjs) => () => {
|
||||
onChangeViewDate(date);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="vm-calendar-years">
|
||||
{months.map(m => (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-calendar-years__year": true,
|
||||
"vm-calendar-years__year_selected": m.format("MM") === currentMonths,
|
||||
"vm-calendar-years__year_today": m.format("MM") === today
|
||||
})}
|
||||
id={`vm-calendar-year-${m.format("MM")}`}
|
||||
key={m.format("MM")}
|
||||
onClick={createHandlerClick(m)}
|
||||
>
|
||||
{m.format("MMMM")}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonthsList;
|
|
@ -9,9 +9,10 @@ interface CalendarYearsProps {
|
|||
|
||||
const YearsList: FC<CalendarYearsProps> = ({ viewDate, onChangeViewDate }) => {
|
||||
|
||||
const today = dayjs().format("YYYY");
|
||||
const currentYear = useMemo(() => viewDate.format("YYYY"), [viewDate]);
|
||||
const years: Dayjs[] = useMemo(() => {
|
||||
const displayYears = 206;
|
||||
const displayYears = 18;
|
||||
const year = dayjs();
|
||||
const startYear = year.subtract(displayYears/2, "year");
|
||||
return new Array(displayYears).fill(startYear).map((d, i) => d.add(i, "year"));
|
||||
|
@ -33,7 +34,8 @@ const YearsList: FC<CalendarYearsProps> = ({ viewDate, onChangeViewDate }) => {
|
|||
<div
|
||||
className={classNames({
|
||||
"vm-calendar-years__year": true,
|
||||
"vm-calendar-years__year_selected": y.format("YYYY") === currentYear
|
||||
"vm-calendar-years__year_selected": y.format("YYYY") === currentYear,
|
||||
"vm-calendar-years__year_today": y.format("YYYY") === today
|
||||
})}
|
||||
id={`vm-calendar-year-${y.format("YYYY")}`}
|
||||
key={y.format("YYYY")}
|
||||
|
|
|
@ -13,12 +13,6 @@
|
|||
padding: 0 $padding-global;
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
margin: 0 0-$padding-global 0-$padding-global;
|
||||
border-top: $border-divider;
|
||||
margin-top: $padding-global;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
|
@ -88,14 +82,14 @@
|
|||
&-body {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 32px);
|
||||
grid-template-rows: repeat(6, 32px);
|
||||
grid-template-rows: repeat(7, 32px);
|
||||
align-items: center;
|
||||
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));
|
||||
grid-template-rows: repeat(7, calc((100vw - ($padding-global * 2) - (6 * 2px))/7));
|
||||
}
|
||||
|
||||
&-cell {
|
||||
|
@ -166,143 +160,9 @@
|
|||
background-color: $color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-time-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-clock {
|
||||
$clock-size: 230px;
|
||||
$clock-offset: 42px;
|
||||
|
||||
position: relative;
|
||||
height: $clock-size;
|
||||
width: $clock-size;
|
||||
border-radius: 50%;
|
||||
border: $border-divider;
|
||||
box-shadow: $box-shadow;
|
||||
box-sizing: content-box;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: $color-primary;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: calc(($clock-size/2) - 1px);
|
||||
width: 2px;
|
||||
margin-top: $padding-small;
|
||||
height: calc(($clock-size/2) - $padding-small);
|
||||
background-color: $color-primary;
|
||||
transform-origin: bottom;
|
||||
transition: transform 200ms ease-in-out;
|
||||
opacity: 0.8;
|
||||
z-index: 0;
|
||||
|
||||
&_offset {
|
||||
margin-top: $clock-offset;
|
||||
height: calc(($clock-size/2) - $clock-offset);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: $color-primary;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&__time {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding-top: $padding-small;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 30px;
|
||||
left: calc(($clock-size/2) - 15px);
|
||||
height: calc($clock-size/2);
|
||||
transform-origin: bottom;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
|
||||
&_hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&_offset {
|
||||
padding: 0;
|
||||
margin-top: $clock-offset;
|
||||
height: calc(($clock-size/2) - $clock-offset);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:hover span {
|
||||
background-color: rgba($color-black, 0.1);
|
||||
}
|
||||
|
||||
span {
|
||||
position: relative;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 30px;
|
||||
min-height: 30px;
|
||||
border-radius: 50%;
|
||||
transform-origin: center;
|
||||
transition: background-color 300ms ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-fields {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: $padding-global;
|
||||
|
||||
&_dark &__input {
|
||||
border-color: $color-text-disabled;
|
||||
}
|
||||
|
||||
span {
|
||||
margin: 0 $padding-small;
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 64px;
|
||||
height: 32px;
|
||||
border: $border-divider;
|
||||
border-radius: $border-radius-small;
|
||||
font-size: $font-size-medium;
|
||||
padding: 2px $padding-small;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
color: $color-text;
|
||||
|
||||
&:focus {
|
||||
border-color: $color-primary;
|
||||
}
|
||||
&_today {
|
||||
border: 1px solid $color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ interface DatePickerProps {
|
|||
date: string | Date | Dayjs,
|
||||
targetRef: Ref<HTMLElement>
|
||||
format?: string
|
||||
timepicker?: boolean
|
||||
label?: string
|
||||
onChange: (val: string) => void
|
||||
}
|
||||
|
@ -18,12 +17,11 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
|
|||
date,
|
||||
targetRef,
|
||||
format = DATE_TIME_FORMAT,
|
||||
timepicker,
|
||||
onChange,
|
||||
label
|
||||
}, ref) => {
|
||||
const [openCalendar, setOpenCalendar] = useState(false);
|
||||
const dateDayjs = useMemo(() => date ? dayjs.tz(date) : dayjs().tz(), [date]);
|
||||
const dateDayjs = useMemo(() => dayjs(date).isValid() ? dayjs.tz(date) : dayjs().tz(), [date]);
|
||||
const { isMobile } = useDeviceDetect();
|
||||
|
||||
const toggleOpenCalendar = () => {
|
||||
|
@ -35,8 +33,8 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
|
|||
};
|
||||
|
||||
const handleChangeDate = (val: string) => {
|
||||
if (!timepicker) handleCloseCalendar();
|
||||
onChange(val);
|
||||
handleCloseCalendar();
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
|
@ -71,9 +69,7 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
|
|||
<Calendar
|
||||
date={dateDayjs}
|
||||
format={format}
|
||||
timepicker={timepicker}
|
||||
onChange={handleChangeDate}
|
||||
onClose={handleCloseCalendar}
|
||||
/>
|
||||
</div>
|
||||
</Popper>
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
import React, { FC, useEffect, useRef, useState } from "preact/compat";
|
||||
import { ChangeEvent, KeyboardEvent } from "react";
|
||||
import { CalendarIcon } from "../../Icons";
|
||||
import DatePicker from "../DatePicker";
|
||||
import Button from "../../Button/Button";
|
||||
import { DATE_TIME_FORMAT } from "../../../../constants/date";
|
||||
import InputMask from "react-input-mask";
|
||||
import dayjs from "dayjs";
|
||||
import classNames from "classnames";
|
||||
import "./style.scss";
|
||||
|
||||
const formatStringDate = (val: string) => {
|
||||
return dayjs(val).isValid() ? dayjs.tz(val).format(DATE_TIME_FORMAT) : val;
|
||||
};
|
||||
|
||||
interface DateTimeInputProps {
|
||||
value?: string;
|
||||
label: string;
|
||||
pickerLabel: string;
|
||||
pickerRef: React.RefObject<HTMLDivElement>;
|
||||
onChange: (date: string) => void;
|
||||
onEnter: () => void;
|
||||
}
|
||||
|
||||
const DateTimeInput: FC<DateTimeInputProps> = ({
|
||||
value = "",
|
||||
label,
|
||||
pickerLabel,
|
||||
pickerRef,
|
||||
onChange,
|
||||
onEnter
|
||||
}) => {
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const [inputRef, setInputRef] = useState<HTMLInputElement | null>(null);
|
||||
|
||||
const [maskedValue, setMaskedValue] = useState(formatStringDate(value));
|
||||
const [focusToTime, setFocusToTime] = useState(false);
|
||||
const [awaitChangeForEnter, setAwaitChangeForEnter] = useState(false);
|
||||
const error = dayjs(maskedValue).isValid() ? "" : "Expected format: YYYY-MM-DD HH:mm:ss";
|
||||
|
||||
const handleMaskedChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setMaskedValue(e.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
onChange(maskedValue);
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
onChange(maskedValue);
|
||||
setAwaitChangeForEnter(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeDate = (val: string) => {
|
||||
setMaskedValue(val);
|
||||
setFocusToTime(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const newValue = formatStringDate(value);
|
||||
if (newValue !== maskedValue) {
|
||||
setMaskedValue(newValue);
|
||||
}
|
||||
|
||||
if (awaitChangeForEnter) {
|
||||
onEnter();
|
||||
setAwaitChangeForEnter(false);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (focusToTime && inputRef) {
|
||||
inputRef.focus();
|
||||
inputRef.setSelectionRange(11, 11);
|
||||
setFocusToTime(false);
|
||||
}
|
||||
}, [focusToTime]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-date-time-input": true,
|
||||
"vm-date-time-input_error": error
|
||||
})}
|
||||
>
|
||||
<label>{label}</label>
|
||||
<InputMask
|
||||
tabIndex={1}
|
||||
inputRef={setInputRef}
|
||||
mask="9999-99-99 99:99:99"
|
||||
placeholder="YYYY-MM-DD HH:mm:ss"
|
||||
value={maskedValue}
|
||||
autoCapitalize={"none"}
|
||||
inputMode={"numeric"}
|
||||
maskChar={null}
|
||||
onChange={handleMaskedChange}
|
||||
onBlur={handleBlur}
|
||||
onKeyUp={handleKeyUp}
|
||||
/>
|
||||
{error && (
|
||||
<span className="vm-date-time-input__error-text">{error}</span>
|
||||
)}
|
||||
<div
|
||||
className="vm-date-time-input__icon"
|
||||
ref={wrapperRef}
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
color="gray"
|
||||
size="small"
|
||||
startIcon={<CalendarIcon/>}
|
||||
/>
|
||||
</div>
|
||||
<DatePicker
|
||||
label={pickerLabel}
|
||||
ref={pickerRef}
|
||||
date={maskedValue}
|
||||
onChange={handleChangeDate}
|
||||
targetRef={wrapperRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateTimeInput;
|
|
@ -0,0 +1,61 @@
|
|||
@use "src/styles/variables" as *;
|
||||
|
||||
.vm-date-time-input {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: $padding-small 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: $padding-global;
|
||||
cursor: pointer;
|
||||
transition: color 200ms ease-in-out, border-bottom-color 300ms ease;
|
||||
|
||||
&:hover input {
|
||||
border-bottom-color: $color-primary;
|
||||
}
|
||||
|
||||
label {
|
||||
grid-column: 1/3;
|
||||
width: 100%;
|
||||
font-size: $font-size-small;
|
||||
color: $color-text-secondary;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0 0 $padding-small;
|
||||
border-bottom: $border-divider;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
background: transparent;
|
||||
color: $color-text;
|
||||
|
||||
&:focus {
|
||||
border-bottom-color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&_error input {
|
||||
border-color: $color-error;
|
||||
|
||||
&:focus {
|
||||
border-bottom-color: $color-error;
|
||||
}
|
||||
}
|
||||
|
||||
&__error-text {
|
||||
color: $color-error;
|
||||
font-size: $font-size-small;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -$font-size-small;
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ created by v1.90.0 or newer versions. The solution is to upgrade to v1.90.0 or n
|
|||
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): add support for [VictoriaMetrics remote write protocol](https://docs.victoriametrics.com/vmagent.html#victoriametrics-remote-write-protocol) when [sending / receiving data to / from Kafka](https://docs.victoriametrics.com/vmagent.html#kafka-integration). This protocol allows saving egress network bandwidth costs when sending data from `vmagent` to `Kafka` located in another datacenter or availability zone. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1225).
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): add `--kafka.consumer.topic.concurrency` command-line flag. It controls the number of Kafka consumer workers to use by `vmagent`. It should eliminate the need to start multiple `vmagent` instances to improve data transfer rate. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1957).
|
||||
* FEATURE: [vmagent](https://docs.victoriametrics.com/vmagent.html): add support for [Kafka producer and consumer](https://docs.victoriametrics.com/vmagent.html#kafka-integration) on `arm64` machines. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2271).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): Add the ability to manually input date and time when selecting a time range. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3968).
|
||||
|
||||
* BUGFIX: prevent from slow [snapshot creating](https://docs.victoriametrics.com/#how-to-work-with-snapshots) under high data ingestion rate. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3551).
|
||||
|
||||
|
|
Loading…
Reference in a new issue