diff --git a/.changeset/red-socks-grow.md b/.changeset/red-socks-grow.md new file mode 100644 index 000000000..c2c7667a4 --- /dev/null +++ b/.changeset/red-socks-grow.md @@ -0,0 +1,5 @@ +--- +'@portaljs/components': patch +--- + +Added pagination and filter properties for the BucketViewer component diff --git a/packages/components/src/components/BucketViewer.tsx b/packages/components/src/components/BucketViewer.tsx index 0d6eeb5ed..3f5520086 100644 --- a/packages/components/src/components/BucketViewer.tsx +++ b/packages/components/src/components/BucketViewer.tsx @@ -1,46 +1,131 @@ -import { useEffect, useState } from 'react'; +import { CSSProperties, ReactNode, useEffect, useState } from 'react'; import LoadingSpinner from './LoadingSpinner'; +export interface BucketViewerFilterSearchedDataEvent { + startDate?: Date; + endDate?: Date; +} + export interface BucketViewerProps { + onLoadTotalNumberOfItems?: (total: number) => void; domain: string; suffix?: string; className?: string; + downloadComponent?: ReactNode; + paginationConfig?: BucketViewerPaginationConfig; + filterState?: BucketViewerFilterSearchedDataEvent; dataMapperFn: (rawData: Response) => Promise; } +export interface BucketViewerPaginationConfig { + containerClassName?: string; + containerStyles?: CSSProperties; + itemsPerPage: number; +} + export interface BucketViewerData { fileName: string; downloadFileUri: string; dateProps?: { date: Date; - dateFormatter: (date: Date) => string; + dateFormatter?: (date: Date) => string; }; } export function BucketViewer({ domain, + downloadComponent, suffix, dataMapperFn, className, + filterState, + paginationConfig, + onLoadTotalNumberOfItems }: BucketViewerProps) { + + suffix = suffix ?? '/'; + downloadComponent = downloadComponent ?? <>; + const [isLoading, setIsLoading] = useState(false); + const [showDownloadComponentOnLine, setShowDownloadComponentOnLine] = useState(0); + const [currentPage, setCurrentPage] = useState(0); + const [lastPage, setLastPage] = useState(0); const [bucketFiles, setBucketFiles] = useState([]); - suffix = suffix ?? '/'; + const [paginatedData, setPaginatedData] = useState([]); + const [filteredData, setFilteredData] = useState([]); useEffect(() => { setIsLoading(true); fetch(`${domain}${suffix}`) .then((res) => dataMapperFn(res)) - .then((data) => setBucketFiles(data)) + .then((data) => { + setBucketFiles(data); + setFilteredData(data); + }) .finally(() => setIsLoading(false)); }, [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 ? (
) : bucketFiles ? ( <> - {...bucketFiles?.map((data, i) => ( + {...(paginationConfig && bucketFiles + ? paginatedData + : filteredData + )?.map((data, i) => (
    { const anchorId = `download_anchor_${i}`; @@ -49,7 +134,7 @@ export function BucketViewer({ document.createElement('a'); a.id = anchorId; if (a.download) a.click(); - else { + else { setIsLoading(true); fetch(data.downloadFileUri) .then((res) => res.blob()) @@ -63,19 +148,78 @@ export function BucketViewer({ } }} key={i} + onMouseEnter={() => setShowDownloadComponentOnLine(i)} + onMouseLeave={() => setShowDownloadComponentOnLine(undefined)} className={`${ className ?? 'mb-2 border-b-[2px] border-b-[red] hover:cursor-pointer' }`} > -
  • {data.fileName}
  • - {data.dateProps ? ( -
  • {data.dateProps.dateFormatter(data.dateProps.date)}
  • - ) : ( - <> - )} + { + downloadComponent && showDownloadComponentOnLine === i + ? downloadComponent + : <> + } +
    +
  • {data.fileName}
  • + {data.dateProps && data.dateProps.dateFormatter ? ( +
  • {data.dateProps.dateFormatter(data.dateProps.date)}
  • + ) : ( + <> + )} +
))} + {paginationConfig ? ( +
    +
  • + +
  • +
  • + +
  • + + +
  • + +
  • + +
  • + +
  • +
+ ) : ( + <> + )} ) : null; } diff --git a/packages/components/stories/BucketViewer.stories.ts b/packages/components/stories/BucketViewer.stories.ts index 1e914d9ac..2bdfed504 100644 --- a/packages/components/stories/BucketViewer.stories.ts +++ b/packages/components/stories/BucketViewer.stories.ts @@ -1,6 +1,7 @@ import { type Meta, type StoryObj } from '@storybook/react'; 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 const meta: Meta = { @@ -16,6 +17,16 @@ const meta: Meta = { description: '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() + } + }) + ) + } + }, +};