diff --git a/package.json b/package.json index 55a7a10ed0ed1a9cc5c1053d0e55562f26a02f03..192ed1cb679635b6e257667aa7d6d2bc4d3c37d9 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "react-jsonschema-form-layout-grid": "^2.1.0", "react-router-dom": "^5.1.2", "react-scripts": "^3.4.1", - "styled-components": "^4.4.1" + "styled-components": "^4.4.1", + "deep-equal": "^2.0.4" }, "scripts": { "start": "react-scripts start", diff --git a/src/components/basket/addtobasket.js b/src/components/basket/addtobasket.js index 10408fd34cd4b902ff8ba7ffffbde840a81fb96b..cebe54106c28bbe290cb189dd5cec0783f5d915a 100644 --- a/src/components/basket/addtobasket.js +++ b/src/components/basket/addtobasket.js @@ -1,19 +1,39 @@ -import React, { useState } from "react"; +import React, { useContext, useState, useRef } from "react"; +import * as deepEqual from "deep-equal"; +import { Form } from "react-bootstrap"; +import { GlobalContext } from "../../contexts/GlobalContext"; +import { BasketContext } from "../../contexts/BasketContext"; -export default function Addtobasket({ add }) { - const [info, setInfo] = useState(""); +export default function Addtobasket(props) { + const { isAuthenticated } = useContext(GlobalContext); + const basketContext = useContext(BasketContext); - return ( - // input field is only here for testing - // this will be replaced by real dataset later. - <div className="Item"> - <input - type="text" - placeholder="New dataset info" - value={info} - onChange={(e) => setInfo(e.target.value)} - ></input> - <button onClick={() => add(info)}>Add to basket</button> - </div> - ); + function isInBasket(testBasketItem) { + const found = basketContext.datasets.some(basketItem => deepEqual(basketItem, testBasketItem)); + return found; + } + + function addToBasket(addToBasketItem) { + basketContext.add(addToBasketItem); + console.log([addToBasketItem, basketContext]); + } + + function removeFromBasket(removeFromBasketItem) { + basketContext.remove(removeFromBasketItem); + console.log([removeFromBasketItem, basketContext]); + } + + if (isAuthenticated){ + return ( + <Form.Check id={props.id} type="checkbox" onChange={(event) => { + const action = event.target.checked ? addToBasket : removeFromBasket; + action(props.item); + }} checked={isInBasket(props.item) ? "checked" : ""} /> + ); + } + else{ + return ( + <Form.Check id={props.id} type="checkbox" disabled /> + ); + } } diff --git a/src/components/basket/savebasket.js b/src/components/basket/savebasket.js new file mode 100644 index 0000000000000000000000000000000000000000..e502b3cb973958ee7a15d9740f03ac5db6649421 --- /dev/null +++ b/src/components/basket/savebasket.js @@ -0,0 +1,55 @@ +import React, { useContext, useState, useRef } from "react"; +import { Button } from "react-bootstrap"; +import { GlobalContext } from "../../contexts/GlobalContext"; +import { BasketContext } from "../../contexts/BasketContext"; +import axios from "axios"; + + +export default function SaveBasket(props) { + const { api_host, isAuthenticated, sessionid } = useContext(GlobalContext); + const basketContext = useContext(BasketContext); + + function saveBasket(basketData){ + const payload = {shopping_cart: basketData}; + console.log(payload); + + const profileUrl = api_host + "accounts/user-profiles/"; + axios + .get(profileUrl, {withCredentials: true}) + .then((response) => { + const userProfileUrl = profileUrl + response.data.results[0].user_name + "/"; + + axios + .patch(userProfileUrl, payload, {withCredentials: true}) + .then((response) => { + console.log("patch", response); + }) + .catch((error) => { + console.log(error); + }); + }) + .catch((error) => { + console.log(error); + }); + } + + + if(isAuthenticated){ + return ( + <Button + type="button" + variant="primary" + onClick={() => saveBasket(basketContext.datasets)} + {...props} + >Save Data Selection</Button> + ); + } + else{ + return (<> + <Button variant="warning" disabled {...props}> + Log In to Enable Data Selection + </Button> + </> + ); + } +} diff --git a/src/components/query/QueryCatalogs.js b/src/components/query/QueryCatalogs.js index 44463f0997b1eecc2b92d2cd26017f81924482d7..0adc7688eb8de8fafcdb5557ef40d75db3aefe8c 100644 --- a/src/components/query/QueryCatalogs.js +++ b/src/components/query/QueryCatalogs.js @@ -4,6 +4,7 @@ import axios from "axios"; import { Container } from "react-bootstrap"; import Form from "react-jsonschema-form"; import { GlobalContext } from "../../contexts/GlobalContext"; +import { BasketContextProvider } from "../../contexts/BasketContext" import { QueryContext } from "../../contexts/QueryContext"; import QueryResults from "./QueryResults"; import parseQueryForm from "../../utils/form/parseQueryForm"; @@ -35,6 +36,9 @@ export default function QueryCatalogs() { case "zooniverse": setConfigName("zooniverse"); break; + case "esap_rucio": + setConfigName("esap_rucio"); + break; case "astron_vo": setConfigName("astron_vo"); break; @@ -133,7 +137,9 @@ export default function QueryCatalogs() { return ( <div key={catalog} className="mt-3"> <h4>Query results for {catalogName}</h4> - <QueryResults catalog={catalog} /> + <BasketContextProvider> + <QueryResults catalog={catalog} /> + </BasketContextProvider> </div> ); })} diff --git a/src/components/query/QueryResults.js b/src/components/query/QueryResults.js index 9350c83c30aedc7977b4ebdbb74c8587a15b1016..69e23c17de7eb00bbe9e39e62a16a18c98bbbd0b 100644 --- a/src/components/query/QueryResults.js +++ b/src/components/query/QueryResults.js @@ -4,6 +4,7 @@ import ASTRONVOResults from "./ASTRONVOResults"; import ZooniverseResults from "./ZooniverseResults"; import VORegListResults from "./VORegListResults"; import LOFARResults from "./LOFARResults"; +import RucioResults from "./RucioResults"; export default function QueryResults({ catalog }) { switch (catalog) { @@ -19,6 +20,8 @@ export default function QueryResults({ catalog }) { return <VORegListResults catalog={catalog} />; case "lofar": return <LOFARResults catalog={catalog} />; + case "rucio": + return <RucioResults catalog={catalog} />; default: return null; } diff --git a/src/components/query/RucioResults.js b/src/components/query/RucioResults.js new file mode 100644 index 0000000000000000000000000000000000000000..8620405f7410720041a3ffc83be7ab38dc5310f1 --- /dev/null +++ b/src/components/query/RucioResults.js @@ -0,0 +1,132 @@ +import React, { useContext, useState } from "react"; +import { Table, Alert, Form, Button } from "react-bootstrap"; +import * as deepEqual from "deep-equal"; +import { QueryContext } from "../../contexts/QueryContext"; +// import { BasketContext } from "../../contexts/BasketContext"; +import LoadingSpinner from "../LoadingSpinner"; +import Paginate, { pagination_fields } from "../Paginate"; +// import SaveBasket from "../basket/savebasket"; + +function titleCase(string) { + var sentence = string.toLowerCase().split(" "); + for (var i = 0; i < sentence.length; i++) { + sentence[i] = sentence[i][0].toUpperCase() + sentence[i].slice(1); + } + return sentence.join(" "); +} + +function newPageCallback(setPage) { + return (args) => { + if (args.target) { + setPage(parseFloat(args.target.text)); + } + }; +} + +export default function RucioResults({ catalog }) { + const context = useContext(QueryContext); + // const basketContext = useContext(BasketContext); + const { queryMap, page, setPage } = context; + + // console.log(queryMap, page, context.queryMap.get(catalog).status); + + if (!context.queryMap) return null; + if (context.queryMap.get(catalog).status === "fetched") { + if (context.queryMap.get(catalog).results.results.length === 0) + return <Alert variant="warning">No matching results found!</Alert>; + else if (catalog === "rucio") { + const result = queryMap.get("rucio").results.results[0]; + const numPages = queryMap.get("rucio").results.pages; + + const fields = Object.keys(result).map( + (key) => key + ); + const headers = Object.keys(result).map((field) => { + const title = titleCase(field.replace("_", " ")); + return <th key={`header_${field}`}>{title}</th>; + }); + + return ( + <> + <Paginate + getNewPage={newPageCallback(setPage)} + currentPage={page} + numAdjacent={3} + numPages={numPages} + /> + <Form> + {/*<SaveBasket />*/} + <Table className="mt-3" responsive> + <thead> + <tr className="bg-light"> + {/* <th> + <InputGroup> + <InputGroup.Checkbox /> + </InputGroup> + </th> */} + {headers} + </tr> + </thead> + <tbody> + {queryMap + .get("rucio") + .results.results.map((result, resultCounter) => { + const cells = fields.map((field) => { + const reactKey = `item_${resultCounter}_${field}`; + return ( + <td key={reactKey}> + {result[field]} + </td> + ); + }); + return ( + <tr key={`item_${resultCounter}`}> + {/* <th> + <InputGroup> + <InputGroup.Checkbox /> + </InputGroup> + </th> */} + {/*<td> + <Form.Check id={`selectClassifications_${result.project_id}`} type="checkbox" onChange={(event) => { + const action = event.target.checked ? addToBasket : removeFromBasket; + action(result.project_id, basketContext, "project", "classifications"); + }} checked={isInBasket(result.project_id, basketContext, "project", "classifications") ? "checked" : ""} /> + </td> + <td> + <Form.Check id={`selectSubjects_${result.project_id}`} type="checkbox" onChange={(event) => { + const action = event.target.checked ? addToBasket : removeFromBasket; + action(result.project_id, basketContext, "project", "subjects"); + }} checked={isInBasket(result.project_id, basketContext, "project", "subjects") ? "checked" : ""} /> + </td>*/} + {/*<td>{result.project_id}</td> + <td>{result.display_name}</td> + <td>{created_at}</td> + <td>{updated_at}</td> + <td>{launch_date}</td> + <td>{live}</td> + <td> + <a href={`https://zooniverse.org/projects/${result.slug}`}> + Link + </a> + </td>*/} + {cells} + </tr> + ); + })} + </tbody> + </Table> + </Form> + <Paginate + getNewPage={newPageCallback(setPage)} + currentPage={page} + numAdjacent={3} + numPages={numPages} + /> + </> + ); + } + } + else { + return <LoadingSpinner />; + } +} diff --git a/src/components/query/ZooniverseResults.js b/src/components/query/ZooniverseResults.js index 69ff383d90af05b207d7549bfd45f6db2558ed42..aef7c46823e3d2cb49b457924272817ba37033a0 100644 --- a/src/components/query/ZooniverseResults.js +++ b/src/components/query/ZooniverseResults.js @@ -1,8 +1,12 @@ import React, { useContext, useState } from "react"; -import { Table, Alert } from "react-bootstrap"; +import { Table, Alert, Form, Button } from "react-bootstrap"; +import * as deepEqual from "deep-equal"; import { QueryContext } from "../../contexts/QueryContext"; +import { BasketContext } from "../../contexts/BasketContext"; import LoadingSpinner from "../LoadingSpinner"; import Paginate, { pagination_fields } from "../Paginate"; +import SaveBasket from "../basket/savebasket"; +import AddToBasket from "../basket/addtobasket"; const DATETIME_OPTIONS = { year: "numeric", @@ -15,7 +19,7 @@ const DATETIME_OPTIONS = { timeZoneName: "short", }; -Object.isObject = function (obj) { +Object.isObject = function(obj) { return (obj && obj.constructor === this) || false; }; @@ -84,10 +88,30 @@ function newPageCallback(setPage) { }; } +function projectBasketItem(projectId, category){ + return { + archive: "zooniverse", + catalog: "project", + project_id: projectId, + category: category + }; +} + +function workflowBasketItem(projectId, workflowId, category){ + return { + archive: "zooniverse", + catalog: "workflow", + project_id: projectId, + workflow_id: workflowId, + category: category + }; +} + + function ZooniverseProjectResults(context) { const { queryMap, page, setPage } = context; const date_formatter = new Intl.DateTimeFormat("default", DATETIME_OPTIONS); - const result = queryMap.get("zooniverse_projects").results.query_results[0]; + const result = queryMap.get("zooniverse_projects").results.results[0]; const numPages = result.pages; const mandatory_fields = [ "launch_date", @@ -114,70 +138,81 @@ function ZooniverseProjectResults(context) { numAdjacent={3} numPages={numPages} /> - <Table className="mt-3" responsive> - <thead> - <tr className="bg-light"> - {/* <th> + <Form> + <SaveBasket /> + <Table className="mt-3" responsive> + <thead> + <tr className="bg-light"> + {/* <th> <InputGroup> <InputGroup.Checkbox /> </InputGroup> </th> */} - <th>ID</th> - <th>Display Name</th> - <th>Created</th> - <th>Updated</th> - <th>Launched</th> - <th>Live</th> - <th>View</th> - {remaining_headers} - </tr> - </thead> - <tbody> - {queryMap - .get("zooniverse_projects") - .results.query_results.map((result) => { - const launch_date = result.launch_date - ? date_formatter.format(new Date(result.launch_date)) - : "Not Launched"; - const created_at = date_formatter.format( - new Date(result.created_at) - ); - const updated_at = date_formatter.format( - new Date(result.updated_at) - ); - const live = result.live ? "Yes" : "No"; - const remaining_cells = remaining_fields.map((field) => { - const reactKey = `project_${result.project_id}_${field}`; - return ( - <td key={reactKey}> - {renderIfCompound(result[field], reactKey)} - </td> + <th>Select Classification Data</th> + <th>Select Subject Data</th> + <th>ID</th> + <th>Display Name</th> + <th>Created</th> + <th>Updated</th> + <th>Launched</th> + <th>Live</th> + <th>View</th> + {remaining_headers} + </tr> + </thead> + <tbody> + {queryMap + .get("zooniverse_projects") + .results.results.map((result) => { + const launch_date = result.launch_date + ? date_formatter.format(new Date(result.launch_date)) + : "Not Launched"; + const created_at = date_formatter.format( + new Date(result.created_at) + ); + const updated_at = date_formatter.format( + new Date(result.updated_at) ); - }); - return ( - <tr key={`project_${result.project_id}`}> - {/* <th> + const live = result.live ? "Yes" : "No"; + const remaining_cells = remaining_fields.map((field) => { + const reactKey = `project_${result.project_id}_${field}`; + return ( + <td key={reactKey}> + {renderIfCompound(result[field], reactKey)} + </td> + ); + }); + return ( + <tr key={`project_${result.project_id}`}> + {/* <th> <InputGroup> <InputGroup.Checkbox /> </InputGroup> </th> */} - <td>{result.project_id}</td> - <td>{result.display_name}</td> - <td>{created_at}</td> - <td>{updated_at}</td> - <td>{launch_date}</td> - <td>{live}</td> - <td> - <a href={`https://zooniverse.org/projects/${result.slug}`}> - Link + <td> + <AddToBasket id={`selectClassifications_${result.project_id}`} item={projectBasketItem(result.project_id, "classifications")} /> + </td> + <td> + <AddToBasket id={`selectSubjects_${result.project_id}`} item={projectBasketItem(result.project_id, "subjects")} /> + </td> + <td>{result.project_id}</td> + <td>{result.display_name}</td> + <td>{created_at}</td> + <td>{updated_at}</td> + <td>{launch_date}</td> + <td>{live}</td> + <td> + <a href={`https://zooniverse.org/projects/${result.slug}`}> + Link </a> - </td> - {remaining_cells} - </tr> - ); - })} - </tbody> - </Table> + </td> + {remaining_cells} + </tr> + ); + })} + </tbody> + </Table> + </Form> <Paginate getNewPage={newPageCallback(setPage)} currentPage={page} @@ -191,7 +226,7 @@ function ZooniverseProjectResults(context) { function ZooniverseWorkflowResults(context) { const { queryMap, page, setPage } = context; let date_formatter = new Intl.DateTimeFormat("default", DATETIME_OPTIONS); - let result = queryMap.get("zooniverse_workflows").results.query_results[0]; + let result = queryMap.get("zooniverse_workflows").results.results[0]; let result_workflow = result.workflows[0]; const numPages = result.pages; let mandatory_fields = [ @@ -208,6 +243,7 @@ function ZooniverseWorkflowResults(context) { let title = titleCase(field.replace("_", " ")); return <th key={`project_header_${field}`}>{title}</th>; }); + const saveBasketStyle = { marginBottom : "10px"} ; return ( <> <Paginate @@ -216,9 +252,11 @@ function ZooniverseWorkflowResults(context) { numAdjacent={3} numPages={numPages} /> + <Form> + <SaveBasket style={saveBasketStyle} /> {queryMap .get("zooniverse_workflows") - .results.query_results.map((project) => { + .results.results.map((project) => { return ( <div key={project.project_id}> <h4>{project.display_name}</h4> @@ -230,6 +268,8 @@ function ZooniverseWorkflowResults(context) { <InputGroup.Checkbox /> </InputGroup> </th> */} + <th>Select Classification Data</th> + <th>Select Subject Data</th> <th>ID</th> <th>Display Name</th> <th>Created</th> @@ -261,6 +301,12 @@ function ZooniverseWorkflowResults(context) { <InputGroup.Checkbox /> </InputGroup> </th> */} + <td> + <AddToBasket id={`selectClassifications_${workflow.workflow_id}`} item={workflowBasketItem(result.project_id, workflow.workflow_id, "classifications")} /> + </td> + <td> + <AddToBasket id={`selectSubjects_${workflow.workflow_id}`} item={workflowBasketItem(result.project_id, workflow.workflow_id, "subjects")} /> + </td> <td>{workflow.workflow_id}</td> <td>{workflow.display_name}</td> <td>{created_at}</td> @@ -275,6 +321,7 @@ function ZooniverseWorkflowResults(context) { </div> ); })} + </Form> <Paginate getNewPage={newPageCallback(setPage)} currentPage={page} @@ -287,14 +334,17 @@ function ZooniverseWorkflowResults(context) { export default function ZooniverseResults({ catalog }) { const context = useContext(QueryContext); + const basketContext = useContext(BasketContext); if (!context.queryMap) return null; if (context.queryMap.get(catalog).status === "fetched") { - if (context.queryMap.get(catalog).results.query_results.length === 0) + if (context.queryMap.get(catalog).results.results.length === 0) return <Alert variant="warning">No matching results found!</Alert>; else if (catalog === "zooniverse_projects") { - return ZooniverseProjectResults(context); + console.log(`basketContext -> ${basketContext}`); + console.log(basketContext); + return ZooniverseProjectResults(context, basketContext); } else if (catalog === "zooniverse_workflows") { - return ZooniverseWorkflowResults(context); + return ZooniverseWorkflowResults(context, basketContext); } else { return <Alert variant="warning">Unrecognised Zooniverse Catalog!</Alert>; } diff --git a/src/contexts/BasketContext.js b/src/contexts/BasketContext.js index fd948e80ea70b9170e8d7bae68e5505de430d580..8948226f8b88cc35dbca72e9bd34de70dda7dc38 100644 --- a/src/contexts/BasketContext.js +++ b/src/contexts/BasketContext.js @@ -1,7 +1,6 @@ import React, { useState, createContext } from "react"; import Databasket from "../components/basket/databasket"; import Addtobasket from "../components/basket/addtobasket"; -import BasketContext from "../contexts/BasketContext"; export const BasketContext = createContext(); @@ -18,6 +17,7 @@ export function BasketContextProvider({ children }) { copy.splice(index, 1); setDatasets(copy); } + return ( <BasketContext.Provider value={{ datasets, add: handleAddDataset, remove: handleRemoveDataset }} diff --git a/src/contexts/GlobalContext.js b/src/contexts/GlobalContext.js index b2471b44e9ca3fb85734436be94ea713300cfbdf..179d9cf96f550c6c8c2fab5ed8178d80f67d5364 100644 --- a/src/contexts/GlobalContext.js +++ b/src/contexts/GlobalContext.js @@ -9,7 +9,7 @@ export function GlobalContextProvider({ children }) { console.log("ASTRON ESAP version ", Date()); const api_host = process.env.NODE_ENV === "development" - ? "http://localhost:15671/esap-api/" + ? "http://localhost:5555/esap-api/" : "/esap-api/"; // "http://localhost:15671/esap-api/" // "http://sdc.astron.nl:5557/esap-api/" diff --git a/src/utils/form/parseQueryForm.js b/src/utils/form/parseQueryForm.js index 08c3162b9737e8e0a72772702312f88c60d0951b..62d7ec47344ccab0d0631caf302fb3369b441a42 100644 --- a/src/utils/form/parseQueryForm.js +++ b/src/utils/form/parseQueryForm.js @@ -4,6 +4,7 @@ import parseLOFARForm from "./parseLOFARForm"; import parseIVOAForm from "./parseIVOAForm"; import parseApertifForm from "./parseApertifForm"; import parseASTRONVOForm from "./parseASTRONVOForm"; +import parseRucioForm from "./parseRucioForm"; export default function parseQueryForm(gui, formData, page) { switch (gui) { @@ -19,6 +20,8 @@ export default function parseQueryForm(gui, formData, page) { return parseASTRONVOForm(formData, page); case "ivoa": return parseIVOAForm(formData, page); + case "rucio": + return parseRucioForm(formData, page); default: return null; } diff --git a/src/utils/form/parseRucioForm.js b/src/utils/form/parseRucioForm.js new file mode 100644 index 0000000000000000000000000000000000000000..a0571bb15f2f15432fc230ea49a9d0309eaa281c --- /dev/null +++ b/src/utils/form/parseRucioForm.js @@ -0,0 +1,21 @@ +export default function parseRucioForm(formData, page) { + let formInput = Object.entries(formData); + + let query = ""; + + for (let [key, value] of formInput) { + query += `${`${query}` ? "&" : ""}` + key + "=" + value; + } + + let esapquery = [ + query, + "archive_uri=esap_rucio", + `catalog=rucio`, + ].join("&"); + + console.log("Rucio Query", query); + return [{ + catalog: "rucio", + esapquery: esapquery + `&page=${page}` + }]; +}