diff --git a/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx b/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx index dd549c5..3668b7a 100644 --- a/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx +++ b/packages/frontend/src/components/projects/project/deployments/FilterForm.tsx @@ -1,10 +1,17 @@ import React, { useEffect, useState } from 'react'; -import { DateRange } from 'react-day-picker'; -import { IconButton, Option, Select } from '@material-tailwind/react'; - -import SearchBar from '../../../SearchBar'; -import DatePicker from '../../../DatePicker'; +import { Input } from 'components/shared/Input'; +import { + CheckRadioOutlineIcon, + CrossCircleIcon, + LoaderIcon, + SearchIcon, + TrendingIcon, + WarningTriangleIcon, +} from 'components/shared/CustomIcon'; +import { DatePicker } from 'components/shared/DatePicker'; +import { Value } from 'react-calendar/dist/cjs/shared/types'; +import { Select, SelectOption } from 'components/shared/Select'; export enum StatusOptions { ALL_STATUS = 'All status', @@ -15,8 +22,8 @@ export enum StatusOptions { export interface FilterValue { searchedBranch: string; - status: StatusOptions; - updateAtRange?: DateRange; + status: StatusOptions | string; + updateAtRange?: Value; } interface FilterFormProps { @@ -27,7 +34,7 @@ interface FilterFormProps { const FilterForm = ({ value, onChange }: FilterFormProps) => { const [searchedBranch, setSearchedBranch] = useState(value.searchedBranch); const [selectedStatus, setSelectedStatus] = useState(value.status); - const [dateRange, setDateRange] = useState(); + const [dateRange, setDateRange] = useState(); useEffect(() => { onChange({ @@ -43,46 +50,68 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => { setDateRange(value.updateAtRange); }, [value]); + const getOptionIcon = (status: StatusOptions) => { + switch (status) { + case StatusOptions.BUILDING: + return ; + case StatusOptions.READY: + return ; + case StatusOptions.ERROR: + return ; + case StatusOptions.ALL_STATUS: + default: + return ; + } + }; + + const statusOptions = Object.values(StatusOptions).map((status) => ({ + label: status, + value: status, + leftIcon: getOptionIcon(status), + })); + + const handleReset = () => { + setSearchedBranch(''); + }; + return ( -
-
- +
+ } + rightIcon={ + searchedBranch && + } value={searchedBranch} - onChange={(event) => setSearchedBranch(event.target.value)} + onChange={(e) => setSearchedBranch(e.target.value)} />
-
- +
+ setDateRange(undefined)} + />
-
+
- {selectedStatus !== StatusOptions.ALL_STATUS && ( -
- setSelectedStatus(StatusOptions.ALL_STATUS)} - className="rounded-full" - size="sm" - placeholder={''} - > - X - -
- )} + value={ + selectedStatus + ? { label: selectedStatus, value: selectedStatus } + : undefined + } + onChange={(item) => + setSelectedStatus((item as SelectOption).value as StatusOptions) + } + onClear={() => setSelectedStatus('')} + />
); diff --git a/packages/frontend/src/components/shared/Calendar/Calendar.tsx b/packages/frontend/src/components/shared/Calendar/Calendar.tsx index bf01dc0..613f785 100644 --- a/packages/frontend/src/components/shared/Calendar/Calendar.tsx +++ b/packages/frontend/src/components/shared/Calendar/Calendar.tsx @@ -19,6 +19,7 @@ import { import './Calendar.css'; import { format } from 'date-fns'; +import { cn } from 'utils/classnames'; type ValuePiece = Date | null; export type Value = ValuePiece | [ValuePiece, ValuePiece]; @@ -63,6 +64,11 @@ export interface CalendarProps extends CustomReactCalendarProps, CalendarTheme { * @returns None */ onCancel?: () => void; + /** + * Optional callback function that is called when a reset action is triggered. + * @returns None + */ + onReset?: () => void; } /** @@ -80,6 +86,7 @@ export const Calendar = ({ actions, onSelect, onCancel, + onReset, onChange: onChangeProp, ...props }: CalendarProps): JSX.Element => { @@ -217,6 +224,11 @@ export const Calendar = ({ [setValue, setActiveDate, changeNavigationLabel, selectRange], ); + const handleReset = useCallback(() => { + setValue(null); + onReset?.(); + }, [setValue, onReset]); + return (
{actions ? ( actions ) : ( <> - - + {value && ( + + )} +
+ + +
)}
diff --git a/packages/frontend/src/components/shared/CustomIcon/CalendarIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CalendarIcon.tsx index 6a51210..7e841bb 100644 --- a/packages/frontend/src/components/shared/CustomIcon/CalendarIcon.tsx +++ b/packages/frontend/src/components/shared/CustomIcon/CalendarIcon.tsx @@ -5,16 +5,16 @@ export const CalendarIcon = (props: CustomIconProps) => { return ( ); diff --git a/packages/frontend/src/components/shared/CustomIcon/CheckRadioOutlineIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CheckRadioOutlineIcon.tsx new file mode 100644 index 0000000..79fe29b --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/CheckRadioOutlineIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const CheckRadioOutlineIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/CirclePlaceholderOnIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CirclePlaceholderOnIcon.tsx new file mode 100644 index 0000000..deb6939 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/CirclePlaceholderOnIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const CirclePlaceholderOnIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/CrossCircleIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CrossCircleIcon.tsx new file mode 100644 index 0000000..78f563c --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/CrossCircleIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const CrossCircleIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/RefreshIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/RefreshIcon.tsx index 3873c37..ca818a2 100644 --- a/packages/frontend/src/components/shared/CustomIcon/RefreshIcon.tsx +++ b/packages/frontend/src/components/shared/CustomIcon/RefreshIcon.tsx @@ -4,36 +4,18 @@ import { CustomIcon, CustomIconProps } from './CustomIcon'; export const RefreshIcon = (props: CustomIconProps) => { return ( - - - - diff --git a/packages/frontend/src/components/shared/CustomIcon/TrendingIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/TrendingIcon.tsx new file mode 100644 index 0000000..19e40ed --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/TrendingIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const TrendingIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/WarningTriangleIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/WarningTriangleIcon.tsx new file mode 100644 index 0000000..d5303a6 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/WarningTriangleIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const WarningTriangleIcon = (props: CustomIconProps) => { + return ( + + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/index.ts b/packages/frontend/src/components/shared/CustomIcon/index.ts index 2137f95..e138de4 100644 --- a/packages/frontend/src/components/shared/CustomIcon/index.ts +++ b/packages/frontend/src/components/shared/CustomIcon/index.ts @@ -42,6 +42,8 @@ export * from './StorageIcon'; export * from './LinkIcon'; export * from './LinkChainIcon'; export * from './CursorBoxIcon'; +export * from './CrossCircleIcon'; +export * from './RefreshIcon'; export * from './CommitIcon'; export * from './RocketIcon'; export * from './RefreshIcon'; @@ -49,6 +51,10 @@ export * from './UndoIcon'; export * from './LoaderIcon'; export * from './MinusCircleIcon'; export * from './CopyIcon'; +export * from './CirclePlaceholderOnIcon'; +export * from './WarningTriangleIcon'; +export * from './CheckRadioOutlineIcon'; +export * from './TrendingIcon'; // Templates export * from './templates'; diff --git a/packages/frontend/src/components/shared/DatePicker/DatePicker.theme.ts b/packages/frontend/src/components/shared/DatePicker/DatePicker.theme.ts index 522bd34..ee1b746 100644 --- a/packages/frontend/src/components/shared/DatePicker/DatePicker.theme.ts +++ b/packages/frontend/src/components/shared/DatePicker/DatePicker.theme.ts @@ -2,7 +2,7 @@ import { VariantProps, tv } from 'tailwind-variants'; export const datePickerTheme = tv({ slots: { - input: [], + input: ['w-full'], }, }); diff --git a/packages/frontend/src/components/shared/DatePicker/DatePicker.tsx b/packages/frontend/src/components/shared/DatePicker/DatePicker.tsx index 99fd82a..464b9b7 100644 --- a/packages/frontend/src/components/shared/DatePicker/DatePicker.tsx +++ b/packages/frontend/src/components/shared/DatePicker/DatePicker.tsx @@ -1,9 +1,12 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { Input, InputProps } from 'components/shared/Input'; import * as Popover from '@radix-ui/react-popover'; import { datePickerTheme } from './DatePicker.theme'; import { Calendar, CalendarProps } from 'components/shared/Calendar'; -import { CalendarIcon } from 'components/shared/CustomIcon'; +import { + CalendarIcon, + ChevronGrabberHorizontal, +} from 'components/shared/CustomIcon'; import { Value } from 'react-calendar/dist/cjs/shared/types'; import { format } from 'date-fns'; @@ -27,6 +30,10 @@ export interface DatePickerProps * Whether to allow the selection of a date range. */ selectRange?: boolean; + /** + * Optional callback function that is called when the date picker is reset. + */ + onReset?: () => void; } /** @@ -39,6 +46,7 @@ export const DatePicker = ({ calendarProps, value, onChange, + onReset, selectRange = false, ...props }: DatePickerProps) => { @@ -50,15 +58,15 @@ export const DatePicker = ({ * Renders the value of the date based on the current state of `props.value`. * @returns {string | undefined} - The formatted date value or `undefined` if `props.value` is falsy. */ - const renderValue = useCallback(() => { - if (!value) return undefined; + const renderValue = useMemo(() => { + if (!value) return ''; if (Array.isArray(value)) { return value .map((date) => format(date as Date, 'dd/MM/yyyy')) .join(' - '); } return format(value, 'dd/MM/yyyy'); - }, [value]); + }, [value, onReset]); /** * Handles the selection of a date from the calendar. @@ -71,15 +79,21 @@ export const DatePicker = ({ [setOpen, onChange], ); + const handleReset = useCallback(() => { + setOpen(false); + onReset?.(); + }, [setOpen, onReset]); + return ( - + setOpen(true)} />} + leftIcon={ setOpen(true)} />} + rightIcon={} readOnly placeholder="Select a date..." - value={renderValue()} + value={renderValue} className={input({ className })} onClick={() => setOpen(true)} /> @@ -93,6 +107,7 @@ export const DatePicker = ({ {...calendarProps} selectRange={selectRange} value={value} + onReset={handleReset} onCancel={() => setOpen(false)} onSelect={handleSelect} /> diff --git a/packages/frontend/src/components/shared/Input/Input.tsx b/packages/frontend/src/components/shared/Input/Input.tsx index e1c84e8..8dcc2be 100644 --- a/packages/frontend/src/components/shared/Input/Input.tsx +++ b/packages/frontend/src/components/shared/Input/Input.tsx @@ -87,7 +87,7 @@ export const Input = ({ }, [cloneIcon, state, helperIconCls, helperText, helperTextCls]); return ( -
+
{renderLabels}
{leftIcon && renderLeftIcon} diff --git a/packages/frontend/src/components/shared/Select/Select.theme.ts b/packages/frontend/src/components/shared/Select/Select.theme.ts index fed6531..56b54f2 100644 --- a/packages/frontend/src/components/shared/Select/Select.theme.ts +++ b/packages/frontend/src/components/shared/Select/Select.theme.ts @@ -2,7 +2,7 @@ import { VariantProps, tv } from 'tailwind-variants'; export const selectTheme = tv({ slots: { - container: ['flex', 'flex-col', 'relative', 'gap-2'], + container: ['flex', 'flex-col', 'relative', 'gap-2', 'w-full'], label: ['text-sm', 'text-elements-high-em'], description: ['text-xs', 'text-elements-low-em'], inputWrapper: [ diff --git a/packages/frontend/src/components/shared/Select/Select.tsx b/packages/frontend/src/components/shared/Select/Select.tsx index 1de4efa..2f1667d 100644 --- a/packages/frontend/src/components/shared/Select/Select.tsx +++ b/packages/frontend/src/components/shared/Select/Select.tsx @@ -10,8 +10,8 @@ import React, { import { useMultipleSelection, useCombobox } from 'downshift'; import { SelectTheme, selectTheme } from './Select.theme'; import { - ChevronDownIcon, - CrossIcon, + ChevronGrabberHorizontal, + CrossCircleIcon, WarningIcon, } from 'components/shared/CustomIcon'; import { cloneIcon } from 'utils/cloneIcon'; @@ -167,7 +167,8 @@ export const Select = ({ } }, [dropdownOpen]); // Re-calculate whenever the dropdown is opened - const handleSelectedItemChange = (selectedItem: SelectOption | null) => { + const handleSelectedItemChange = (selectedItem: SelectOption | undefined) => { + if (!selectedItem) return; setSelectedItem(selectedItem); setInputValue(selectedItem ? selectedItem.label : ''); onChange?.(selectedItem as SelectOption); @@ -185,7 +186,7 @@ export const Select = ({ onSelectedItemsChange: multiple ? undefined : ({ selectedItems }) => { - handleSelectedItemChange(selectedItems?.[0] || null); + handleSelectedItemChange(selectedItems?.[0]); }, }); @@ -279,16 +280,19 @@ export const Select = ({ const renderLeftIcon = useMemo(() => { return (
- {cloneIcon(leftIcon, { className: theme.icon(), 'aria-hidden': true })} + {cloneIcon(selectedItem?.leftIcon ? selectedItem.leftIcon : leftIcon, { + className: theme.icon(), + 'aria-hidden': true, + })}
); - }, [cloneIcon, theme, leftIcon]); + }, [cloneIcon, theme, leftIcon, selectedItem]); const renderRightIcon = useMemo(() => { return (
{clearable && (selectedItems.length > 0 || selectedItem) && ( - @@ -296,11 +300,11 @@ export const Select = ({ {rightIcon ? ( cloneIcon(rightIcon, { className: theme.icon(), 'aria-hidden': true }) ) : ( - + )}
); - }, [cloneIcon, theme, rightIcon]); + }, [cloneIcon, theme, rightIcon, selectedItem, selectedItems, clearable]); const renderHelperText = useMemo(() => { if (!helperText) return null; @@ -343,7 +347,7 @@ export const Select = ({ onClick={() => !dropdownOpen && openMenu()} > {/* Left icon */} - {leftIcon && renderLeftIcon} + {renderLeftIcon} {/* Multiple input values */} {isMultipleHasValue && diff --git a/packages/frontend/src/components/shared/Select/SelectItem/SelectItem.tsx b/packages/frontend/src/components/shared/Select/SelectItem/SelectItem.tsx index a6bbc48..8ac9377 100644 --- a/packages/frontend/src/components/shared/Select/SelectItem/SelectItem.tsx +++ b/packages/frontend/src/components/shared/Select/SelectItem/SelectItem.tsx @@ -62,7 +62,9 @@ const SelectItem = forwardRef(

{label}

- {orientation === 'horizontal' && } + {orientation === 'horizontal' && description && ( + + )} {description && (

{description} diff --git a/packages/frontend/src/layouts/ProjectSearch.tsx b/packages/frontend/src/layouts/ProjectSearch.tsx index c471ab5..5805ba3 100644 --- a/packages/frontend/src/layouts/ProjectSearch.tsx +++ b/packages/frontend/src/layouts/ProjectSearch.tsx @@ -32,7 +32,7 @@ const ProjectSearch = () => { }, []); return ( -

+
@@ -64,7 +64,7 @@ const ProjectSearch = () => {
-
+
diff --git a/packages/frontend/src/pages/org-slug/projects/Id.tsx b/packages/frontend/src/pages/org-slug/projects/Id.tsx index 9db6dee..cf333ba 100644 --- a/packages/frontend/src/pages/org-slug/projects/Id.tsx +++ b/packages/frontend/src/pages/org-slug/projects/Id.tsx @@ -98,8 +98,8 @@ const Id = () => {
-
- +
+ Overview @@ -115,7 +115,7 @@ const Id = () => { {/* Not wrapping in Tab.Content because we are using Outlet */} -
+
diff --git a/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx b/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx index d5f0346..1437754 100644 --- a/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx +++ b/packages/frontend/src/pages/org-slug/projects/id/Deployments.tsx @@ -2,19 +2,19 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Deployment, Domain } from 'gql-client'; import { useOutletContext } from 'react-router-dom'; -import { Button, Typography } from '@material-tailwind/react'; - -import DeploymentDetailsCard from '../../../../components/projects/project/deployments/DeploymentDetailsCard'; +import DeploymentDetailsCard from 'components/projects/project/deployments/DeploymentDetailsCard'; import FilterForm, { FilterValue, StatusOptions, -} from '../../../../components/projects/project/deployments/FilterForm'; -import { OutletContextType } from '../../../../types'; -import { useGQLClient } from '../../../../context/GQLClientContext'; +} from 'components/projects/project/deployments/FilterForm'; +import { OutletContextType } from 'types'; +import { useGQLClient } from 'context/GQLClientContext'; +import { Button } from 'components/shared/Button'; +import { RefreshIcon } from 'components/shared/CustomIcon'; const DEFAULT_FILTER_VALUE: FilterValue = { searchedBranch: '', - status: StatusOptions.ALL_STATUS, + status: '', }; const FETCH_DEPLOYMENTS_INTERVAL = 5000; @@ -73,12 +73,19 @@ const DeploymentsTabPanel = () => { // TODO: match status field types (deployment.status as unknown as StatusOptions) === filterValue.status; + const startDate = + filterValue.updateAtRange instanceof Array + ? filterValue.updateAtRange[0] + : null; + const endDate = + filterValue.updateAtRange instanceof Array + ? filterValue.updateAtRange[1] + : null; + const dateMatch = !filterValue.updateAtRange || - (new Date(Number(deployment.createdAt)) >= - filterValue.updateAtRange!.from! && - new Date(Number(deployment.createdAt)) <= - filterValue.updateAtRange!.to!); + (new Date(Number(deployment.createdAt)) >= startDate! && + new Date(Number(deployment.createdAt)) <= endDate!); return branchMatch && statusMatch && dateMatch; }); @@ -93,12 +100,12 @@ const DeploymentsTabPanel = () => { }; return ( -
+
setFilterValue(value)} /> -
+
{Boolean(filteredDeployments.length) ? ( filteredDeployments.map((deployment, key) => { return ( @@ -113,27 +120,27 @@ const DeploymentsTabPanel = () => { ); }) ) : ( -
-
- + // TODO: Update the height based on the layout, need to re-styling the layout similar to create project layout +
+
+

No deployments found - - - Please change your search query or filters - - +

+

+ Please change your search query or filters. +

+
)}
-
+
); };