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 { 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<DateRange>();
const [dateRange, setDateRange] = useState<Value>();
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 <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 (
<div className="grid items-center grid-cols-8 gap-2 text-sm text-gray-600">
<div className="col-span-4">
<SearchBar
<div className="xl:grid xl:grid-cols-8 flex flex-col xl:gap-3 gap-0">
<div className="col-span-4 flex items-center">
<Input
placeholder="Search branches"
leftIcon={<SearchIcon />}
rightIcon={
searchedBranch && <CrossCircleIcon onClick={handleReset} />
}
value={searchedBranch}
onChange={(event) => setSearchedBranch(event.target.value)}
onChange={(e) => setSearchedBranch(e.target.value)}
/>
</div>
<div className="col-span-2">
<DatePicker mode="range" selected={dateRange} onSelect={setDateRange} />
<div className="col-span-2 flex items-center">
<DatePicker
className="w-full"
selectRange
value={dateRange}
onChange={setDateRange}
onReset={() => setDateRange(undefined)}
/>
</div>
<div className="col-span-2 relative">
<div className="col-span-2 flex items-center">
<Select
value={selectedStatus}
onChange={(value) => setSelectedStatus(value as StatusOptions)}
leftIcon={getOptionIcon(selectedStatus as StatusOptions)}
options={statusOptions}
clearable
placeholder="All status"
>
{Object.values(StatusOptions).map((status) => (
<Option
className={status === StatusOptions.ALL_STATUS ? 'hidden' : ''}
key={status}
value={status}
>
^ {status}
</Option>
))}
</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>
)}
value={
selectedStatus
? { label: selectedStatus, value: selectedStatus }
: undefined
}
onChange={(item) =>
setSelectedStatus((item as SelectOption).value as StatusOptions)
}
onClear={() => setSelectedStatus('')}
/>
</div>
</div>
);

View File

@ -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 (
<div
{...wrapperProps}
@ -276,21 +288,30 @@ export const Calendar = ({
{/* Footer or CTA */}
<div
{...footerProps}
className={footer({ className: footerProps?.className })}
className={cn(footer({ className: footerProps?.className }), {
'justify-between': Boolean(value),
})}
>
{actions ? (
actions
) : (
<>
<Button variant="tertiary" onClick={onCancel}>
Cancel
</Button>
<Button
disabled={!value}
onClick={() => (value ? onSelect?.(value) : null)}
>
Select
</Button>
{value && (
<Button variant="danger" onClick={handleReset}>
Reset
</Button>
)}
<div className="space-x-3">
<Button variant="tertiary" onClick={onCancel}>
Cancel
</Button>
<Button
disabled={!value}
onClick={() => (value ? onSelect?.(value) : null)}
>
Select
</Button>
</div>
</>
)}
</div>

View File

@ -5,16 +5,16 @@ export const CalendarIcon = (props: CustomIconProps) => {
return (
<CustomIcon
width="18"
height="19"
viewBox="0 0 18 19"
height="18"
viewBox="0 0 18 18"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
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"
fill="currentColor"
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"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</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) => {
return (
<CustomIcon
width="16"
height="16"
viewBox="0 0 16 16"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
{...props}
>
<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"
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"
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"
fill="currentColor"
/>
<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"
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"
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"
/>
</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 './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';

View File

@ -2,7 +2,7 @@ import { VariantProps, tv } from 'tailwind-variants';
export const datePickerTheme = tv({
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 * 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 (
<Popover.Root open={open}>
<Popover.Trigger>
<Popover.Trigger className="w-full">
<Input
{...props}
rightIcon={<CalendarIcon onClick={() => setOpen(true)} />}
leftIcon={<CalendarIcon onClick={() => setOpen(true)} />}
rightIcon={<ChevronGrabberHorizontal />}
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}
/>

View File

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

View File

@ -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: [

View File

@ -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 (
<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>
);
}, [cloneIcon, theme, leftIcon]);
}, [cloneIcon, theme, leftIcon, selectedItem]);
const renderRightIcon = useMemo(() => {
return (
<div className={theme.iconContainer({ class: 'pr-4 right-0' })}>
{clearable && (selectedItems.length > 0 || selectedItem) && (
<CrossIcon
<CrossCircleIcon
className={theme.icon({ class: 'h-4 w-4' })}
onClick={handleClear}
/>
@ -296,11 +300,11 @@ export const Select = ({
{rightIcon ? (
cloneIcon(rightIcon, { className: theme.icon(), 'aria-hidden': true })
) : (
<ChevronDownIcon className={theme.icon()} />
<ChevronGrabberHorizontal className={theme.icon()} />
)}
</div>
);
}, [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 &&

View File

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

View File

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

View File

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

View File

@ -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 (
<div className="max-w-[1440px]">
<section className="h-full">
<FilterForm
value={filterValue}
onChange={(value) => setFilterValue(value)}
/>
<div className="mt-3">
<div className="mt-2 h-full">
{Boolean(filteredDeployments.length) ? (
filteredDeployments.map((deployment, key) => {
return (
@ -113,27 +120,27 @@ const DeploymentsTabPanel = () => {
);
})
) : (
<div className="h-[50vh] bg-gray-100 flex rounded items-center justify-center">
<div className="text-center">
<Typography variant="h5" placeholder={''}>
// TODO: Update the height based on the layout, need to re-styling the layout similar to create project layout
<div className="h-3/4 bg-base-bg-alternate flex flex-col rounded-xl items-center justify-center text-center gap-5">
<div className="space-y-1">
<p className="font-medium tracking-[-0.011em] text-elements-high-em">
No deployments found
</Typography>
<Typography placeholder={''}>
Please change your search query or filters
</Typography>
<Button
className="rounded-full mt-5"
color="white"
onClick={handleResetFilters}
placeholder={''}
>
^ Reset filters
</Button>
</p>
<p className="text-sm tracking-[-0.006em] text-elements-mid-em">
Please change your search query or filters.
</p>
</div>
<Button
variant="tertiary"
leftIcon={<RefreshIcon />}
onClick={handleResetFilters}
>
Reset filters
</Button>
</div>
)}
</div>
</div>
</section>
);
};