From c09dd37e66feb799d497424d0caa409013424256 Mon Sep 17 00:00:00 2001 From: Ankur Kumar Date: Mon, 3 Jun 2024 17:59:51 -0400 Subject: [PATCH] Feat(cpgweb: search widget): Add csv file viewer --- cpgweb/package-lock.json | 127 ++++++++++++++++++++++++ cpgweb/package.json | 3 + cpgweb/src/app/layout.tsx | 1 + cpgweb/src/app/search/columns.tsx | 81 +-------------- cpgweb/src/app/search/file-viewer.tsx | 103 +++++++++++++++---- cpgweb/src/app/search/page.tsx | 18 ++-- cpgweb/src/app/search/project-table.tsx | 5 +- cpgweb/src/components/ui/dialog.tsx | 2 +- cpgweb/src/lib/cpg_ops.ts | 22 ++-- cpgweb/src/styles/globals.css | 33 +++++- 10 files changed, 279 insertions(+), 116 deletions(-) diff --git a/cpgweb/package-lock.json b/cpgweb/package-lock.json index 07a2841..865d0d0 100644 --- a/cpgweb/package-lock.json +++ b/cpgweb/package-lock.json @@ -27,12 +27,14 @@ "bootstrap": "^5.3.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "d3-dsv": "^3.0.1", "immutable": "5.0.0-beta.5", "lucide-react": "^0.378.0", "next": "14.2.0", "next-themes": "^0.3.0", "react": "^18", "react-bootstrap": "^2.10.2", + "react-datasheet-grid": "^4.11.4", "react-dom": "^18", "react-wrap-balancer": "^1.1.0", "style-loader": "^4.0.0", @@ -44,6 +46,7 @@ "xterm-addon-webgl": "^0.16.0" }, "devDependencies": { + "@types/d3-dsv": "^3.0.7", "@types/fontfaceobserver": "^2.1.3", "@types/node": "^20", "@types/react": "^18", @@ -3036,6 +3039,22 @@ "react-dom": ">=16.8" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.5.0.tgz", + "integrity": "sha512-rtvo7KwuIvqK9zb0VZ5IL7fiJAEnG+0EiFZz8FUOs+2mhGqdGmjKIaT1XU7Zq0eFqL0jonLlhbayJI/J2SA/Bw==", + "dependencies": { + "@tanstack/virtual-core": "3.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@tanstack/table-core": { "version": "8.17.3", "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.17.3.tgz", @@ -3048,6 +3067,15 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/virtual-core": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.5.0.tgz", + "integrity": "sha512-KnPRCkQTyqhanNC0K63GBG3wA8I+D1fQuVnAvcBF8f13akOKeQp1gSbu6f77zCxhEk727iV5oQnbHLYzHrECLg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/command-line-args": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.0.tgz", @@ -3058,6 +3086,12 @@ "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.2.tgz", "integrity": "sha512-n7RlEEJ+4x4TS7ZQddTmNSxP+zziEG0TNsMfiRIxcIVXt71ENJ9ojeXmGO3wPoTdn7pJcU2xc3CJYMktNT6DPg==" }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -4203,6 +4237,38 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -5498,6 +5564,17 @@ "node": ">= 0.4" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -6167,6 +6244,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.assignwith": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz", @@ -6985,6 +7067,21 @@ } } }, + "node_modules/react-datasheet-grid": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/react-datasheet-grid/-/react-datasheet-grid-4.11.4.tgz", + "integrity": "sha512-fWSOOHCPAv1Qkdk3+Io/Z8b02NC+V1Q/4FIAbhEAv9GmwaBAu/B68pySstY7eIDbgVLxnDLiAq8oCt98HZ8FHg==", + "dependencies": { + "@tanstack/react-virtual": "^3.0.0-beta.18", + "classnames": "^2.3.1", + "fast-deep-equal": "^3.1.3", + "react-resize-detector": "^7.1.2", + "throttle-debounce": "^3.0.1" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -7052,6 +7149,18 @@ } } }, + "node_modules/react-resize-detector": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-7.1.2.tgz", + "integrity": "sha512-zXnPJ2m8+6oq9Nn8zsep/orts9vQv3elrpA+R8XTcW7DVVUJ9vwDwMXaBtykAYjMnkCIaOoK9vObyR7ZgFNlOw==", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -7268,6 +7377,11 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "node_modules/safe-array-concat": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", @@ -7323,6 +7437,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -7949,6 +8068,14 @@ "node": ">=0.8" } }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "engines": { + "node": ">=10" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/cpgweb/package.json b/cpgweb/package.json index 990ddd1..33e6708 100644 --- a/cpgweb/package.json +++ b/cpgweb/package.json @@ -28,12 +28,14 @@ "bootstrap": "^5.3.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "d3-dsv": "^3.0.1", "immutable": "5.0.0-beta.5", "lucide-react": "^0.378.0", "next": "14.2.0", "next-themes": "^0.3.0", "react": "^18", "react-bootstrap": "^2.10.2", + "react-datasheet-grid": "^4.11.4", "react-dom": "^18", "react-wrap-balancer": "^1.1.0", "style-loader": "^4.0.0", @@ -45,6 +47,7 @@ "xterm-addon-webgl": "^0.16.0" }, "devDependencies": { + "@types/d3-dsv": "^3.0.7", "@types/fontfaceobserver": "^2.1.3", "@types/node": "^20", "@types/react": "^18", diff --git a/cpgweb/src/app/layout.tsx b/cpgweb/src/app/layout.tsx index 2f019c5..b2cc3e1 100644 --- a/cpgweb/src/app/layout.tsx +++ b/cpgweb/src/app/layout.tsx @@ -2,6 +2,7 @@ "use client"; import Link from "next/link"; import { Inter as FontSans } from "next/font/google"; +import 'react-datasheet-grid/dist/style.css'; import "../styles/globals.css"; import { cn } from "@/src/lib/utils"; import { ThemeProvider } from "@/src/components/theme-provider"; diff --git a/cpgweb/src/app/search/columns.tsx b/cpgweb/src/app/search/columns.tsx index 501205f..8547889 100644 --- a/cpgweb/src/app/search/columns.tsx +++ b/cpgweb/src/app/search/columns.tsx @@ -7,84 +7,7 @@ import { Button } from "../../components/ui/button"; import { Download, Eye, Loader2 } from "lucide-react"; import { get_key_file } from "../../lib/cpg_ops"; import React from "react"; -import { FileViewer } from "./file-viewer" - -// export const columns: ColumnDef[] = [ -// { -// accessorKey: "objectID", -// header: "Object ID", -// cell: ({ row }) =>
{row.getValue("objectID")}
, -// }, -// { -// accessorKey: "project_id", -// header: "Project ID", -// cell: ({ row }) =>
{row.getValue("project_id")}
, -// }, -// { -// accessorKey: "Col", -// header: "Col", -// cell: ({ row }) =>
{row.getValue("Col")}
, -// }, -// { -// accessorKey: "Assay_Plate_Barcode", -// header: "Assay Plate Barcode", -// cell: ({ row }) =>
{row.getValue("Assay_Plate_Barcode")}
, -// }, -// { -// accessorKey: "Name", -// header: "Name", -// cell: ({ row }) =>
{row.getValue("Name")}
, -// }, -// { -// accessorKey: "PlateName", -// header: "PlateName", -// cell: ({ row }) =>
{row.getValue("PlateName")}
, -// }, -// { -// accessorKey: "Plate_Map_Name", -// header: "Plate Map Name", -// cell: ({ row }) =>
{row.getValue("Plate_Map_Name")}
, -// }, -// { -// accessorKey: "PublicID", -// header: "Public ID", -// cell: ({ row }) =>
{row.getValue("PublicID")}
, -// }, -// { -// accessorKey: "Row", -// header: "Row", -// cell: ({ row }) =>
{row.getValue("Row")}
, -// }, -// { -// accessorKey: "key", -// header: "Key", -// cell: ({ row }) =>
{row.getValue("key")}
, -// }, -// { -// accessorKey: "_highlightResult", -// header: "Hightlight Result", -// cell: ({ row }) => { -// const value: any = row.getValue("_highlightResult"); - -// const keys = Object.keys(value).filter( -// (key) => key !== "objectID" && key !== "project_id" -// ); - -// return ( -//
-// {keys.map((key) => { -// return ( -//
-//
{key}
-//
{value[key].value}
-//
-// ); -// })} -//
-// ); -// }, -// }, -// ]; +import { FileViewer } from "./file-viewer"; export const columns: ColumnDef[] = [ { @@ -153,7 +76,7 @@ function ProjectKeyActions(props: TProjectKeyActions) { setIsDownloadLoading(false); return; } - downloadData(data); + downloadData(data as Uint8Array); }} > {isDownloadLoading && ( diff --git a/cpgweb/src/app/search/file-viewer.tsx b/cpgweb/src/app/search/file-viewer.tsx index 0b40722..4d53079 100644 --- a/cpgweb/src/app/search/file-viewer.tsx +++ b/cpgweb/src/app/search/file-viewer.tsx @@ -1,48 +1,115 @@ -import { Button } from "@/src/components/ui/button" +import { Button } from "@/src/components/ui/button"; +import { + DataSheetGrid, + textColumn, + keyColumn, +} from 'react-datasheet-grid'; import { Dialog, DialogContent, - DialogDescription, - DialogFooter, DialogHeader, DialogTitle, DialogTrigger, -} from "@/src/components/ui/dialog" +} from "@/src/components/ui/dialog"; +import { DSVRowArray, csvParse, tsvParse } from "d3-dsv"; import { get_key_file } from "@/src/lib/cpg_ops"; -import { Eye } from "lucide-react" +import { Eye, Loader2 } from "lucide-react"; import React from "react"; +import Image from "next/image"; -interface FileViewerProps { - fileName: string, +interface DSVRendererProps { + data: DSVRowArray +} +const DSVRenderer = (props: DSVRendererProps) => { + const { data } = props; + // const [data, setData] = React.useState([ + // { active: true, firstName: 'Elon', lastName: 'Musk' }, + // { active: false, firstName: 'Jeff', lastName: 'Bezos' }, + // ]) + const columns = data.columns.map((col) => { return { ...keyColumn(col, textColumn), title: col, disabled: true } }) + return ( + + ) + +} +interface FileViewerProps { + fileName: string; } export function FileViewer(props: FileViewerProps) { const { fileName } = props; const [viewContent, setViewContent] = React.useState(""); + const [isLoading, setIsLoading] = React.useState(true); async function fetchData() { - const data = await get_key_file(fileName); - return data || ""; + if (fileName.endsWith(".xslx")) { + setViewContent("Please download to view this file.") + setIsLoading(false); + return; + } + const encoding = fileName.endsWith(".png") ? "base64" : "utf-8"; + const data = await get_key_file(fileName, { toString: { encoding } }); + + if (data && typeof data === "string") { + if (fileName.endsWith(".png")) { + setViewContent(`data:image/png;base64,${data}`); + } else { + // Only showing the first 100 characters + setViewContent(data); + } + } else { + console.log("Failed to load file.", data); + } + + setIsLoading(false); } + + const loading = isLoading; + const failedToLoad = !isLoading && !viewContent; + const loaded = !isLoading && viewContent; + const isImage = fileName.endsWith(".png"); + const isCSV = fileName.endsWith(".csv"); + const isTSV = fileName.endsWith(".tsv"); + return ( - - + {fileName.split("/").pop()} - - {fileName} - -
- {viewContent} -
+ {loading && ( +
+ +
+ )} + + {failedToLoad &&
Failed to load.
} + {loaded && isCSV &&
} + {loaded && isTSV &&
} + {loaded && isImage && ( +
+ {fileName} +
+ )}
- ) + ); } diff --git a/cpgweb/src/app/search/page.tsx b/cpgweb/src/app/search/page.tsx index f24adaa..12d769c 100644 --- a/cpgweb/src/app/search/page.tsx +++ b/cpgweb/src/app/search/page.tsx @@ -51,11 +51,11 @@ export default function DataTable() { }); const facets = React.useMemo(() => { - if (!data || !data.facets) return []; - const d = data?.facets?.project_id; - const facets = Object.keys(data?.facets?.project_id).map((project) => ({ + if (!data || !data.facets || !data.facets.project_id) return []; + const project_id_keys = data.facets.project_id; + const facets = Object.keys(project_id_keys).map((project) => ({ value: project, - count: d[project], + count: project_id_keys[project], })); return facets; @@ -104,9 +104,9 @@ export default function DataTable() { {header.isPlaceholder ? null : flexRender( - header.column.columnDef.header, - header.getContext() - )} + header.column.columnDef.header, + header.getContext() + )} ); })} @@ -117,7 +117,9 @@ export default function DataTable() { {table.getRowModel().rows?.length ? ( table .getRowModel() - .rows.map((row) => ) + .rows.map((row) => ( + + )) ) : ( { - if (!data || !data.facets) return []; - return Object.keys(data?.facets?.key).map((key) => ({ + if (!data || !data.facets || !data.facets.key) return []; + return Object.keys(data.facets.key).map((key) => ({ value: key, + count: data.facets?.key[key] })); }, [data]); diff --git a/cpgweb/src/components/ui/dialog.tsx b/cpgweb/src/components/ui/dialog.tsx index 2e0a7c0..499ea00 100644 --- a/cpgweb/src/components/ui/dialog.tsx +++ b/cpgweb/src/components/ui/dialog.tsx @@ -38,7 +38,7 @@ const DialogContent = React.forwardRef< { }); }; -const get_key_file = async (Key: string) => { +export type TGetKeyFileOptions = { + toString?: { + encoding: "utf-8" | "base64"; + }; +}; + +const get_key_file = async ( + Key: string, + options = {} as TGetKeyFileOptions +) => { const command = new GetObjectCommand({ Bucket: "cellpainting-gallery", Key, }); const client = get_anon_s3_client(); - + const { toString } = options; try { const response = await client.send(command); - // if (Key.endsWith(".png")) { - // const body = await response.Body?.transformToString("base64"); - // return body; - // } - const body = await response.Body?.transformToByteArray(); + const body = + typeof toString?.encoding === "string" + ? await response.Body?.transformToString(toString.encoding) + : await response.Body?.transformToByteArray(); return body; } catch (err) { console.log(err); diff --git a/cpgweb/src/styles/globals.css b/cpgweb/src/styles/globals.css index 5c42da6..f3b80e0 100644 --- a/cpgweb/src/styles/globals.css +++ b/cpgweb/src/styles/globals.css @@ -2,7 +2,6 @@ @tailwind components; @tailwind utilities; - @layer base { :root { --background: 0 0% 100%; @@ -25,6 +24,22 @@ --input: 240 5.9% 90%; --ring: 346.8 77.2% 49.8%; --radius: 0.5rem; + /* --dsg-border-color: var(--muted-foreground); */ + /* --dsg-selection-border-color: var(--accent-foreground); */ + /* --dsg-selection-border-radius: 2px; */ + /* --dsg-selection-border-width: 2px; */ + /* --dsg-selection-background-color: var(--muted); */ + /* --dsg-selection-disabled-border-color: var(--muted); */ + /* --dsg-selection-disabled-background-color: var(--muted); */ + /* --dsg-corner-indicator-width: 10px; */ + /* --dsg-header-text-color: var(--secondary-foreground); */ + /* --dsg-header-active-text-color: var(--primary-foreground); */ + /* --dsg-cell-background-color: var(--background); */ + /* --dsg-cell-disabled-background-color: var(--background); */ + /* --dsg-transition-duration: 0.1s; */ + /* --dsg-expand-rows-indicator-width: 10px; */ + /* --dsg-scroll-shadow-width: 7px; */ + /* --dsg-scroll-shadow-color: rgba(0, 0, 0, 0.2); */ } .dark { @@ -47,6 +62,22 @@ --border: 240 3.7% 15.9%; --input: 240 3.7% 15.9%; --ring: 346.8 77.2% 49.8%; + /* --dsg-border-color: var(--muted-foreground); */ + /* --dsg-selection-border-color: var(--accent-foreground); */ + /* --dsg-selection-border-radius: 2px; */ + /* --dsg-selection-border-width: 2px; */ + /* --dsg-selection-background-color: var(--muted); */ + /* --dsg-selection-disabled-border-color: var(--muted); */ + /* --dsg-selection-disabled-background-color: var(--muted); */ + /* --dsg-corner-indicator-width: 10px; */ + /* --dsg-header-text-color: var(--secondary-foreground); */ + /* --dsg-header-active-text-color: var(--primary-foreground); */ + /* --dsg-cell-background-color: var(--background); */ + /* --dsg-cell-disabled-background-color: var(--background); */ + /* --dsg-transition-duration: 0.1s; */ + /* --dsg-expand-rows-indicator-width: 10px; */ + /* --dsg-scroll-shadow-width: 7px; */ + /* --dsg-scroll-shadow-color: rgba(0, 0, 0, 0.2); */ } }