From 2650fa88672b798db3967c6c5fe09290fcbda2b7 Mon Sep 17 00:00:00 2001 From: Nabarun Gogoi Date: Thu, 21 Dec 2023 12:00:07 +0530 Subject: [PATCH] Implement filtering deployments by date range selector (#17) * Add date picker component for selecting range of dates * Filter deployments by updatedAt date range --- packages/frontend/package.json | 2 + .../frontend/src/components/DatePicker.tsx | 172 ++++++++++++++++++ .../frontend/src/components/SearchBar.tsx | 6 +- .../src/components/projects/ProjectSearch.tsx | 4 +- .../projects/project/DeploymentsTabPanel.tsx | 14 +- .../project/deployments/FilterForm.tsx | 16 +- yarn.lock | 10 + 7 files changed, 210 insertions(+), 14 deletions(-) create mode 100644 packages/frontend/src/components/DatePicker.tsx diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 545500ea..fedec4b1 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -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", diff --git a/packages/frontend/src/components/DatePicker.tsx b/packages/frontend/src/components/DatePicker.tsx new file mode 100644 index 00000000..59114bb0 --- /dev/null +++ b/packages/frontend/src/components/DatePicker.tsx @@ -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(); + + 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((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 }) => ( + + {'<'} + + ), + IconRight: ({ ...props }) => ( + + {'>'} + + ), + }; + + const commonDayPickerProps = { + components, + className: 'border-0', + classNames: DAY_PICKER_CLASS_NAMES, + showOutsideDays: true, + }; + + return ( + setIsOpen(value)} + > + + null} + value={inputValue} + crossOrigin={undefined} + /> + + + {mode === 'single' && ( + + )} + {mode === 'range' && ( + <> + + +
+ + +
+ + )} +
+
+ ); +}; + +export default DatePicker; diff --git a/packages/frontend/src/components/SearchBar.tsx b/packages/frontend/src/components/SearchBar.tsx index 95e6ec97..aef23c96 100644 --- a/packages/frontend/src/components/SearchBar.tsx +++ b/packages/frontend/src/components/SearchBar.tsx @@ -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 > = ({ value, onChange, placeholder = 'Search', ...props }, ref) => { return (
@@ -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} />
^ diff --git a/packages/frontend/src/components/projects/ProjectSearch.tsx b/packages/frontend/src/components/projects/ProjectSearch.tsx index c4f7890e..b2d5e1fc 100644 --- a/packages/frontend/src/components/projects/ProjectSearch.tsx +++ b/packages/frontend/src/components/projects/ProjectSearch.tsx @@ -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()), + ) : [], ); }, diff --git a/packages/frontend/src/components/projects/project/DeploymentsTabPanel.tsx b/packages/frontend/src/components/projects/project/DeploymentsTabPanel.tsx index 214a92cb..64d1715b 100644 --- a/packages/frontend/src/components/projects/project/DeploymentsTabPanel.tsx +++ b/packages/frontend/src/components/projects/project/DeploymentsTabPanel.tsx @@ -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]); diff --git a/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx b/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx index f4fec28f..b5b1860e 100644 --- a/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx +++ b/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx @@ -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(); 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) => { />
- +