From b29598edd7aeead44af4b5373b359fd305761a0d Mon Sep 17 00:00:00 2001 From: Amit Yathirajadasan Date: Mon, 18 Dec 2023 14:57:11 -0800 Subject: [PATCH] feat: time range picker --- CHANGELOG.md | 6 + README.md | 9 + src/components/index.js | 3 + src/components/time-range-picker/README.md | 35 +++ src/components/time-range-picker/index.js | 238 +++++++++++++++++++ src/components/time-range-picker/styles.scss | 78 ++++++ 6 files changed, 369 insertions(+) create mode 100644 src/components/time-range-picker/README.md create mode 100644 src/components/time-range-picker/index.js create mode 100644 src/components/time-range-picker/styles.scss diff --git a/CHANGELOG.md b/CHANGELOG.md index 47febd7..18a3212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- TimePicker component by [@amit-y](https://github.com/amit-y) +- DateTimePicker component by [@amit-y](https://github.com/amit-y) +- TimeRangePicker component by [@amit-y](https://github.com/amit-y) + ## [1.17.0] - 2023-12-14 ### Added diff --git a/README.md b/README.md index 0524cbf..88e2bd7 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,15 @@ Component that allows a user to filter options. ### [DatePicker](src/components/date-picker) Component that displays a calendar to select a date +### [TimePicker](src/components/time-picker) +Component to select time values + +### [DateTimePicker](src/components/date-time-picker) +Component to select date/time + +### [TimeRangePicker](src/components/time-range-picker) +Component to select time range + ## Utilities ### [timeRangeToNrql](src/utils/time-range-to-nrql/) diff --git a/src/components/index.js b/src/components/index.js index 312e90f..ad8997e 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -9,3 +9,6 @@ export { default as StatusIconsLayout } from './status-icons-layout'; export { default as ProgressBar } from './progress-bar'; export { default as FilterBar } from './filter-bar'; export { default as DatePicker } from './date-picker'; +export { default as TimePicker } from './time-picker'; +export { default as DateTimePicker } from './date-time-picker'; +export { default as TimeRangePicker } from './time-range-picker'; diff --git a/src/components/time-range-picker/README.md b/src/components/time-range-picker/README.md new file mode 100644 index 0000000..3e09082 --- /dev/null +++ b/src/components/time-range-picker/README.md @@ -0,0 +1,35 @@ +# TimeRangePicker + +The `TimeRangePicker` component is used to select a time period. + +## Usage + +To use the component, simply import and use: + +```jsx +import React, { useState } from 'react'; +import { TimeRangePicker } from '@newrelic/nr-labs-components'; + +function MyComponent() { + const [timeRange, setTimeRange] = useState(new Date()); + + return ( +
+ +
+ ); +} +``` +### Props + +- date (date): The default time period +- onChange (function): A function that is called when the user selects a time range. The function is passed an abject in the following format: + +```javascript +{ + "begin_time": null, // start date/time, if custom time, or null if not + "duration": 1800000, // duration in milliseconds + "end_time": null // end date/time, if custom time, or null if not +} +``` + diff --git a/src/components/time-range-picker/index.js b/src/components/time-range-picker/index.js new file mode 100644 index 0000000..6c8ad5b --- /dev/null +++ b/src/components/time-range-picker/index.js @@ -0,0 +1,238 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; + +import { + Icon, + Popover, + PopoverTrigger, + PopoverBody, + List, + ListItem, + Button, +} from 'nr1'; + +import DateTimePicker from '../date-time-picker'; + +import styles from './styles.scss'; + +const TEXTS = { + APPLY: 'Apply', + CANCEL: 'Cancel', + CUSTOM: 'Custom', + DEFAULT: 'Default', +}; + +const TIME_RANGES = [ + { label: TEXTS.DEFAULT, offset: null }, + { break: true }, + { label: '30 minutes', offset: 1000 * 60 * 30 }, + { label: '60 minutes', offset: 1000 * 60 * 60 }, + { break: true }, + { label: '3 hours', offset: 1000 * 60 * 60 * 3 }, + { label: '6 hours', offset: 1000 * 60 * 60 * 6 }, + { label: '24 hours', offset: 1000 * 60 * 60 * 24 }, + { break: true }, + { label: '3 days', offset: 1000 * 60 * 60 * 24 * 3 }, + { label: '7 days', offset: 1000 * 60 * 60 * 24 * 7 }, + { break: true }, +]; + +const normalizedDateTime = (dt = new Date()) => + new Date( + dt.getFullYear(), + dt.getMonth(), + dt.getDate(), + dt.getHours(), + dt.getMinutes() + ); + +const TimeRangePicker = ({ timeRange, onChange }) => { + const [opened, setOpened] = useState(false); + const [isCustomOpen, setIsCustomOpen] = useState(false); + const [selected, setSelected] = useState(''); + const [beginTime, setBeginTime] = useState(); + const [endTime, setEndTime] = useState(); + + useEffect(() => { + if (!timeRange) { + setSelected(TEXTS.DEFAULT); + setBeginTime(null); + setEndTime(null); + } else if (timeRange.duration) { + setSelected( + TIME_RANGES.find((tr) => tr.offset === timeRange.duration)?.label || + TEXTS.DEFAULT + ); + setBeginTime(null); + setEndTime(null); + } else { + setSelected(TEXTS.CUSTOM); + setBeginTime(timeRange['begin_time']); + setEndTime(timeRange['end_time']); + } + }, [timeRange]); + + const setDurationHandler = (duration) => { + if (onChange) + onChange( + duration + ? { + begin_time: null, + duration, + end_time: null, + } + : null + ); + setOpened(false); + setBeginTime(null); + setEndTime(null); + setIsCustomOpen(false); + }; + + const changeHandler = useCallback((_, o) => { + if (!o) { + setBeginTime(null); + setEndTime(null); + setIsCustomOpen(false); + } + setOpened(o); + }, []); + + const toggleCustomHandler = () => { + if (!isCustomOpen) { + if (timeRange['begin_time'] && timeRange['end_time']) { + setBeginTime(timeRange['begin_time']); + setEndTime(timeRange['end_time']); + } else { + const thirtyMinsAgo = new Date(); + thirtyMinsAgo.setMinutes(thirtyMinsAgo.getMinutes() - 30); + setEndTime(normalizedDateTime()); + setBeginTime(normalizedDateTime(thirtyMinsAgo)); + } + } + setIsCustomOpen((c) => !c); + }; + + const setCustomHandler = useCallback(() => { + if (onChange) + onChange({ + begin_time: beginTime, + duration: null, + end_time: endTime, + }); + setOpened(false); + }, [beginTime, endTime]); + + const cancelCustomHandler = useCallback((e) => { + e.stopPropagation(); + setIsCustomOpen(false); + }, []); + + return ( + + + + + +
+ + {TIME_RANGES.map((tr, i) => ( + + {tr.break ? ( +
+ ) : ( + + )} +
+ ))} + + + {isCustomOpen ? ( +
+ + +
+ + +
+
+ ) : null} +
+
+
+
+
+ ); +}; + +TimeRangePicker.propTypes = { + timeRange: PropTypes.shape({ + begin_time: PropTypes.number, + duration: PropTypes.number, + end_time: PropTypes.number, + }), + onChange: PropTypes.func, +}; + +export default TimeRangePicker; diff --git a/src/components/time-range-picker/styles.scss b/src/components/time-range-picker/styles.scss new file mode 100644 index 0000000..e808f02 --- /dev/null +++ b/src/components/time-range-picker/styles.scss @@ -0,0 +1,78 @@ +.time-range-picker-button { + span:first-of-type { + display: inline-flex; + align-items: center; + gap: 4px; + font-weight: normal; + } + .button-chevron { + font-size: 8px !important; + } +} + +.time-range-list { + padding: 4px 0; +} + +.time-range-list-items { + flex: auto; + overflow-y: auto; +} + +.time-range-list-item { + display: inline-flex; + justify-content: start; + height: 24px; + width: 100%; + appearance: none; + background: none; + border: none; + font-size: 12px; + font-family: inherit; + color: #293338; + text-align: left; + cursor: pointer; + padding: 0 12px; + white-space: nowrap; + font-weight: normal; + border-radius: 0; + min-height: 24px; + + + &:hover { + box-shadow: none; + } + + &.custom { + span:first-of-type { + width: 100%; + display: inline-flex; + justify-content: end; + } + + } + + &.open { + background-color: #E8E8E8; + } +} + +.time-range-list-break { + border: 0; + border-top: 1px solid #E7E9EA; + margin-top: 4px; + margin-bottom: 4px; +} + +.custom-entry { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + padding: 8px 12px; +} + +.custom-buttons { + grid-column: 1 / -1; + display: flex; + gap: 4px; +}