From 03960c8bac82e0bc6d1482309e99bec1901ec670 Mon Sep 17 00:00:00 2001 From: "leonardo.farias" Date: Mon, 20 Nov 2023 23:50:04 -0300 Subject: [PATCH 1/2] feature: Created bucket viewer component --- package-lock.json | 47 +++++++- packages/components/package.json | 7 +- .../src/components/BucketViewer.tsx | 112 ++++++++++++++++++ .../stories/BucketViewer.stories.ts | 34 ++++++ 4 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 packages/components/src/components/BucketViewer.tsx create mode 100644 packages/components/stories/BucketViewer.stories.ts diff --git a/package-lock.json b/package-lock.json index bf21c403c..9bc6806f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25726,6 +25726,27 @@ "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", @@ -43382,6 +43403,11 @@ "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", @@ -48307,6 +48333,7 @@ "@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", @@ -48734,7 +48761,7 @@ }, "packages/core": { "name": "@portaljs/core", - "version": "1.0.6", + "version": "1.0.8", "license": "MIT", "dependencies": { "@docsearch/react": "^3.3.3", @@ -48773,7 +48800,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 +48808,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", @@ -57179,6 +57206,7 @@ "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", @@ -68614,6 +68642,14 @@ "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", @@ -81832,6 +81868,11 @@ "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 f7ccb13e6..c30a9826d 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -29,14 +29,18 @@ "@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", + "fast-xml-parser": "^4.3.2", "flexsearch": "0.7.21", "leaflet": "^1.9.4", "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 +51,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..9c8aeefa9 --- /dev/null +++ b/packages/components/src/components/BucketViewer.tsx @@ -0,0 +1,112 @@ +import { useEffect, useState } from 'react'; +import LoadingSpinner from './LoadingSpinner'; +import { XMLParser } from 'fast-xml-parser'; + +export interface BucketViewerProps { + domain: string; + suffix?: string; +} + +interface BucketResponse { + ListBucketResult: ListBucketResult; +} + +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) { + const [isLoading, setIsLoading] = useState(false); + const [bucket, setBucket] = 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); + }) + .finally(() => setIsLoading(false)); + }, [domain, suffix]); + return isLoading ? ( +
+ +
+ ) : bucket ? ( + <> + {...bucket?.ListBucketResult?.Contents?.map((c, 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(`${domain}${suffix}${c.Key}`) + .then((res) => res.blob()) + .then((res) => { + a.href = URL.createObjectURL(res); + a.download = res.name ?? c.ETag.replace(/\"/g, ''); + document.body.appendChild(a); + a.click(); + }) + .finally(() => setIsLoading(false)); + } + }} + key={i} + 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}
  • +
+ ))} + + ) : null; +} diff --git a/packages/components/stories/BucketViewer.stories.ts b/packages/components/stories/BucketViewer.stories.ts new file mode 100644 index 000000000..7f73cd032 --- /dev/null +++ b/packages/components/stories/BucketViewer.stories.ts @@ -0,0 +1,34 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { BucketViewer, 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://nwguide.fra1.digitaloceanspaces.com', + suffix: '/' + }, +}; + From 712f4a3b0f074e654879bb75059f51e06b422b32 Mon Sep 17 00:00:00 2001 From: "leonardo.farias" Date: Thu, 23 Nov 2023 21:41:28 -0300 Subject: [PATCH 2/2] Finished the development of the BucketViewer component --- .changeset/dirty-coats-obey.md | 5 + package-lock.json | 41 -------- packages/components/package.json | 1 - .../src/components/BucketViewer.tsx | 95 +++++++------------ .../stories/BucketViewer.stories.ts | 22 ++++- 5 files changed, 54 insertions(+), 110 deletions(-) create mode 100644 .changeset/dirty-coats-obey.md 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() + } + }) + ) + } }, }; -