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/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",
|
||||||
|
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';
|
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>
|
||||||
|
@ -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()),
|
||||||
|
)
|
||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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]);
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user