diff --git a/.changeset/dirty-coats-obey.md b/.changeset/dirty-coats-obey.md new file mode 100644 index 000000000..d5b3a3117 --- /dev/null +++ b/.changeset/dirty-coats-obey.md @@ -0,0 +1,5 @@ +--- +'@portaljs/components': minor +--- + +Creation of BucketViewer component to show the data of public buckets diff --git a/package-lock.json b/package-lock.json index bf21c403c..347ab5210 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48734,7 +48734,7 @@ }, "packages/core": { "name": "@portaljs/core", - "version": "1.0.6", + "version": "1.0.8", "license": "MIT", "dependencies": { "@docsearch/react": "^3.3.3", @@ -48773,7 +48773,7 @@ }, "packages/remark-embed": { "name": "@portaljs/remark-embed", - "version": "1.0.4", + "version": "1.0.5", "license": "MIT", "dependencies": { "unist-util-visit": "^4.1.1" @@ -48781,7 +48781,7 @@ }, "packages/remark-wiki-link": { "name": "@portaljs/remark-wiki-link", - "version": "1.1.0", + "version": "1.1.2", "license": "MIT", "dependencies": { "mdast-util-to-markdown": "^1.5.0", diff --git a/packages/components/package.json b/packages/components/package.json index f7ccb13e6..22c5f0321 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -29,6 +29,8 @@ "@githubocto/flat-ui": "^0.14.1", "@heroicons/react": "^2.0.17", "@planet/maps": "^8.1.0", + "@react-pdf-viewer/core": "3.6.0", + "@react-pdf-viewer/default-layout": "3.6.0", "@tanstack/react-table": "^8.8.5", "ag-grid-react": "^30.0.4", "chroma-js": "^2.4.2", @@ -37,6 +39,7 @@ "next-mdx-remote": "^4.4.1", "ol": "^7.4.0", "papaparse": "^5.4.1", + "pdfjs-dist": "2.15.349", "postcss-url": "^10.1.3", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -47,9 +50,6 @@ "vega": "5.25.0", "vega-lite": "5.1.0", "vitest": "^0.31.4", - "@react-pdf-viewer/core": "3.6.0", - "@react-pdf-viewer/default-layout": "3.6.0", - "pdfjs-dist": "2.15.349", "xlsx": "^0.18.5" }, "devDependencies": { diff --git a/packages/components/src/components/BucketViewer.tsx b/packages/components/src/components/BucketViewer.tsx new file mode 100644 index 000000000..0d6eeb5ed --- /dev/null +++ b/packages/components/src/components/BucketViewer.tsx @@ -0,0 +1,81 @@ +import { useEffect, useState } from 'react'; +import LoadingSpinner from './LoadingSpinner'; + +export interface BucketViewerProps { + domain: string; + suffix?: string; + className?: string; + dataMapperFn: (rawData: Response) => Promise; +} + +export interface BucketViewerData { + fileName: string; + downloadFileUri: string; + dateProps?: { + date: Date; + dateFormatter: (date: Date) => string; + }; +} + +export function BucketViewer({ + domain, + suffix, + dataMapperFn, + className, +}: BucketViewerProps) { + const [isLoading, setIsLoading] = useState(false); + const [bucketFiles, setBucketFiles] = useState([]); + suffix = suffix ?? '/'; + + useEffect(() => { + setIsLoading(true); + fetch(`${domain}${suffix}`) + .then((res) => dataMapperFn(res)) + .then((data) => setBucketFiles(data)) + .finally(() => setIsLoading(false)); + }, [domain, suffix]); + return isLoading ? ( +
+ +
+ ) : bucketFiles ? ( + <> + {...bucketFiles?.map((data, i) => ( +
    { + const anchorId = `download_anchor_${i}`; + const a: HTMLAnchorElement = + (document.getElementById(anchorId) as HTMLAnchorElement | null) ?? + document.createElement('a'); + a.id = anchorId; + if (a.download) a.click(); + else { + setIsLoading(true); + fetch(data.downloadFileUri) + .then((res) => res.blob()) + .then((res) => { + a.href = URL.createObjectURL(res); + a.download = res.name ?? data.fileName; + document.body.appendChild(a); + a.click(); + }) + .finally(() => setIsLoading(false)); + } + }} + key={i} + className={`${ + className ?? + 'mb-2 border-b-[2px] border-b-[red] hover:cursor-pointer' + }`} + > +
  • {data.fileName}
  • + {data.dateProps ? ( +
  • {data.dateProps.dateFormatter(data.dateProps.date)}
  • + ) : ( + <> + )} +
+ ))} + + ) : null; +} diff --git a/packages/components/stories/BucketViewer.stories.ts b/packages/components/stories/BucketViewer.stories.ts new file mode 100644 index 000000000..c3bf42c2d --- /dev/null +++ b/packages/components/stories/BucketViewer.stories.ts @@ -0,0 +1,46 @@ +import { raw, type Meta, type StoryObj } from '@storybook/react'; + +import { BucketViewer, BucketViewerData, BucketViewerProps } from '../src/components/BucketViewer'; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta: Meta = { + title: 'Components/BucketViewer', + component: BucketViewer, + tags: ['autodocs'], + argTypes: { + domain: { + description: + 'Bucket domain URI', + }, + suffix: { + description: + 'Suffix of bucket domain', + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +export const Normal: Story = { + name: 'Bucket viewer', + args: { + domain: 'https://ssen-smart-meter.datopian.workers.dev', + suffix: '/', + 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() + } + }) + ) + } + }, +};