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:
Nabarun Gogoi 2023-12-21 12:00:07 +05:30 committed by GitHub
parent 5c762f3583
commit 2650fa8867
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 210 additions and 14 deletions

View File

@ -11,9 +11,11 @@
"@types/node": "^16.18.68", "@types/node": "^16.18.68",
"@types/react": "^18.2.42", "@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"date-fns": "^3.0.1",
"downshift": "^8.2.3", "downshift": "^8.2.3",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-day-picker": "^8.9.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-dropdown": "^1.11.0", "react-dropdown": "^1.11.0",
"react-hook-form": "^7.49.0", "react-hook-form": "^7.49.0",

View 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;

View File

@ -1,10 +1,10 @@
import React, { forwardRef } from 'react'; import React, { forwardRef, RefAttributes } from 'react';
import { Input, InputProps } from '@material-tailwind/react'; import { Input, InputProps } from '@material-tailwind/react';
const SearchBar: React.ForwardRefRenderFunction< const SearchBar: React.ForwardRefRenderFunction<
HTMLInputElement, HTMLInputElement,
InputProps InputProps & RefAttributes<HTMLInputElement>
> = ({ value, onChange, placeholder = 'Search', ...props }, ref) => { > = ({ value, onChange, placeholder = 'Search', ...props }, ref) => {
return ( return (
<div className="relative flex w-full gap-2"> <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 // TODO: Debug issue: https://github.com/creativetimofficial/material-tailwind/issues/427
crossOrigin={undefined} crossOrigin={undefined}
{...props} {...props}
ref={ref} inputRef={ref}
/> />
<div className="!absolute left-3 top-[13px]"> <div className="!absolute left-3 top-[13px]">
<i>^</i> <i>^</i>

View File

@ -31,7 +31,9 @@ const ProjectSearch = ({ onChange }: ProjectsSearchProps) => {
onInputValueChange({ inputValue }) { onInputValueChange({ inputValue }) {
setItems( setItems(
inputValue inputValue
? projectsData.filter((project) => project.title.includes(inputValue)) ? projectsData.filter((project) =>
project.title.toLowerCase().includes(inputValue.toLowerCase()),
)
: [], : [],
); );
}, },

View File

@ -4,9 +4,12 @@ import { Button, Typography } from '@material-tailwind/react';
import deploymentData from '../../../assets/deployments.json'; import deploymentData from '../../../assets/deployments.json';
import DeployDetailsCard from './deployments/DeploymentDetailsCard'; 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: '', searchedBranch: '',
status: 'All status', status: 'All status',
}; };
@ -28,7 +31,12 @@ const DeploymentsTabPanel = () => {
filterValue.status === StatusOptions.ALL_STATUS || filterValue.status === StatusOptions.ALL_STATUS ||
deployment.status === filterValue.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]); }, [filterValue]);

View File

@ -1,8 +1,10 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { DateRange } from 'react-day-picker';
import { Option, Select } from '@material-tailwind/react'; import { Option, Select } from '@material-tailwind/react';
import SearchBar from '../../../SearchBar'; import SearchBar from '../../../SearchBar';
import DatePicker from '../../../DatePicker';
export enum StatusOptions { export enum StatusOptions {
ALL_STATUS = 'All status', ALL_STATUS = 'All status',
@ -11,9 +13,10 @@ export enum StatusOptions {
ERROR = 'Error', ERROR = 'Error',
} }
interface FilterValue { export interface FilterValue {
searchedBranch: string; searchedBranch: string;
status: string; status: string;
updateAtRange?: DateRange;
} }
interface FilterFormProps { interface FilterFormProps {
@ -24,17 +27,20 @@ interface FilterFormProps {
const FilterForm = ({ value, onChange }: FilterFormProps) => { const FilterForm = ({ value, onChange }: FilterFormProps) => {
const [searchedBranch, setSearchedBranch] = useState(value.searchedBranch); const [searchedBranch, setSearchedBranch] = useState(value.searchedBranch);
const [selectedStatus, setSelectedStatus] = useState(value.status); const [selectedStatus, setSelectedStatus] = useState(value.status);
const [dateRange, setDateRange] = useState<DateRange>();
useEffect(() => { useEffect(() => {
onChange({ onChange({
searchedBranch, searchedBranch,
status: selectedStatus, status: selectedStatus,
updateAtRange: dateRange,
}); });
}, [searchedBranch, selectedStatus]); }, [searchedBranch, selectedStatus, dateRange]);
useEffect(() => { useEffect(() => {
setSearchedBranch(value.searchedBranch); setSearchedBranch(value.searchedBranch);
setSelectedStatus(value.status); setSelectedStatus(value.status);
setDateRange(value.updateAtRange);
}, [value]); }, [value]);
return ( return (
@ -47,11 +53,7 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
/> />
</div> </div>
<div className="col-span-1"> <div className="col-span-1">
<input <DatePicker mode="range" selected={dateRange} onSelect={setDateRange} />
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"
/>
</div> </div>
<div className="col-span-1"> <div className="col-span-1">
<Select <Select

View File

@ -4751,6 +4751,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0" whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.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: dateformat@^3.0.3:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" 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" regenerator-runtime "^0.13.9"
whatwg-fetch "^3.6.2" 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: react-dev-utils@^12.0.1:
version "12.0.1" version "12.0.1"
resolved "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz" resolved "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz"