Implement filtering deployments by date range selector (#17)
* Add date picker component for selecting range of dates * Filter deployments by updatedAt date range
This commit is contained in:
parent
5c762f3583
commit
2650fa8867
@ -11,9 +11,11 @@
|
||||
"@types/node": "^16.18.68",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"date-fns": "^3.0.1",
|
||||
"downshift": "^8.2.3",
|
||||
"luxon": "^3.4.4",
|
||||
"react": "^18.2.0",
|
||||
"react-day-picker": "^8.9.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropdown": "^1.11.0",
|
||||
"react-hook-form": "^7.49.0",
|
||||
|
172
packages/frontend/src/components/DatePicker.tsx
Normal file
172
packages/frontend/src/components/DatePicker.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
DayPicker,
|
||||
SelectSingleEventHandler,
|
||||
DateRange,
|
||||
} from 'react-day-picker';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverHandler,
|
||||
} from '@material-tailwind/react';
|
||||
|
||||
import HorizontalLine from './HorizontalLine';
|
||||
|
||||
// https://www.material-tailwind.com/docs/react/plugins/date-picker#date-picker
|
||||
const DAY_PICKER_CLASS_NAMES = {
|
||||
caption: 'flex justify-center py-2 mb-4 relative items-center',
|
||||
caption_label: 'text-sm font-medium text-gray-900',
|
||||
nav: 'flex items-center',
|
||||
nav_button:
|
||||
'h-6 w-6 bg-transparent hover:bg-blue-gray-50 p-1 rounded-md transition-colors duration-300',
|
||||
nav_button_previous: 'absolute left-1.5',
|
||||
nav_button_next: 'absolute right-1.5',
|
||||
table: 'w-full border-collapse',
|
||||
head_row: 'flex font-medium text-gray-900',
|
||||
head_cell: 'm-0.5 w-9 font-normal text-sm',
|
||||
row: 'flex w-full mt-2',
|
||||
cell: 'text-gray-600 rounded-md h-9 w-9 text-center text-sm p-0 m-0.5 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-gray-900/20 [&:has([aria-selected].day-outside)]:text-white [&:has([aria-selected])]:bg-gray-900/50 first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
|
||||
day: 'h-9 w-9 p-0 font-normal',
|
||||
day_range_end: 'day-range-end',
|
||||
day_selected:
|
||||
'rounded-md bg-gray-900 text-white hover:bg-gray-900 hover:text-white focus:bg-gray-900 focus:text-white',
|
||||
day_today: 'rounded-md bg-gray-200 text-gray-900',
|
||||
day_outside:
|
||||
'day-outside text-gray-500 opacity-50 aria-selected:bg-gray-500 aria-selected:text-gray-900 aria-selected:bg-opacity-10',
|
||||
day_disabled: 'text-gray-500 opacity-50',
|
||||
day_hidden: 'invisible',
|
||||
};
|
||||
|
||||
type SingleDateHandler = (value: Date) => void;
|
||||
type RangeDateHandler = (value: DateRange) => void;
|
||||
|
||||
interface SingleDatePickerProps {
|
||||
mode: 'single';
|
||||
selected?: Date;
|
||||
onSelect: SingleDateHandler;
|
||||
}
|
||||
|
||||
interface RangeDatePickerProps {
|
||||
mode: 'range';
|
||||
selected?: DateRange;
|
||||
onSelect: RangeDateHandler;
|
||||
}
|
||||
|
||||
const DatePicker = ({
|
||||
mode = 'single',
|
||||
selected,
|
||||
onSelect,
|
||||
}: SingleDatePickerProps | RangeDatePickerProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [rangeSelected, setRangeSelected] = useState<DateRange>();
|
||||
|
||||
const inputValue = useMemo(() => {
|
||||
if (mode === 'single') {
|
||||
return selected ? format(selected as Date, 'PPP') : 'Select Date';
|
||||
}
|
||||
|
||||
if (mode === 'range') {
|
||||
const selectedRange = selected as DateRange | undefined;
|
||||
return selectedRange && selectedRange.from && selectedRange.to
|
||||
? format(selectedRange.from, 'PP') +
|
||||
'-' +
|
||||
format(selectedRange.to, 'PP')
|
||||
: 'All time';
|
||||
}
|
||||
}, [selected, mode]);
|
||||
|
||||
const handleSingleSelect = useCallback<SelectSingleEventHandler>((value) => {
|
||||
if (value) {
|
||||
(onSelect as SingleDateHandler)(value);
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleRangeSelect = useCallback(() => {
|
||||
if (rangeSelected?.to) {
|
||||
(onSelect as RangeDateHandler)(rangeSelected);
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [rangeSelected]);
|
||||
|
||||
const components = {
|
||||
IconLeft: ({ ...props }) => (
|
||||
<i {...props} className="h-4 w-4 stroke-2">
|
||||
{'<'}
|
||||
</i>
|
||||
),
|
||||
IconRight: ({ ...props }) => (
|
||||
<i {...props} className="h-4 w-4 stroke-2">
|
||||
{'>'}
|
||||
</i>
|
||||
),
|
||||
};
|
||||
|
||||
const commonDayPickerProps = {
|
||||
components,
|
||||
className: 'border-0',
|
||||
classNames: DAY_PICKER_CLASS_NAMES,
|
||||
showOutsideDays: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
placement="bottom"
|
||||
open={isOpen}
|
||||
handler={(value) => setIsOpen(value)}
|
||||
>
|
||||
<PopoverHandler>
|
||||
<Input
|
||||
onChange={() => null}
|
||||
value={inputValue}
|
||||
crossOrigin={undefined}
|
||||
/>
|
||||
</PopoverHandler>
|
||||
<PopoverContent>
|
||||
{mode === 'single' && (
|
||||
<DayPicker
|
||||
mode="single"
|
||||
onSelect={handleSingleSelect}
|
||||
selected={selected as Date}
|
||||
{...commonDayPickerProps}
|
||||
/>
|
||||
)}
|
||||
{mode === 'range' && (
|
||||
<>
|
||||
<DayPicker
|
||||
mode="range"
|
||||
onSelect={setRangeSelected}
|
||||
selected={rangeSelected as DateRange}
|
||||
{...commonDayPickerProps}
|
||||
/>
|
||||
<HorizontalLine />
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-full mr-2"
|
||||
variant="outlined"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-full"
|
||||
color="gray"
|
||||
onClick={() => handleRangeSelect()}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePicker;
|
@ -1,10 +1,10 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import React, { forwardRef, RefAttributes } from 'react';
|
||||
|
||||
import { Input, InputProps } from '@material-tailwind/react';
|
||||
|
||||
const SearchBar: React.ForwardRefRenderFunction<
|
||||
HTMLInputElement,
|
||||
InputProps
|
||||
InputProps & RefAttributes<HTMLInputElement>
|
||||
> = ({ value, onChange, placeholder = 'Search', ...props }, ref) => {
|
||||
return (
|
||||
<div className="relative flex w-full gap-2">
|
||||
@ -23,7 +23,7 @@ const SearchBar: React.ForwardRefRenderFunction<
|
||||
// TODO: Debug issue: https://github.com/creativetimofficial/material-tailwind/issues/427
|
||||
crossOrigin={undefined}
|
||||
{...props}
|
||||
ref={ref}
|
||||
inputRef={ref}
|
||||
/>
|
||||
<div className="!absolute left-3 top-[13px]">
|
||||
<i>^</i>
|
||||
|
@ -31,7 +31,9 @@ const ProjectSearch = ({ onChange }: ProjectsSearchProps) => {
|
||||
onInputValueChange({ inputValue }) {
|
||||
setItems(
|
||||
inputValue
|
||||
? projectsData.filter((project) => project.title.includes(inputValue))
|
||||
? projectsData.filter((project) =>
|
||||
project.title.toLowerCase().includes(inputValue.toLowerCase()),
|
||||
)
|
||||
: [],
|
||||
);
|
||||
},
|
||||
|
@ -4,9 +4,12 @@ import { Button, Typography } from '@material-tailwind/react';
|
||||
|
||||
import deploymentData from '../../../assets/deployments.json';
|
||||
import DeployDetailsCard from './deployments/DeploymentDetailsCard';
|
||||
import FilterForm, { StatusOptions } from './deployments/FilterForm';
|
||||
import FilterForm, {
|
||||
FilterValue,
|
||||
StatusOptions,
|
||||
} from './deployments/FilterForm';
|
||||
|
||||
const DEFAULT_FILTER_VALUE = {
|
||||
const DEFAULT_FILTER_VALUE: FilterValue = {
|
||||
searchedBranch: '',
|
||||
status: 'All status',
|
||||
};
|
||||
@ -28,7 +31,12 @@ const DeploymentsTabPanel = () => {
|
||||
filterValue.status === StatusOptions.ALL_STATUS ||
|
||||
deployment.status === filterValue.status;
|
||||
|
||||
return branchMatch && statusMatch;
|
||||
const dateMatch =
|
||||
!filterValue.updateAtRange ||
|
||||
(new Date(deployment.updatedAt) >= filterValue.updateAtRange!.from! &&
|
||||
new Date(deployment.updatedAt) <= filterValue.updateAtRange!.to!);
|
||||
|
||||
return branchMatch && statusMatch && dateMatch;
|
||||
});
|
||||
}, [filterValue]);
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { DateRange } from 'react-day-picker';
|
||||
|
||||
import { Option, Select } from '@material-tailwind/react';
|
||||
|
||||
import SearchBar from '../../../SearchBar';
|
||||
import DatePicker from '../../../DatePicker';
|
||||
|
||||
export enum StatusOptions {
|
||||
ALL_STATUS = 'All status',
|
||||
@ -11,9 +13,10 @@ export enum StatusOptions {
|
||||
ERROR = 'Error',
|
||||
}
|
||||
|
||||
interface FilterValue {
|
||||
export interface FilterValue {
|
||||
searchedBranch: string;
|
||||
status: string;
|
||||
updateAtRange?: DateRange;
|
||||
}
|
||||
|
||||
interface FilterFormProps {
|
||||
@ -24,17 +27,20 @@ interface FilterFormProps {
|
||||
const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
||||
const [searchedBranch, setSearchedBranch] = useState(value.searchedBranch);
|
||||
const [selectedStatus, setSelectedStatus] = useState(value.status);
|
||||
const [dateRange, setDateRange] = useState<DateRange>();
|
||||
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
searchedBranch,
|
||||
status: selectedStatus,
|
||||
updateAtRange: dateRange,
|
||||
});
|
||||
}, [searchedBranch, selectedStatus]);
|
||||
}, [searchedBranch, selectedStatus, dateRange]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchedBranch(value.searchedBranch);
|
||||
setSelectedStatus(value.status);
|
||||
setDateRange(value.updateAtRange);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
@ -47,11 +53,7 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded p-2 w-full focus:border-blue-300 focus:outline-none focus:shadow-outline-blue"
|
||||
placeholder="All time"
|
||||
/>
|
||||
<DatePicker mode="range" selected={dateRange} onSelect={setDateRange} />
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Select
|
||||
|
10
yarn.lock
10
yarn.lock
@ -4751,6 +4751,11 @@ data-urls@^2.0.0:
|
||||
whatwg-mimetype "^2.3.0"
|
||||
whatwg-url "^8.0.0"
|
||||
|
||||
date-fns@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.0.1.tgz#a95b3e8296e72cf57c99819f37679aa27ca65ec4"
|
||||
integrity sha512-cr9igCUa0QSqgAMj7JOrYTY6Nh1rmyGrFDko7ADqfmaQqP/I2N4rlfrLl7AWuzDaoIpz6MNjoEcTPzgZYIrhnA==
|
||||
|
||||
dateformat@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
|
||||
@ -10324,6 +10329,11 @@ react-app-polyfill@^3.0.0:
|
||||
regenerator-runtime "^0.13.9"
|
||||
whatwg-fetch "^3.6.2"
|
||||
|
||||
react-day-picker@^8.9.1:
|
||||
version "8.9.1"
|
||||
resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-8.9.1.tgz#62dcc2bc1282ac72d057266112d9c8558334e757"
|
||||
integrity sha512-W0SPApKIsYq+XCtfGeMYDoU0KbsG3wfkYtlw8l+vZp6KoBXGOlhzBUp4tNx1XiwiOZwhfdGOlj7NGSCKGSlg5Q==
|
||||
|
||||
react-dev-utils@^12.0.1:
|
||||
version "12.0.1"
|
||||
resolved "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz"
|
||||
|
Loading…
Reference in New Issue
Block a user