From 03960c8bac82e0bc6d1482309e99bec1901ec670 Mon Sep 17 00:00:00 2001 From: "leonardo.farias" Date: Mon, 20 Nov 2023 23:50:04 -0300 Subject: [PATCH] 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 bf21c403..9bc6806f 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 f7ccb13e..c30a9826 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 00000000..9c8aeefa --- /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 00000000..7f73cd03 --- /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: '/' + }, +}; +