Skip to content

Commit

Permalink
feat: time range picker
Browse files Browse the repository at this point in the history
  • Loading branch information
amit-y committed Dec 18, 2023
1 parent 8613c1d commit b29598e
Show file tree
Hide file tree
Showing 6 changed files with 369 additions and 0 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand Down
3 changes: 3 additions & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
35 changes: 35 additions & 0 deletions src/components/time-range-picker/README.md
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<TimeRangePicker timeRange={timeRange} onChange={setTimeRange} />
</div>
);
}
```
### 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
}
```

238 changes: 238 additions & 0 deletions src/components/time-range-picker/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<Popover opened={opened} onChange={changeHandler}>
<PopoverTrigger>
<Button
className={styles['time-range-picker-button']}
type={Button.TYPE.PLAIN}
sizeType={Button.SIZE_TYPE.SMALL}
>
<Icon type={Icon.TYPE.DATE_AND_TIME__DATE_AND_TIME__TIME} />
{selected}
<Icon
className={styles['button-chevron']}
type={
opened
? Icon.TYPE.INTERFACE__CHEVRON__CHEVRON_TOP
: Icon.TYPE.INTERFACE__CHEVRON__CHEVRON_BOTTOM
}
/>
</Button>
</PopoverTrigger>
<PopoverBody>
<div className={styles['time-range-list']}>
<List className={styles['time-range-list-items']}>
{TIME_RANGES.map((tr, i) => (
<ListItem key={i}>
{tr.break ? (
<hr className={styles['time-range-list-break']} />
) : (
<Button
className={`${styles['time-range-list-item']} ${
tr.label === selected ? styles.open : ''
}`}
type={Button.TYPE.PLAIN}
onClick={() => setDurationHandler(tr.offset)}
>
{tr.label}
</Button>
)}
</ListItem>
))}
<ListItem>
<Button
className={`${styles['time-range-list-item']} ${
styles.custom
} ${
isCustomOpen || selected === TEXTS.CUSTOM ? styles.open : ''
}`}
type={Button.TYPE.PLAIN}
onClick={toggleCustomHandler}
>
{TEXTS.CUSTOM}
<Icon
className={styles['button-chevron']}
type={
isCustomOpen || selected === TEXTS.CUSTOM
? Icon.TYPE.INTERFACE__CHEVRON__CHEVRON_TOP
: Icon.TYPE.INTERFACE__CHEVRON__CHEVRON_BOTTOM
}
/>
</Button>
{isCustomOpen ? (
<div className={styles['custom-entry']}>
<DateTimePicker
datetime={beginTime}
onChange={setBeginTime}
/>
<DateTimePicker
datetime={endTime}
onChange={setEndTime}
validFrom={beginTime}
validTill={normalizedDateTime()}
/>
<div className={styles['custom-buttons']}>
<Button
type={Button.TYPE.PRIMARY}
sizeType={Button.SIZE_TYPE.SMALL}
onClick={setCustomHandler}
>
{TEXTS.APPLY}
</Button>
<Button
type={Button.TYPE.PLAIN}
sizeType={Button.SIZE_TYPE.SMALL}
onClick={cancelCustomHandler}
>
{TEXTS.CANCEL}
</Button>
</div>
</div>
) : null}
</ListItem>
</List>
</div>
</PopoverBody>
</Popover>
);
};

TimeRangePicker.propTypes = {
timeRange: PropTypes.shape({
begin_time: PropTypes.number,
duration: PropTypes.number,
end_time: PropTypes.number,
}),
onChange: PropTypes.func,
};

export default TimeRangePicker;
78 changes: 78 additions & 0 deletions src/components/time-range-picker/styles.scss
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit b29598e

Please sign in to comment.