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 9bc6806f2..347ab5210 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25726,27 +25726,6 @@ "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" }, - "node_modules/fast-xml-parser": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz", - "integrity": "sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - } - ], - "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/fastest-stable-stringify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", @@ -43403,11 +43382,6 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/strnum": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", - "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" - }, "node_modules/strong-log-transformer": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz", @@ -48333,7 +48307,6 @@ "@tanstack/react-table": "^8.8.5", "ag-grid-react": "^30.0.4", "chroma-js": "^2.4.2", - "fast-xml-parser": "^4.3.2", "flexsearch": "0.7.21", "leaflet": "^1.9.4", "next-mdx-remote": "^4.4.1", @@ -57206,7 +57179,6 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.3.4", "eslint-plugin-storybook": "^0.6.11", - "fast-xml-parser": "^4.3.2", "flexsearch": "0.7.21", "json": "^11.0.0", "leaflet": "^1.9.4", @@ -68642,14 +68614,6 @@ "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" }, - "fast-xml-parser": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz", - "integrity": "sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==", - "requires": { - "strnum": "^1.0.5" - } - }, "fastest-stable-stringify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", @@ -81868,11 +81832,6 @@ "acorn": "^8.8.2" } }, - "strnum": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", - "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" - }, "strong-log-transformer": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz", diff --git a/packages/components/package.json b/packages/components/package.json index c30a9826d..22c5f0321 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -34,7 +34,6 @@ "@tanstack/react-table": "^8.8.5", "ag-grid-react": "^30.0.4", "chroma-js": "^2.4.2", - "fast-xml-parser": "^4.3.2", "flexsearch": "0.7.21", "leaflet": "^1.9.4", "next-mdx-remote": "^4.4.1", diff --git a/packages/components/src/components/BucketViewer.tsx b/packages/components/src/components/BucketViewer.tsx index 9c8aeefa9..0d6eeb5ed 100644 --- a/packages/components/src/components/BucketViewer.tsx +++ b/packages/components/src/components/BucketViewer.tsx @@ -1,78 +1,46 @@ import { useEffect, useState } from 'react'; import LoadingSpinner from './LoadingSpinner'; -import { XMLParser } from 'fast-xml-parser'; export interface BucketViewerProps { domain: string; suffix?: string; + className?: string; + dataMapperFn: (rawData: Response) => Promise; } -interface BucketResponse { - ListBucketResult: ListBucketResult; +export interface BucketViewerData { + fileName: string; + downloadFileUri: string; + dateProps?: { + date: Date; + dateFormatter: (date: Date) => string; + }; } -interface ListBucketResult { - Name: string; - Prefix: string; - MaxKeys: number; - IsTruncated: boolean; - Contents: Content[]; - Marker: string; - NextMarker: string; -} - -interface Content { - Key: string; - LastModified: string; - ETag: string; - Size: number; - StorageClass: StorageClass; - Owner?: Owner; - Type: Type; -} - -interface Owner { - ID: number; - DisplayName: number; -} - -enum StorageClass { - Standard = 'STANDARD', -} - -enum Type { - Normal = 'Normal', -} - -export function BucketViewer({ domain, suffix }: BucketViewerProps) { +export function BucketViewer({ + domain, + suffix, + dataMapperFn, + className, +}: BucketViewerProps) { const [isLoading, setIsLoading] = useState(false); - const [bucket, setBucket] = useState(); + const [bucketFiles, setBucketFiles] = useState([]); suffix = suffix ?? '/'; useEffect(() => { setIsLoading(true); fetch(`${domain}${suffix}`) - .then((res) => res.text()) - .then((res) => { - const parsedXml: BucketResponse = new XMLParser().parse(res); - let { - ListBucketResult: { Contents }, - } = parsedXml; - Contents = Contents ?? []; - parsedXml.ListBucketResult.Contents = Array.isArray(Contents) - ? Contents - : [Contents]; - setBucket(parsedXml); - }) + .then((res) => dataMapperFn(res)) + .then((data) => setBucketFiles(data)) .finally(() => setIsLoading(false)); }, [domain, suffix]); return isLoading ? (
- ) : bucket ? ( + ) : bucketFiles ? ( <> - {...bucket?.ListBucketResult?.Contents?.map((c, i) => ( + {...bucketFiles?.map((data, i) => (
    { const anchorId = `download_anchor_${i}`; @@ -83,11 +51,11 @@ export function BucketViewer({ domain, suffix }: BucketViewerProps) { if (a.download) a.click(); else { setIsLoading(true); - fetch(`${domain}${suffix}${c.Key}`) + fetch(data.downloadFileUri) .then((res) => res.blob()) .then((res) => { a.href = URL.createObjectURL(res); - a.download = res.name ?? c.ETag.replace(/\"/g, ''); + a.download = res.name ?? data.fileName; document.body.appendChild(a); a.click(); }) @@ -95,16 +63,17 @@ export function BucketViewer({ domain, suffix }: BucketViewerProps) { } }} key={i} - className="mb-2 border-b-[2px] border-b-[red] hover:cursor-pointer" + className={`${ + className ?? + 'mb-2 border-b-[2px] border-b-[red] hover:cursor-pointer' + }`} > -
  • {c.Key}
  • -
  • {c.ETag}
  • -
  • {c.Owner?.DisplayName}
  • -
  • {c.Owner?.ID}
  • -
  • {c.Size}
  • -
  • {c.StorageClass}
  • -
  • {c.Type}
  • -
  • {c.LastModified}
  • +
  • {data.fileName}
  • + {data.dateProps ? ( +
  • {data.dateProps.dateFormatter(data.dateProps.date)}
  • + ) : ( + <> + )}
))} diff --git a/packages/components/stories/BucketViewer.stories.ts b/packages/components/stories/BucketViewer.stories.ts index 7f73cd032..c3bf42c2d 100644 --- a/packages/components/stories/BucketViewer.stories.ts +++ b/packages/components/stories/BucketViewer.stories.ts @@ -1,6 +1,6 @@ -import type { Meta, StoryObj } from '@storybook/react'; +import { raw, type Meta, type StoryObj } from '@storybook/react'; -import { BucketViewer, BucketViewerProps } from '../src/components/BucketViewer'; +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 = { @@ -27,8 +27,20 @@ type Story = StoryObj; export const Normal: Story = { name: 'Bucket viewer', args: { - domain: 'https://nwguide.fra1.digitaloceanspaces.com', - suffix: '/' + 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() + } + }) + ) + } }, }; -