1+ // import { FormInputSize } from '../Forms/types';
2+ // import { Icon } from '../Icon/Icon';
3+ import 'rc-picker/assets/index.css';
4+
5+ import { css, cx } from '@emotion/css';
6+ import { DateTime, dateTime, dateTimeAsMoment, GrafanaTheme2, isDateTimeInput } from '@grafana/data';
7+ import { FormInputSize, Icon, useStyles2 } from '@grafana/ui';
8+ import { DateTime as LuxonDateTime } from 'luxon';
9+ import RcPicker, { PickerProps } from 'rc-picker';
10+ import generateConfig from 'rc-picker/lib/generate/luxon'; // Changed from moment to luxon
11+ import locale from 'rc-picker/lib/locale/en_US';
12+ import * as React from 'react';
13+
14+ // import { useStyles2 } from '../../themes/ThemeContext';
15+ import { getFocusStyles } from '../../themes/mixins';
16+ import { inputSizes } from '../Forms/commonStyles';
17+
18+ interface BaseProps {
19+ onChange: (value: DateTime) => void | ((value?: DateTime) => void);
20+ value?: DateTime;
21+ showHour?: boolean;
22+ showSeconds?: boolean;
23+ minuteStep?: PickerProps['minuteStep'];
24+ size?: FormInputSize;
25+ disabled?: boolean;
26+ disabledHours?: () => number[];
27+ disabledMinutes?: () => number[];
28+ disabledSeconds?: () => number[];
29+ placeholder?: string;
30+ allowEmpty?: boolean;
31+ }
32+
33+ interface AllowEmptyProps extends BaseProps {
34+ allowEmpty: true;
35+ onChange: (value?: DateTime) => void;
36+ }
37+
38+ interface NoAllowEmptyProps extends BaseProps {
39+ allowEmpty?: false;
40+ onChange: (value: DateTime) => void;
41+ }
42+
43+ export type Props = AllowEmptyProps | NoAllowEmptyProps;
44+
45+ export const POPUP_CLASS_NAME = 'time-of-day-picker-panel';
46+
47+ export const TimeOfDayPicker = ({
48+ minuteStep = 1,
49+ showHour = true,
50+ showSeconds = false,
51+ value,
52+ size = 'auto',
53+ disabled,
54+ disabledHours,
55+ disabledMinutes,
56+ disabledSeconds,
57+ placeholder,
58+ // @ts-ignore
59+ use12Hours,
60+ // note: we can't destructure allowEmpty/onChange here
61+ // in order to discriminate the types properly later in the onChange handler
62+ ...restProps
63+ }: Props & { use12Hours?: boolean }) => {
64+ const styles = useStyles2(getStyles);
65+ const allowClear = restProps.allowEmpty ?? false;
66+
67+ return (
68+ <RcPicker<LuxonDateTime> // Changed from Moment to LuxonDateTime
69+ generateConfig={generateConfig}
70+ locale={locale}
71+ allowClear={
72+ allowClear && {
73+ clearIcon: <Icon name="times" className={styles.clearIcon} />,
74+ }
75+ }
76+ className={cx(inputSizes()[size], styles.input)}
77+ classNames={{
78+ popup: cx(styles.picker, POPUP_CLASS_NAME),
79+ }}
80+ defaultValue={restProps.allowEmpty ? undefined : toLuxonDateTime(dateTime())} // Updated to use Luxon conversion
81+ disabled={disabled}
82+ disabledTime={() => ({
83+ disabledHours,
84+ disabledMinutes,
85+ disabledSeconds,
86+ })}
87+ format={generateFormat(showHour, showSeconds, use12Hours)}
88+ minuteStep={minuteStep}
89+ onChange={(value) => {
90+ if (value && !Array.isArray(value)) {
91+ if (restProps.allowEmpty) {
92+ return restProps.onChange(value ? fromLuxonDateTime(value) : undefined);
93+ } else {
94+ return restProps.onChange(fromLuxonDateTime(value));
95+ }
96+ }
97+ }}
98+ use12Hours={use12Hours}
99+ picker="time"
100+ placeholder={placeholder}
101+ showNow={false}
102+ needConfirm={false}
103+ suffixIcon={<Caret wrapperStyle={styles.caretWrapper} />}
104+ value={value ? toLuxonDateTime(value) : undefined} // Updated to use Luxon conversion
105+ />
106+ );
107+ };
108+
109+ // Helper functions to convert between Grafana DateTime and Luxon DateTime
110+ function toLuxonDateTime(grafanaDateTime: DateTime): LuxonDateTime {
111+ return LuxonDateTime.fromJSDate(grafanaDateTime.toDate());
112+ }
113+
114+ function fromLuxonDateTime(luxonDateTime: LuxonDateTime): DateTime {
115+ return dateTime(luxonDateTime.toJSDate());
116+ }
117+
118+ function generateFormat(
119+ showHour = true,
120+ showSeconds = false,
121+ use12Hours = false // Optional parameter for 12-hour format
122+ ) {
123+ const maybeHour = showHour
124+ ? use12Hours
125+ ? 'h:'
126+ : 'H:' // 'h' for 12-hour, 'H' for 24-hour (no leading zero)
127+ : '';
128+
129+ const maybeSecond = showSeconds ? ':ss' : '';
130+ const amPm = use12Hours ? ' a' : ''; // Changed to lowercase 'a' for Luxon format
131+
132+ return maybeHour + 'mm' + maybeSecond + amPm;
133+ }
134+
135+ interface CaretProps {
136+ wrapperStyle?: string;
137+ }
138+
139+ const Caret = ({ wrapperStyle = '' }: CaretProps) => {
140+ return (
141+ <div className={wrapperStyle}>
142+ <Icon name="angle-down" />
143+ </div>
144+ );
145+ };
146+
147+ const getStyles = (theme: GrafanaTheme2) => {
148+ const bgColor = theme.components.input.background;
149+ const optionBgHover = theme.colors.action.hover;
150+ const borderRadius = theme.shape.radius.default;
151+ const borderColor = theme.components.input.borderColor;
152+
153+ return {
154+ caretWrapper: css({
155+ position: 'relative',
156+ top: '50%',
157+ transform: 'translateY(-50%)',
158+ display: 'inline-block',
159+ color: theme.colors.text.secondary,
160+ }),
161+ clearIcon: css({
162+ color: theme.colors.text.secondary,
163+
164+ '&:hover': {
165+ color: theme.colors.text.maxContrast,
166+ },
167+ }),
168+ picker: css({
169+ '&.rc-picker-dropdown': {
170+ boxShadow: 'none',
171+ zIndex: theme.zIndex.portal,
172+ },
173+ '.rc-picker-time-panel-column': {
174+ fontSize: theme.typography.htmlFontSize,
175+ backgroundColor: bgColor,
176+ color: theme.colors.text.secondary,
177+ padding: 'unset',
178+ overflowY: 'auto',
179+ scrollbarWidth: 'thin',
180+ width: theme.spacing(8),
181+ li: {
182+ paddingRight: theme.spacing(2),
183+ width: 'auto',
184+ '&.rc-picker-time-panel-cell-selected': {
185+ backgroundColor: 'inherit',
186+ border: `1px solid ${theme.colors.action.selectedBorder}`,
187+ borderRadius,
188+ color: theme.colors.text.primary,
189+ },
190+
191+ '&:hover': {
192+ background: optionBgHover,
193+ color: theme.colors.text.primary,
194+ },
195+
196+ '&.rc-picker-time-panel-cell-disabled': {
197+ color: theme.colors.action.disabledText,
198+ },
199+ },
200+
201+ '.rc-picker-time-panel-cell-inner': {
202+ color: 'inherit',
203+ },
204+
205+ '&:not(:last-of-type)': {
206+ borderRight: `1px solid ${borderColor}`,
207+ },
208+ },
209+
210+ '.rc-picker-panel': {
211+ boxShadow: theme.shadows.z3,
212+ backgroundColor: bgColor,
213+ borderColor,
214+ borderRadius,
215+ overflow: 'hidden',
216+ },
217+ }),
218+ input: css({
219+ '&.rc-picker-focused': {
220+ border: 'none',
221+
222+ '.rc-picker-input': getFocusStyles(theme),
223+ },
224+
225+ '&.rc-picker-disabled': {
226+ '.rc-picker-input': {
227+ backgroundColor: theme.colors.action.disabledBackground,
228+ color: theme.colors.action.disabledText,
229+ border: `1px solid ${theme.colors.action.disabledBackground}`,
230+ '&:focus': {
231+ boxShadow: 'none',
232+ },
233+ },
234+ },
235+
236+ '.rc-picker-input': {
237+ backgroundColor: bgColor,
238+ borderRadius,
239+ borderColor,
240+ borderStyle: 'solid',
241+ borderWidth: '1px',
242+ color: theme.colors.text.primary,
243+ height: theme.spacing(4),
244+ padding: theme.spacing(0, 1),
245+
246+ input: {
247+ color: 'unset',
248+ backgroundColor: 'unset',
249+ '&:focus': {
250+ outline: 'none',
251+ },
252+
253+ '&::placeholder': {
254+ color: theme.colors.text.disabled,
255+ },
256+ },
257+ },
258+
259+ '.rc-picker-clear': {
260+ alignItems: 'center',
261+ display: 'flex',
262+ insetInlineEnd: 'unset',
263+ position: 'relative',
264+ },
265+ }),
266+ };
267+ };
0 commit comments