mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-12-01 14:47:38 +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/marked": "^4.0.2",
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^17.0.21",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
|
"@types/react-input-mask": "^3.0.2",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/webpack-env": "^1.16.3",
|
"@types/webpack-env": "^1.16.3",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
|
@ -28,6 +29,7 @@
|
||||||
"marked": "^4.0.14",
|
"marked": "^4.0.14",
|
||||||
"preact": "^10.7.1",
|
"preact": "^10.7.1",
|
||||||
"qs": "^6.10.3",
|
"qs": "^6.10.3",
|
||||||
|
"react-input-mask": "^2.0.4",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"sass": "^1.56.0",
|
"sass": "^1.56.0",
|
||||||
"typescript": "~4.6.2",
|
"typescript": "~4.6.2",
|
||||||
|
@ -4392,6 +4394,14 @@
|
||||||
"@types/react": "*"
|
"@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": {
|
"node_modules/@types/react-router": {
|
||||||
"version": "5.1.20",
|
"version": "5.1.20",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
|
||||||
|
@ -10236,6 +10246,14 @@
|
||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz",
|
||||||
|
@ -16403,6 +16421,19 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": 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": {
|
"node_modules/react-is": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
|
@ -18811,6 +18842,14 @@
|
||||||
"makeerror": "1.0.12"
|
"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": {
|
"node_modules/watchpack": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||||
|
@ -18851,9 +18890,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack": {
|
"node_modules/webpack": {
|
||||||
"version": "5.75.0",
|
"version": "5.76.2",
|
||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.2.tgz",
|
||||||
"integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==",
|
"integrity": "sha512-Th05ggRm23rVzEOlX8y67NkYCHa9nTNcwHPBhdg+lKG+mtiW7XgggjAeeLnADAe7mLjJ6LUNfgHAuRRh+Z6J7w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"@types/marked": "^4.0.2",
|
"@types/marked": "^4.0.2",
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^17.0.21",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
|
"@types/react-input-mask": "^3.0.2",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/webpack-env": "^1.16.3",
|
"@types/webpack-env": "^1.16.3",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
|
@ -24,6 +25,7 @@
|
||||||
"marked": "^4.0.14",
|
"marked": "^4.0.14",
|
||||||
"preact": "^10.7.1",
|
"preact": "^10.7.1",
|
||||||
"qs": "^6.10.3",
|
"qs": "^6.10.3",
|
||||||
|
"react-input-mask": "^2.0.4",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"sass": "^1.56.0",
|
"sass": "^1.56.0",
|
||||||
"typescript": "~4.6.2",
|
"typescript": "~4.6.2",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
@use "src/styles/variables" as *;
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
.vm-time-duration {
|
.vm-time-duration {
|
||||||
max-height: 200px;
|
max-height: 227px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
font-size: $font-size;
|
font-size: $font-size;
|
||||||
|
|
||||||
|
|
|
@ -4,18 +4,18 @@ import TimeDurationSelector from "../TimeDurationSelector/TimeDurationSelector";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { getAppModeEnable } from "../../../../utils/app-mode";
|
import { getAppModeEnable } from "../../../../utils/app-mode";
|
||||||
import { useTimeDispatch, useTimeState } from "../../../../state/time/TimeStateContext";
|
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 Button from "../../../Main/Button/Button";
|
||||||
import Popper from "../../../Main/Popper/Popper";
|
import Popper from "../../../Main/Popper/Popper";
|
||||||
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
import Tooltip from "../../../Main/Tooltip/Tooltip";
|
||||||
import { DATE_TIME_FORMAT } from "../../../../constants/date";
|
import { DATE_TIME_FORMAT } from "../../../../constants/date";
|
||||||
import useResize from "../../../../hooks/useResize";
|
import useResize from "../../../../hooks/useResize";
|
||||||
import DatePicker from "../../../Main/DatePicker/DatePicker";
|
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import useClickOutside from "../../../../hooks/useClickOutside";
|
import useClickOutside from "../../../../hooks/useClickOutside";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useAppState } from "../../../../state/common/StateContext";
|
import { useAppState } from "../../../../state/common/StateContext";
|
||||||
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||||
|
import DateTimeInput from "../../../Main/DatePicker/DateTimeInput/DateTimeInput";
|
||||||
|
|
||||||
export const TimeSelector: FC = () => {
|
export const TimeSelector: FC = () => {
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
|
@ -27,9 +27,6 @@ export const TimeSelector: FC = () => {
|
||||||
const [until, setUntil] = useState<string>();
|
const [until, setUntil] = useState<string>();
|
||||||
const [from, setFrom] = 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 { period: { end, start }, relativeTime, timezone, duration } = useTimeState();
|
||||||
const dispatch = useTimeDispatch();
|
const dispatch = useTimeDispatch();
|
||||||
const appModeEnable = getAppModeEnable();
|
const appModeEnable = getAppModeEnable();
|
||||||
|
@ -55,10 +52,7 @@ export const TimeSelector: FC = () => {
|
||||||
const formatRange = useMemo(() => {
|
const formatRange = useMemo(() => {
|
||||||
const startFormat = dayjs.tz(dateFromSeconds(start)).format(DATE_TIME_FORMAT);
|
const startFormat = dayjs.tz(dateFromSeconds(start)).format(DATE_TIME_FORMAT);
|
||||||
const endFormat = dayjs.tz(dateFromSeconds(end)).format(DATE_TIME_FORMAT);
|
const endFormat = dayjs.tz(dateFromSeconds(end)).format(DATE_TIME_FORMAT);
|
||||||
return {
|
return { start: startFormat, end: endFormat };
|
||||||
start: startFormat,
|
|
||||||
end: endFormat
|
|
||||||
};
|
|
||||||
}, [start, end, timezone]);
|
}, [start, end, timezone]);
|
||||||
|
|
||||||
const dateTitle = useMemo(() => {
|
const dateTitle = useMemo(() => {
|
||||||
|
@ -66,8 +60,6 @@ export const TimeSelector: FC = () => {
|
||||||
return isRelativeTime ? relativeTime.replace(/_/g, " ") : `${formatRange.start} - ${formatRange.end}`;
|
return isRelativeTime ? relativeTime.replace(/_/g, " ") : `${formatRange.start} - ${formatRange.end}`;
|
||||||
}, [relativeTime, formatRange]);
|
}, [relativeTime, formatRange]);
|
||||||
|
|
||||||
const fromRef = useRef<HTMLDivElement>(null);
|
|
||||||
const untilRef = useRef<HTMLDivElement>(null);
|
|
||||||
const fromPickerRef = useRef<HTMLDivElement>(null);
|
const fromPickerRef = useRef<HTMLDivElement>(null);
|
||||||
const untilPickerRef = useRef<HTMLDivElement>(null);
|
const untilPickerRef = useRef<HTMLDivElement>(null);
|
||||||
const [openOptions, setOpenOptions] = useState(false);
|
const [openOptions, setOpenOptions] = useState(false);
|
||||||
|
@ -82,11 +74,6 @@ export const TimeSelector: FC = () => {
|
||||||
}
|
}
|
||||||
setOpenOptions(false);
|
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" });
|
const onSwitchToNow = () => dispatch({ type: "RUN_QUERY_TO_NOW" });
|
||||||
|
|
||||||
|
@ -116,11 +103,9 @@ export const TimeSelector: FC = () => {
|
||||||
useClickOutside(wrapperRef, (e) => {
|
useClickOutside(wrapperRef, (e) => {
|
||||||
if (isMobile) return;
|
if (isMobile) return;
|
||||||
const target = e.target as HTMLElement;
|
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 isFromPicker = fromPickerRef?.current && fromPickerRef?.current?.contains(target);
|
||||||
const isUntilPicker = untilPickerRef?.current && untilPickerRef?.current?.contains(target);
|
const isUntilPicker = untilPickerRef?.current && untilPickerRef?.current?.contains(target);
|
||||||
if (isFromButton || isUntilButton || isFromPicker || isUntilPicker) return;
|
if (isFromPicker || isUntilPicker) return;
|
||||||
handleCloseOptions();
|
handleCloseOptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -174,38 +159,22 @@ export const TimeSelector: FC = () => {
|
||||||
"vm-time-selector-left-inputs_dark": isDarkTheme
|
"vm-time-selector-left-inputs_dark": isDarkTheme
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<DateTimeInput
|
||||||
className="vm-time-selector-left-inputs__date"
|
value={from}
|
||||||
ref={fromRef}
|
label="From:"
|
||||||
>
|
pickerLabel="Date From"
|
||||||
<label>From:</label>
|
pickerRef={fromPickerRef}
|
||||||
<span>{formFormat}</span>
|
onChange={setFrom}
|
||||||
<CalendarIcon/>
|
onEnter={setTimeAndClosePicker}
|
||||||
<DatePicker
|
/>
|
||||||
label={"Date From"}
|
<DateTimeInput
|
||||||
ref={fromPickerRef}
|
value={until}
|
||||||
date={from || ""}
|
label="To:"
|
||||||
onChange={handleFromChange}
|
pickerLabel="Date To"
|
||||||
targetRef={fromRef}
|
pickerRef={untilPickerRef}
|
||||||
timepicker={true}
|
onChange={setUntil}
|
||||||
/>
|
onEnter={setTimeAndClosePicker}
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="vm-time-selector-left-timezone">
|
<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__title">{activeTimezone.region}</div>
|
||||||
|
@ -228,7 +197,7 @@ export const TimeSelector: FC = () => {
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={onApplyClick}
|
onClick={setTimeAndClosePicker}
|
||||||
>
|
>
|
||||||
Apply
|
Apply
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -31,48 +31,6 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: stretch;
|
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 {
|
&-timezone {
|
||||||
|
|
|
@ -3,65 +3,45 @@ import dayjs, { Dayjs } from "dayjs";
|
||||||
import CalendarHeader from "./CalendarHeader/CalendarHeader";
|
import CalendarHeader from "./CalendarHeader/CalendarHeader";
|
||||||
import CalendarBody from "./CalendarBody/CalendarBody";
|
import CalendarBody from "./CalendarBody/CalendarBody";
|
||||||
import YearsList from "./YearsList/YearsList";
|
import YearsList from "./YearsList/YearsList";
|
||||||
import TimePicker from "../TImePicker/TimePicker";
|
|
||||||
import { DATE_TIME_FORMAT } from "../../../../constants/date";
|
import { DATE_TIME_FORMAT } from "../../../../constants/date";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import { CalendarIcon, ClockIcon } from "../../Icons";
|
|
||||||
import Tabs from "../../Tabs/Tabs";
|
|
||||||
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
import useDeviceDetect from "../../../../hooks/useDeviceDetect";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import MonthsList from "./MonthsList/MonthsList";
|
||||||
|
|
||||||
interface DatePickerProps {
|
interface DatePickerProps {
|
||||||
date: Date | Dayjs
|
date: Date | Dayjs
|
||||||
format?: string
|
format?: string
|
||||||
timepicker?: boolean,
|
|
||||||
onChange: (date: string) => void
|
onChange: (date: string) => void
|
||||||
onClose?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = [
|
enum CalendarTypeView {
|
||||||
{ value: "date", icon: <CalendarIcon/> },
|
"days",
|
||||||
{ value: "time", icon: <ClockIcon/> }
|
"months",
|
||||||
];
|
"years"
|
||||||
|
}
|
||||||
|
|
||||||
const Calendar: FC<DatePickerProps> = ({
|
const Calendar: FC<DatePickerProps> = ({
|
||||||
date,
|
date,
|
||||||
timepicker = false,
|
|
||||||
format = DATE_TIME_FORMAT,
|
format = DATE_TIME_FORMAT,
|
||||||
onChange,
|
onChange,
|
||||||
onClose
|
|
||||||
}) => {
|
}) => {
|
||||||
const [displayYears, setDisplayYears] = useState(false);
|
const [viewType, setViewType] = useState<CalendarTypeView>(CalendarTypeView.days);
|
||||||
const [viewDate, setViewDate] = useState(dayjs.tz(date));
|
const [viewDate, setViewDate] = useState(dayjs.tz(date));
|
||||||
const [selectDate, setSelectDate] = useState(dayjs.tz(date));
|
const [selectDate, setSelectDate] = useState(dayjs.tz(date));
|
||||||
const [tab, setTab] = useState(tabs[0].value);
|
|
||||||
const { isMobile } = useDeviceDetect();
|
const { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const toggleDisplayYears = () => {
|
const toggleDisplayYears = () => {
|
||||||
setDisplayYears(prev => !prev);
|
setViewType(prev => prev === CalendarTypeView.years ? CalendarTypeView.days : CalendarTypeView.years);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeViewDate = (date: Dayjs) => {
|
const handleChangeViewDate = (date: Dayjs) => {
|
||||||
setViewDate(date);
|
setViewDate(date);
|
||||||
setDisplayYears(false);
|
setViewType(prev => prev === CalendarTypeView.years ? CalendarTypeView.months : CalendarTypeView.days);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeSelectDate = (date: Dayjs) => {
|
const handleChangeSelectDate = (date: Dayjs) => {
|
||||||
setSelectDate(date);
|
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(() => {
|
useEffect(() => {
|
||||||
|
@ -69,6 +49,12 @@ const Calendar: FC<DatePickerProps> = ({
|
||||||
onChange(selectDate.format(format));
|
onChange(selectDate.format(format));
|
||||||
}, [selectDate]);
|
}, [selectDate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const value = dayjs.tz(date);
|
||||||
|
setViewDate(value);
|
||||||
|
setSelectDate(value);
|
||||||
|
}, [date]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
|
@ -76,51 +62,32 @@ const Calendar: FC<DatePickerProps> = ({
|
||||||
"vm-calendar_mobile": isMobile,
|
"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}
|
viewDate={viewDate}
|
||||||
onChangeViewDate={handleChangeViewDate}
|
onChangeViewDate={handleChangeViewDate}
|
||||||
toggleDisplayYears={toggleDisplayYears}
|
|
||||||
displayYears={displayYears}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{viewType === CalendarTypeView.months && (
|
||||||
{tab === "date" && (
|
<MonthsList
|
||||||
<>
|
|
||||||
{!displayYears && (
|
|
||||||
<CalendarBody
|
|
||||||
viewDate={viewDate}
|
|
||||||
selectDate={selectDate}
|
|
||||||
onChangeSelectDate={handleChangeSelectDate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{displayYears && (
|
|
||||||
<YearsList
|
|
||||||
viewDate={viewDate}
|
|
||||||
onChangeViewDate={handleChangeViewDate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{tab === "time" && (
|
|
||||||
<TimePicker
|
|
||||||
selectDate={selectDate}
|
selectDate={selectDate}
|
||||||
onChangeTime={handleChangeTime}
|
viewDate={viewDate}
|
||||||
onClose={handleClose}
|
onChangeViewDate={handleChangeViewDate}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{timepicker && (
|
|
||||||
<div className="vm-calendar__tabs">
|
|
||||||
<Tabs
|
|
||||||
activeItem={tab}
|
|
||||||
items={tabs}
|
|
||||||
onChange={handleChangeTab}
|
|
||||||
indicatorPlacement="top"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,11 +5,11 @@ import { ArrowDownIcon, ArrowDropDownIcon } from "../../../Icons";
|
||||||
interface CalendarHeaderProps {
|
interface CalendarHeaderProps {
|
||||||
viewDate: Dayjs
|
viewDate: Dayjs
|
||||||
onChangeViewDate: (date: Dayjs) => void
|
onChangeViewDate: (date: Dayjs) => void
|
||||||
displayYears: boolean
|
showArrowNav: boolean
|
||||||
toggleDisplayYears: () => void
|
toggleDisplayYears: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const CalendarHeader: FC<CalendarHeaderProps> = ({ viewDate, displayYears, onChangeViewDate, toggleDisplayYears }) => {
|
const CalendarHeader: FC<CalendarHeaderProps> = ({ viewDate, showArrowNav, onChangeViewDate, toggleDisplayYears }) => {
|
||||||
|
|
||||||
const setPrevMonth = () => {
|
const setPrevMonth = () => {
|
||||||
onChangeViewDate(viewDate.subtract(1, "month"));
|
onChangeViewDate(viewDate.subtract(1, "month"));
|
||||||
|
@ -32,7 +32,7 @@ const CalendarHeader: FC<CalendarHeaderProps> = ({ viewDate, displayYears, onCha
|
||||||
<ArrowDropDownIcon/>
|
<ArrowDropDownIcon/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!displayYears && (
|
{showArrowNav && (
|
||||||
<div className="vm-calendar-header-right">
|
<div className="vm-calendar-header-right">
|
||||||
<div
|
<div
|
||||||
className="vm-calendar-header-right__prev"
|
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 YearsList: FC<CalendarYearsProps> = ({ viewDate, onChangeViewDate }) => {
|
||||||
|
|
||||||
|
const today = dayjs().format("YYYY");
|
||||||
const currentYear = useMemo(() => viewDate.format("YYYY"), [viewDate]);
|
const currentYear = useMemo(() => viewDate.format("YYYY"), [viewDate]);
|
||||||
const years: Dayjs[] = useMemo(() => {
|
const years: Dayjs[] = useMemo(() => {
|
||||||
const displayYears = 206;
|
const displayYears = 18;
|
||||||
const year = dayjs();
|
const year = dayjs();
|
||||||
const startYear = year.subtract(displayYears/2, "year");
|
const startYear = year.subtract(displayYears/2, "year");
|
||||||
return new Array(displayYears).fill(startYear).map((d, i) => d.add(i, "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
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"vm-calendar-years__year": true,
|
"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")}`}
|
id={`vm-calendar-year-${y.format("YYYY")}`}
|
||||||
key={y.format("YYYY")}
|
key={y.format("YYYY")}
|
||||||
|
|
|
@ -13,12 +13,6 @@
|
||||||
padding: 0 $padding-global;
|
padding: 0 $padding-global;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__tabs {
|
|
||||||
margin: 0 0-$padding-global 0-$padding-global;
|
|
||||||
border-top: $border-divider;
|
|
||||||
margin-top: $padding-global;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-header {
|
&-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
|
@ -88,14 +82,14 @@
|
||||||
&-body {
|
&-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, 32px);
|
grid-template-columns: repeat(7, 32px);
|
||||||
grid-template-rows: repeat(6, 32px);
|
grid-template-rows: repeat(7, 32px);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
grid-template-columns: repeat(7, calc((100vw - ($padding-global * 2) - (6 * 2px))/7));
|
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 {
|
&-cell {
|
||||||
|
@ -166,143 +160,9 @@
|
||||||
background-color: $color-primary;
|
background-color: $color-primary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-time-picker {
|
&_today {
|
||||||
display: flex;
|
border: 1px solid $color-primary;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ interface DatePickerProps {
|
||||||
date: string | Date | Dayjs,
|
date: string | Date | Dayjs,
|
||||||
targetRef: Ref<HTMLElement>
|
targetRef: Ref<HTMLElement>
|
||||||
format?: string
|
format?: string
|
||||||
timepicker?: boolean
|
|
||||||
label?: string
|
label?: string
|
||||||
onChange: (val: string) => void
|
onChange: (val: string) => void
|
||||||
}
|
}
|
||||||
|
@ -18,12 +17,11 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
|
||||||
date,
|
date,
|
||||||
targetRef,
|
targetRef,
|
||||||
format = DATE_TIME_FORMAT,
|
format = DATE_TIME_FORMAT,
|
||||||
timepicker,
|
|
||||||
onChange,
|
onChange,
|
||||||
label
|
label
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const [openCalendar, setOpenCalendar] = useState(false);
|
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 { isMobile } = useDeviceDetect();
|
||||||
|
|
||||||
const toggleOpenCalendar = () => {
|
const toggleOpenCalendar = () => {
|
||||||
|
@ -35,8 +33,8 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeDate = (val: string) => {
|
const handleChangeDate = (val: string) => {
|
||||||
if (!timepicker) handleCloseCalendar();
|
|
||||||
onChange(val);
|
onChange(val);
|
||||||
|
handleCloseCalendar();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyUp = (e: KeyboardEvent) => {
|
const handleKeyUp = (e: KeyboardEvent) => {
|
||||||
|
@ -71,9 +69,7 @@ const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(({
|
||||||
<Calendar
|
<Calendar
|
||||||
date={dateDayjs}
|
date={dateDayjs}
|
||||||
format={format}
|
format={format}
|
||||||
timepicker={timepicker}
|
|
||||||
onChange={handleChangeDate}
|
onChange={handleChangeDate}
|
||||||
onClose={handleCloseCalendar}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Popper>
|
</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 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 `--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: [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).
|
* 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