Added pagination and filter properties for the BucketViewer component

This commit is contained in:
Gutts-n 2024-01-23 14:37:03 -03:00
parent 96904aef0d
commit 27c99adde8
3 changed files with 219 additions and 13 deletions

View File

@ -0,0 +1,5 @@
---
'@portaljs/components': patch
---
Added pagination and filter properties for the BucketViewer component

View File

@ -1,46 +1,131 @@
import { useEffect, useState } from 'react'; import { CSSProperties, ReactNode, useEffect, useState } from 'react';
import LoadingSpinner from './LoadingSpinner'; import LoadingSpinner from './LoadingSpinner';
export interface BucketViewerFilterSearchedDataEvent {
startDate?: Date;
endDate?: Date;
}
export interface BucketViewerProps { export interface BucketViewerProps {
onLoadTotalNumberOfItems?: (total: number) => void;
domain: string; domain: string;
suffix?: string; suffix?: string;
className?: string; className?: string;
downloadComponent?: ReactNode;
paginationConfig?: BucketViewerPaginationConfig;
filterState?: BucketViewerFilterSearchedDataEvent;
dataMapperFn: (rawData: Response) => Promise<BucketViewerData[]>; dataMapperFn: (rawData: Response) => Promise<BucketViewerData[]>;
} }
export interface BucketViewerPaginationConfig {
containerClassName?: string;
containerStyles?: CSSProperties;
itemsPerPage: number;
}
export interface BucketViewerData { export interface BucketViewerData {
fileName: string; fileName: string;
downloadFileUri: string; downloadFileUri: string;
dateProps?: { dateProps?: {
date: Date; date: Date;
dateFormatter: (date: Date) => string; dateFormatter?: (date: Date) => string;
}; };
} }
export function BucketViewer({ export function BucketViewer({
domain, domain,
downloadComponent,
suffix, suffix,
dataMapperFn, dataMapperFn,
className, className,
filterState,
paginationConfig,
onLoadTotalNumberOfItems
}: BucketViewerProps) { }: BucketViewerProps) {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [bucketFiles, setBucketFiles] = useState<BucketViewerData[]>([]);
suffix = suffix ?? '/'; suffix = suffix ?? '/';
downloadComponent = downloadComponent ?? <></>;
const [isLoading, setIsLoading] = useState<boolean>(false);
const [showDownloadComponentOnLine, setShowDownloadComponentOnLine] = useState(0);
const [currentPage, setCurrentPage] = useState<number>(0);
const [lastPage, setLastPage] = useState<number>(0);
const [bucketFiles, setBucketFiles] = useState<BucketViewerData[]>([]);
const [paginatedData, setPaginatedData] = useState<BucketViewerData[]>([]);
const [filteredData, setFilteredData] = useState<BucketViewerData[]>([]);
useEffect(() => { useEffect(() => {
setIsLoading(true); setIsLoading(true);
fetch(`${domain}${suffix}`) fetch(`${domain}${suffix}`)
.then((res) => dataMapperFn(res)) .then((res) => dataMapperFn(res))
.then((data) => setBucketFiles(data)) .then((data) => {
setBucketFiles(data);
setFilteredData(data);
})
.finally(() => setIsLoading(false)); .finally(() => setIsLoading(false));
}, [domain, suffix]); }, [domain, suffix]);
useEffect(
() => {
if(paginationConfig) {
const startIndex = paginationConfig
? currentPage * paginationConfig.itemsPerPage
: 0;
const endIndex = paginationConfig
? startIndex + paginationConfig.itemsPerPage
: 0;
setLastPage(Math.ceil(filteredData.length / paginationConfig.itemsPerPage) - 1);
setPaginatedData(filteredData.slice(startIndex, endIndex));
}
},
[currentPage, filteredData]
);
useEffect(
() => {
if(onLoadTotalNumberOfItems) onLoadTotalNumberOfItems(filteredData.length);
},
[filteredData]
)
useEffect(() => {
if(!filterState) return;
if(filterState.startDate && filterState.startDate) {
setFilteredData(bucketFiles.filter(({ dateProps }) =>
dateProps
?
dateProps.date.getTime() >= filterState.startDate.getTime()
&& dateProps.date.getTime() <= filterState.endDate.getTime()
: true
));
} else if(filterState.startDate) {
setFilteredData(bucketFiles.filter(({ dateProps }) =>
dateProps ? dateProps.date.getTime() >= filterState.startDate.getTime() : true
));
} else if(filterState.endDate) {
setFilteredData(bucketFiles.filter(({ dateProps }) =>
dateProps ? dateProps.date.getTime() <= filterState.endDate.getTime() : true
));
} else {
setFilteredData(bucketFiles);
}
},
[filterState]
)
return isLoading ? ( return isLoading ? (
<div className="w-full flex items-center justify-center h-[300px]"> <div className="w-full flex items-center justify-center h-[300px]">
<LoadingSpinner /> <LoadingSpinner />
</div> </div>
) : bucketFiles ? ( ) : bucketFiles ? (
<> <>
{...bucketFiles?.map((data, i) => ( {...(paginationConfig && bucketFiles
? paginatedData
: filteredData
)?.map((data, i) => (
<ul <ul
onClick={() => { onClick={() => {
const anchorId = `download_anchor_${i}`; const anchorId = `download_anchor_${i}`;
@ -49,7 +134,7 @@ export function BucketViewer({
document.createElement('a'); document.createElement('a');
a.id = anchorId; a.id = anchorId;
if (a.download) a.click(); if (a.download) a.click();
else { else {
setIsLoading(true); setIsLoading(true);
fetch(data.downloadFileUri) fetch(data.downloadFileUri)
.then((res) => res.blob()) .then((res) => res.blob())
@ -63,19 +148,78 @@ export function BucketViewer({
} }
}} }}
key={i} key={i}
onMouseEnter={() => setShowDownloadComponentOnLine(i)}
onMouseLeave={() => setShowDownloadComponentOnLine(undefined)}
className={`${ className={`${
className ?? className ??
'mb-2 border-b-[2px] border-b-[red] hover:cursor-pointer' 'mb-2 border-b-[2px] border-b-[red] hover:cursor-pointer'
}`} }`}
> >
<li>{data.fileName}</li> {
{data.dateProps ? ( downloadComponent && showDownloadComponentOnLine === i
<li>{data.dateProps.dateFormatter(data.dateProps.date)}</li> ? downloadComponent
) : ( : <></>
<></> }
)} <div>
<li>{data.fileName}</li>
{data.dateProps && data.dateProps.dateFormatter ? (
<li>{data.dateProps.dateFormatter(data.dateProps.date)}</li>
) : (
<></>
)}
</div>
</ul> </ul>
))} ))}
{paginationConfig ? (
<ul className={
paginationConfig.containerClassName
? paginationConfig.containerClassName
: "flex justify-end gap-x-[0.5rem] w-full"
}
style={paginationConfig.containerStyles ?? {}}
>
<li>
<button
className="hover:cursor-pointer hover:disabled:cursor-not-allowed"
disabled={currentPage === 0}
onClick={() => setCurrentPage(0)}>First</button>
</li>
<li>
<button
className="hover:cursor-pointer hover:disabled:cursor-not-allowed"
onClick={() => setCurrentPage(currentPage - 1)}
disabled={currentPage === 0}
>
Previous
</button>
</li>
<label>
{currentPage + 1}
</label>
<li>
<button
onClick={() => setCurrentPage(currentPage + 1)}
disabled={currentPage >= lastPage}
className="hover:cursor-pointer hover:disabled:cursor-not-allowed"
>
Next
</button>
</li>
<li>
<button
onClick={() => setCurrentPage(lastPage)}
disabled={currentPage >= lastPage}
className="hover:cursor-pointer hover:disabled:cursor-not-allowed"
>
Last
</button>
</li>
</ul>
) : (
<></>
)}
</> </>
) : null; ) : null;
} }

View File

@ -1,6 +1,7 @@
import { type Meta, type StoryObj } from '@storybook/react'; import { type Meta, type StoryObj } from '@storybook/react';
import { BucketViewer, BucketViewerProps } from '../src/components/BucketViewer'; import { BucketViewer, BucketViewerProps } from '../src/components/BucketViewer';
import LoadingSpinner from '../src/components/LoadingSpinner';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = { const meta: Meta = {
@ -16,6 +17,16 @@ const meta: Meta = {
description: description:
'Suffix of bucket domain', 'Suffix of bucket domain',
}, },
downloadComponent: {
description:
'Component to be displayed on hover of each bucket data',
},
filterState: {
description: `State with values used to filter the bucket files`
},
paginationConfig: {
description: `Configuration to show and stylise the pagination on the component`,
},
}, },
}; };
@ -44,3 +55,49 @@ export const Normal: Story = {
} }
}, },
}; };
export const WithPagination: Story = {
name: 'With pagination',
args: {
domain: 'https://ssen-smart-meter.datopian.workers.dev',
suffix: '/',
paginationConfig: {
itemsPerPage: 3
},
dataMapperFn: async (rawData: Response) => {
const result = await rawData.json();
return result.objects.map(
e => ({
downloadFileUri: e.downloadLink,
fileName: e.key.replace(/^(\w+\/)/g, '') ,
dateProps: {
date: new Date(e.uploaded),
dateFormatter: (date) => date.toLocaleDateString()
}
})
)
}
},
};
export const WithComponentOnHoverOfEachBucketFile: Story = {
name: 'With component on hover of each bucket file',
args: {
domain: 'https://ssen-smart-meter.datopian.workers.dev',
suffix: '/',
downloadComponent: LoadingSpinner(),
dataMapperFn: async (rawData: Response) => {
const result = await rawData.json();
return result.objects.map(
e => ({
downloadFileUri: e.downloadLink,
fileName: e.key.replace(/^(\w+\/)/g, '') ,
dateProps: {
date: new Date(e.uploaded),
dateFormatter: (date) => date.toLocaleDateString()
}
})
)
}
},
};