From 16fe388a6f9c856ab0f7e7ab90489fa111227961 Mon Sep 17 00:00:00 2001 From: longshuicy Date: Mon, 9 Aug 2021 11:43:55 -0500 Subject: [PATCH] Dataset page (#1) * breadcrum updates * make app component; seperate dataset * styling * connect to API get files within dataset * use endpint to get files in dataset * put common style in to main css * add about dataset * add thumbnails * thumbnail works * temp; thumbnail not working * finally thumbnail works * fix thumbnail * hook with click effect --- src/actions/dataset.js | 56 ++++++++++ src/actions/file.js | 27 ----- src/components/App.jsx | 114 ++++++++++++++++++++ src/components/Dataset.jsx | 214 +++++++++++++++++++++++++------------ src/components/File.jsx | 53 +++------ src/containers/App.jsx | 47 ++++++++ src/containers/Dataset.jsx | 32 +----- src/containers/File.jsx | 26 ----- src/reducers/dataset.js | 16 +++ src/reducers/file.js | 4 +- src/reducers/index.js | 4 +- src/routes.js | 10 +- src/styles/main.css | 38 +++++++ src/utils/file.js | 19 ++++ src/utils/thumbnail.js | 31 ++++++ 15 files changed, 494 insertions(+), 197 deletions(-) create mode 100644 src/components/App.jsx create mode 100644 src/containers/App.jsx create mode 100644 src/reducers/dataset.js create mode 100644 src/utils/file.js create mode 100644 src/utils/thumbnail.js diff --git a/src/actions/dataset.js b/src/actions/dataset.js index e69de29..0335b5b 100644 --- a/src/actions/dataset.js +++ b/src/actions/dataset.js @@ -0,0 +1,56 @@ +import config from "../app.config"; +import {getHeader} from "../utils/common"; + +export const RECEIVE_FILES_IN_DATASET= "RECEIVE_FILES_IN_DATASET"; +export function receiveFilesInDataset(type, json){ + return (dispatch) => { + dispatch({ + type: type, + files: json, + receivedAt: Date.now(), + }); + }; +} +export function fetchFilesInDataset(id="610d54a15e0e9253e65863f8"){ + let url = `${config.hostname}/clowder/api/datasets/${id}/files?superAdmin=true`; + return (dispatch) => { + return fetch(url, {mode:"cors", headers: getHeader()}) + .then((response) => { + if (response.status === 200) { + response.json().then(json =>{ + dispatch(receiveFilesInDataset(RECEIVE_FILES_IN_DATASET, json)); + }); + } + else { + dispatch(receiveFilesInDataset(RECEIVE_FILES_IN_DATASET, [])); + } + }); + }; +} + +export const RECEIVE_DATASET_ABOUT = "RECEIVE_DATASET_ABOUT"; +export function receiveDatasetAbout(type, json){ + return (dispatch) => { + dispatch({ + type: type, + about: json, + receivedAt: Date.now(), + }); + }; +} +export function fetchDatasetAbout(id="610d54a15e0e9253e65863f8"){ + let url = `${config.hostname}/clowder/api/datasets/${id}?superAdmin=true`; + return (dispatch) => { + return fetch(url, {mode:"cors", headers: getHeader()}) + .then((response) => { + if (response.status === 200) { + response.json().then(json =>{ + dispatch(receiveDatasetAbout(RECEIVE_DATASET_ABOUT, json)); + }); + } + else { + dispatch(receiveDatasetAbout(RECEIVE_DATASET_ABOUT, [])); + } + }); + }; +} diff --git a/src/actions/file.js b/src/actions/file.js index a99d3cc..9873380 100644 --- a/src/actions/file.js +++ b/src/actions/file.js @@ -1,33 +1,6 @@ import config from "../app.config"; import {getHeader} from "../utils/common"; -export const RECEIVE_FILE_METADATA = "RECEIVE_FILE_METADATA"; -export function receiveFileMetadata(type, json){ - return (dispatch) => { - dispatch({ - type: type, - metadata: json, - receivedAt: Date.now(), - }); - }; -} -export function fetchFileMetadata(id){ - let url = `${config.hostname}/clowder/api/files/${id}/metadata?superAdmin=true`; - return (dispatch) => { - return fetch(url, {mode:"cors", headers: getHeader()}) - .then((response) => { - if (response.status === 200) { - response.json().then(json =>{ - dispatch(receiveFileMetadata(RECEIVE_FILE_METADATA, json)); - }); - } - else { - dispatch(receiveFileMetadata(RECEIVE_FILE_METADATA, [])); - } - }); - }; -} - export const RECEIVE_FILE_EXTRACTED_METADATA = "RECEIVE_FILE_EXTRACTED_METADATA"; export function receiveFileExtractedMetadata(type, json){ return (dispatch) => { diff --git a/src/components/App.jsx b/src/components/App.jsx new file mode 100644 index 0000000..3284471 --- /dev/null +++ b/src/components/App.jsx @@ -0,0 +1,114 @@ +import React, {useEffect, useState} from "react"; +import TopBar from "./childComponents/TopBar"; +import Breadcrumbs from "./childComponents/BreadCrumb"; +import {makeStyles} from "@material-ui/core/styles"; +import File from "./File"; +import Dataset from "./Dataset"; +import {fetchFileMetadata} from "../utils/file"; +import {downloadThumbnail} from "../utils/thumbnail"; + + +const useStyles = makeStyles((theme) => ({ + +})); + +export default function App(props) { + const classes = useStyles(); + + const [datasetId, setDatasetId] = useState(""); + const [selectedFileId, setSelectedFileId] = useState(""); + const [fileMetadataList, setFileMetadataList] = useState([]); + const [thumbnailList, setThumbnailList] = useState([]); + + const [paths, setPaths] = useState(["explore", "collection", "dataset", ""]); + + const { + // files + listFileExtractedMetadata, fileExtractedMetadata, + listFileMetadataJsonld, fileMetadataJsonld, + listFilePreviews, filePreviews, + + //dataset + listFilesInDataset, filesInDataset, + listDatasetAbout, datasetAbout, + ...other + } = props; + + // component did mount + useEffect(() => { + listFilesInDataset(); + listDatasetAbout(); + }, []); + + // // set breadcrumbs + // useEffect(() => { + // setPaths(paths => [...paths.slice(0, paths.length - 1), fileMetadata["filename"]]); + // }, [fileMetadata]); + + // get metadata of each files; because we need the thumbnail of each file!!! + useEffect(() => { + + (async () => { + if (filesInDataset !== undefined && filesInDataset.length > 0){ + + let fileMetadataListTemp = []; + let thumbnailListTemp = []; + await Promise.all(filesInDataset.map(async (fileInDataset) => { + + let fileMetadata = await fetchFileMetadata(fileInDataset["id"]); + + // add thumbnails + if (fileMetadata["thumbnail"] !== null && fileMetadata["thumbnail"] !== undefined){ + let thumbnailURL = await downloadThumbnail(fileMetadata["thumbnail"]); + fileMetadataListTemp.push({"id":fileInDataset["id"], "metadata": fileMetadata, "thumbnail": thumbnailURL}); + thumbnailListTemp.push({"id":fileInDataset["id"], "thumbnail": thumbnailURL}) + } + })); + + setFileMetadataList(fileMetadataListTemp); + setThumbnailList(thumbnailListTemp); + } + })(); + }, [filesInDataset]) + + const selectFile = (selectedFileId) => { + // pass that id to file component + setSelectedFileId(selectedFileId); + + // load file information + listFileExtractedMetadata(selectedFileId); + listFileMetadataJsonld(selectedFileId); + listFilePreviews(selectedFileId); + } + + return ( +
+ +
+ + { + selectedFileId === "" ? + // Dataset page + + : + // file page + fileMetadataList.map((fileMetadata) =>{ + if (selectedFileId === fileMetadata["id"]){ + return ( + + ) + } + }) + } +
+
+ ); +} diff --git a/src/components/Dataset.jsx b/src/components/Dataset.jsx index f8928f0..0d27bdf 100644 --- a/src/components/Dataset.jsx +++ b/src/components/Dataset.jsx @@ -1,97 +1,175 @@ import React, {useEffect, useState} from "react"; -import TopBar from "./childComponents/TopBar"; -import Breadcrumbs from "./childComponents/BreadCrumb"; import {makeStyles} from "@material-ui/core/styles"; -import {Link, Button} from "@material-ui/core"; -import File from "./File"; +import {AppBar, Box, Button, Divider, Grid, ListItem, Tab, Tabs, Typography} from "@material-ui/core"; +import {ClowderInput} from "./styledComponents/ClowderInput"; +import {ClowderButton} from "./styledComponents/ClowderButton"; +import DescriptionIcon from '@material-ui/icons/Description'; const useStyles = makeStyles((theme) => ({ - root: { - flexGrow: 1, - }, - appBar:{ + appBar: { background: "#FFFFFF", boxShadow: "none", }, - tab:{ + tab: { fontStyle: "normal", fontWeight: "normal", fontSize: "16px", color: "#495057", - textTransform:"capitalize", + textTransform: "capitalize", }, - infoCard:{ - padding: "48px 0", + fileCard:{ + background: "#FFFFFF", + border: "1px solid #DFDFDF", + boxSizing: "border-box", + borderRadius: "4px", + margin:"20px auto", + "& > .MuiGrid-item":{ + padding:0, + height:"150px", + } }, - title:{ - fontWeight: "600", - fontSize: "16px", - color: "#000000", - marginBottom:"8px" + fileCardImg:{ + height: "50%", + margin:"40px auto", + display:"block" + }, + fileCardText:{ + padding: "40px 20px", + fontSize:"16px", + fontWeight:"normal", + color:"#212529" }, - content:{ - fontSize: "14px", - color: "#000000", - } })); export default function Dataset(props) { const classes = useStyles(); - const [fileId, setFileId] = useState(""); - const [paths, setPaths] = useState(["explore","collection", "datasetID"]); - - const { - listFileMetadata, fileMetadata, - listFileExtractedMetadata, fileExtractedMetadata, - listFileMetadataJsonld, fileMetadataJsonld, - listFilePreviews, filePreviews, - ...other - } = props; + const {files, thumbnails, about, selectFile, ...other} = props; - useEffect(() => { - // set breadcrumbs - setPaths(paths => [...paths.slice(0, paths.length), fileMetadata["filename"]]); - }, [fileMetadata]); + const [selectedTabIndex, setSelectedTabIndex] = useState(0); - const selectFile = (selectedFileId) => { - // pass that id to file component - setFileId(selectedFileId); + const handleTabChange = (event, newTabIndex) => { + setSelectedTabIndex(newTabIndex); + }; - // load file information - listFileMetadata(selectedFileId); - listFileExtractedMetadata(selectedFileId); - listFileMetadataJsonld(selectedFileId); - listFilePreviews(selectedFileId); + return ( +
+ + + + + + + + + + + + - {/**/} - {/*
*/} - {/* */} + { + files !== undefined && files.length > 0 && thumbnails !== undefined && thumbnails.length > 0 + ? + files.map((file) => { + let thumbnailComp = ; + thumbnails.map((thumbnail) => { + if (file["id"] !== undefined && thumbnail["id"] !== undefined && + thumbnail["thumbnail"] !== null && thumbnail["thumbnail"] !== undefined && + file["id"] === thumbnail["id"]) { + thumbnailComp = thumbnail; + } + }); + return ( + selectFile(file["id"])}> + + {thumbnailComp} + + + + File Size: {file["size"]} + Created on: {file["date-created"]} + Content type: {file["contentType"]} + + + + ); + }) + : + <> + } + + + + + + + + { + about !== undefined ? + + About + Owner: {about["authorId"]} + Description: {about["description"]} + Created on: {about["created"]} + {/*/!*TODO use this to get thumbnail*!/*/} + Thumbnail: {about["thumbnail"]} + {/*Belongs to spaces: {about["authorId"]}*/} + {/*/!*TODO not sure how to use this info*!/*/} + {/*Resource type: {about["resource_type"]}*/} + : <> + } + + + Statistics + Views: 10 + Last viewed: Jun 07, 2021 21:49:09 + Downloads: 0 + Last downloaded: Never + + + + Tags + + + + + + Search + + + + + + +
+ ); +} - } +function TabPanel(props) { + const {children, value, index, ...other} = props; return ( -
- -
- - - - - - - - { - fileId !== "" ? - - : - <> - } -
+ ); } + +function a11yProps(index) { + return { + id: `dataset-tab-${index}`, + "aria-controls": `dataset-tabpanel-${index}`, + }; +} diff --git a/src/components/File.jsx b/src/components/File.jsx index 37cfedf..f5fb1ef 100644 --- a/src/components/File.jsx +++ b/src/components/File.jsx @@ -6,9 +6,6 @@ import {ClowderInput} from "./styledComponents/ClowderInput"; import {ClowderButton} from "./styledComponents/ClowderButton"; const useStyles = makeStyles((theme) => ({ - root: { - flexGrow: 1, - }, appBar: { background: "#FFFFFF", boxShadow: "none", @@ -19,19 +16,6 @@ const useStyles = makeStyles((theme) => ({ fontSize: "16px", color: "#495057", textTransform: "capitalize", - }, - infoCard: { - padding: "48px 0", - }, - title: { - fontWeight: "600", - fontSize: "16px", - color: "#000000", - marginBottom: "8px" - }, - content: { - fontSize: "14px", - color: "#000000", } })); @@ -89,9 +73,7 @@ export default function File(props) { }; return ( -
- {/*tabs*/} -
+
@@ -152,33 +134,33 @@ export default function File(props) { { fileMetadata !== undefined ? - - About + + About Type: {fileMetadata["content-type"]} - File + className="content">Type: {fileMetadata["content-type"]} + File size: {fileMetadata["size"]} - Uploaded + Uploaded on: {fileMetadata["date-created"]} - Uploaded + Uploaded as: {fileMetadata["filename"]} - Uploaded + Uploaded by: {fileMetadata["authorId"]} Status: {fileMetadata["status"]} + className="content">Status: {fileMetadata["status"]} : <> } - - Statistics - Views:10 - Last viewed:Jun 07, 2021 21:49:09 - Downloads:0 - Last downloaded:Never + + Statistics + Views: 10 + Last viewed: Jun 07, 2021 21:49:09 + Downloads: 0 + Last downloaded: Never - - Tags + + Tags @@ -192,7 +174,6 @@ export default function File(props) {
-
); } diff --git a/src/containers/App.jsx b/src/containers/App.jsx new file mode 100644 index 0000000..53341fe --- /dev/null +++ b/src/containers/App.jsx @@ -0,0 +1,47 @@ +import {connect} from "react-redux"; +import AppComponent from "../components/App"; +import { + fetchFileExtractedMetadata, + fetchFileMetadata, + fetchFileMetadataJsonld, + fetchFilePreviews +} from "../actions/file"; + +import { + fetchFilesInDataset, + fetchDatasetAbout +} from "../actions/dataset"; + +const mapStateToProps = (state) => { + return { + fileExtractedMetadata: state.file.extractedMetadata, + fileMetadataJsonld: state.file.metadataJsonld, + filePreviews: state.file.previews, + filesInDataset: state.dataset.files, + datasetAbout: state.dataset.about + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + listFileExtractedMetadata: (fileId) => { + dispatch(fetchFileExtractedMetadata(fileId)); + }, + listFileMetadataJsonld: (fileId) => { + dispatch(fetchFileMetadataJsonld(fileId)); + }, + listFilePreviews: (fileId) => { + dispatch(fetchFilePreviews(fileId)); + }, + listFilesInDataset: (datasetId) => { + dispatch(fetchFilesInDataset(datasetId)); + }, + listDatasetAbout: (datasetId) => { + dispatch(fetchDatasetAbout(datasetId)); + } + }; +}; + +const App = connect(mapStateToProps, mapDispatchToProps)(AppComponent); + +export default App; diff --git a/src/containers/Dataset.jsx b/src/containers/Dataset.jsx index c9cdc5f..1f0562c 100644 --- a/src/containers/Dataset.jsx +++ b/src/containers/Dataset.jsx @@ -1,37 +1,9 @@ import {connect} from "react-redux"; import DatasetComponents from "../components/Dataset"; -import { - fetchFileExtractedMetadata, - fetchFileMetadata, - fetchFileMetadataJsonld, - fetchFilePreviews -} from "../actions/file"; -const mapStateToProps = (state) => { - return { - fileMetadata: state.file.metadata, - fileExtractedMetadata: state.file.extractedMetadata, - fileMetadataJsonld: state.file.metadataJsonld, - filePreviews: state.file.previews, - }; -}; +const mapStateToProps = (state) => {}; -const mapDispatchToProps = (dispatch) => { - return { - listFileMetadata: (id) => { - dispatch(fetchFileMetadata(id)); - }, - listFileExtractedMetadata: (id) => { - dispatch(fetchFileExtractedMetadata(id)); - }, - listFileMetadataJsonld: (id) => { - dispatch(fetchFileMetadataJsonld(id)); - }, - listFilePreviews: (fileId) => { - dispatch(fetchFilePreviews(fileId)); - } - }; -}; +const mapDispatchToProps = (dispatch) => {}; const Dataset = connect(mapStateToProps, mapDispatchToProps)(DatasetComponents); diff --git a/src/containers/File.jsx b/src/containers/File.jsx index cb7a200..f9e0454 100644 --- a/src/containers/File.jsx +++ b/src/containers/File.jsx @@ -1,37 +1,11 @@ import {connect} from "react-redux"; import FileComponent from "../components/File"; -import { - fetchFileExtractedMetadata, - fetchFileMetadata, - fetchFileMetadataJsonld, - fetchFilePreviews -} from "../actions/file"; const mapStateToProps = (state) => { - return { - fileMetadata: state.file.metadata, - fileExtractedMetadata: state.file.extractedMetadata, - fileMetadataJsonld: state.file.metadataJsonld, - filePreviews: state.file.previews, - }; }; const mapDispatchToProps = (dispatch) => { - return { - listFileMetadata: (id) => { - dispatch(fetchFileMetadata(id)); - }, - listFileExtractedMetadata: (id) => { - dispatch(fetchFileExtractedMetadata(id)); - }, - listFileMetadataJsonld: (id) => { - dispatch(fetchFileMetadataJsonld(id)); - }, - listFilePreviews: (fileId) => { - dispatch(fetchFilePreviews(fileId)); - } - }; }; const File = connect(mapStateToProps, mapDispatchToProps)(FileComponent); diff --git a/src/reducers/dataset.js b/src/reducers/dataset.js new file mode 100644 index 0000000..c937772 --- /dev/null +++ b/src/reducers/dataset.js @@ -0,0 +1,16 @@ +import { RECEIVE_FILES_IN_DATASET, RECEIVE_DATASET_ABOUT } from "../actions/dataset"; + +const defaultState = {files: []}; + +const dataset = (state=defaultState, action) => { + switch(action.type) { + case RECEIVE_FILES_IN_DATASET: + return Object.assign({}, state, {files: action.files}); + case RECEIVE_DATASET_ABOUT: + return Object.assign({}, state, {about: action.about}); + default: + return state; + } +}; + +export default dataset; diff --git a/src/reducers/file.js b/src/reducers/file.js index 201a822..abbb8fc 100644 --- a/src/reducers/file.js +++ b/src/reducers/file.js @@ -1,11 +1,9 @@ -import { RECEIVE_FILE_METADATA, RECEIVE_FILE_EXTRACTED_METADATA, RECEIVE_FILE_METADATA_JSONLD, RECEIVE_PREVIEWS } from "../actions/file"; +import { RECEIVE_FILE_EXTRACTED_METADATA, RECEIVE_FILE_METADATA_JSONLD, RECEIVE_PREVIEWS } from "../actions/file"; const defaultState = {metadata: {}, extractedMetadata: {}, metadataJsonld: [], previews: []}; const file = (state=defaultState, action) => { switch(action.type) { - case RECEIVE_FILE_METADATA: - return Object.assign({}, state, {metadata: action.metadata}); case RECEIVE_FILE_EXTRACTED_METADATA: return Object.assign({}, state, {extractedMetadata: action.extractedMetadata}); case RECEIVE_FILE_METADATA_JSONLD: diff --git a/src/reducers/index.js b/src/reducers/index.js index a7b0aeb..56330a5 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,8 +1,10 @@ import { combineReducers } from "redux"; import file from "./file"; +import dataset from "./dataset"; const rootReducer = combineReducers({ - file: file + file: file, + dataset: dataset }); export default rootReducer; diff --git a/src/routes.js b/src/routes.js index fc61e17..85b6a94 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,15 +1,13 @@ import React from "react"; import {Route, Switch} from "react-router-dom"; -import File from "./containers/File"; -import Dataset from "./containers/Dataset"; +import App from "./containers/App"; export default ( - { - return (); - }}/> - {return ();}}/> + {return ();}}/> + {/* {return ();}}/>*/} + {/* {return ();}}/>*/} ); diff --git a/src/styles/main.css b/src/styles/main.css index 19509a4..c82558a 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -10,3 +10,41 @@ body { .inner-container{ padding: 20px 0; } + +.fileCard{ + border: 1px solid #DFDFDF; + box-sizing: border-box; + border-radius: 4px; + height:140px; + margin: 25px auto; +} + +.infoCard { + padding: 48px 0; +} + +.infoCard .title{ + font-weight: 600; + font-size: 16px; + color: #000000; + margin-bottom: 8px; +} + +.infoCard .content{ + font-size: 14px; + color: #000000; + margin-bottom:2px; +} + +.appBar { + background: #FFFFFF; + box-shadow: none; +} + +.tab{ + font-style: normal; + font-weight: normal; + font-size: 16px; + color: #495057; + text-transform: capitalize; +} diff --git a/src/utils/file.js b/src/utils/file.js new file mode 100644 index 0000000..8791b08 --- /dev/null +++ b/src/utils/file.js @@ -0,0 +1,19 @@ +import {getHeader} from "./common"; +import config from "../app.config"; + + +export async function fetchFileMetadata(id){ + let url = `${config.hostname}/clowder/api/files/${id}/metadata?superAdmin=true`; + let response = await fetch(url, {mode:"cors", headers: getHeader()}); + if (response.status === 200){ + return await response.json(); + } + else if (response.status === 401){ + // TODO handle error + return {}; + } + else { + // TODO handle error + return {}; + } +} diff --git a/src/utils/thumbnail.js b/src/utils/thumbnail.js new file mode 100644 index 0000000..b86716d --- /dev/null +++ b/src/utils/thumbnail.js @@ -0,0 +1,31 @@ +import {getHeader} from "./common"; +import config from "../app.config"; + + +export async function downloadThumbnail(thumbnailId, title=null) { + let url = `${config.hostname}/clowder/api/thumbnails/${thumbnailId}/blob?superAdmin=true`; + let authHeader = getHeader(); + let response = await fetch(url, { + method: "GET", + mode: "cors", + headers: authHeader, + }); + + if (response.status === 200){ + let blob = await response.blob(); + if (window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(blob, thumbnailId); + return null; + } else { + return window.URL.createObjectURL(blob); + } + } + else if (response.status === 401){ + // TODO handle error + return null; + } + else { + // TODO handle error + return null; + } +}