Merge pull request #142 from snowball-tools/ayungavis/T-4917-project-deployments-layout-and-empty-state

[T-4917: style] Re-styling and refactor project page deployments layout and empty state
This commit is contained in:
Wahyu Kurniawan 2024-03-06 11:29:21 +07:00 committed by GitHub
commit 8d3ef3bafc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 305 additions and 135 deletions

View File

@ -1,10 +1,17 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { DateRange } from 'react-day-picker';
import { IconButton, Option, Select } from '@material-tailwind/react'; import { Input } from 'components/shared/Input';
import {
import SearchBar from '../../../SearchBar'; CheckRadioOutlineIcon,
import DatePicker from '../../../DatePicker'; 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 { export enum StatusOptions {
ALL_STATUS = 'All status', ALL_STATUS = 'All status',
@ -15,8 +22,8 @@ export enum StatusOptions {
export interface FilterValue { export interface FilterValue {
searchedBranch: string; searchedBranch: string;
status: StatusOptions; status: StatusOptions | string;
updateAtRange?: DateRange; updateAtRange?: Value;
} }
interface FilterFormProps { interface FilterFormProps {
@ -27,7 +34,7 @@ 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>(); const [dateRange, setDateRange] = useState<Value>();
useEffect(() => { useEffect(() => {
onChange({ onChange({
@ -43,46 +50,68 @@ const FilterForm = ({ value, onChange }: FilterFormProps) => {
setDateRange(value.updateAtRange); setDateRange(value.updateAtRange);
}, [value]); }, [value]);
const getOptionIcon = (status: StatusOptions) => {
switch (status) {
case StatusOptions.BUILDING:
return <LoaderIcon />;
case StatusOptions.READY:
return <CheckRadioOutlineIcon />;
case StatusOptions.ERROR:
return <WarningTriangleIcon />;
case StatusOptions.ALL_STATUS:
default:
return <TrendingIcon />;
}
};
const statusOptions = Object.values(StatusOptions).map((status) => ({
label: status,
value: status,
leftIcon: getOptionIcon(status),
}));
const handleReset = () => {
setSearchedBranch('');
};
return ( return (
<div className="grid items-center grid-cols-8 gap-2 text-sm text-gray-600"> <div className="xl:grid xl:grid-cols-8 flex flex-col xl:gap-3 gap-0">
<div className="col-span-4"> <div className="col-span-4 flex items-center">
<SearchBar <Input
placeholder="Search branches" placeholder="Search branches"
leftIcon={<SearchIcon />}
rightIcon={
searchedBranch && <CrossCircleIcon onClick={handleReset} />
}
value={searchedBranch} value={searchedBranch}
onChange={(event) => setSearchedBranch(event.target.value)} onChange={(e) => setSearchedBranch(e.target.value)}
/> />
</div> </div>
<div className="col-span-2"> <div className="col-span-2 flex items-center">
<DatePicker mode="range" selected={dateRange} onSelect={setDateRange} /> <DatePicker
className="w-full"
selectRange
value={dateRange}
onChange={setDateRange}
onReset={() => setDateRange(undefined)}
/>
</div> </div>
<div className="col-span-2 relative"> <div className="col-span-2 flex items-center">
<Select <Select
value={selectedStatus} leftIcon={getOptionIcon(selectedStatus as StatusOptions)}
onChange={(value) => setSelectedStatus(value as StatusOptions)} options={statusOptions}
clearable
placeholder="All status" placeholder="All status"
> value={
{Object.values(StatusOptions).map((status) => ( selectedStatus
<Option ? { label: selectedStatus, value: selectedStatus }
className={status === StatusOptions.ALL_STATUS ? 'hidden' : ''} : undefined
key={status} }
value={status} onChange={(item) =>
> setSelectedStatus((item as SelectOption).value as StatusOptions)
^ {status} }
</Option> onClear={() => setSelectedStatus('')}
))} />
</Select>
{selectedStatus !== StatusOptions.ALL_STATUS && (
<div className="absolute end-1 inset-y-0 my-auto h-8">
<IconButton
onClick={() => setSelectedStatus(StatusOptions.ALL_STATUS)}
className="rounded-full"
size="sm"
placeholder={''}
>
X
</IconButton>
</div>
)}
</div> </div>
</div> </div>
); );

View File

@ -19,6 +19,7 @@ import {
import './Calendar.css'; import './Calendar.css';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { cn } from 'utils/classnames';
type ValuePiece = Date | null; type ValuePiece = Date | null;
export type Value = ValuePiece | [ValuePiece, ValuePiece]; export type Value = ValuePiece | [ValuePiece, ValuePiece];
@ -63,6 +64,11 @@ export interface CalendarProps extends CustomReactCalendarProps, CalendarTheme {
* @returns None * @returns None
*/ */
onCancel?: () => void; 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, actions,
onSelect, onSelect,
onCancel, onCancel,
onReset,
onChange: onChangeProp, onChange: onChangeProp,
...props ...props
}: CalendarProps): JSX.Element => { }: CalendarProps): JSX.Element => {
@ -217,6 +224,11 @@ export const Calendar = ({
[setValue, setActiveDate, changeNavigationLabel, selectRange], [setValue, setActiveDate, changeNavigationLabel, selectRange],
); );
const handleReset = useCallback(() => {
setValue(null);
onReset?.();
}, [setValue, onReset]);
return ( return (
<div <div
{...wrapperProps} {...wrapperProps}
@ -276,12 +288,20 @@ export const Calendar = ({
{/* Footer or CTA */} {/* Footer or CTA */}
<div <div
{...footerProps} {...footerProps}
className={footer({ className: footerProps?.className })} className={cn(footer({ className: footerProps?.className }), {
'justify-between': Boolean(value),
})}
> >
{actions ? ( {actions ? (
actions actions
) : ( ) : (
<> <>
{value && (
<Button variant="danger" onClick={handleReset}>
Reset
</Button>
)}
<div className="space-x-3">
<Button variant="tertiary" onClick={onCancel}> <Button variant="tertiary" onClick={onCancel}>
Cancel Cancel
</Button> </Button>
@ -291,6 +311,7 @@ export const Calendar = ({
> >
Select Select
</Button> </Button>
</div>
</> </>
)} )}
</div> </div>

View File

@ -5,16 +5,16 @@ export const CalendarIcon = (props: CustomIconProps) => {
return ( return (
<CustomIcon <CustomIcon
width="18" width="18"
height="19" height="18"
viewBox="0 0 18 19" viewBox="0 0 18 18"
fill="none" fill="none"
{...props} {...props}
> >
<path <path
fillRule="evenodd" d="M2.625 7.125H15.375M5.625 3.375V1.875M12.375 3.375V1.875M4.125 15.375H13.875C14.7034 15.375 15.375 14.7034 15.375 13.875V4.875C15.375 4.04657 14.7034 3.375 13.875 3.375H4.125C3.29657 3.375 2.625 4.04657 2.625 4.875V13.875C2.625 14.7034 3.29657 15.375 4.125 15.375Z"
clipRule="evenodd" stroke="currentColor"
d="M5 0C5.55228 0 6 0.447715 6 1V2H12V1C12 0.447715 12.4477 0 13 0C13.5523 0 14 0.447715 14 1V2H16C17.1046 2 18 2.89543 18 4V17C18 18.1046 17.1046 19 16 19H2C0.89543 19 0 18.1046 0 17V4C0 2.89543 0.895431 2 2 2H4V1C4 0.447715 4.44772 0 5 0ZM2 9V17H16V9H2Z" strokeLinecap="round"
fill="currentColor" strokeLinejoin="round"
/> />
</CustomIcon> </CustomIcon>
); );

View File

@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const CheckRadioOutlineIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
{...props}
>
<path
d="M11.25 7.125L7.875 11.25L6.375 9.75M16.125 9C16.125 12.935 12.935 16.125 9 16.125C5.06497 16.125 1.875 12.935 1.875 9C1.875 5.06497 5.06497 1.875 9 1.875C12.935 1.875 16.125 5.06497 16.125 9Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</CustomIcon>
);
};

View File

@ -0,0 +1,20 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const CirclePlaceholderOnIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
{...props}
>
<path
d="M16.125 9C16.125 12.935 12.935 16.125 9 16.125C5.06497 16.125 1.875 12.935 1.875 9C1.875 5.06497 5.06497 1.875 9 1.875C12.935 1.875 16.125 5.06497 16.125 9Z"
stroke="currentColor"
strokeLinejoin="round"
/>
</CustomIcon>
);
};

View File

@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const CrossCircleIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.5 9C1.5 4.85786 4.85786 1.5 9 1.5C13.1421 1.5 16.5 4.85786 16.5 9C16.5 13.1421 13.1421 16.5 9 16.5C4.85786 16.5 1.5 13.1421 1.5 9ZM7.01516 6.48484C6.86872 6.33839 6.63128 6.33839 6.48484 6.48484C6.33839 6.63128 6.33839 6.86872 6.48484 7.01516L8.46967 9L6.48484 10.9848C6.33839 11.1313 6.33839 11.3687 6.48484 11.5152C6.63128 11.6616 6.86872 11.6616 7.01516 11.5152L9 9.53033L10.9848 11.5152C11.1313 11.6616 11.3687 11.6616 11.5152 11.5152C11.6616 11.3687 11.6616 11.1313 11.5152 10.9848L9.53033 9L11.5152 7.01516C11.6616 6.86872 11.6616 6.63128 11.5152 6.48484C11.3687 6.33839 11.1313 6.33839 10.9848 6.48484L9 8.46967L7.01516 6.48484Z"
fill="currentColor"
/>
</CustomIcon>
);
};

View File

@ -4,36 +4,18 @@ import { CustomIcon, CustomIconProps } from './CustomIcon';
export const RefreshIcon = (props: CustomIconProps) => { export const RefreshIcon = (props: CustomIconProps) => {
return ( return (
<CustomIcon <CustomIcon
width="16" width="18"
height="16" height="18"
viewBox="0 0 16 16" viewBox="0 0 18 18"
fill="none" fill="none"
{...props} {...props}
> >
<path <path
d="M7.125 10.875V15.375H2.625M6.75 14.9666C4.33948 14.0571 2.625 11.7288 2.625 9C2.625 5.47918 5.47918 2.625 9 2.625C9.93578 2.625 10.8245 2.82663 11.625 3.1888" d="M4.7586 5.25C5.80171 4.1001 7.33209 3.375 9 3.375C12.1066 3.375 14.625 5.8934 14.625 9C14.625 9.23861 14.6102 9.47349 14.5815 9.70383C14.543 10.0121 14.7618 10.2932 15.07 10.3316C15.3783 10.3701 15.6594 10.1513 15.6978 9.84304C15.7323 9.56663 15.75 9.28526 15.75 9C15.75 5.27208 12.7279 2.25 9 2.25C7.10933 2.25 5.36655 3.02807 4.125 4.28342V2.8125C4.125 2.50184 3.87316 2.25 3.5625 2.25C3.25184 2.25 3 2.50184 3 2.8125L3 5.25C3 5.87132 3.50368 6.375 4.125 6.375H6.5625C6.87316 6.375 7.125 6.12316 7.125 5.8125C7.125 5.50184 6.87316 5.25 6.5625 5.25H4.7586Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10.3125 15.1877C10.3125 15.4984 10.0607 15.7502 9.75 15.7502C9.43934 15.7502 9.1875 15.4984 9.1875 15.1877C9.1875 14.8771 9.43934 14.6252 9.75 14.6252C10.0607 14.6252 10.3125 14.8771 10.3125 15.1877Z"
fill="currentColor" fill="currentColor"
/> />
<path <path
d="M13.2304 13.7025C13.3857 13.9716 13.2936 14.3156 13.0245 14.4709C12.7555 14.6262 12.4115 14.5341 12.2561 14.265C12.1008 13.996 12.193 13.652 12.462 13.4966C12.7311 13.3413 13.0751 13.4335 13.2304 13.7025Z" d="M3.41855 8.29617C3.45699 7.98789 3.23825 7.70683 2.92997 7.66839C2.6217 7.62994 2.34063 7.84869 2.30219 8.15696C2.26773 8.43337 2.25 8.71474 2.25 9C2.25 12.7279 5.27208 15.75 9 15.75C10.8952 15.75 12.6418 14.9682 13.8839 13.7076V15.1875C13.8839 15.4982 14.1357 15.75 14.4464 15.75C14.757 15.75 15.0089 15.4982 15.0089 15.1875V12.75C15.0089 12.1287 14.5052 11.625 13.8839 11.625L11.4464 11.625C11.1357 11.625 10.8839 11.8768 10.8839 12.1875C10.8839 12.4982 11.1357 12.75 11.4464 12.75H13.2414C12.1983 13.8999 10.6679 14.625 9 14.625C5.8934 14.625 3.375 12.1066 3.375 9C3.375 8.76139 3.38982 8.52651 3.41855 8.29617Z"
fill="currentColor"
/>
<path
d="M15.0147 10.9573C15.2838 11.1126 15.3759 11.4567 15.2206 11.7257C15.0653 11.9947 14.7213 12.0869 14.4522 11.9316C14.1832 11.7763 14.091 11.4322 14.2463 11.1632C14.4017 10.8942 14.7457 10.802 15.0147 10.9573Z"
fill="currentColor"
/>
<path
d="M14.2657 5.74407C13.9967 5.8994 13.6527 5.80722 13.4973 5.53818C13.342 5.26914 13.4342 4.92512 13.7032 4.76979C13.9723 4.61446 14.3163 4.70664 14.4716 4.97568C14.6269 5.24472 14.5348 5.58874 14.2657 5.74407Z"
fill="currentColor"
/>
<path
d="M15.75 8.25023C15.75 8.56089 15.4982 8.81273 15.1875 8.81273C14.8768 8.81273 14.625 8.56089 14.625 8.25023C14.625 7.93957 14.8768 7.68773 15.1875 7.68773C15.4982 7.68773 15.75 7.93957 15.75 8.25023Z"
fill="currentColor" fill="currentColor"
/> />
</CustomIcon> </CustomIcon>

View File

@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const TrendingIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
{...props}
>
<path
d="M1.875 9.79623L2.68636 7.88111C2.95597 7.24472 3.86788 7.28002 4.08767 7.93535L5.09939 10.9518C5.33069 11.6415 6.30748 11.6322 6.52567 10.9383L8.97436 3.15067C9.2032 2.42289 10.2437 2.46059 10.4194 3.20303L13.1643 14.7969C13.3368 15.5254 14.3502 15.5807 14.6007 14.8754L16.125 10.583"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</CustomIcon>
);
};

View File

@ -0,0 +1,21 @@
import React from 'react';
import { CustomIcon, CustomIconProps } from './CustomIcon';
export const WarningTriangleIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
{...props}
>
<path
d="M9.00004 6.75007V10.1251M7.71074 2.91845L2.09776 12.3585C1.50324 13.3583 2.22379 14.6251 3.38706 14.6251H14.613C15.7763 14.6251 16.4968 13.3583 15.9023 12.3585L10.2893 2.91844C9.70792 1.9406 8.29216 1.9406 7.71074 2.91845Z"
stroke="currentColor"
strokeLinecap="round"
/>
<circle cx="9" cy="12" r="0.5625" fill="currentColor" />
</CustomIcon>
);
};

View File

@ -42,6 +42,8 @@ export * from './StorageIcon';
export * from './LinkIcon'; export * from './LinkIcon';
export * from './LinkChainIcon'; export * from './LinkChainIcon';
export * from './CursorBoxIcon'; export * from './CursorBoxIcon';
export * from './CrossCircleIcon';
export * from './RefreshIcon';
export * from './CommitIcon'; export * from './CommitIcon';
export * from './RocketIcon'; export * from './RocketIcon';
export * from './RefreshIcon'; export * from './RefreshIcon';
@ -49,6 +51,10 @@ export * from './UndoIcon';
export * from './LoaderIcon'; export * from './LoaderIcon';
export * from './MinusCircleIcon'; export * from './MinusCircleIcon';
export * from './CopyIcon'; export * from './CopyIcon';
export * from './CirclePlaceholderOnIcon';
export * from './WarningTriangleIcon';
export * from './CheckRadioOutlineIcon';
export * from './TrendingIcon';
// Templates // Templates
export * from './templates'; export * from './templates';

View File

@ -2,7 +2,7 @@ import { VariantProps, tv } from 'tailwind-variants';
export const datePickerTheme = tv({ export const datePickerTheme = tv({
slots: { slots: {
input: [], input: ['w-full'],
}, },
}); });

View File

@ -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 { Input, InputProps } from 'components/shared/Input';
import * as Popover from '@radix-ui/react-popover'; import * as Popover from '@radix-ui/react-popover';
import { datePickerTheme } from './DatePicker.theme'; import { datePickerTheme } from './DatePicker.theme';
import { Calendar, CalendarProps } from 'components/shared/Calendar'; 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 { Value } from 'react-calendar/dist/cjs/shared/types';
import { format } from 'date-fns'; import { format } from 'date-fns';
@ -27,6 +30,10 @@ export interface DatePickerProps
* Whether to allow the selection of a date range. * Whether to allow the selection of a date range.
*/ */
selectRange?: boolean; selectRange?: boolean;
/**
* Optional callback function that is called when the date picker is reset.
*/
onReset?: () => void;
} }
/** /**
@ -39,6 +46,7 @@ export const DatePicker = ({
calendarProps, calendarProps,
value, value,
onChange, onChange,
onReset,
selectRange = false, selectRange = false,
...props ...props
}: DatePickerProps) => { }: DatePickerProps) => {
@ -50,15 +58,15 @@ export const DatePicker = ({
* Renders the value of the date based on the current state of `props.value`. * 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. * @returns {string | undefined} - The formatted date value or `undefined` if `props.value` is falsy.
*/ */
const renderValue = useCallback(() => { const renderValue = useMemo(() => {
if (!value) return undefined; if (!value) return '';
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value return value
.map((date) => format(date as Date, 'dd/MM/yyyy')) .map((date) => format(date as Date, 'dd/MM/yyyy'))
.join(' - '); .join(' - ');
} }
return format(value, 'dd/MM/yyyy'); return format(value, 'dd/MM/yyyy');
}, [value]); }, [value, onReset]);
/** /**
* Handles the selection of a date from the calendar. * Handles the selection of a date from the calendar.
@ -71,15 +79,21 @@ export const DatePicker = ({
[setOpen, onChange], [setOpen, onChange],
); );
const handleReset = useCallback(() => {
setOpen(false);
onReset?.();
}, [setOpen, onReset]);
return ( return (
<Popover.Root open={open}> <Popover.Root open={open}>
<Popover.Trigger> <Popover.Trigger className="w-full">
<Input <Input
{...props} {...props}
rightIcon={<CalendarIcon onClick={() => setOpen(true)} />} leftIcon={<CalendarIcon onClick={() => setOpen(true)} />}
rightIcon={<ChevronGrabberHorizontal />}
readOnly readOnly
placeholder="Select a date..." placeholder="Select a date..."
value={renderValue()} value={renderValue}
className={input({ className })} className={input({ className })}
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
/> />
@ -93,6 +107,7 @@ export const DatePicker = ({
{...calendarProps} {...calendarProps}
selectRange={selectRange} selectRange={selectRange}
value={value} value={value}
onReset={handleReset}
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onSelect={handleSelect} onSelect={handleSelect}
/> />

View File

@ -87,7 +87,7 @@ export const Input = ({
}, [cloneIcon, state, helperIconCls, helperText, helperTextCls]); }, [cloneIcon, state, helperIconCls, helperText, helperTextCls]);
return ( return (
<div className="flex flex-col gap-y-2"> <div className="flex flex-col gap-y-2 w-full">
{renderLabels} {renderLabels}
<div className={containerCls({ class: className })}> <div className={containerCls({ class: className })}>
{leftIcon && renderLeftIcon} {leftIcon && renderLeftIcon}

View File

@ -2,7 +2,7 @@ import { VariantProps, tv } from 'tailwind-variants';
export const selectTheme = tv({ export const selectTheme = tv({
slots: { slots: {
container: ['flex', 'flex-col', 'relative', 'gap-2'], container: ['flex', 'flex-col', 'relative', 'gap-2', 'w-full'],
label: ['text-sm', 'text-elements-high-em'], label: ['text-sm', 'text-elements-high-em'],
description: ['text-xs', 'text-elements-low-em'], description: ['text-xs', 'text-elements-low-em'],
inputWrapper: [ inputWrapper: [

View File

@ -10,8 +10,8 @@ import React, {
import { useMultipleSelection, useCombobox } from 'downshift'; import { useMultipleSelection, useCombobox } from 'downshift';
import { SelectTheme, selectTheme } from './Select.theme'; import { SelectTheme, selectTheme } from './Select.theme';
import { import {
ChevronDownIcon, ChevronGrabberHorizontal,
CrossIcon, CrossCircleIcon,
WarningIcon, WarningIcon,
} from 'components/shared/CustomIcon'; } from 'components/shared/CustomIcon';
import { cloneIcon } from 'utils/cloneIcon'; import { cloneIcon } from 'utils/cloneIcon';
@ -167,7 +167,8 @@ export const Select = ({
} }
}, [dropdownOpen]); // Re-calculate whenever the dropdown is opened }, [dropdownOpen]); // Re-calculate whenever the dropdown is opened
const handleSelectedItemChange = (selectedItem: SelectOption | null) => { const handleSelectedItemChange = (selectedItem: SelectOption | undefined) => {
if (!selectedItem) return;
setSelectedItem(selectedItem); setSelectedItem(selectedItem);
setInputValue(selectedItem ? selectedItem.label : ''); setInputValue(selectedItem ? selectedItem.label : '');
onChange?.(selectedItem as SelectOption); onChange?.(selectedItem as SelectOption);
@ -185,7 +186,7 @@ export const Select = ({
onSelectedItemsChange: multiple onSelectedItemsChange: multiple
? undefined ? undefined
: ({ selectedItems }) => { : ({ selectedItems }) => {
handleSelectedItemChange(selectedItems?.[0] || null); handleSelectedItemChange(selectedItems?.[0]);
}, },
}); });
@ -279,16 +280,19 @@ export const Select = ({
const renderLeftIcon = useMemo(() => { const renderLeftIcon = useMemo(() => {
return ( return (
<div className={theme.iconContainer({ class: 'left-0 pl-4' })}> <div className={theme.iconContainer({ class: 'left-0 pl-4' })}>
{cloneIcon(leftIcon, { className: theme.icon(), 'aria-hidden': true })} {cloneIcon(selectedItem?.leftIcon ? selectedItem.leftIcon : leftIcon, {
className: theme.icon(),
'aria-hidden': true,
})}
</div> </div>
); );
}, [cloneIcon, theme, leftIcon]); }, [cloneIcon, theme, leftIcon, selectedItem]);
const renderRightIcon = useMemo(() => { const renderRightIcon = useMemo(() => {
return ( return (
<div className={theme.iconContainer({ class: 'pr-4 right-0' })}> <div className={theme.iconContainer({ class: 'pr-4 right-0' })}>
{clearable && (selectedItems.length > 0 || selectedItem) && ( {clearable && (selectedItems.length > 0 || selectedItem) && (
<CrossIcon <CrossCircleIcon
className={theme.icon({ class: 'h-4 w-4' })} className={theme.icon({ class: 'h-4 w-4' })}
onClick={handleClear} onClick={handleClear}
/> />
@ -296,11 +300,11 @@ export const Select = ({
{rightIcon ? ( {rightIcon ? (
cloneIcon(rightIcon, { className: theme.icon(), 'aria-hidden': true }) cloneIcon(rightIcon, { className: theme.icon(), 'aria-hidden': true })
) : ( ) : (
<ChevronDownIcon className={theme.icon()} /> <ChevronGrabberHorizontal className={theme.icon()} />
)} )}
</div> </div>
); );
}, [cloneIcon, theme, rightIcon]); }, [cloneIcon, theme, rightIcon, selectedItem, selectedItems, clearable]);
const renderHelperText = useMemo(() => { const renderHelperText = useMemo(() => {
if (!helperText) return null; if (!helperText) return null;
@ -343,7 +347,7 @@ export const Select = ({
onClick={() => !dropdownOpen && openMenu()} onClick={() => !dropdownOpen && openMenu()}
> >
{/* Left icon */} {/* Left icon */}
{leftIcon && renderLeftIcon} {renderLeftIcon}
{/* Multiple input values */} {/* Multiple input values */}
{isMultipleHasValue && {isMultipleHasValue &&

View File

@ -62,7 +62,9 @@ const SelectItem = forwardRef<HTMLLIElement, SelectItemProps>(
<p className={theme.label()} data-disabled={disabled}> <p className={theme.label()} data-disabled={disabled}>
{label} {label}
</p> </p>
{orientation === 'horizontal' && <span className={theme.dot()} />} {orientation === 'horizontal' && description && (
<span className={theme.dot()} />
)}
{description && ( {description && (
<p className={theme.description()} data-disabled={disabled}> <p className={theme.description()} data-disabled={disabled}>
{description} {description}

View File

@ -32,7 +32,7 @@ const ProjectSearch = () => {
}, []); }, []);
return ( return (
<div> <div className="h-full">
<div className="sticky top-0 bg-white z-30"> <div className="sticky top-0 bg-white z-30">
<div className="flex pl-3 pr-8 pt-3 pb-3 items-center"> <div className="flex pl-3 pr-8 pt-3 pb-3 items-center">
<div className="grow"> <div className="grow">
@ -64,7 +64,7 @@ const ProjectSearch = () => {
</div> </div>
<HorizontalLine /> <HorizontalLine />
</div> </div>
<div className="z-0"> <div className="z-0 h-full">
<Outlet /> <Outlet />
</div> </div>
</div> </div>

View File

@ -98,8 +98,8 @@ const Id = () => {
</div> </div>
</div> </div>
<WavyBorder /> <WavyBorder />
<div className="px-6 "> <div className="px-6 h-full">
<Tabs value={currentTab} className="flex-col pt-6"> <Tabs value={currentTab} className="flex-col pt-6 h-full">
<Tabs.List> <Tabs.List>
<Tabs.Trigger value=""> <Tabs.Trigger value="">
<Link to="">Overview</Link> <Link to="">Overview</Link>
@ -115,7 +115,7 @@ const Id = () => {
</Tabs.Trigger> </Tabs.Trigger>
</Tabs.List> </Tabs.List>
{/* Not wrapping in Tab.Content because we are using Outlet */} {/* Not wrapping in Tab.Content because we are using Outlet */}
<div className="py-7"> <div className="py-7 h-full">
<Outlet context={{ project, onUpdate }} /> <Outlet context={{ project, onUpdate }} />
</div> </div>
</Tabs> </Tabs>

View File

@ -2,19 +2,19 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Deployment, Domain } from 'gql-client'; import { Deployment, Domain } from 'gql-client';
import { useOutletContext } from 'react-router-dom'; 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, { import FilterForm, {
FilterValue, FilterValue,
StatusOptions, StatusOptions,
} from '../../../../components/projects/project/deployments/FilterForm'; } from 'components/projects/project/deployments/FilterForm';
import { OutletContextType } from '../../../../types'; import { OutletContextType } from 'types';
import { useGQLClient } from '../../../../context/GQLClientContext'; import { useGQLClient } from 'context/GQLClientContext';
import { Button } from 'components/shared/Button';
import { RefreshIcon } from 'components/shared/CustomIcon';
const DEFAULT_FILTER_VALUE: FilterValue = { const DEFAULT_FILTER_VALUE: FilterValue = {
searchedBranch: '', searchedBranch: '',
status: StatusOptions.ALL_STATUS, status: '',
}; };
const FETCH_DEPLOYMENTS_INTERVAL = 5000; const FETCH_DEPLOYMENTS_INTERVAL = 5000;
@ -73,12 +73,19 @@ const DeploymentsTabPanel = () => {
// TODO: match status field types // TODO: match status field types
(deployment.status as unknown as StatusOptions) === filterValue.status; (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 = const dateMatch =
!filterValue.updateAtRange || !filterValue.updateAtRange ||
(new Date(Number(deployment.createdAt)) >= (new Date(Number(deployment.createdAt)) >= startDate! &&
filterValue.updateAtRange!.from! && new Date(Number(deployment.createdAt)) <= endDate!);
new Date(Number(deployment.createdAt)) <=
filterValue.updateAtRange!.to!);
return branchMatch && statusMatch && dateMatch; return branchMatch && statusMatch && dateMatch;
}); });
@ -93,12 +100,12 @@ const DeploymentsTabPanel = () => {
}; };
return ( return (
<div className="max-w-[1440px]"> <section className="h-full">
<FilterForm <FilterForm
value={filterValue} value={filterValue}
onChange={(value) => setFilterValue(value)} onChange={(value) => setFilterValue(value)}
/> />
<div className="mt-3"> <div className="mt-2 h-full">
{Boolean(filteredDeployments.length) ? ( {Boolean(filteredDeployments.length) ? (
filteredDeployments.map((deployment, key) => { filteredDeployments.map((deployment, key) => {
return ( return (
@ -113,27 +120,27 @@ const DeploymentsTabPanel = () => {
); );
}) })
) : ( ) : (
<div className="h-[50vh] bg-gray-100 flex rounded items-center justify-center"> // TODO: Update the height based on the layout, need to re-styling the layout similar to create project layout
<div className="text-center"> <div className="h-3/4 bg-base-bg-alternate flex flex-col rounded-xl items-center justify-center text-center gap-5">
<Typography variant="h5" placeholder={''}> <div className="space-y-1">
<p className="font-medium tracking-[-0.011em] text-elements-high-em">
No deployments found No deployments found
</Typography> </p>
<Typography placeholder={''}> <p className="text-sm tracking-[-0.006em] text-elements-mid-em">
Please change your search query or filters Please change your search query or filters.
</Typography> </p>
<Button
className="rounded-full mt-5"
color="white"
onClick={handleResetFilters}
placeholder={''}
>
^ Reset filters
</Button>
</div> </div>
<Button
variant="tertiary"
leftIcon={<RefreshIcon />}
onClick={handleResetFilters}
>
Reset filters
</Button>
</div> </div>
)} )}
</div> </div>
</div> </section>
); );
}; };